Compare commits

...

129 Commits

Author SHA1 Message Date
0c8f37ca19 bump 2018-03-30 15:37:04 +09:00
5dea0312ac Plugging NodeInfo reference 2018-03-30 15:23:05 +09:00
f074007f67 Refactoring clipboard copy code 2018-03-30 15:23:05 +09:00
88818ece29 Both regular and lightning copy tabs with new simplified styles 2018-03-30 15:23:05 +09:00
fa0fa28949 Complete switch to new styles for regular copy tab 2018-03-30 15:23:05 +09:00
08e31f6fe8 Clearing up label styles and using new input for all textboxes 2018-03-30 15:23:04 +09:00
b976adeefe Refactoring styles, simplifying the hierarchy 2018-03-30 15:23:04 +09:00
53c53b98e6 Adding new translation strings 2018-03-30 15:23:04 +09:00
a171e00280 Adding PeerInfo textbox
We'll need to heavily refactor this HTML and CSS... way to many styles and complex structure
2018-03-30 15:23:03 +09:00
46f94d7175 Merge pull request #86 from Saevar2000/master
Add Icelandic
2018-03-29 23:21:27 +09:00
2e555cac22 Add Icelandic 2018-03-29 08:19:07 +00:00
0d91b3286a bump 2018-03-29 13:00:04 +09:00
396432b873 Remove ESSLint errors 2018-03-29 12:54:58 +09:00
15c58434e8 Renaming CreateInvoiceResponse to CLightningInvoice 2018-03-29 12:54:07 +09:00
daad1bdd25 Fix bug of lightning invoice notification spam at startup 2018-03-29 12:36:10 +09:00
c60966c725 Revert "Add temporary log for stufftech debug"
This reverts commit fb57d8c3ce9f4b6e5d1ced035cda4cd385e6c75a.
2018-03-29 12:25:26 +09:00
fb57d8c3ce Add temporary log for stufftech debug 2018-03-29 12:21:20 +09:00
799ce74f65 Add temporary log for stufftech debug 2018-03-29 12:20:06 +09:00
8e38d7ceb4 Revert "Add temporary log to debug stufftech"
This reverts commit a1c22e807146b46e90cf78dd542bd4e3a6f67bf7.
2018-03-29 12:17:03 +09:00
a1c22e8071 Add temporary log to debug stufftech 2018-03-29 12:14:51 +09:00
6d8acf54d6 Revert "Fix SQLite bug: New invoice repeating"
This reverts commit 9eb3aad072c0c28dc937e6317425b2a3a0e1ed94.
2018-03-29 12:10:03 +09:00
a500a89138 Revert "add hack sqlite specific"
This reverts commit c6d44e7a8936fe9ac7bb85f221ed176afd8b8540.
2018-03-29 12:09:57 +09:00
c6d44e7a89 add hack sqlite specific 2018-03-29 12:02:13 +09:00
9eb3aad072 Fix SQLite bug: New invoice repeating 2018-03-29 11:57:17 +09:00
9355454953 Merge pull request #85 from pajasevi/lang-cs-fix
Fixed cs translation
2018-03-28 14:53:40 -05:00
6467f06c54 Fixed cs translation 2018-03-28 21:45:23 +02:00
b9b4b5ea39 log invoice event if Lightning max value exceeded 2018-03-28 23:15:10 +09:00
e23243565f Refactor CreateInvoiceCore to better give feedback on payment method errors to the merchant, be faster, and give NodeInfo 2018-03-28 22:37:01 +09:00
d3420532ae bump 2018-03-28 15:14:35 +09:00
ade1b9d4eb Merge pull request #84 from lepipele/dev-bugfix
Bugfixing currency icon positioning on smaller screens
2018-03-28 15:11:56 +09:00
fc278d12fc Bugfixing currency icon positioning on smaller screens 2018-03-28 01:09:53 -05:00
8e5ec822dc Powered by BTCPay Server 2018-03-27 15:22:48 +09:00
26aac66a76 Allow merchant to customize their checkout page 2018-03-27 15:14:50 +09:00
a562e90bdb Separate Checkout Experience settings from General store settings 2018-03-27 14:48:32 +09:00
a0f3698701 bump 2018-03-27 11:21:06 +09:00
02163f9482 Rewrite CanParseDerivationScheme 2018-03-27 11:21:06 +09:00
b74fe171e2 Merge pull request #83 from lepipele/master
Bugfixing isLightning compare for Conversion tab
2018-03-27 10:39:56 +09:00
2785bb4d9b Bugfixing isLightning compare for Conversion tab 2018-03-26 15:02:53 -05:00
5eac84d3a3 Fix bug: bitcoinAddress field of Invoice was showing ligthning BOLT11 address 2018-03-26 12:38:14 +09:00
a0a2ab6fcd update publish-docker 2018-03-26 11:54:10 +09:00
7730ead8e4 bump 2018-03-26 09:49:03 +09:00
8eee0dd14c Merge pull request #81 from pajasevi/lang-CS
Czech language support
2018-03-26 09:46:59 +09:00
7dd88d8d8f Can send max invoice value for lightning payments 2018-03-26 01:57:44 +09:00
56d1d3e645 Czech language support 2018-03-25 17:17:38 +02:00
c2308675b2 Better doc on the StoreUsers page 2018-03-25 14:09:40 +09:00
cb866a1c05 Make JP a bit shorter 2018-03-24 23:55:23 +09:00
95290e8331 Disable convertir tab for all lightning payments 2018-03-24 23:43:02 +09:00
f5e62c775b Remove BTC mentions from ConversionTab_Lightning 2018-03-24 23:37:18 +09:00
f533309b49 plug japanese translation 2018-03-24 23:02:41 +09:00
d1c70a7cb3 Merge pull request #78 from junderw/fixJA
Fix Japanese
2018-03-24 22:58:26 +09:00
2f8590ca7a Fix Japanese 2018-03-24 22:06:03 +09:00
08badbde56 bump 2018-03-24 20:40:48 +09:00
8e38da80e0 Better UX to set the xpub correctly 2018-03-24 20:40:26 +09:00
cd2e3350b0 Japanese support WIP 2018-03-24 20:15:42 +09:00
a0d2790491 Activate spanish 2018-03-24 14:35:49 +09:00
8ca99e5635 Merge branch 'spanish-language' of https://github.com/marcosrdz/btcpayserver into marcosrdz-spanish-language 2018-03-24 14:35:05 +09:00
5a2563ca7f Spanish translation 2018-03-24 01:15:43 -04:00
a23cd28531 Merge pull request #76 from LinoxBE/french-translation-fix
Fix French translation
2018-03-24 14:12:18 +09:00
58a967b59e Fix French translation 2018-03-23 19:41:27 +01:00
9bf0c20198 bump 2018-03-24 02:40:05 +09:00
6b7ac0e000 Merge pull request #75 from lepipele/dev-i18n
Fixing problems on expiry page for different languages
2018-03-24 02:39:37 +09:00
188c0a9a86 Fixing third line in expiry translation for Dutch 2018-03-23 12:33:57 -05:00
c49479c8ad Styling changes to make expiry text fit in different languages 2018-03-23 12:32:00 -05:00
2072b6e136 Fix english selection when the store has not set default language 2018-03-24 01:58:11 +09:00
08d82390b0 Remove language not yet translated 2018-03-24 01:15:28 +09:00
b845a545e2 Plug Dutch to LanguageService 2018-03-24 01:10:19 +09:00
db958b2401 Add Dutch 2018-03-24 01:06:00 +09:00
7266420eec Plug portugeuse to language service 2018-03-24 01:04:05 +09:00
f36fbe7a76 Merge branch 'patch-1' of https://github.com/rsandrade/btcpayserver into rsandrade-patch-1 2018-03-24 00:59:38 +09:00
8e279b110c use full language code 2018-03-24 00:58:37 +09:00
d626870e46 Create pt.js 2018-03-23 10:48:03 -03:00
df49b094d5 French translation 2018-03-23 18:04:44 +09:00
7d17bf7f2a Can set store default language 2018-03-23 17:27:48 +09:00
e51f3dd1ae Merge branch 'lepipele-dev-i18n' 2018-03-23 16:44:07 +09:00
b810b88c6c Merge branch 'dev-i18n' of https://github.com/lepipele/btcpayserver into lepipele-dev-i18n 2018-03-23 16:39:44 +09:00
39b34ff4ed Can invite user to manage your store 2018-03-23 16:24:57 +09:00
f72fd63113 Merge remote-tracking branch 'source/master' into dev-i18n
# Conflicts:
#	BTCPayServer/Properties/launchSettings.json
2018-03-22 23:35:45 -05:00
97eedc2c9f German test translation completed 2018-03-22 23:33:47 -05:00
db222c53e3 Faster language selection on page load 2018-03-22 23:16:38 -05:00
61e919b88d Removing flicker on invoice load 2018-03-22 23:15:54 -05:00
d14040c142 Foundation for future translations of supported languages 2018-03-22 23:04:42 -05:00
13a3a581d8 Detecting language from querystring 2018-03-22 23:02:53 -05:00
f6dbae1cef Extracting translation strings from core.js 2018-03-22 22:48:16 -05:00
ccbcda86ac Binding translation for Your email placeholder 2018-03-22 13:26:10 -05:00
b74e8cf756 Translating Invoice expired state 2018-03-22 12:57:51 -05:00
8f8266f15d Extracting complex Checkout body structure for easier navigation 2018-03-22 12:08:49 -05:00
ab8d3f5813 Extracting strings for translation 2018-03-22 12:02:55 -05:00
08220dbea5 Reorganizing file structure for i18n support 2018-03-22 11:12:15 -05:00
3b2cf2f1de Can add administrator, fix #65 2018-03-22 19:55:14 +09:00
c3beca27be Bugfix: Pressing enter no longer reloads page when providing email 2018-03-22 00:21:26 -05:00
28b820241f Integrating dropdown for language selection
So hard to find good jquery alternative that drops up
2018-03-22 00:07:30 -05:00
e985224092 Bugfixing IExplorer bug
Doesn't allow i18n without key:value format
2018-03-22 00:06:56 -05:00
f1c467aa7d Fix nodeinfo 2018-03-22 12:26:38 +09:00
ae7cfe90ab Show the NodeInfo when testing connection 2018-03-22 12:02:38 +09:00
718a36ddd0 Remove dev time stuff 2018-03-22 01:10:14 +09:00
c0b903d79c Wallet page is now an action link in the store settings 2018-03-22 01:07:11 +09:00
48eaf906b0 Update text in AddLightningNode 2018-03-21 23:51:28 +09:00
f42fde970a bump 2018-03-21 15:07:26 +09:00
59afebaa57 Translating few items and testing how it works 2018-03-20 14:30:37 -05:00
3e06e45054 Cleanup, removing outdated classes and spinner 2018-03-20 14:19:10 -05:00
fe55acb268 Reorganizing Checkout page resources and adding i18n libs 2018-03-20 13:24:11 -05:00
710dbb51f4 Remove useless code 2018-03-21 03:11:03 +09:00
d426d66819 fill exisitng values in AddDerivationScheme and AddLightningNode 2018-03-21 03:05:51 +09:00
265cddc38b Change the UX to set lightning node or derivation schemes 2018-03-21 02:48:11 +09:00
21b91ac8f7 Add btc.lightning and ltc.lightning 2018-03-21 02:09:25 +09:00
e656813844 Html cleanup, removing comments and extracting bp-spinner 2018-03-20 11:51:52 -05:00
e8730f74be update docker-compose 2018-03-21 01:10:10 +09:00
6d611d7d05 Can connect directly to CLightning via TCP or UNIX socket 2018-03-21 00:31:19 +09:00
392f3a16f1 Introduce LightningClientFactory 2018-03-20 12:10:35 +09:00
2b2e12b290 Abstract ChargeClient to prepare for support of other lightning implementation 2018-03-20 11:59:43 +09:00
73cc75fe66 Fixing display bug when invoice is paid 2018-03-19 11:16:57 +09:00
cc186fc8b3 Fix bug: Incorrect confirmation count in Invoice screen under some circumstances. 2018-03-19 09:45:54 +09:00
632ad81b94 update .net core 2018-03-19 00:44:12 +09:00
1d243910ae bump 2018-03-18 15:29:21 +09:00
e624649cd8 Merge branch 'lepipele-dev-shapeshift' 2018-03-18 15:28:42 +09:00
e6ca07e9b5 Merge remote-tracking branch 'source/master' into dev-shapeshift 2018-03-18 00:42:55 -05:00
b3d6435772 Custom text in Conversion tab in case of BTC LN payment 2018-03-18 00:41:16 -05:00
806474c8c6 Allow account selection of the ledger 2018-03-18 14:15:23 +09:00
1524fb4499 Merge remote-tracking branch 'source/master' into dev-shapeshift 2018-03-17 23:50:11 -05:00
14b70ff35e Rendering of Conversion tab on Invoice if enabled in store settings 2018-03-17 23:49:09 -05:00
c36a900627 Store setting for allowing conversion through Shapeshift 2018-03-17 23:48:06 -05:00
d3befb5b86 Fixing slider style when there are only two tabs 2018-03-17 23:36:32 -05:00
7f0ce1f802 Changing text to clarify usage 2018-03-17 23:28:39 -05:00
8342ad9175 Remove reference to browser mode of ledger 2018-03-18 12:58:14 +09:00
da77d278fb Adding Shapeshift button in new Altcoins tab 2018-03-16 23:46:39 -05:00
5e9f6f3542 Refactoring jquery logic for tab switching
Need to be more general to incorporate third tab
2018-03-16 23:15:01 -05:00
f337470f09 Adding styles for third tab - altcoin payments 2018-03-16 23:14:13 -05:00
636224d0c8 Checkout html and js cleanup 2018-03-16 22:46:30 -05:00
9e16b83202 Testing Shapeshift integration 2018-03-13 12:20:22 -05:00
109 changed files with 6750 additions and 1561 deletions

View File

@ -1,5 +1,7 @@
using BTCPayServer.Configuration;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
@ -84,7 +86,7 @@ namespace BTCPayServer.Tests
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
config.AppendLine($"internallightningnode={IntegratedLightning.AbsoluteUri}");
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (Postgres != null)
config.AppendLine($"postgres=" + Postgres);
@ -118,6 +120,7 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
((LightningLikePaymentHandler)_Host.Services.GetService(typeof(IPaymentMethodHandler<LightningSupportedPaymentMethod>))).SkipP2PTest = !InContainer;
}
public string HostName
@ -127,6 +130,7 @@ namespace BTCPayServer.Tests
}
public InvoiceRepository InvoiceRepository { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()
{

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.CLightning.RPC;
using NBitcoin;
namespace BTCPayServer.Tests

View File

@ -18,7 +18,8 @@ using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning.RPC;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
namespace BTCPayServer.Tests
{
@ -56,7 +57,9 @@ namespace BTCPayServer.Tests
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "http://127.0.0.1:30992/")), btc);
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
@ -68,6 +71,7 @@ namespace BTCPayServer.Tests
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
PayTester.Start();
}
@ -89,8 +93,10 @@ namespace BTCPayServer.Tests
{
while (true)
{
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
var channel = (await CustomerLightningD.ListPeersAsync())
.SelectMany(p => p.Channels)
.Where(c => !skippedStates.Contains(c.State ?? ""))
.FirstOrDefault();
switch (channel?.State)
{
@ -117,7 +123,7 @@ namespace BTCPayServer.Tests
}
}
private async Task<Payments.Lightning.CLightning.GetInfoResponse> WaitLNSynched()
private async Task<GetInfoResponse> WaitLNSynched()
{
while (true)
{
@ -147,6 +153,7 @@ namespace BTCPayServer.Tests
}
public CLightningRPCClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
internal string GetEnvironment(string variable, string defaultValue)

View File

@ -1,4 +1,5 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Invoices;
@ -11,6 +12,8 @@ using System.Text;
using System.Threading.Tasks;
using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Tests
{
@ -53,10 +56,12 @@ namespace BTCPayServer.Tests
public async Task<StoresController> CreateStoreAsync()
{
var store = parent.PayTester.GetController<StoresController>(UserId);
var store = parent.PayTester.GetController<UserStoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
return store;
var store2 = parent.PayTester.GetController<StoresController>(UserId);
store2.CreatedStoreId = store.CreatedStoreId;
return store2;
}
public BTCPayNetwork SupportedNetwork { get; set; }
@ -77,11 +82,9 @@ namespace BTCPayServer.Tests
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = cryptoCode,
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
});
}, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }
@ -111,20 +114,23 @@ namespace BTCPayServer.Tests
{
get; set;
}
public void RegisterLightningNode(string cryptoCode)
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
RegisterLightningNodeAsync(cryptoCode).GetAwaiter().GetResult();
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode)
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{
var storeController = parent.PayTester.GetController<StoresController>(UserId);
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = parent.MerchantCharge.Client.Uri.AbsoluteUri
}, "save");
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString())
}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
}
}
}

View File

@ -303,24 +303,95 @@ namespace BTCPayServer.Tests
user.GrantAccess();
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult());
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "test").GetAwaiter().GetResult();
}, "test", "BTC").GetAwaiter().GetResult();
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
Assert.True(storeController.ModelState.IsValid);
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "save").GetAwaiter().GetResult());
}, "save", "BTC").GetAwaiter().GetResult());
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model);
Assert.Single(storeVm.LightningNodes);
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
}
}
[Fact]
public void CanParseLightningURL()
{
LightningConnectionString conn = null;
Assert.True(LightningConnectionString.TryParse("/test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("tcp://test/a", out conn));
Assert.Equal("tcp://test/a", conn.ToString());
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", out conn));
Assert.Equal("http://aaa:bbb@test/a", conn.ToString());
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
Assert.Equal("aaa", conn.Username);
Assert.Equal("bbb", conn.Password);
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", out conn));
Assert.False(LightningConnectionString.TryParse("https://test/a", out conn));
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", out conn));
}
[Fact]
public void CanSendLightningPayment2()
{
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
tester.SendLightningPayment(invoice);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
}
}
@ -334,7 +405,7 @@ namespace BTCPayServer.Tests
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC");
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
@ -735,6 +806,66 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanParseCurrencyValue()
{
Assert.True(CurrencyValue.TryParse("1.50USD", out var result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.50 USD", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.50 usd", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1 usd", out result));
Assert.Equal("1 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1usd", out result));
Assert.Equal("1 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.501 usd", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.False(CurrencyValue.TryParse("1.501 WTFF", out result));
Assert.False(CurrencyValue.TryParse("1,501 usd", out result));
Assert.False(CurrencyValue.TryParse("1.501", out result));
}
[Fact]
public void CanParseDerivationScheme()
{
var parser = new DerivationSchemeParser(Network.TestNet, NBXplorer.ChainType.Test);
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
// Passing electrum stuff
// Native
result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
Assert.Equal("tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString());
// P2SH
result = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString());
result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", result.ToString());
////////////////
var tpub = "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
result = parser.Parse(tpub);
Assert.Equal(tpub, result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal(tpub, result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[legacy]", result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
result = parser.Parse($"{tpub}-[legacy]");
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{

View File

@ -17,14 +17,19 @@ services:
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_CUSTOMERLIGHTNINGD: http://customer_lightningd:9835/
TEST_MERCHANTLIGHTNINGD: "/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
TESTS_INCONTAINER: "true"
expose:
- "80"
links:
- dev
extra_hosts:
- "tests:127.0.0.1"
volumes:
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
@ -41,7 +46,7 @@ services:
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.22
image: nicolasdorier/nbxplorer:1.0.1.24
ports:
- "32838:32838"
expose:
@ -86,10 +91,12 @@ services:
customer_lightningd:
image: nicolasdorier/clightning
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
ipaddr=customer_lightningd
log-level=debug
ports:
- "30992:9835" # api port
@ -125,9 +132,11 @@ services:
merchant_lightningd:
image: nicolasdorier/clightning
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
ipaddr=merchant_lightningd
network=regtest
log-level=debug
ports:
@ -142,7 +151,7 @@ services:
- bitcoind
litecoind:
image: nicolasdorier/docker-litecoin:0.14.2
image: nicolasdorier/docker-litecoin:0.15.1
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83

View File

@ -0,0 +1 @@
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"

View File

@ -68,7 +68,6 @@ namespace BTCPayServer
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string CLightningNetworkName { get; internal set; }
public override string ToString()
{

View File

@ -28,10 +28,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
LightningImagePath = "imlegacy/btc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'"),
CLightningNetworkName = ChainType == ChainType.Main ? "bitcoin" :
ChainType == ChainType.Test ? "testnet" :
ChainType == ChainType.Regtest ? "regtest" : null
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
});
}
}

View File

@ -27,9 +27,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
LightningImagePath = "imlegacy/ltc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'"),
CLightningNetworkName = ChainType == ChainType.Main ? "litecoin" :
ChainType == ChainType.Test ? "litecoin-testnet" : null
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
});
}
}

View File

@ -2,18 +2,22 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.50</Version>
<Version>1.0.1.71</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Remove="Build\dockerfiles\**" />
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
<EmbeddedResource Remove="Build\dockerfiles\**" />
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
<None Remove="Build\dockerfiles\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Currencies.txt" />
@ -56,7 +60,7 @@
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\js\core.js" />
<None Include="wwwroot\js\checkout\core.js" />
<None Include="wwwroot\js\creative.js" />
<None Include="wwwroot\js\creative.min.js" />
<None Include="wwwroot\js\site.js" />

View File

@ -10,6 +10,7 @@ using System.Text;
using StandardConfiguration;
using Microsoft.Extensions.Configuration;
using NBXplorer;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Configuration
{
@ -73,6 +74,17 @@ 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)
{
if(!LightningConnectionString.TryParse(lightning, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, you need to pass either " +
$"the absolute path to the unix socket of a running CLightning instance (eg. /root/.lightning/lightning-rpc), " +
$"or the url to a charge server with crendetials (eg. https://apitoken@API_TOKEN_SECRET:charge.example.com/)");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
@ -80,10 +92,12 @@ namespace BTCPayServer.Configuration
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
InternalLightningNode = conf.GetOrDefault<Uri>("internallightningnode", null);
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if(old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
}
public Uri InternalLightningNode { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString

View File

@ -36,9 +36,9 @@ namespace BTCPayServer.Configuration
var crypto = network.CryptoCode.ToLowerInvariant();
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", 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("--internallightningnode", $"An internal lightning node which can be used without https requirement and easily configured by the admin (default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
return app;
}
@ -105,6 +105,8 @@ namespace BTCPayServer.Configuration
{
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
builder.AppendLine($"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");
builder.AppendLine($"#{n.CryptoCode}.lightning=https://apitoken:API_TOKEN_SECRET@charge.example.com/");
}
return builder.ToString();
}

View File

@ -21,6 +21,7 @@ using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Controllers
{
@ -75,8 +76,8 @@ namespace BTCPayServer.Controllers
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if(onchainMethod != null)
{
if (onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = FormatCurrency(data);
@ -96,15 +97,19 @@ namespace BTCPayServer.Controllers
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
if ( (paymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (paymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
paymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(paymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
else
{
confirmationCount = paymentData.ConfirmationCount;
}
if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
@ -112,7 +117,7 @@ namespace BTCPayServer.Controllers
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = paymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
@ -121,7 +126,7 @@ namespace BTCPayServer.Controllers
})
.ToArray();
await Task.WhenAll(payments);
model.Addresses = invoice.HistoricalAddresses.Select(h=> new InvoiceDetailsModel.AddressModel
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
{
Destination = h.GetAddress(),
PaymentMethod = ToString(h.GetPaymentMethodId()),
@ -186,7 +191,7 @@ namespace BTCPayServer.Controllers
return null;
if (!invoice.Support(paymentMethodId))
{
if(!isDefaultCrypto)
if (!isDefaultCrypto)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
@ -197,16 +202,20 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var storeBlob = store.GetStoreBlob();
var currency = invoice.ProductInformation.Currency;
var accounting = paymentMethod.Calculate();
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
PaymentMethodId = paymentMethodId.ToString(),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en-US",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
@ -221,26 +230,28 @@ namespace BTCPayServer.Controllers
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
throw new NotSupportedException(),
PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo,
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
throw new NotSupportedException(),
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + GetImage(paymentMethodId, network),
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
AllowCoinConversion = storeBlob.AllowCoinConversion,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv=> new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.Select(kv => new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.ToList()
};
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
var isMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
if (isMultiCurrency)
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
@ -382,7 +393,7 @@ namespace BTCPayServer.Controllers
{
var ago = DateTime.UtcNow - invoiceTime;
if(ago.TotalMinutes < 1)
if (ago.TotalMinutes < 1)
{
return $"{(int)ago.TotalSeconds} second{Plural((int)ago.TotalSeconds)} ago";
}
@ -412,7 +423,7 @@ namespace BTCPayServer.Controllers
if (stores.Count() == 0)
{
StatusMessage = "Error: You need to create at least one store before creating a transaction";
return RedirectToAction(nameof(StoresController.ListStores), "Stores");
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
return View(new CreateInvoiceModel() { Stores = stores });
}
@ -429,9 +440,18 @@ namespace BTCPayServer.Controllers
return View(model);
}
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
StatusMessage = null;
if (store.Role != StoreRoles.Owner)
{
StatusMessage = "Error: You need to be owner of this store to create an invoice";
}
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
}
if(StatusMessage != null)
{
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{
storeId = store.Id

View File

@ -79,33 +79,10 @@ namespace BTCPayServer.Controllers
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode),
IsAvailable: Task.FromResult(false)))
.Where(c => c.Network != null)
.Select(c =>
{
c.IsAvailable = c.Handler.IsAvailable(c.SupportedPaymentMethod, c.Network);
return c;
})
.ToList();
foreach(var supportedPaymentMethod in supportedPaymentMethods.ToList())
{
if(!await supportedPaymentMethod.IsAvailable)
{
supportedPaymentMethods.Remove(supportedPaymentMethod);
}
}
if (supportedPaymentMethods.Count == 0)
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow
};
entity.SetSupportedPaymentMethods(supportedPaymentMethods.Select(s => s.SupportedPaymentMethod));
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
@ -133,33 +110,54 @@ namespace BTCPayServer.Controllers
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
var methods = supportedPaymentMethods
.Select(async o =>
{
var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency);
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate;
var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
#pragma warning disable CS0618
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
return paymentMethod;
});
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)))
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, storeBlob)))
.ToList();
List<string> paymentMethodErrors = new List<string>();
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
var paymentMethods = new PaymentMethodDictionary();
foreach (var method in methods)
foreach (var o in supportedPaymentMethods)
{
paymentMethods.Add(await method);
try
{
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
catch (PaymentMethodUnavailableException ex)
{
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
}
catch (Exception ex)
{
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
}
}
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
foreach(var error in paymentMethodErrors)
{
errors.AppendLine(error);
}
throw new BitpayHttpException(400, errors.ToString());
}
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
#pragma warning disable CS0618
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
@ -177,15 +175,58 @@ namespace BTCPayServer.Controllers
}
#pragma warning restore CS0618
}
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreBlob storeBlob)
{
var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(entity.ProductInformation.Currency);
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
// Check if Lightning Max value is exceeded
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
storeBlob.LightningMaxValue != null)
{
var lightningMaxValue = storeBlob.LightningMaxValue;
var lightningMaxValueRate = 0.0m;
if (lightningMaxValue.Currency == entity.ProductInformation.Currency)
lightningMaxValueRate = paymentMethod.Rate;
else
lightningMaxValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(lightningMaxValue.Currency);
var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / lightningMaxValueRate);
if (paymentMethod.Calculate().Due > lightningMaxValueCrypto)
{
throw new PaymentMethodUnavailableException("Lightning max value exceeded");
}
}
///////////////
#pragma warning disable CS0618
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
return paymentMethod;
}
#pragma warning disable CS0618
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
{

View File

@ -43,6 +43,50 @@ namespace BTCPayServer.Controllers
return View(users);
}
[Route("server/users/{userId}")]
public new async Task<IActionResult> User(string userId)
{
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var userVM = new UserViewModel();
userVM.Id = user.Id;
userVM.IsAdmin = IsAdmin(roles);
return View(userVM);
}
private static bool IsAdmin(IList<string> roles)
{
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
}
[Route("server/users/{userId}")]
[HttpPost]
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
{
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var isAdmin = IsAdmin(roles);
bool updated = false;
if(isAdmin != viewModel.IsAdmin)
{
if (viewModel.IsAdmin)
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
else
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
updated = true;
}
if(updated)
{
viewModel.StatusMessage = "User successfully updated";
}
return View(viewModel);
}
[Route("server/users/{userId}/delete")]
public async Task<IActionResult> DeleteUser(string userId)

View File

@ -20,40 +20,53 @@ namespace BTCPayServer.Controllers
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
{
selectedScheme = selectedScheme ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
vm.CryptoCode = cryptoCode;
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
}
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm)
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
{
vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode;
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_ExplorerProvider, vm.CryptoCurrency);
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
return NotFound();
}
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
return NotFound();
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
@ -62,7 +75,7 @@ namespace BTCPayServer.Controllers
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
vm.DerivationScheme = strategy.ToString();
}
}
@ -73,8 +86,38 @@ namespace BTCPayServer.Controllers
return View(vm);
}
if (!vm.Confirmation && strategy != null)
return ShowAddresses(vm, strategy);
if (vm.Confirmation || strategy == null)
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
{
BitcoinAddress address = null;
try
{
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
return ShowAddresses(vm, strategy);
}
try
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
return ShowAddresses(vm, strategy);
}
vm.HintAddress = "";
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
return ShowAddresses(vm, strategy);
}
else
{
try
{
@ -92,23 +135,24 @@ namespace BTCPayServer.Controllers
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
}
}
vm.Confirmation = true;
return View(vm);
}
}
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
{
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString()));
}
}
vm.Confirmation = true;
return View(vm);
}
public class GetInfoResult
@ -129,6 +173,8 @@ namespace BTCPayServer.Controllers
string command,
// getinfo
string cryptoCode = null,
// getxpub
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
@ -204,7 +250,10 @@ namespace BTCPayServer.Controllers
}
if (command == "getxpub")
{
result = await hw.GetExtPubKey(network);
var getxpubResult = await hw.GetExtPubKey(network, account);
;
getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000);
result = getxpubResult;
}
if (command == "getinfo")
{
@ -223,13 +272,13 @@ namespace BTCPayServer.Controllers
if (command == "sendtoaddress")
{
if(!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,

View File

@ -8,38 +8,66 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning.CLightning;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
using BTCPayServer.Data;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/lightning")]
public async Task<IActionResult> AddLightningNode(string storeId, string selectedCrypto = null)
[Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
{
selectedCrypto = selectedCrypto ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
vm.CryptoCode = cryptoCode;
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
SetExistingValues(store, vm);
return View(vm);
}
[HttpPost]
[Route("{storeId}/lightning")]
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command)
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
{
if (_BtcpayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
{
return CanUseInternalLightning() ? connectionString : null;
}
return null;
}
[HttpPost]
[Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
if (network == null || network.CLightningNetworkName == null)
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
var internalLightning = GetInternalLighningNode(network.CryptoCode);
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
return View(vm);
}
@ -47,41 +75,39 @@ namespace BTCPayServer.Controllers
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.Url))
{
Uri uri;
if (!Uri.TryCreate(vm.Url, UriKind.Absolute, out uri))
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
{
ModelState.AddModelError(nameof(vm.Url), "Invalid URL");
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
return View(vm);
}
var domain = GetDomain(uri.AbsoluteUri);
if (uri.Scheme != "https" && domain != "127.0.0.1" && domain != "localhost")
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
isLocal;
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
{
var internalNode = GetInternalLightningNodeIfAuthorized();
if (internalNode == null || GetDomain(internalNode) != domain)
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
{
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
return View(vm);
}
}
if (!CanUseInternalLightning() && GetDomain(_BtcpayServerOptions.InternalLightningNode.AbsoluteUri) == GetDomain(uri.AbsoluteUri))
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
return View(vm);
}
if (string.IsNullOrEmpty(uri.UserInfo) || uri.UserInfo.Split(':').Length != 2)
{
ModelState.AddModelError(nameof(vm.Url), "The url is missing user and password");
return View(vm);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningChargeUrl(uri);
paymentMethod.SetLightningUrl(connectionString);
}
if (command == "save")
{
@ -100,36 +126,21 @@ namespace BTCPayServer.Controllers
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
await handler.Test(paymentMethod, network);
var info = await handler.Test(paymentMethod, network);
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
vm.StatusMessage = "Connection to the lightning node succeed";
}
return View(vm);
}
}
private string GetInternalLightningNodeIfAuthorized()
{
if (_BtcpayServerOptions.InternalLightningNode != null &&
CanUseInternalLightning())
{
return _BtcpayServerOptions.InternalLightningNode.AbsoluteUri;
}
return null;
}
private bool CanUseInternalLightning()
{
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
}
string GetDomain(string uri)
{
return new UriBuilder(uri).Host;
}
}
}

View File

@ -27,10 +27,11 @@ namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[Authorize(Policy = "CanAccessStore")]
[Authorize(Policy = StorePolicies.OwnStore)]
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{
public string CreatedStoreId { get; set; }
public StoresController(
NBXplorerDashboard dashboard,
IServiceProvider serviceProvider,
@ -45,12 +46,14 @@ namespace BTCPayServer.Controllers
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env)
{
_Dashboard = dashboard;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
_LangService = langService;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_Env = env;
@ -75,6 +78,7 @@ namespace BTCPayServer.Controllers
StoreRepository _Repo;
TokenRepository _TokenRepository;
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
IHostingEnvironment _Env;
[TempData]
@ -84,41 +88,15 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("create")]
public IActionResult CreateStore()
{
return View();
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id;
StatusMessage = "Store successfully created";
return RedirectToAction(nameof(ListStores));
}
public string CreatedStoreId
{
get; set;
}
[HttpGet]
[Route("{storeId}/wallet")]
public async Task<IActionResult> Wallet(string storeId)
[Route("{storeId}/wallet/{cryptoCode}")]
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(storeId);
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
model.CryptoCurrency = cryptoCode;
return View(model);
}
@ -128,76 +106,144 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
public async Task<IActionResult> ListStores()
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId)
{
StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
result.Stores.Add(new StoresViewModel.StoreViewModel()
{
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
Balances = balances[i].Select(t => t.Result).ToArray()
});
}
return View(result);
StoreUsersViewModel vm = new StoreUsersViewModel();
await FillUsers(storeId, vm);
return View(vm);
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
var users = await _Repo.GetStoreUsers(storeId);
vm.StoreId = storeId;
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
Email = u.Email,
Id = u.Id,
Role = u.Role
}).ToList();
}
[HttpPost]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
{
await FillUsers(storeId, vm);
if (!ModelState.IsValid)
{
return View(vm);
}
var user = await _UserManager.FindByEmailAsync(vm.Email);
if (user == null)
{
ModelState.AddModelError(nameof(vm.Email), "User not found");
return View(vm);
}
if (!StoreRoles.AllRoles.Contains(vm.Role))
{
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
return View(vm);
}
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
{
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
return View(vm);
}
StatusMessage = "User added successfully";
return RedirectToAction(nameof(StoreUsers));
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
[Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
StoreUsersViewModel vm = new StoreUsersViewModel();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return NotFound();
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Title = $"Remove store user",
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
Action = "Delete"
});
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
[Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
{
await _Repo.RemoveStoreUser(storeId, userId);
StatusMessage = "User removed successfully";
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
}
[HttpGet]
[Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId)
{
var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
await _Repo.RemoveStore(storeId, userId);
StatusMessage = "Store removed successfully";
return RedirectToAction(nameof(ListStores));
var storeBlob = store.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.CustomCSS = storeBlob.CustomCSS;
vm.CustomLogo = storeBlob.CustomLogo;
return View(vm);
}
[HttpPost]
[Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
{
CurrencyValue currencyValue = null;
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
{
if (!CurrencyValue.TryParse(model.LightningMaxValue, out currencyValue))
{
ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid currency value");
}
}
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
bool needUpdate = false;
var blob = store.GetStoreBlob();
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
{
needUpdate = true;
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
model.SetLanguages(_LangService, model.DefaultLang);
blob.DefaultLang = model.DefaultLang;
blob.AllowCoinConversion = model.AllowCoinConversion;
blob.LightningMaxValue = currencyValue;
blob.CustomLogo = model.CustomLogo;
blob.CustomCSS = model.CustomCSS;
if (store.SetStoreBlob(blob))
{
needUpdate = true;
}
if (needUpdate)
{
await _Repo.UpdateStore(store);
StatusMessage = "Store successfully updated";
}
return RedirectToAction(nameof(CheckoutExperience), new
{
storeId = storeId
});
}
[HttpGet]
@ -212,12 +258,10 @@ namespace BTCPayServer.Controllers
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
AddPaymentMethods(store, vm);
vm.StatusMessage = StatusMessage;
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
@ -225,27 +269,36 @@ namespace BTCPayServer.Controllers
return View(vm);
}
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
{
foreach(var strategy in store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>())
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.ToDictionary(c => c.Network.CryptoCode);
foreach (var network in _NetworkProvider.GetAll())
{
var strategy = derivationByCryptoCode.TryGet(network.CryptoCode);
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = strategy.PaymentId.CryptoCode,
Value = strategy.DerivationStrategyBase.ToString()
Crypto = network.CryptoCode,
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
});
}
foreach(var lightning in store
var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>()
.ToDictionary(c => c.CryptoCode);
foreach (var network in _NetworkProvider.GetAll())
{
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = lightning.CryptoCode,
Address = lightning.GetLightningChargeUrl(false).AbsoluteUri
CryptoCode = network.CryptoCode,
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
});
}
}
@ -282,13 +335,6 @@ namespace BTCPayServer.Controllers
store.StoreWebsite = model.StoreWebsite;
}
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
{
needUpdate = true;
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
@ -329,41 +375,11 @@ namespace BTCPayServer.Controllers
});
}
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
{
if (format == "Electrum")
{
//Unsupported Electrum
//var p2wsh_p2sh = 0x295b43fU;
//var p2wsh = 0x2aa7ed3U;
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
var standard = 0x0488b21eU;
electrumMapping.Add(standard, new[] { "legacy" });
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var data = Encoders.Base58Check.DecodeData(derivationScheme);
if (data.Length < 4)
throw new FormatException("data.Length < 4");
var prefix = Utils.ToUInt32(data, false);
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), network.NBitcoinNetwork).ToString();
foreach (var label in labels)
{
derivationScheme = derivationScheme + $"-[{label}]";
}
}
return new DerivationStrategy(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme), network);
var parser = new DerivationSchemeParser(network.NBitcoinNetwork, network.DefaultSettings.ChainType);
parser.HintScriptPubKey = hint;
return new DerivationStrategy(parser.Parse(derivationScheme), network);
}
[HttpGet]
@ -393,15 +409,17 @@ namespace BTCPayServer.Controllers
return View(model);
}
model.Label = model.Label ?? String.Empty;
if (storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url
storeId = model.StoreId ?? storeId;
var userId = GetUserId();
if (userId == null)
return Unauthorized();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return Unauthorized();
if (store.Role != StoreRoles.Owner)
{
storeId = model.StoreId;
var userId = GetUserId();
if (userId == null)
return Unauthorized();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return Unauthorized();
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
var tokenRequest = new TokenRequest()
@ -481,11 +499,13 @@ namespace BTCPayServer.Controllers
[Route("/api-access-request")]
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
{
if (pairingCode == null)
return NotFound();
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if (pairing == null)
{
StatusMessage = "Unknown pairing code";
return RedirectToAction(nameof(ListStores));
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
else
{
@ -507,7 +527,7 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("api-access-request")]
[Route("/api-access-request")]
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
{
if (pairingCode == null)
@ -517,6 +537,12 @@ namespace BTCPayServer.Controllers
if (store == null || pairing == null)
return NotFound();
if (store.Role != StoreRoles.Owner)
{
StatusMessage = "Error: You can't approve a pairing without being owner of the store";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{

View File

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[AutoValidateAntiforgeryToken]
public partial class UserStoresController : Controller
{
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
private UserManager<ApplicationUser> _UserManager;
private BTCPayWalletProvider _WalletProvider;
public UserStoresController(
UserManager<ApplicationUser> userManager,
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository)
{
_Repo = storeRepository;
_NetworkProvider = networkProvider;
_UserManager = userManager;
_WalletProvider = walletProvider;
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Action = "Delete"
});
}
[HttpGet]
[Route("create")]
public IActionResult CreateStore()
{
return View();
}
public string CreatedStoreId
{
get; set;
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
await _Repo.RemoveStore(storeId, userId);
StatusMessage = "Store removed successfully";
return RedirectToAction(nameof(ListStores));
}
[TempData]
public string StatusMessage { get; set; }
[HttpGet]
public async Task<IActionResult> ListStores()
{
StoresViewModel result = new StoresViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
result.Stores.Add(new StoresViewModel.StoreViewModel()
{
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
IsOwner = store.Role == StoreRoles.Owner,
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
});
}
return View(result);
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id;
StatusMessage = "Store successfully created";
return RedirectToAction(nameof(ListStores));
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer
{
public class CurrencyValue
{
static Regex _Regex = new Regex("^([0-9]+(\\.[0-9]+)?)\\s*([a-zA-Z]+)$");
static CurrencyNameTable _CurrencyTable = new CurrencyNameTable();
public static bool TryParse(string str, out CurrencyValue value)
{
value = null;
var match = _Regex.Match(str);
if (!match.Success ||
!decimal.TryParse(match.Groups[1].Value, out var v))
return false;
var currency = match.Groups.Last().Value.ToUpperInvariant();
var currencyData = _CurrencyTable.GetCurrencyData(currency);
if (currencyData == null)
return false;
v = Math.Round(v, currencyData.Divisibility);
value = new CurrencyValue()
{
Value = v,
Currency = currency
};
return true;
}
public decimal Value { get; set; }
public string Currency { get; set; }
public override string ToString()
{
return Value.ToString(CultureInfo.InvariantCulture) + " " + Currency;
}
}
}

View File

@ -13,6 +13,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates;
using BTCPayServer.Payments;
using BTCPayServer.JsonConverters;
namespace BTCPayServer.Data
{
@ -213,6 +214,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool AllowCoinConversion
{
get; set;
}
public string DefaultLang { get; set; }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int MonitoringExpiration
@ -249,6 +255,14 @@ namespace BTCPayServer.Data
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
public string PreferredExchange { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomLogo { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomCSS { get; set; }
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
{
if (!PreferredExchange.IsCoinAverage())

View File

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
public class DerivationSchemeParser
{
public Network Network { get; set; }
public ChainType ChainType { get; set; }
public Script HintScriptPubKey { get; set; }
public DerivationSchemeParser(Network expectedNetwork, ChainType chainType)
{
Network = expectedNetwork;
ChainType = chainType;
}
public DerivationStrategyBase Parse(string str)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
str = str.Trim();
HashSet<string> hintedLabels = new HashSet<string>();
var hintDestination = HintScriptPubKey?.GetDestination();
if (hintDestination != null)
{
if (hintDestination is KeyId)
{
hintedLabels.Add("legacy");
}
if (hintDestination is ScriptId)
{
hintedLabels.Add("p2sh");
}
}
try
{
var result = new DerivationStrategyFactory(Network).Parse(str);
return FindMatch(hintedLabels, result);
}
catch
{
}
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
var standard = 0x0488b21eU;
electrumMapping.Add(standard, new[] { "legacy" });
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var parts = str.Split('-');
for (int i = 0; i < parts.Length; i++)
{
if (IsLabel(parts[i]))
{
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
continue;
}
try
{
var data = Encoders.Base58Check.DecodeData(parts[i]);
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
electrumMapping.TryGetValue(prefix, out string[] labels);
if (labels != null)
{
foreach (var label in labels)
{
hintedLabels.Add(label.ToLowerInvariant());
}
}
parts[i] = derivationScheme;
}
catch { continue; }
}
if (hintDestination != null)
{
if (hintDestination is WitKeyId)
{
hintedLabels.Remove("legacy");
hintedLabels.Remove("p2sh");
}
}
str = string.Join('-', parts.Where(p => !IsLabel(p)));
foreach (var label in hintedLabels)
{
str = $"{str}-[{label}]";
}
return FindMatch(hintedLabels, new DerivationStrategyFactory(Network).Parse(str));
}
private DerivationStrategyBase FindMatch(HashSet<string> hintLabels, DerivationStrategyBase result)
{
var facto = new DerivationStrategyFactory(Network);
var firstKeyPath = new KeyPath("0/0");
if (HintScriptPubKey == null)
return result;
if (HintScriptPubKey == result.Derive(firstKeyPath).ScriptPubKey)
return result;
if (result is MultisigDerivationStrategy)
hintLabels.Add("keeporder");
var resultNoLabels = result.ToString();
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
foreach (var labels in ItemCombinations(hintLabels.ToList()))
{
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
return hinted;
}
throw new FormatException("Could not find any match");
}
private static bool IsLabel(string v)
{
return v.StartsWith('[') && v.EndsWith(']');
}
/// <summary>
/// Method to create lists containing possible combinations of an input list of items. This is
/// basically copied from code by user "jaolho" on this thread:
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
/// </summary>
/// <typeparam name="T">type of the items on the input list</typeparam>
/// <param name="inputList">list of items</param>
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
/// if zero the empty combination is included,
/// default is one</param>
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
/// default is no maximum limit</param>
/// <returns>list of lists for possible combinations of the input items</returns>
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
int maximumItems = int.MaxValue)
{
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
if (minimumItems == 0) // Optimize default case
listOfLists.Add(new List<T>());
for (int i = 1; i <= nonEmptyCombinations; i++)
{
List<T> thisCombination = new List<T>(inputList.Count);
for (int j = 0; j < inputList.Count; j++)
{
if ((i >> j & 1) == 1)
thisCombination.Add(inputList[j]);
}
if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems)
listOfLists.Add(thisCombination);
}
return listOfLists;
}
}
}

View File

@ -126,6 +126,7 @@ namespace BTCPayServer.Hosting
}
return dbContext;
});
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
@ -133,6 +134,7 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
@ -147,7 +149,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.ChargeListener>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
@ -172,14 +174,14 @@ namespace BTCPayServer.Hosting
services.AddAuthorization(o =>
{
o.AddPolicy("CanAccessStore", builder =>
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
});
o.AddPolicy("OwnStore", builder =>
o.AddPolicy(StorePolicies.OwnStore, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner"));
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
});
});

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
namespace BTCPayServer.JsonConverters
{
public class CurrencyValueJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
CurrencyValue.TryParse((string)reader.Value, out var result) ? result :
throw new JsonObjectException("Invalid Currency value", reader);
}
catch (InvalidCastException)
{
throw new JsonObjectException("Invalid Currency value", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if(value != null)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
namespace BTCPayServer.JsonConverters
{
public class UriJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Uri).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
Uri.TryCreate((string)reader.Value, UriKind.Absolute, out var result) ? result :
throw new JsonObjectException("Invalid Currency value", reader);
}
catch (InvalidCastException)
{
throw new JsonObjectException("Invalid Currency value", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(((Uri)value).AbsoluteUri);
}
}
}

View File

@ -13,7 +13,11 @@ namespace BTCPayServer.Models.InvoicingModels
public string CryptoImage { get; set; }
public string Link { get; set; }
}
public string CustomCSSLink { get; set; }
public string CustomLogoLink { get; set; }
public string DefaultLang { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }
@ -43,5 +47,8 @@ namespace BTCPayServer.Models.InvoicingModels
public int MaxTimeMinutes { get; internal set; }
public string PaymentType { get; internal set; }
public string PaymentMethodId { get; internal set; }
public bool AllowCoinConversion { get; set; }
public string PeerInfo { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class UserViewModel
{
public string Id { get; set; }
public string Email { get; set; }
[Display(Name = "Is admin")]
public bool IsAdmin { get; set; }
public string StatusMessage { get; set; }
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class CheckoutExperienceViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public SelectList CryptoCurrencies { get; set; }
public SelectList Languages { get; set; }
[Display(Name = "Default crypto currency on checkout")]
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; }
[Display(Name = "Link to a custom CSS stylesheet")]
[Url]
public Uri CustomCSS { get; set; }
[Display(Name = "Link to a custom logo")]
[Url]
public Uri CustomLogo { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
}
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = defaultLang ?? "en-US";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultLang = chosen.Value;
}
}
}

View File

@ -9,20 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class DerivationSchemeViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public DerivationSchemeViewModel()
{
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
DerivationSchemeFormat = btcPay.Value;
DerivationSchemeFormats = new SelectList(new Format[]
{
btcPay,
new Format { Name = "Electrum", Value = "Electrum" },
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
}
public string DerivationScheme
{
@ -34,33 +22,12 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
} = new List<(string KeyPath, string Address)>();
[Display(Name = "Derivation Scheme format")]
public string DerivationSchemeFormat
{
get;
set;
}
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
public string CryptoCode { get; set; }
[Display(Name = "Hint address")]
public string HintAddress { get; set; }
public bool Confirmation { get; set; }
public SelectList CryptoCurrencies { get; set; }
public SelectList DerivationSchemeFormats { get; set; }
public string ServerUrl { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
public string StatusMessage { get; internal set; }
}
}

View File

@ -9,11 +9,6 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class LightningNodeViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
[Display(Name = "Lightning charge url")]
public string Url
{
@ -21,24 +16,12 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[Display(Name = "Crypto currency")]
public string CryptoCurrency
public string CryptoCode
{
get;
set;
}
public SelectList CryptoCurrencies { get; set; }
public string StatusMessage { get; set; }
public string InternalLightningNode { get; internal set; }
public void SetCryptoCurrencies(BTCPayNetworkProvider networkProvider, string selectedScheme)
{
var choices = networkProvider.GetAll()
.Where(n => n.CLightningNetworkName != null)
.Select(o => new Format() { Name = o.CryptoCode, Value = o.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.StoreViewModels
{
public class StoreUsersViewModel
{
public class StoreUserViewModel
{
public string Email { get; set; }
public string Role { get; set; }
public string Id { get; set; }
}
public StoreUsersViewModel()
{
Role = StoreRoles.Guest;
}
[Required]
[EmailAddress]
public string Email { get; set; }
public string StoreId { get; set; }
public string Role { get; set; }
public List<StoreUserViewModel> Users { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
@ -16,11 +17,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string Crypto { get; set; }
public string Value { get; set; }
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public StoreViewModel()
{
@ -94,15 +91,6 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
public string StatusMessage
{
get; set;
}
public SelectList CryptoCurrencies { get; set; }
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
public class LightningNode
{
public string CryptoCode { get; set; }
@ -113,12 +101,5 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
} = new List<LightningNode>();
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == defaultCrypto) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
}
}
}

View File

@ -8,14 +8,11 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class StoresViewModel
{
public string StatusMessage
{
get; set;
}
public List<StoreViewModel> Stores
{
get; set;
} = new List<StoreViewModel>();
public class StoreViewModel
{
public string Name
@ -32,6 +29,11 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
}
public bool IsOwner
{
get;
set;
}
public string[] Balances
{
get; set;

View File

@ -10,25 +10,10 @@ namespace BTCPayServer.Models.StoreViewModels
public class WalletModel
{
public string ServerUrl { get; set; }
public SelectList CryptoCurrencies { get; set; }
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

View File

@ -29,6 +29,8 @@ namespace BTCPayServer.Payments.Bitcoin
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
{
if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available");
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
@ -37,10 +39,5 @@ namespace BTCPayServer.Payments.Bitcoin
onchainMethod.DepositAddress = (await getAddress).ToString();
return onchainMethod;
}
public override Task<bool> IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network)
{
return Task.FromResult(_ExplorerProvider.IsAvailable(network));
}
}
}

View File

@ -11,14 +11,6 @@ namespace BTCPayServer.Payments
/// </summary>
public interface IPaymentMethodHandler
{
/// <summary>
/// Returns true if the dependencies for a specific payment method are satisfied.
/// </summary>
/// <param name="supportedPaymentMethod"></param>
/// <param name="network"></param>
/// <returns>true if this payment method is available</returns>
Task<bool> IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network);
/// <summary>
/// Create needed to track payments of this invoice
/// </summary>
@ -31,7 +23,6 @@ namespace BTCPayServer.Payments
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{
Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
}
@ -47,16 +38,5 @@ namespace BTCPayServer.Payments
}
throw new NotSupportedException("Invalid supportedPaymentMethod");
}
public abstract Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
Task<bool> IPaymentMethodHandler.IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if(supportedPaymentMethod is T method)
{
return IsAvailable(method, network);
}
return Task.FromResult(false);
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class CLightningInvoice
{
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
[JsonProperty("payment_hash")]
public uint256 PaymentHash { get; set; }
[JsonProperty("msatoshi")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney MilliSatoshi { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
[JsonProperty("expiry_time")]
public DateTimeOffset ExpiryTime { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
[JsonProperty("expires_at")]
public DateTimeOffset ExpiryAt { get; set; }
[JsonProperty("bolt11")]
public string BOLT11 { get; set; }
[JsonProperty("pay_index")]
public int? PayIndex { get; set; }
public string Label { get; set; }
public string Status { get; set; }
[JsonProperty("paid_at")]
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? PaidAt { get; set; }
}
}

View File

@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Mono.Unix;
using NBitcoin;
using NBitcoin.RPC;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class LightningRPCException : Exception
{
public LightningRPCException(string message) : base(message)
{
}
}
public class CLightningRPCClient : ILightningInvoiceClient, ILightningListenInvoiceSession
{
public Network Network { get; private set; }
public Uri Address { get; private set; }
public CLightningRPCClient(Uri address, Network network)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
if(address.Scheme == "file")
{
address = new UriBuilder(address) { Scheme = "unix" }.Uri;
}
Address = address;
Network = network;
}
public Task<Charge.GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
{
return SendCommandAsync<Charge.GetInfoResponse>("getinfo", cancellation: cancellation);
}
public Task SendAsync(string bolt11)
{
return SendCommandAsync<object>("pay", new[] { bolt11 }, true);
}
public async Task<PeerInfo[]> ListPeersAsync()
{
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
foreach (var peer in peers)
{
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
}
return peers;
}
public Task FundChannelAsync(NodeInfo nodeInfo, Money money)
{
return SendCommandAsync<object>("fundchannel", new object[] { nodeInfo.NodeId, money.Satoshi }, true);
}
public Task ConnectAsync(NodeInfo nodeInfo)
{
return SendCommandAsync<object>("connect", new[] { $"{nodeInfo.NodeId}@{nodeInfo.Host}:{nodeInfo.Port}" }, true);
}
static Encoding UTF8 = new UTF8Encoding(false);
private async Task<T> SendCommandAsync<T>(string command, object[] parameters = null, bool noReturn = false, bool isArray = false, CancellationToken cancellation = default(CancellationToken))
{
parameters = parameters ?? Array.Empty<string>();
using (Socket socket = await Connect())
{
using (var networkStream = new NetworkStream(socket))
{
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
{
using (var jsonWriter = new JsonTextWriter(textWriter))
{
var req = new JObject();
req.Add("id", 0);
req.Add("method", command);
req.Add("params", new JArray(parameters));
await req.WriteToAsync(jsonWriter, cancellation);
await jsonWriter.FlushAsync(cancellation);
}
await textWriter.FlushAsync();
}
await networkStream.FlushAsync(cancellation);
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
{
using (var jsonReader = new JsonTextReader(textReader))
{
var resultAsync = JObject.LoadAsync(jsonReader, cancellation);
// without this hack resultAsync is blocking even if cancellation happen
using (cancellation.Register(() => { socket.Dispose(); }))
{
var result = await resultAsync;
var error = result.Property("error");
if (error != null)
{
throw new LightningRPCException(error.Value["message"].Value<string>());
}
if (noReturn)
return default(T);
if (isArray)
{
return result["result"].Children().First().Children().First().ToObject<T>();
}
return result["result"].ToObject<T>();
}
}
}
}
}
}
private async Task<Socket> Connect()
{
Socket socket = null;
EndPoint endpoint = null;
if (Address.Scheme == "tcp" || Address.Scheme == "tcp")
{
var domain = Address.DnsSafeHost;
if (!IPAddress.TryParse(domain, out IPAddress address))
{
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
if (address == null)
throw new Exception("Host not found");
}
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
endpoint = new IPEndPoint(address, Address.Port);
}
else if (Address.Scheme == "unix")
{
var path = Address.AbsoluteUri.Remove(0, "unix:".Length);
if (!path.StartsWith('/'))
path = "/" + path;
while (path.Length >= 2 && (path[0] != '/' || path[1] == '/'))
{
path = path.Remove(0, 1);
}
if (path.Length < 2)
throw new FormatException("Invalid unix url");
socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
endpoint = new UnixEndPoint(path);
}
else
throw new NotSupportedException($"Protocol {Address.Scheme} for clightning not supported");
await socket.ConnectAsync(endpoint);
return socket;
}
public async Task<BitcoinAddress> NewAddressAsync()
{
var obj = await SendCommandAsync<JObject>("newaddr");
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
}
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoices = await SendCommandAsync<CLightningInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
if (invoices.Length == 0)
return null;
return ToLightningInvoice(invoices[0]);
}
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
{
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
invoice.Label = id;
invoice.MilliSatoshi = amount;
invoice.Status = "unpaid";
return ToLightningInvoice(invoice);
}
private static LightningInvoice ToLightningInvoice(CLightningInvoice invoice)
{
return new LightningInvoice()
{
Id = invoice.Label,
Amount = invoice.MilliSatoshi,
BOLT11 = invoice.BOLT11,
Status = invoice.Status,
PaidAt = invoice.PaidAt
};
}
Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
{
return Task.FromResult<ILightningListenInvoiceSession>(this);
}
long lastInvoiceIndex = 99999999999;
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation)
{
var invoice = await SendCommandAsync<CLightningInvoice>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
lastInvoiceIndex = invoice.PayIndex.Value;
return ToLightningInvoice(invoice);
}
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
{
var info = await GetInfoAsync(cancellation);
var address = info.Address.Select(a => a.Address).FirstOrDefault();
var port = info.Port;
return new LightningNodeInformation()
{
NodeId = info.Id,
P2PPort = port,
Address = address,
BlockHeight = info.BlockHeight
};
}
void IDisposable.Dispose()
{
}
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class NodeInfo
{
@ -20,5 +20,10 @@ namespace BTCPayServer.Payments.Lightning.CLightning.RPC
public string NodeId { get; private set; }
public string Host { get; private set; }
public int Port { get; private set; }
public override string ToString()
{
return $"{NodeId}@{Host}:{Port}";
}
}
}

View File

@ -5,7 +5,7 @@ using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class ChannelInfo
{

View File

@ -1,118 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.RPC;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
{
public class CLightningRPCClient
{
public Network Network { get; private set; }
public Uri Address { get; private set; }
public CLightningRPCClient(Uri address, Network network)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
Address = address;
Network = network;
}
public Task<GetInfoResponse> GetInfoAsync()
{
return SendCommandAsync<GetInfoResponse>("getinfo");
}
public Task SendAsync(string bolt11)
{
return SendCommandAsync<object>("pay", new[] { bolt11 }, true);
}
public async Task<PeerInfo[]> ListPeersAsync()
{
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
foreach(var peer in peers)
{
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
}
return peers;
}
public Task FundChannelAsync(NodeInfo nodeInfo, Money money)
{
return SendCommandAsync<object>("fundchannel", new object[] { nodeInfo.NodeId, money.Satoshi }, true);
}
public Task ConnectAsync(NodeInfo nodeInfo)
{
return SendCommandAsync<object>("connect", new[] { $"{nodeInfo.NodeId}@{nodeInfo.Host}:{nodeInfo.Port}" }, true);
}
static Encoding UTF8 = new UTF8Encoding(false);
private async Task<T> SendCommandAsync<T>(string command, object[] parameters = null, bool noReturn = false, bool isArray = false)
{
parameters = parameters ?? Array.Empty<string>();
var domain = Address.DnsSafeHost;
if (!IPAddress.TryParse(domain, out IPAddress address))
{
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
if (address == null)
throw new Exception("Host not found");
}
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(new IPEndPoint(address, Address.Port));
using (var networkStream = new NetworkStream(socket))
{
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
{
using (var jsonWriter = new JsonTextWriter(textWriter))
{
var req = new JObject();
req.Add("id", 0);
req.Add("method", command);
req.Add("params", new JArray(parameters));
await req.WriteToAsync(jsonWriter);
await jsonWriter.FlushAsync();
}
await textWriter.FlushAsync();
}
await networkStream.FlushAsync();
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
{
using (var jsonReader = new JsonTextReader(textReader))
{
var result = await JObject.LoadAsync(jsonReader);
var error = result.Property("error");
if(error != null)
{
throw new Exception(error.Value.ToString());
}
if (noReturn)
return default(T);
if (isArray)
{
return result["result"].Children().First().Children().First().ToObject<T>();
}
return result["result"].ToObject<T>();
}
}
}
}
public async Task<BitcoinAddress> NewAddressAsync()
{
var obj = await SendCommandAsync<JObject>("newaddr");
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
}
}
}

View File

@ -0,0 +1,140 @@
//
// Mono.Unix.UnixEndPoint: EndPoint derived class for AF_UNIX family sockets.
//
// Authors:
// Gonzalo Paniagua Javier (gonzalo@ximian.com)
//
// (C) 2003 Ximian, Inc (http://www.ximian.com)
//
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Mono.Unix
{
[Serializable]
public class UnixEndPoint : EndPoint
{
string filename;
public UnixEndPoint(string filename)
{
if (filename == null)
throw new ArgumentNullException("filename");
if (filename.Length == 0)
throw new ArgumentException("Cannot be empty.", "filename");
this.filename = filename;
}
public string Filename
{
get
{
return (filename);
}
set
{
filename = value;
}
}
public override AddressFamily AddressFamily
{
get { return AddressFamily.Unix; }
}
public override EndPoint Create(SocketAddress socketAddress)
{
/*
* Should also check this
*
int addr = (int) AddressFamily.Unix;
if (socketAddress [0] != (addr & 0xFF))
throw new ArgumentException ("socketAddress is not a unix socket address.");
if (socketAddress [1] != ((addr & 0xFF00) >> 8))
throw new ArgumentException ("socketAddress is not a unix socket address.");
*/
if (socketAddress.Size == 2)
{
// Empty filename.
// Probably from RemoteEndPoint which on linux does not return the file name.
UnixEndPoint uep = new UnixEndPoint("a");
uep.filename = "";
return uep;
}
int size = socketAddress.Size - 2;
byte[] bytes = new byte[size];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = socketAddress[i + 2];
// There may be junk after the null terminator, so ignore it all.
if (bytes[i] == 0)
{
size = i;
break;
}
}
string name = Encoding.Default.GetString(bytes, 0, size);
return new UnixEndPoint(name);
}
public override SocketAddress Serialize()
{
byte[] bytes = Encoding.Default.GetBytes(filename);
SocketAddress sa = new SocketAddress(AddressFamily, 2 + bytes.Length + 1);
// sa [0] -> family low byte, sa [1] -> family high byte
for (int i = 0; i < bytes.Length; i++)
sa[2 + i] = bytes[i];
//NULL suffix for non-abstract path
sa[2 + bytes.Length] = 0;
return sa;
}
public override string ToString()
{
return (filename);
}
public override int GetHashCode()
{
return filename.GetHashCode(StringComparison.Ordinal);
}
public override bool Equals(object o)
{
UnixEndPoint other = o as UnixEndPoint;
if (other == null)
return false;
return (other.filename == filename);
}
}
}

View File

@ -13,9 +13,9 @@ using NBitcoin;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
namespace BTCPayServer.Payments.Lightning.Charge
{
public class ChargeClient
public class ChargeClient : ILightningInvoiceClient
{
private Uri _Uri;
public Uri Uri
@ -126,5 +126,48 @@ namespace BTCPayServer.Payments.Lightning.CLightning
uri += "/";
return new Uri(uri + partialUrl);
}
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoice = await GetInvoice(invoiceId, cancellation);
return ChargeClient.ToLightningInvoice(invoice);
}
async Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
{
return await Listen(cancellation);
}
internal static LightningInvoice ToLightningInvoice(ChargeInvoice invoice)
{
return new LightningInvoice()
{
Id = invoice.Id ?? invoice.Label,
Amount = invoice.MilliSatoshi,
BOLT11 = invoice.PaymentRequest,
PaidAt = invoice.PaidAt,
Status = invoice.Status
};
}
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
{
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amont = amount, Expiry = expiry });
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
}
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
{
var info = await GetInfoAsync(cancellation);
var address = info.Address.Select(a => a.Address).FirstOrDefault();
var port = info.Port;
return new LightningNodeInformation()
{
NodeId = info.Id,
P2PPort = port,
Address = address,
BlockHeight = info.BlockHeight
};
}
}
}

View File

@ -8,7 +8,7 @@ using System.Threading.Tasks;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
namespace BTCPayServer.Payments.Lightning.Charge
{
public class ChargeInvoice
{
@ -27,8 +27,9 @@ namespace BTCPayServer.Payments.Lightning.CLightning
[JsonProperty("payreq")]
public string PaymentRequest { get; set; }
public string Label { get; set; }
}
public class ChargeSession : IDisposable
public class ChargeSession : ILightningListenInvoiceSession
{
private ClientWebSocket socket;
@ -42,7 +43,7 @@ namespace BTCPayServer.Payments.Lightning.CLightning
}
ArraySegment<byte> _Buffer;
public async Task<ChargeInvoice> NextEvent(CancellationToken cancellation = default(CancellationToken))
public async Task<ChargeInvoice> WaitInvoice(CancellationToken cancellation = default(CancellationToken))
{
var buffer = _Buffer;
var array = _Buffer.Array;
@ -120,5 +121,15 @@ namespace BTCPayServer.Payments.Lightning.CLightning
{
await this.socket.CloseSocket();
}
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken token)
{
return ChargeClient.ToLightningInvoice(await WaitInvoice(token));
}
void IDisposable.Dispose()
{
Dispose();
}
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
namespace BTCPayServer.Payments.Lightning.Charge
{
public class CreateInvoiceRequest
{

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
namespace BTCPayServer.Payments.Lightning.Charge
{
public class CreateInvoiceResponse
{

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
namespace BTCPayServer.Payments.Lightning.Charge
{
//[{"type":"ipv4","address":"52.166.90.122","port":9735}]
public class GetInfoResponse

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
namespace BTCPayServer.Payments.Lightning
{
public class LightningInvoice
{
public string Id { get; set; }
public string Status { get; set; }
public string BOLT11 { get; set; }
public DateTimeOffset? PaidAt
{
get; set;
}
public LightMoney Amount { get; set; }
}
public class LightningNodeInformation
{
public string NodeId { get; set; }
public string Address { get; internal set; }
public int P2PPort { get; internal set; }
public int BlockHeight { get; set; }
}
public interface ILightningInvoiceClient
{
Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken));
Task<LightningInvoice> CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
}
public interface ILightningListenInvoiceSession : IDisposable
{
Task<LightningInvoice> WaitInvoice(CancellationToken cancellation);
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
namespace BTCPayServer.Payments.Lightning
{
public class LightningClientFactory
{
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
var uri = supportedPaymentMethod.GetLightningUrl();
if (uri.ConnectionType == LightningConnectionType.Charge)
{
return new ChargeClient(uri.ToUri(true), network.NBitcoinNetwork);
}
else if (uri.ConnectionType == LightningConnectionType.CLightning)
{
return new CLightningRPCClient(uri.ToUri(false), network.NBitcoinNetwork);
}
else
throw new NotSupportedException($"Unsupported connection string for lightning server ({uri.ConnectionType})");
}
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning
{
public enum LightningConnectionType
{
Charge,
CLightning
}
public class LightningConnectionString
{
public static bool TryParse(string str, out LightningConnectionString connectionString)
{
return TryParse(str, out connectionString, out var error);
}
public static bool TryParse(string str, out LightningConnectionString connectionString, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
if (str.StartsWith('/'))
str = "unix:" + str;
var result = new LightningConnectionString();
connectionString = null;
error = null;
Uri uri;
if (!System.Uri.TryCreate(str, UriKind.Absolute, out uri))
{
error = "Invalid URL";
return false;
}
var supportedDomains = new string[] { "unix", "tcp", "http", "https" };
if (!supportedDomains.Contains(uri.Scheme))
{
var protocols = String.Join(",", supportedDomains);
error = $"The url support the following protocols {protocols}";
return false;
}
if (uri.Scheme == "unix")
{
str = uri.AbsoluteUri.Substring("unix:".Length);
while (str.Length >= 1 && str[0] == '/')
{
str = str.Substring(1);
}
uri = new Uri("unix://" + str, UriKind.Absolute);
}
if (uri.Scheme == "http" || uri.Scheme == "https")
{
var parts = uri.UserInfo.Split(':');
if (string.IsNullOrEmpty(uri.UserInfo) || parts.Length != 2)
{
error = "The url is missing user and password";
return false;
}
result.Username = parts[0];
result.Password = parts[1];
}
else if (!string.IsNullOrEmpty(uri.UserInfo))
{
error = "The url should not have user information";
return false;
}
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
connectionString = result;
return true;
}
public LightningConnectionString()
{
}
public string Username { get; set; }
public string Password { get; set; }
public Uri BaseUri { get; set; }
public LightningConnectionType ConnectionType
{
get
{
return BaseUri.Scheme == "http" || BaseUri.Scheme == "https" ? LightningConnectionType.Charge
: LightningConnectionType.CLightning;
}
}
public Uri ToUri(bool withCredentials)
{
if (withCredentials)
{
return new UriBuilder(BaseUri) { UserName = Username ?? "", Password = Password ?? "" }.Uri;
}
else
{
return BaseUri;
}
}
public override string ToString()
{
return ToUri(true).AbsoluteUri;
}
}
}

View File

@ -6,6 +6,7 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Services.Invoices;
@ -14,87 +15,94 @@ namespace BTCPayServer.Payments.Lightning
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
{
NBXplorerDashboard _Dashboard;
public LightningLikePaymentHandler(NBXplorerDashboard dashboard)
LightningClientFactory _LightningClientFactory;
public LightningLikePaymentHandler(
LightningClientFactory lightningClientFactory,
NBXplorerDashboard dashboard)
{
_LightningClientFactory = lightningClientFactory;
_Dashboard = dashboard;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
{
var test = Test(supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = GetClient(supportedPaymentMethod, network);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
var lightningInvoice = await client.CreateInvoiceAsync(new CreateInvoiceRequest()
if (expiry < TimeSpan.Zero)
expiry = TimeSpan.FromSeconds(1);
LightningInvoice lightningInvoice = null;
try
{
Amont = new LightMoney(due, LightMoneyUnit.BTC),
Expiry = expiry < TimeSpan.Zero ? TimeSpan.FromSeconds(1) : expiry
});
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry);
}
catch(Exception ex)
{
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
}
var nodeInfo = await test;
return new LightningLikePaymentMethodDetails()
{
BOLT11 = lightningInvoice.PayReq,
InvoiceId = lightningInvoice.Id
BOLT11 = lightningInvoice.BOLT11,
InvoiceId = lightningInvoice.Id,
NodeInfo = nodeInfo.ToString()
};
}
public async override Task<bool> IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
try
{
await Test(supportedPaymentMethod, network);
return true;
}
catch { return false; }
}
/// <summary>
/// Used for testing
/// </summary>
public bool SkipP2PTest { get; set; }
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"Full node not available");
throw new PaymentMethodUnavailableException($"Full node not available");
var cts = new CancellationTokenSource(5000);
var client = GetClient(supportedPaymentMethod, network);
GetInfoResponse info = null;
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
try
{
info = await client.GetInfoAsync(cts.Token);
info = await client.GetInfo(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the lightning charge {client.Uri} ({ex.Message})");
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
var address = info.Address.Select(a=>a.Address).FirstOrDefault();
var port = info.Port;
address = address ?? client.Uri.DnsSafeHost;
if (info.Network != network.CLightningNetworkName)
if (info.Address == null)
{
throw new Exception($"Lightning node network {info.Network}, but expected is {network.CLightningNetworkName}");
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
throw new Exception($"The lightning is not synched ({blocksGap} blocks)");
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
try
{
await TestConnection(address, port, cts.Token);
if (!SkipP2PTest)
{
await TestConnection(info.Address, info.P2PPort, cts.Token);
}
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the lightning node via {address}:{port} ({ex.Message})");
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
}
private static ChargeClient GetClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
}
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation)
private async Task TestConnection(string addressStr, int port, CancellationToken cancellation)
{
IPAddress address = null;
try
@ -103,25 +111,16 @@ namespace BTCPayServer.Payments.Lightning
}
catch
{
try
{
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
}
catch { }
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
}
if (address == null)
throw new Exception($"DNS did not resolved {addressStr}");
throw new PaymentMethodUnavailableException($"DNS did not resolved {addressStr}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
try
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
}
catch { return false; }
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
}
return true;
}
static Task WithTimeout(Task task, CancellationToken token)

View File

@ -9,6 +9,7 @@ namespace BTCPayServer.Payments.Lightning
{
public string BOLT11 { get; set; }
public string InvoiceId { get; set; }
public string NodeInfo { get; set; }
public string GetPaymentDestination()
{

View File

@ -6,14 +6,13 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Hosting;
using NBXplorer;
namespace BTCPayServer.Payments.Lightning
{
public class ChargeListener : IHostedService
public class LightningListener : IHostedService
{
class ListenedInvoice
{
@ -28,13 +27,16 @@ namespace BTCPayServer.Payments.Lightning
EventAggregator _Aggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
public ChargeListener(EventAggregator aggregator,
LightningClientFactory _LightningClientFactory;
public LightningListener(EventAggregator aggregator,
InvoiceRepository invoiceRepository,
LightningClientFactory lightningClientFactory,
BTCPayNetworkProvider networkProvider)
{
_Aggregator = aggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_LightningClientFactory = lightningClientFactory;
}
CompositeDisposable leases = new CompositeDisposable();
@ -77,7 +79,7 @@ namespace BTCPayServer.Payments.Lightning
var listenedInvoice = new ListenedInvoice()
{
Uri = lightningSupportedMethod.GetLightningChargeUrl(false).AbsoluteUri,
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
PaymentMethodDetails = lightningMethod,
SupportedPaymentMethod = lightningSupportedMethod,
PaymentMethod = paymentMethod,
@ -87,7 +89,7 @@ namespace BTCPayServer.Payments.Lightning
if (poll)
{
var charge = GetChargeClient(lightningSupportedMethod, network);
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
if (chargeInvoice == null)
continue;
@ -123,18 +125,17 @@ namespace BTCPayServer.Payments.Lightning
{
try
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
var charge = GetChargeClient(supportedPaymentMethod, network);
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
var charge = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var session = await charge.Listen(_Cts.Token);
while (true)
{
var notification = await session.NextEvent(_Cts.Token);
var notification = await session.WaitInvoice(_Cts.Token);
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
if (listenedInvoice == null)
continue;
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
notification.PaymentRequest == listenedInvoice.PaymentMethodDetails.BOLT11)
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
{
if (notification.Status == "paid" && notification.PaidAt.HasValue)
{
@ -155,28 +156,23 @@ namespace BTCPayServer.Payments.Lightning
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
DoneListening(supportedPaymentMethod.GetLightningChargeUrl(false));
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
DoneListening(supportedPaymentMethod.GetLightningUrl());
}
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
}
private async Task AddPayment(BTCPayNetwork network, ChargeInvoice notification, ListenedInvoice listenedInvoice)
private async Task AddPayment(BTCPayNetwork network, LightningInvoice notification, ListenedInvoice listenedInvoice)
{
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
{
BOLT11 = notification.PaymentRequest,
Amount = notification.MilliSatoshi
BOLT11 = notification.BOLT11,
Amount = notification.Amount
}, network.CryptoCode, accounted: true);
if(payment != null)
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
}
private static ChargeClient GetChargeClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
}
List<Task> _ListeningLightning = new List<Task>();
MultiValueDictionary<string, ListenedInvoice> _ListenedInvoiceByLightningUrl = new MultiValueDictionary<string, ListenedInvoice>();
Dictionary<string, ListenedInvoice> _ListenedInvoiceByChargeInvoiceId = new Dictionary<string, ListenedInvoice>();
@ -207,8 +203,9 @@ namespace BTCPayServer.Payments.Lightning
/// Stop listening all invoices on this server
/// </summary>
/// <param name="uri"></param>
private void DoneListening(Uri uri)
private void DoneListening(LightningConnectionString connectionString)
{
var uri = connectionString.BaseUri;
lock (_ListenedInvoiceByChargeInvoiceId)
{
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])

View File

@ -8,41 +8,36 @@ namespace BTCPayServer.Payments.Lightning
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
{
public string CryptoCode { get; set; }
[Obsolete("Use Get/SetLightningChargeUrl")]
[Obsolete("Use Get/SetLightningUrl")]
public string LightningChargeUrl { get; set; }
public Uri GetLightningChargeUrl(bool withCredentials)
public LightningConnectionString GetLightningUrl()
{
#pragma warning disable CS0618 // Type or member is obsolete
UriBuilder uri = new UriBuilder(LightningChargeUrl);
if (withCredentials)
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
if(!LightningConnectionString.TryParse(fullUri, out var connectionString, out var error))
{
uri.UserName = Username;
uri.Password = Password;
throw new FormatException(error);
}
return connectionString;
#pragma warning restore CS0618 // Type or member is obsolete
return uri.Uri;
}
public void SetLightningChargeUrl(Uri uri)
public void SetLightningUrl(LightningConnectionString connectionString)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
if (string.IsNullOrEmpty(uri.UserInfo))
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
var splitted = uri.UserInfo.Split(':');
if (splitted.Length != 2)
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
#pragma warning disable CS0618 // Type or member is obsolete
Username = splitted[0];
Password = splitted[1];
LightningChargeUrl = new UriBuilder(uri) { UserName = "", Password = "" }.Uri.AbsoluteUri;
Username = connectionString.Username;
Password = connectionString.Password;
LightningChargeUrl = connectionString.BaseUri.AbsoluteUri;
#pragma warning restore CS0618 // Type or member is obsolete
}
[Obsolete("Use Get/SetLightningChargeUrl")]
[Obsolete("Use Get/SetLightningUrl")]
public string Username { get; set; }
[Obsolete("Use Get/SetLightningChargeUrl")]
[Obsolete("Use Get/SetLightningUrl")]
public string Password { get; set; }
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments
{
public class PaymentMethodUnavailableException : Exception
{
public PaymentMethodUnavailableException(string message) : base(message)
{
}
public PaymentMethodUnavailableException(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -8,11 +8,12 @@
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_INTERNALLIGHTNINGNODE": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
"BTCPAY_BTCLIGHTNING": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_BUNDLEJSCSS": "false"
},
"applicationUrl": "http://localhost:14142/"
"applicationUrl": "http://127.0.0.1:14142/"
}
}
}

View File

@ -8,6 +8,7 @@ using BTCPayServer.Services.Wallets;
using LedgerWallet;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
namespace BTCPayServer.Services
{
@ -76,18 +77,19 @@ namespace BTCPayServer.Services
return new LedgerTestResult() { Success = true };
}
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network)
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
var pubkey = await GetExtPubKey(_Ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true), false);
var path = new KeyPath("49'").Derive(network.CoinType).Derive(account, true);
var pubkey = await GetExtPubKey(_Ledger, network, path, false);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = true,
Legacy = false
});
return new GetXPubResult() { ExtPubKey = derivation.ToString() };
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
@ -231,5 +233,8 @@ namespace BTCPayServer.Services
public class GetXPubResult
{
public string ExtPubKey { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public int CoinType { get; internal set; }
}
}

View File

@ -242,7 +242,7 @@ namespace BTCPayServer.Services.Invoices
#pragma warning disable CS0618
public List<PaymentEntity> GetPayments()
{
return Payments.ToList();
return Payments?.ToList() ?? new List<PaymentEntity>();
}
public List<PaymentEntity> GetPayments(string cryptoCode)
{
@ -375,7 +375,8 @@ namespace BTCPayServer.Services.Invoices
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
};
}
if (info.GetId().PaymentType == PaymentTypes.LightningLike)
var paymentId = info.GetId();
if (paymentId.PaymentType == PaymentTypes.LightningLike)
{
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
@ -383,7 +384,7 @@ namespace BTCPayServer.Services.Invoices
};
}
#pragma warning disable CS0618
if (info.CryptoCode == "BTC")
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
{
dto.Url = cryptoInfo.Url;
dto.BTCPrice = cryptoInfo.Price;

View File

@ -101,7 +101,7 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, IEnumerable<string> creationLogs, BTCPayNetworkProvider networkProvider)
{
List<string> textSearch = new List<string>();
invoice = Clone(invoice, null);
@ -146,6 +146,17 @@ namespace BTCPayServer.Services.Invoices
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
}
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
foreach(var log in creationLogs)
{
context.InvoiceEvents.Add(new InvoiceEventData()
{
InvoiceDataId = invoice.Id,
Message = log,
Timestamp = invoice.InvoiceTime,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
}
await context.SaveChangesAsync().ConfigureAwait(false);
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public class Language
{
public Language(string code, string displayName)
{
DisplayName = displayName;
Code = code;
}
public string Code { get; set; }
public string DisplayName { get; set; }
}
public class LanguageService
{
public Language[] GetLanguages()
{
return new[]
{
new Language("en-US", "English"),
new Language("de-DE", "Deutsch"),
new Language("ja-JP", "日本語"),
new Language("fr-FR", "Français"),
new Language("es-ES", "Spanish"),
new Language("pt-BR", "Portuguese (Brazil)"),
new Language("nl-NL", "Dutch"),
new Language("cs-CZ", "Česky"),
new Language("is-IS", "Íslenska"),
};
}
}
}

View File

@ -41,6 +41,8 @@ namespace BTCPayServer.Services.Rates
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
{
if (currency == "BTC")
return 1.0m;
if (rates.TryGetValue(currency, out decimal result))
return result;
throw new RateUnavailableException(currency);

View File

@ -48,14 +48,72 @@ namespace BTCPayServer.Services.Stores
}
}
public class StoreUser
{
public string Id { get; set; }
public string Email { get; set; }
public string Role { get; set; }
}
public async Task<StoreUser[]> GetStoreUsers(string storeId)
{
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx
.UserStore
.Where(u => u.StoreDataId == storeId)
.Select(u => new StoreUser()
{
Id = u.ApplicationUserId,
Email = u.ApplicationUser.Email,
Role = u.Role
}).ToArrayAsync();
}
}
public async Task<StoreData[]> GetStoresByUserId(string userId)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
return (await ctx.UserStore
.Where(u => u.ApplicationUserId == userId)
.Select(u => u.StoreData)
.ToArrayAsync();
.Select(u => new { u.StoreData, u.Role })
.ToArrayAsync())
.Select(u =>
{
u.StoreData.Role = u.Role;
return u.StoreData;
}).ToArray();
}
}
public async Task<bool> AddStoreUser(string storeId, string userId, string role)
{
using (var ctx = _ContextFactory.CreateContext())
{
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId, Role = role };
ctx.UserStore.Add(userStore);
try
{
await ctx.SaveChangesAsync();
return true;
}
catch (DbUpdateException)
{
return false;
}
}
}
public async Task RemoveStoreUser(string storeId, string userId)
{
using (var ctx = _ContextFactory.CreateContext())
{
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId };
ctx.UserStore.Add(userStore);
ctx.Entry<UserStore>(userStore).State = EntityState.Deleted;
await ctx.SaveChangesAsync();
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class StorePolicies
{
public const string CanAccessStores = "CanAccessStore";
public const string OwnStore = "OwnStore";
}
public class StoreRoles
{
public const string Owner = "Owner";
public const string Guest = "Guest";
public static IEnumerable<String> AllRoles
{
get
{
yield return Owner;
yield return Guest;
}
}
}
}

View File

@ -0,0 +1,570 @@
@model PaymentModel
<div class="top-header">
<div class="header">
<div class="header__icon">
@if (Model.CustomLogoLink != null)
{
<img class="header__icon__img" src="@Model.CustomLogoLink" height="40">
}
else
{
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
}
</div>
</div>
<div class="timer-row">
<div class="timer-row__progress-bar" style="width: 0%;"></div>
<div class="timer-row__spinner">
@Html.Partial("Checkout-Spinner")
</div>
<div class="timer-row__message">
<span v-if="srvModel.status === 'expired' || srvModel.status === 'invalid'">
{{$t("Invoice expired")}}
</span>
<span v-else-if="expiringSoon">
{{$t("Invoice expiring soon...")}}
</span>
<span v-else>
{{$t("Awaiting Payment...")}}
</span>
</div>
<div class="timer-row__time-left">@Model.TimeLeft</div>
</div>
</div>
<div class="order-details">
@if (Model.AvailableCryptos.Count > 1)
{
<div class="currency-selection">
<div class="single-item-order__left">
<div style="font-weight: 600;">
{{$t("Pay with")}}
</div>
</div>
<div class="single-item-order__right">
<div class="payment__currencies">
@foreach (var crypto in Model.AvailableCryptos)
{
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
<img style="height:32px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" />
</a>
}
</div>
<div class="payment__spinner">
@Html.Partial("Checkout-Spinner")
</div>
</div>
</div>
}
<div class="single-item-order buyerTotalLine">
<div class="single-item-order__left">
<div class="single-item-order__left__name">
{{ srvModel.storeName }}
</div>
<div class="single-item-order__left__description">
{{ srvModel.itemDesc }}
</div>
</div>
<div class="single-item-order__right">
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
</div>
<div class="single-item-order__right__ex-rate">
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
</div>
</div>
<span class="fa fa-angle-double-down"></span>
<span class="fa fa-angle-double-up"></span>
</div>
<line-items>
<div class="line-items">
<div class="line-items__item">
<div class="line-items__item__label">{{$t("Order Amount")}}</div>
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span>{{$t("Network Cost")}}</span>
</div>
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span>{{$t("Already Paid")}}</span>
</div>
<div class="line-items__item__value">-{{srvModel.btcPaid }} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item line-items__item--total">
<div class="line-items__item__label">{{$t("Due")}}</div>
<div class="line-items__item__value">{{srvModel.btcDue}} {{ srvModel.cryptoCode }}</div>
</div>
</div>
</line-items>
<div class="payment-tabs">
<div class="payment-tabs__tab active" id="scan-tab">
<span>{{$t("Scan")}}</span>
</div>
<div class="payment-tabs__tab" id="copy-tab">
<span>{{$t("Copy")}}</span>
</div>
@if (Model.AllowCoinConversion)
{
<div class="payment-tabs__tab" id="altcoins-tab">
<span>{{$t("Conversion")}}</span>
</div>
<div id="tabsSlider" class="payment-tabs__slider three-tabs"></div>
}
else
{
<div id="tabsSlider" class="payment-tabs__slider"></div>
}
</div>
</div>
<div adjust-height="" class="payment-box">
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
<div class="manual__step-one__header">
<span>{{$t("Contact and Refund Email")}}</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<span>{{$t("Contact_Body")}}</span>
</span>
<span class="submission-error-label">{{$t("Please enter a valid email address")}}</span>
</div>
<div class="input-wrapper">
<input class="bp-input email-input ng-pristine ng-invalid ng-touched" id="emailAddressFormInput" v-bind:placeholder="$t('Your email')" type="email">
<bp-loading-button>
<button type="submit" class="action-button" style="margin-top: 15px;">
<span class="button-text">{{$t("Continue")}}</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
</div>
</button>
</bp-loading-button>
</div>
</form>
</div>
<div class="bp-view payment scan" id="scan">
<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>
</div>
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span>{{$t("Open in wallet")}}</span>
<span class="glyphicon glyphicon-new-window"></span>
</a>
</div>
</div>
<div class="bp-view payment manual-flow" id="copy">
<div class="manual__step-two__instructions">
<span i18n="">{{$t("CompletePay_Body", srvModel)}}</span>
</div>
<div class="copyLabelPopup">
<span>{{$t("Copied")}}</span>
</div>
<nav v-if="srvModel.isLightning" class="copyBox">
<div class="copySectionBox bottomBorder">
<label>{{$t("BOLT 11 Invoice")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly" />
<img v-bind:src="srvModel.cryptoImage" />
</div>
</div>
<div class="separatorGem"></div>
<div class="copySectionBox">
<label>{{$t("Peer Info")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.peerInfo" readonly="readonly" />
<img v-bind:src="srvModel.cryptoImage" />
</div>
</div>
</nav>
<nav v-else class="copyBox">
<div class="copySectionBox bottomBorder">
<label>{{$t("Amount")}}</label>
<div class="copyAmountText copy-cursor _copySpan">
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
</div>
</div>
<div class="separatorGem"></div>
<div class="copySectionBox">
<label>{{$t("Address")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly" />
<img v-bind:src="srvModel.cryptoImage" />
</div>
</div>
</nav>
</div>
@if (Model.AllowCoinConversion)
{
<div id="altcoins" class="bp-view payment manual-flow">
<nav v-if="srvModel.isLightning">
<div class="manual__step-two__instructions">
<span>
{{$t("ConversionTab_Lightning")}}
</span>
</div>
</nav>
<nav v-else>
<div class="manual__step-two__instructions">
<span>
{{$t("ConversionTab_BodyTop", srvModel)}}
<br /><br />
{{$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>
</nav>
</div>
}
<div class="bp-view pad" id="paid">
<div class="status-block">
<div class="success-block">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline"></div>
</div>
</div>
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<button class="action-button">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</button>
</div>
</div>
<div class="button-wrapper refund-address-form-container" id="refund-overpayment-button">
</div>
</div>
<div class="bp-view expired" id="archived">
<div class="expired-icon">
<img src="~/imlegacy/archived.svg">
</div>
<div class="archived__message">
<div class="archived__message__header">
<span>{{$t("This invoice has been archived")}}</span>
</div>
<div>
<span>{{$t("Archived_Body")}}</span>
</div>
</div>
</div>
<div class="bp-view expired" id="expired">
<div>
<div class="expired__body">
<div class="expired__header">{{$t("What happened?")}}</div>
<div class="expired__text" i18n="">
{{$t("InvoiceExpired_Body_1", {storeName: srvModel.storeName, maxTimeMinutes: @Model.MaxTimeMinutes})}}
</div>
<div class="expired__text">
{{$t("InvoiceExpired_Body_2")}}
</div>
<div class="expired__text">
{{$t("InvoiceExpired_Body_3")}}
</div>
<div class="expired__text expired__text__smaller">
<span class="expired__text__bullet">{{$t("Invoice ID")}}</span>:
{{srvModel.invoiceId}}
<br />
<span class="expired__text__bullet">{{$t("Order ID")}}</span>:
{{srvModel.orderId}}
</div>
</div>
<a href="/invoices" class="action-button" style="margin-top: 20px;">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</a>
</div>
</div>
@* Obsolete? Start *@
<div class="bp-view" id="link-expired" style="padding-top: 3.6rem;">
<div class="manual__step-one refund-address-form" novalidate="">
<div class="manual__step-one__header" i18n="">Link Expired</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">Sorry, this link has expired. Please try requesting another refund by clicking the button below.</span>
</div>
<div class="input-wrapper">
<bp-loading-button i18n="">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
</div>
</button>
</bp-loading-button>
</div>
</div>
</div>
<div class="bp-view confirm-contact-email-view">
<form class="manual__step-one refund-address-form contact-email-form ng-untouched ng-pristine" novalidate="">
<div class="manual__step-one__header" i18n="">Contact &amp; Refund Email</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">If there is an issue with this payment, or a refund needs to be made, we will contact you at this address.</span>
</div>
<div class="input-wrapper">
<input bp-focus="focusEmailInput" class="bp-input email-input ng-untouched ng-pristine" disabled="disabled" name="receiptEmail" placeholder="Your email"
style="opacity: 1;" type="email">
<button type="submit" class="action-button" style="margin-top: 15px;">
<span i18n="">Confirm email address</span>
</button>
</div>
<div class="refund-address-form__link" id="wrong-email-button" style="color: #a9a9a9;">
<span i18n="">Wrong email?</span>
</div>
</form>
</div>
<div class="bp-view wrong-email-view" id="compromised-invoice">
<div class="manual__step-one refund-address-form" novalidate="" style="margin-top: -1rem;">
<div class="manual__step-one__header">
<span i18n="">There seems to be a problem</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">
This invoice was previously opened, and the address <strong class="placeholder-refundEmail">{Entered email address}</strong> was submitted as your contact email. If you entered this email, you can still safely make your payment. <br> <br>
If you did not submit the email address, it's possible a thief falsely
submitted this address to steal refunds. Please contact the merchant
about this security incident, and try your payment again.
</span>
</div>
<div class="input-wrapper">
<a class="action-button" style="margin-top: 15px;" target="_blank" href="mailto:@Model.StoreEmail">
<span i18n="">Contact {{srvModel.storeName}}</span>
</a>
</div>
<div class="refund-address-form__link">
<span i18n="">I understand, continue to payment&nbsp;→</span>
</div>
</div>
</div>
<div class="bp-view confirm-bitcoin-address-view" id="confirm-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-valid" novalidate="" style="padding-top: 1.6rem;">
<div><img src="~/imlegacy/mail.svg"></div>
<div class="manual__step-one__header">
<span i18n="">Please confirm your address</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">You should receive an email from us in a moment at <strong class="placeholder-refundEmail">{enterd refund email}</strong>. To ensure your refund is sent to the correct address, please confirm your bitcoin address by clicking the link in the email. </span>
</div>
<bp-resend-link id="resend-link">
<div class="bp-resend__link">
<span class="link-text">
<span i18n="">Resend email</span>
</span>
<div class="success-text">
<img src="~/imlegacy/circle-check.svg">
<div i18n="">Email resent</div>
</div>
</div>
</bp-resend-link>
</form>
</div>
<div class="bp-view refund-address-view" id="enter-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-invalid" name="refundAddressForm" novalidate="" style="margin-top: 28px; margin-bottom: 4rem;">
<div class="manual__step-one__header">
<span i18n="">Please provide a refund address.</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<span i18n="">
To send your refund of {BTC to refund} BTC,
well need a bitcoin address from your wallet. Please open your bitcoin
wallet, copy a receiving address, and paste it below.
</span>
</span>
<span class="submission-error-label" i18n="" id="invalid-bitcoin-address">Please enter a valid bitcoin address.</span>
</div>
<div class="input-wrapper">
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
<div class="bp-refund-address">
<div class="bitcoin-logo">
<div><img src="@Model.CryptoImage"></div>
</div>
<input class="bp-input {'not-empty': addressValue.length &gt; 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length &gt; 0}">
</div>
</bp-refund-address>
<bp-loading-button i18n="" id="request-refund-button">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
</div>
</button>
</bp-loading-button>
</div>
<div class="refund-address-form__cancel">
<span i18n="">Cancel</span>
</div>
</form>
</div>
<div class="bp-view" id="refund-pending">
<div class="status-block">
<div class="pending-block" style="position: relative; padding-bottom: 1.6rem;">
<img src="~/imlegacy/refund-pending.svg">
<div class="pending-block__header" i18n="">Processing Refund</div>
<span>
<span class="pending-block__message" i18n="">The amount below will be refunded to you within 1-2 business days. </span>
</span>
</div>
</div>
<div class="manual-box" style="margin-bottom: 30px;">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label">&nbsp;</span>
<span class="final-label" i18n="">Amount To Be Refunded</span>
</div>
<div class="manual-box__amount__value">{BTC Amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Will Be Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bp-view expired" id="low-fee">
<div class="expired__body">
<div class="expired__header" i18n="" style="font-weight: 400; font-size: 22px;">Payment Confirming</div>
<div class="expired__text" i18n="">This payment was made with a low <a href="https://bitcoin.org/en/glossary/transaction-fee">bitcoin miner fee</a>, which may prevent it from being accepted by the Bitcoin network.</div>
<div class="expired__text" i18n="">This is an issue with the configuration of your bitcoin wallet.</div>
<div class="expired__text" i18n="">
If the transaction
doesn't confirm, the funds will be spendable again in your wallet.
Depending on the wallet, this may take 48-72 hours.
</div>
<low-fee-timeline>
<div class="timeline">
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--complete">
<img src="~/imlegacy/checkmark-small.svg">
</div>
<div class="timeline__item__name" i18n="">Transaction created</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--pending">
<img src="~/imlegacy/pending.svg">
</div>
<div class="timeline__item__name">
<span i18n="">Transaction confirming — funds have not yet moved</span>
</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon"></div>
<div class="timeline__item__name" i18n="">Payment received by {{srvModel.storeName}}</div>
</div>
</div>
</low-fee-timeline>
</div>
<button class="action-button" style="margin-top: .75rem;">
<bp-done-text>
<span i18n="">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
<div class="bp-view" id="refund-complete">
<div class="status-block">
<div class="success-block" style="opacity: 1;">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline" style="height: 117px; width: 117px;"></div>
</div>
</div>
</div>
<div class="success-message">
<span>
<span i18n="">Refund Complete</span>
</span>
</div>
</div>
</div>
<div class="manual-box">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label" i18n="">Overpaid By</span>
<span class="final-label">
<span i18n="">Amount Refunded</span>
</span>
</div>
<div class="manual-box__amount__value">{BTC amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper flipped-initially">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
<button class="action-button finished" style="margin-top: 23px;">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</button>
</div>
<div class="footer-button enter-different-address-button">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</div>
@* Obsolete? End *@
</div>

View File

@ -0,0 +1,5 @@
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>

View File

@ -1,8 +1,8 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.Services.LanguageService langService
@model PaymentModel
@{
Layout = null;
ViewData["Title"] = "Payment";
}
<!DOCTYPE html>
<html>
@ -20,6 +20,12 @@
</script>
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
@if(Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
</head>
<body style="background: #E4E4E4">
<noscript>
@ -46,589 +52,84 @@
<invoice>
<div class="no-bounce" id="checkoutCtrl">
@*<div class="modal-backdrop fade-in"></div>*@
<!---->
<div class="modal page">
<div class="modal-dialog open opened" role="document">
<div class="modal-dialog open opened enter-purchaser-email" role="document">
<div class="modal-content long">
<div class="content">
<div class="invoice">
<div class="top-header">
<div class="header">
<div class="header__icon">
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
</div>
<!---->
</div>
<div class="timer-row">
<div class="timer-row__progress-bar" style="width: 0%;"></div>
<!---->
<div class="timer-row__spinner">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
<div class="timer-row__message">
<!---->
<span>
<!---->
<span i18n="">Awaiting Payment...</span>
<!---->
</span>
<!---->
</div>
<div class="timer-row__time-left">@Model.TimeLeft</div>
</div>
</div>
<div class="order-details">
@if (Model.AvailableCryptos.Count > 1)
{
<div class="currency-selection">
<div class="single-item-order__left">
<div style="font-weight: 600;">
Pay with
</div>
</div>
<div class="single-item-order__right">
<div class="payment__currencies">
@foreach (var crypto in Model.AvailableCryptos)
{
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
<img style="height:32px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" />
</a>
}
</div>
<div class="payment__spinner">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</div>
</div>
}
<!---->
<div class="single-item-order buyerTotalLine">
<div class="single-item-order__left">
<div class="single-item-order__left__name">
{{ srvModel.storeName }}
</div>
<div class="single-item-order__left__description">
{{ srvModel.itemDesc }}
</div>
</div>
<!---->
<div class="single-item-order__right">
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
</div>
<!---->
<div class="single-item-order__right__ex-rate">
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
</div>
<!---->
</div>
<span class="fa fa-angle-double-down"></span>
<span class="fa fa-angle-double-up"></span>
</div>
<!---->
<line-items>
<div class="line-items">
<!---->
<div class="line-items__item">
<div class="line-items__item__label" i18n="">Order Amount</div>
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span i18n="">Network Cost</span>
</div>
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span i18n="">Already Paid</span>
</div>
<div class="line-items__item__value" i18n="">-{{srvModel.btcPaid }} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item line-items__item--total">
<div class="line-items__item__label" i18n="">Due </div>
<div class="line-items__item__value">{{srvModel.btcDue}} {{ srvModel.cryptoCode }}</div>
</div>
<!---->
</div>
</line-items>
<div class="payment-tabs">
<div class="payment-tabs__tab active" id="scan-tab">
<span i18n="">Scan</span>
</div>
<div class="payment-tabs__tab" id="copy-tab">
<span i18n="">Copy</span>
</div>
<div class="payment-tabs__slider"></div>
</div>
</div>
<div adjust-height="" class="payment-box">
<div class="bp-view payment scan" id="scan">
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
<qrcode v-bind:val="srvModel.invoiceBitcoinUrlQR" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
</qrcode>
</div>
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span i18n="">Open in wallet</span>
<span class="glyphicon glyphicon-new-window"></span>
</a>
</div>
</div>
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
<div class="manual__step-one__header">
<!---->
<span i18n="">Contact &amp; Refund Email</span>
<!---->
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<!---->
<span i18n="">Please provide an email address below. Well contact you at this address if there is an issue with your payment. </span>
<!---->
</span>
<span class="submission-error-label" i18n="">Please enter a valid email address.</span>
</div>
<div class="input-wrapper">
<input class="bp-input email-input ng-pristine ng-invalid ng-touched" id="emailAddressFormInput" placeholder="Your email" type="email">
<bp-loading-button i18n="">
<button class="action-button" style="margin-top: 15px;" type="button">
<span class="button-text" lcl="">Continue</span>
<div class="loader-wrapper">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</button>
</bp-loading-button>
</div>
</form>
</div>
<div class="bp-view" id="link-expired" style="padding-top: 3.6rem;">
<div class="manual__step-one refund-address-form" novalidate="">
<div class="manual__step-one__header" i18n="">Link Expired</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">Sorry, this link has expired. Please try requesting another refund by clicking the button below.</span>
</div>
<div class="input-wrapper">
<bp-loading-button i18n="">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</button>
</bp-loading-button>
</div>
</div>
</div>
<div class="bp-view confirm-contact-email-view">
<form class="manual__step-one refund-address-form contact-email-form ng-untouched ng-pristine" novalidate="">
<div class="manual__step-one__header" i18n="">Contact &amp; Refund Email</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">If there is an issue with this payment, or a refund needs to be made, we will contact you at this address.</span>
</div>
<div class="input-wrapper">
<input bp-focus="focusEmailInput" class="bp-input email-input ng-untouched ng-pristine" disabled="disabled" name="receiptEmail" placeholder="Your email" style="opacity: 1;" type="email">
<button class="action-button" style="margin-top: 15px;">
<span i18n="">Confirm email address</span>
</button>
</div>
<div class="refund-address-form__link" id="wrong-email-button" style="color: #a9a9a9;">
<span i18n="">Wrong email?</span>
</div>
</form>
</div>
<div class="bp-view wrong-email-view" id="compromised-invoice">
<div class="manual__step-one refund-address-form" novalidate="" style="margin-top: -1rem;">
<div class="manual__step-one__header">
<span i18n="">There seems to be a problem</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">
This invoice was previously opened, and the address <strong class="placeholder-refundEmail">{Entered email address}</strong> was submitted as your contact email. If you entered this email, you can still safely make your payment. <br> <br>
If you did not submit the email address, it's possible a thief falsely
submitted this address to steal refunds. Please contact the merchant
about this security incident, and try your payment again.
</span>
</div>
<div class="input-wrapper">
<a class="action-button" style="margin-top: 15px;" target="_blank" href="mailto:@Model.StoreEmail">
<span i18n="">Contact {{srvModel.storeName}}</span>
</a>
</div>
<div class="refund-address-form__link">
<span i18n="">I understand, continue to payment&nbsp;→</span>
</div>
</div>
</div>
<div class="bp-view confirm-bitcoin-address-view" id="confirm-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-valid" novalidate="" style="padding-top: 1.6rem;">
<div><img src="~/imlegacy/mail.svg"></div>
<div class="manual__step-one__header">
<span i18n="">Please confirm your address</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">You should receive an email from us in a moment at <strong class="placeholder-refundEmail">{enterd refund email}</strong>. To ensure your refund is sent to the correct address, please confirm your bitcoin address by clicking the link in the email. </span>
</div>
<bp-resend-link id="resend-link">
<div class="bp-resend__link">
<span class="link-text">
<!---->
<span i18n="">Resend email</span>
<!---->
</span>
<div class="success-text">
<!---->
<img src="~/imlegacy/circle-check.svg">
<!---->
<div i18n="">Email resent</div>
<!---->
</div>
</div>
</bp-resend-link>
</form>
</div>
<div class="bp-view refund-address-view" id="enter-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-invalid" name="refundAddressForm" novalidate="" style="margin-top: 28px; margin-bottom: 4rem;">
<div class="manual__step-one__header">
<!---->
<span i18n="">Please provide a refund address.</span>
<!---->
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<!---->
<span i18n="">
To send your refund of {BTC to refund} BTC,
well need a bitcoin address from your wallet. Please open your bitcoin
wallet, copy a receiving address, and paste it below.
</span>
<!---->
</span>
<span class="submission-error-label" i18n="" id="invalid-bitcoin-address">Please enter a valid bitcoin address.</span>
</div>
<div class="input-wrapper">
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
<div class="bp-refund-address">
<div class="bitcoin-logo">
<div><img src="@Model.CryptoImage"></div>
</div>
<input class="bp-input {'not-empty': addressValue.length &gt; 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length &gt; 0}">
</div>
</bp-refund-address>
<bp-loading-button i18n="" id="request-refund-button">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</button>
</bp-loading-button>
</div>
<div class="refund-address-form__cancel">
<span i18n="">Cancel</span>
</div>
</form>
</div>
<div class="bp-view payment manual-flow" id="copy">
<div class="manual__step-two__instructions">
<span i18n="">To complete your payment, please send {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} to the address below.</span>
</div>
<div class="manual-box flipped" style="margin-bottom: 30px;">
<div class="manual-box__amount">
<div class="manual-box__amount__label label" i18n="">Amount</div>
<!---->
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
<div class="copied-label">
<span i18n="">Copied</span>
</div>
</div>
<!---->
</div>
<div class="manual-box__address">
<div class="flipper flipped-initially">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Address</div>
<!---->
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img :src="srvModel.cryptoImage" height="16" />
</div>
<div class="manual-box__address__wrapper__value" style="overflow:hidden;max-width:240px;">{{srvModel.btcAddress}}</div>
</div>
<div class="copied-label" style="top: 5px;">
<span i18n="">Copied</span>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
<div class="bp-view pad" id="paid">
<div class="status-block">
<div class="success-block">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline"></div>
</div>
</div>
</div>
<!---->
<div class="success-message" i18n="">This invoice has been paid.</div>
<!---->
<button class="action-button" style="margin-top: 0px;">
<bp-done-text>
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
</div>
<!---->
<div class="button-wrapper refund-address-form-container" id="refund-overpayment-button">
<!---->
</div>
<!---->
</div>
<div class="bp-view" id="refund-pending">
<div class="status-block">
<div class="pending-block" style="position: relative; padding-bottom: 1.6rem;">
<img src="~/imlegacy/refund-pending.svg">
<div class="pending-block__header" i18n="">Processing Refund</div>
<span>
<!---->
<!---->
<!---->
<span class="pending-block__message" i18n="">The amount below will be refunded to you within 1-2 business days. </span>
</span>
</div>
</div>
<div class="manual-box" style="margin-bottom: 30px;">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label">&nbsp;</span>
<span class="final-label" i18n="">Amount To Be Refunded</span>
</div>
<div class="manual-box__amount__value">{BTC Amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Will Be Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
<!---->
</div>
<div class="bp-view expired" id="low-fee">
<div class="expired__body">
<div class="expired__header" i18n="" style="font-weight: 400; font-size: 22px;">Payment Confirming</div>
<div class="expired__text" i18n="">This payment was made with a low <a href="https://bitcoin.org/en/glossary/transaction-fee">bitcoin miner fee</a>, which may prevent it from being accepted by the Bitcoin network.</div>
<div class="expired__text" i18n="">This is an issue with the configuration of your bitcoin wallet.</div>
<div class="expired__text" i18n="">
If the transaction
doesn't confirm, the funds will be spendable again in your wallet.
Depending on the wallet, this may take 48-72 hours.
</div>
<low-fee-timeline>
<div class="timeline">
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--complete">
<img src="~/imlegacy/checkmark-small.svg">
</div>
<div class="timeline__item__name" i18n="">Transaction created</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--pending">
<img src="~/imlegacy/pending.svg">
</div>
<div class="timeline__item__name">
<span i18n="">Transaction confirming — funds have not yet moved</span>
</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon"></div>
<div class="timeline__item__name" i18n="">Payment received by {{srvModel.storeName}}</div>
</div>
</div>
</low-fee-timeline>
</div>
<!---->
<!---->
<button class="action-button" style="margin-top: .75rem;">
<bp-done-text>
<!---->
<!---->
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
<div class="bp-view expired" id="expired">
<!---->
<div>
<div class="expired__body">
<div class="expired__header" i18n="">What happened?</div>
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for @Model.MaxTimeMinutes minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
<div class="expired__text" i18n="">If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.</div>
<div class="expired__text" i18n="">
If the transaction
is not accepted by the Bitcoin network, the funds will be spendable
again in your wallet. Depending on your wallet, this may take 48-72
hours.
</div>
<div class="expired__text">
<span class="expired__text__bullet" i18n="">Invoice ID:</span> {{srvModel.invoiceId}}<br>
<!---->
<span>
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.orderId}}
</span>
</div>
</div>
<a href="/invoices" class="action-button" style="margin-top: 20px;">
<bp-done-text>
<!---->
<!---->
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</a>
</div>
<!---->
<!---->
</div>
<div class="bp-view expired" id="archived">
<div class="expired-icon">
<img src="~/imlegacy/archived.svg">
</div>
<div class="archived__message">
<div class="archived__message__header">
<span i18n="">This invoice has been archived.</span>
</div>
<div>
<span i18n="">Please contact the store for order information or assistance.</span>
</div>
</div>
</div>
<!---->
<div class="bp-view" id="refund-complete">
<div class="status-block">
<div class="success-block" style="opacity: 1;">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline" style="height: 117px; width: 117px;"></div>
</div>
</div>
</div>
<div class="success-message">
<!---->
<span>
<span i18n="">Refund Complete</span>
</span>
<!---->
</div>
</div>
</div>
<div class="manual-box">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label" i18n="">Overpaid By</span>
<!---->
<span class="final-label">
<span i18n="">Amount Refunded</span>
</span>
<!---->
</div>
<div class="manual-box__amount__value">{BTC amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper flipped-initially">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
<button class="action-button finished" style="margin-top: 23px;">
<bp-done-text>
<!---->
<!---->
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
<!---->
<!---->
<div class="footer-button enter-different-address-button">
<bp-done-text>
<!---->
<!---->
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</div>
</div>
@Html.Partial("Checkout-Body")
</div>
</div>
</div>
<div style="margin-top: 10px; text-align: right;">
@* Not working because of nsSeparator: false, keySeparator: false,
{{$t("nested.lang")}} >>
*@
<select class="cmblang reverse invisible" onchange="changeLanguage($(this).val())">
@foreach(var lang in langService.GetLanguages())
{
<option value="@lang.Code">@lang.DisplayName</option>
}
</select>
<script>
$(function () {
var storeDefaultLang = '@Model.DefaultLang';
if (urlParams.lang) {
$(".cmblang").val(urlParams.lang);
} else if (storeDefaultLang) {
$(".cmblang").val(storeDefaultLang);
}
$('select').prettyDropdown({
classic: false,
height: 30,
reverse: true,
hoverIntent: 5000
});
});
</script>
</div>
<div style="margin-top: 10px; text-align: right;" class="form-text small text-muted">
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
</div>
</div>
</div>
</div>
</invoice>
<script type="text/javascript">
var storeDefaultLang = '@Model.DefaultLang';
// initialization
i18next.init({
lng: storeDefaultLang,
fallbackLng: 'en-US',
nsSeparator: false,
keySeparator: false,
resources: {
'en-US': { translation: locales_en },
'de-DE': { translation: locales_de },
'es-ES': { translation: locales_es },
'ja-JP': { translation: locales_ja },
'fr-FR': { translation: locales_fr },
'pt-BR': { translation: locales_pt_br },
'nl': { translation: locales_nl },
'cs-CZ': { translation: locales_cs },
'is-IS': { translation: locales_is }
},
});
function changeLanguage(lang) {
i18next.changeLanguage(lang);
}
if (urlParams.lang) {
changeLanguage(urlParams.lang);
}
else if (storeDefaultLang) {
changeLanguage(storeDefaultLang);
}
const i18n = new VueI18next(i18next);
// TODO: Move all logic from core.js to Vue controller
Vue.config.ignoredElements = [
'line-items',
@ -637,12 +138,14 @@
/^bp-/
];
var checkoutCtrl = new Vue({
i18n: i18n,
el: '#checkoutCtrl',
components: {
qrcode: VueQr
},
data: {
srvModel: srvModel
srvModel: srvModel,
expiringSoon: false
}
});
</script>

View File

@ -23,7 +23,8 @@
<tr>
<td>@user.Name</td>
<td>@user.Email</td>
<td><a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
</tr>}
<td><a asp-action="User" asp-route-userId="@user.Id">Modify</a> <span> - </span> <a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
</tr>
}
</tbody>
</table>

View File

@ -0,0 +1,22 @@
@model UserViewModel
@{
ViewData["Title"] = Model.Email;
ViewData.AddActivePage(ServerNavPages.Users);
}
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<div class="row">
<div class="col-md-8">
<form method="post">
<div class="form-group">
<label asp-for="IsAdmin"></label>
<input asp-for="IsAdmin" type="checkbox" class="form-check" />
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
</form>
</div>
</div>

View File

@ -54,7 +54,7 @@
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>

View File

@ -5,6 +5,7 @@
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
}
@Html.Partial("_StatusMessage", Model.StatusMessage)
<h4>@ViewData["Title"]</h4>
<div class="row">
@ -16,101 +17,107 @@
<div class="col-md-8">
<form method="post">
@if(!Model.Confirmation)
{
<div class="form-group">
<h5>Derivation Scheme</h5>
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
</div>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="DerivationScheme"></label>
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
No ledger wallet detected. If you own one, use chrome, open the app, activate the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a>, and refresh this page.
</p>
<p id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, please use our <a id="ledger-info-recommended" href="#">recommended choice</a></span>
</p>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
</tr>
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-info">Continue</button>
}
else
{
<div class="form-group">
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
</div>
<input type="hidden" asp-for="CryptoCurrency" />
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="DerivationSchemeFormat" />
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-success">Confirm</button>
}
<div class="form-group">
<h5>Derivation Scheme</h5>
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
</div>
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
<div class="form-group">
<label asp-for="DerivationScheme"></label>
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page.
</p>
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use?</span>
<ul>
@for(int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (49'/<span class="ledger-info-cointype">0</span>'/@i')</a></li>
}
</ul>
</div>
</div>
<div class="form-group">
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
</tr>
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-info">Continue</button>
}
else
{
<div class="form-group">
<h5>Confirm the addresses (@Model.CryptoCode)</h5>
<span>Please check that your @Model.CryptoCode wallet is generating the same addresses as below.</span>
</div>
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
</div>
<div class="form-group">
<h5>Wrong addresses?</h5>
<span>Help us to find the correct settings by telling us the first address of your wallet</span>
</div>
<div class="form-group">
<label asp-for="HintAddress"></label>
<input asp-for="HintAddress" class="form-control" />
<span asp-validation-for="HintAddress" class="text-danger"></span>
</div>
<button name="command" type="submit" class="btn btn-success">Confirm</button>
}
</form>
</div>
</div>

View File

@ -16,14 +16,14 @@
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>
<span>A connection to a lightning charge node is required to generate lignting network enabled invoices. <br /></span>
<span>A connection to a lightning charge node or clightning unix socket is required to generate lignting network enabled invoices. <br /></span>
<span>This is experimental and not advised for production so keep in mind:</span>
</p>
<ul>
<li>You might lose your money</li>
<li>The devs of BTCPay Server don't know what they are doing and won't be able to help you if shit hit the fan</li>
<li>You approve being #reckless and being the sole responsible party for your loss</li>
<li>BTCPay Server relies on a <a href="https://github.com/ElementsProject/lightning-charge">Lightning Charge</a> node</li>
<li>BTCPay Server relies on a <a href="https://github.com/ElementsProject/lightning-charge">Lightning Charge</a> node or CLightning unix socket</li>
<li>If you have no idea what above mean, search by yourself</li>
<li>If you still have no idea how to use lightning, give up for now, we'll make it easier later</li>
</ul>
@ -33,13 +33,8 @@
<form method="post">
<div class="form-group">
<h5>Lightning node url</h5>
<span>This URL should point to an installed lightning charge server</span>
<span>This URL should point to an installed lightning charge server for @Model.CryptoCode</span>
</div>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="Url"></label>
<input id="lightningurl" asp-for="Url" class="form-control" />

View File

@ -0,0 +1,54 @@
@model CheckoutExperienceViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Checkout experience";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Checkout);
}
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", TempData["TempDataProperty-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">
<form method="post">
<div class="form-group">
<label asp-for="CustomLogo"></label>
<input asp-for="CustomLogo" class="form-control" />
<span asp-validation-for="CustomLogo" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomCSS"></label>
<input asp-for="CustomCSS" class="form-control" />
<span asp-validation-for="CustomCSS" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultCryptoCurrency"></label>
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<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="LightningMaxValue"></label>
<input asp-for="LightningMaxValue" class="form-control" />
<span asp-validation-for="LightningMaxValue" class="text-danger"></span>
<p class="form-text text-muted">Example: 5.50 USD</p>
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -13,31 +13,39 @@
</div>
</div>
<div class="row">
<table class="table">
<tr>
<th>Label</th>
<td>@Model.Label</td>
</tr>
<tr>
<th>Facade</th>
<td>@Model.Facade</td>
</tr>
<tr>
<th>SIN</th>
<td>@Model.SIN</td>
</tr>
</table>
<div class="col-md-4"></div>
<div class="col-md-4">
<table class="table">
<tr>
<th>Label</th>
<td style="text-align:right;">@Model.Label</td>
</tr>
<tr>
<th>Facade</th>
<td style="text-align:right;">@Model.Facade</td>
</tr>
<tr>
<th>SIN</th>
<td style="text-align:right;">@Model.SIN</td>
</tr>
</table>
</div>
<div class="col-md-4"></div>
</div>
<div class="row">
<form asp-action="Pair" method="post">
<div class="form-group">
<label asp-for="SelectedStore"></label>
<select asp-for="SelectedStore" asp-items="@(new SelectList(Model.Stores,"Id","Name"))" class="form-control"></select>
<span asp-validation-for="SelectedStore" class="text-danger"></span>
</div>
<input type="hidden" name="pairingCode" value="@Model.Id" />
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
</form>
<div class="col-md-4"></div>
<div class="col-md-4">
<form asp-action="Pair" method="post">
<div class="form-group">
<label asp-for="SelectedStore"></label>
<select asp-for="SelectedStore" asp-items="@(new SelectList(Model.Stores,"Id","Name"))" class="form-control"></select>
<span asp-validation-for="SelectedStore" class="text-danger"></span>
</div>
<input type="hidden" name="pairingCode" value="@Model.Id" />
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
</form>
</div>
<div class="col-md-4"></div>
</div>
</div>
</section>

View File

@ -11,14 +11,14 @@ namespace BTCPayServer.Views.Stores
{
public static string ActivePageKey => "ActivePage";
public static string Index => "Index";
public static string Checkout => "Checkout experience";
public static string Tokens => "Tokens";
public static string Wallet => "Wallet";
public static string Users => "Users";
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
public static string WalletNavClass(ViewContext viewContext) => PageNavClass(viewContext, Wallet);
public static string CheckoutNavClass(ViewContext viewContext) => PageNavClass(viewContext, Checkout);
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
public static string PageNavClass(ViewContext viewContext, string page)

View File

@ -0,0 +1,57 @@
@model StoreUsersViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage users";
ViewData.AddActivePage(StoreNavPages.Users);
}
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", TempData["TempDataProperty-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">
<h5>Users</h5>
<span>Add access to your store to other users (Guest will not be able to see and modify the store settings)<br />
Note that the user must have a registered account on this BTCPay Server.</span>
</div>
<div class="form-inline">
<form method="post">
<input asp-for="Email" type="text" class="form-control" placeholder="user@example.com">
<select asp-for="Role" class="form-control">
<option value="@StoreRoles.Owner">Owner</option>
<option value="@StoreRoles.Guest">Guest</option>
</select>
<button type="submit" role="button" class="form-control btn btn-success"><span class="glyphicon glyphicon-plus"></span>Add user</button>
</form>
</div>
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Email</th>
<th>Role</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var user in Model.Users)
{
<tr>
<td>@user.Email</td>
<td>@user.Role</td>
<td style="text-align:right">
<a asp-action="DeleteStoreUser" asp-route-storeId="@Model.StoreId" asp-route-userId="@user.Id">Remove</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -6,7 +6,7 @@
}
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
<div class="row">
<div class="col-md-6">
@ -30,10 +30,6 @@
<input asp-for="StoreWebsite" class="form-control" />
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultCryptoCurrency"></label>
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
@ -76,12 +72,12 @@
</div>
<div class="form-group">
<a asp-action="AddDerivationScheme" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span>Add or modify a derivation scheme</a>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Derivation Scheme</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@ -89,7 +85,14 @@
{
<tr>
<td>@scheme.Crypto</td>
<td style="max-width:400px;overflow:hidden;">@scheme.Value</td>
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
<td style="text-align:right">
@if(!string.IsNullOrWhiteSpace(scheme.Value))
{
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
}
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
</td>
</tr>
}
</tbody>
@ -101,16 +104,16 @@
<h5>Lightning nodes (Experimental)</h5>
<p>
<span>A connection to a lightning charge node is required to generate lignting network enabled invoices.<br /></span>
<span>This is experimental and not advised for production so keep in mind:</span>
<span>This is experimental and not advised for production.</span>
</p>
</div>
<div class="form-group">
<a asp-action="AddLightningNode" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span>Add or modify a lightning node</a>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Address</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@ -119,6 +122,7 @@
<tr>
<td>@scheme.CryptoCode</td>
<td>@scheme.Address</td>
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
</tr>
}
</tbody>

View File

@ -2,7 +2,6 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.AddActivePage(StoreNavPages.Wallet);
}
<h4>@ViewData["Title"]</h4>
@ -18,7 +17,7 @@
If your Ledger wallet is not detected:
</p>
<ul>
<li>Activate <i class="icon-upload icon-large"></i> the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a> and refresh this page</li>
<li>Make sure you are running the Ledger Bitcoin or Litecoin 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>
</ul>
<p id="hw-loading"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
@ -30,45 +29,44 @@
<p id="check-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
</div>
</div>
<div class="col-md-6">
<form id="sendform" style="display:none;">
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select id="cryptoCurrencies" asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label>Destination</label>
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
<span id="Destination-Error" class="text-danger"></span>
</div>
<div class="form-group">
<label>Amount</label>
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
</p>
</div>
<div class="form-group">
<label>Fee rate (satoshi per byte)</label>
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
<span id="FeeRate-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
</p>
</div>
<div class="form-group">
<label>Subtract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
</form>
<div class="row">
<div class="col-md-6">
<form id="sendform" style="display:none;">
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
<div class="form-group">
<label>Destination</label>
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
<span id="Destination-Error" class="text-danger"></span>
</div>
<div class="form-group">
<label>Amount</label>
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
</p>
</div>
<div class="form-group">
<label>Fee rate (satoshi per byte)</label>
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
<span id="FeeRate-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
</p>
</div>
<div class="form-group">
<label>Subtract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
</form>
</div>
</div>
@section Scripts
{
<script type="text/javascript">
@section Scripts
{
<script type="text/javascript">
@Model.ToJSVariableModel("srvModel")
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}

View File

@ -2,8 +2,9 @@
@inject SignInManager<ApplicationUser> SignInManager
<ul class="nav nav-pills nav-stacked">
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">General settings</a></li>
<li class="@StoreNavPages.CheckoutNavClass(ViewContext)"><a asp-action="CheckoutExperience">Checkout experience</a></li>
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
<li class="@StoreNavPages.WalletNavClass(ViewContext)"><a asp-action="Wallet">Wallet</a></li>
<li class="@StoreNavPages.UsersNavClass(ViewContext)"><a asp-action="StoreUsers">Users</a></li>
</ul>

View File

@ -8,7 +8,7 @@
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", Model.StatusMessage)
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
</div>
</div>
@ -27,8 +27,8 @@
<tr>
<th>Name</th>
<th>Website</th>
<th>Balances</th>
<th>Actions</th>
<th>On-Chain balances</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@ -52,7 +52,13 @@
}
}
</td>
<td><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a> - <a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a></td>
<td style="text-align:right">
@if(store.IsOwner)
{
<a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@store.Id">Settings</a><span> - </span>
}
<a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a>
</td>
</tr>
}
</tbody>

View File

@ -33,7 +33,8 @@
"inputFiles": [
"wwwroot/vendor/font-awesome/css/font-awesome.css",
"wwwroot/css/css.css",
"wwwroot/css/normalizer.css"
"wwwroot/css/normalizer.css",
"wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css"
]
},
{
@ -41,9 +42,12 @@
"inputFiles": [
"wwwroot/vendor/clipboard.js/clipboard.js",
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/js/vue.min.js",
"wwwroot/js/vue-qrcode.js",
"wwwroot/js/core.js"
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vuejs/vue-qrcode.js",
"wwwroot/vendor/i18next/i18next.js",
"wwwroot/vendor/i18next/vue-i18next.js",
"wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js",
"wwwroot/js/checkout/**/*.js"
]
}
]

View File

@ -8562,10 +8562,23 @@ strong {
transition: all .2s ease;
}
.payment-tabs__slider.slide-right {
.payment-tabs__slider.slide-copy {
right: 0;
}
.payment-tabs__slider.three-tabs {
width: 33%;
right: 67%;
}
.payment-tabs__slider.three-tabs.slide-copy {
right: 33%;
}
.payment-tabs__slider.three-tabs.slide-altcoins {
right: 0;
}
.manual__step-one__header {
padding-top: 20px;
text-align: center;
@ -8612,10 +8625,10 @@ strong {
max-height: 300px;
}
.copy-cursor {
cursor: copy;
}
/*
After refactoring Copy tab, this section until EOF REFACTOR can likely be deleted
Leaving it since there are some references in refund that I need to look into
*/
.manual-box {
border-radius: 5px;
margin-left: calc(-40px + 10%);
@ -8869,6 +8882,10 @@ strong {
opacity: 1;
}
.manual-box__address__value .copied-label {
margin-top: 5px;
}
.manual-box__address__wrapper {
background: rgba(182, 182, 182, 0.13);
border: 1px solid rgba(77, 77, 77, 0.07);
@ -8890,6 +8907,7 @@ strong {
font-size: 10.2px;
color: #4A4A4A;
}
/* EOF REFACTOR */
.status-block {
position: relative;
@ -9537,8 +9555,8 @@ strong {
}
.expired__body {
padding: 14px 10px;
padding-top: 8px;
padding: 0px 8px 0px;
margin-top: -10px;
}
.expired__header {
@ -9549,10 +9567,14 @@ strong {
.expired__text {
margin-top: 20px;
font-weight: 100;
font-size: 14.5px;
font-size: 14px;
opacity: .8;
}
.expired__text .expired__text__smaller {
font-size: 11px;
}
.expired__text__bullet {
font-weight: 500;
}
@ -9866,6 +9888,17 @@ strong {
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.qr_currency_icon {
height: 64px;
width: 64px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
/* Warning Page */
.manual__step-two--warning {
display: block;
@ -11258,3 +11291,128 @@ low-fee-timeline {
.no-bounce * .status-icon__wrapper__outline {
animation: none !important;
}
/* checkout additions */
.copyBox {
text-align: center;
position: relative;
border-radius: 5px;
border: 1px solid #E9E9E9;
background: #fff;
}
.copySectionBox {
padding: 12px;
}
.copySectionBox label {
text-transform: uppercase;
font-size: 10px;
color: #515664;
opacity: .5;
letter-spacing: .6px;
margin-bottom: 10px;
}
.copySectionBox.bottomBorder {
border-bottom: 1px solid #e9e9e9;
}
.separatorGem {
position: relative;
height: 10px;
width: 10px;
left: 50%;
border-right: 1px solid #E9E9E9;
border-bottom: 1px solid #E9E9E9;
transform: rotateZ(45deg);
margin-left: -5px;
top: -5px;
background: #329F80;
transform-style: preserve-3d;
backface-visibility: hidden;
margin-bottom: -10px;
}
.checkoutTextbox {
width: 100%;
border: 1px solid #e9e9e9;
border-radius: 4px;
outline: none;
padding: 8px 10px;
background: #f6f6f6;
box-sizing: border-box;
transition: .3s;
font-size: 11px;
color: #4a4a4a;
cursor: copy;
}
.inputWithIcon .checkoutTextbox {
padding-left: 40px;
}
.inputWithIcon {
position: relative;
}
.inputWithIcon img {
position: absolute;
left: 2px;
top: 8px;
color: #aaa;
height: 16px;
padding: 0px 6px;
border-right: 1px solid #e9e9e9;
}
.inputWithIcon.inputIconBg img {
background-color: #aaa;
color: #fff;
padding: 9px 4px;
border-radius: 4px 0 0 4px;
}
.copyAmountText {
color: #4A4A4A;
font-size: 30px;
font-weight: 300;
letter-spacing: 1px;
margin-top: -10px;
position: relative;
}
.copy-cursor {
cursor: copy;
}
.clipboardCopied {
transition: opacity 1s ease;
opacity: 0;
}
.copyLabelPopup {
pointer-events: none;
cursor: default;
transition: opacity 1s ease;
position: fixed;
width: 130px;
top: 0;
left: 0;
opacity: 0;
font-weight: 300;
font-size: 14px;
color: white;
background: #4A4A4A;
padding: 10px 40px;
border-radius: 3px;
letter-spacing: .5px;
z-index: 999999;
}
.copyLabelPopup.copied {
pointer-events: auto;
opacity: 1;
transition: opacity 1s ease;
}

View File

@ -1,8 +1,12 @@
$(function () {
var ledgerDetected = false;
var recommendedPubKey = "";
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
var cryptoSelector = $("#CryptoCurrency");
function GetSelectedCryptoCode() {
return cryptoSelector.val();
}
function WriteAlert(type, message) {
}
@ -14,37 +18,41 @@
}
}
$("#ledger-info-recommended").on("click", function (elem) {
$(".ledger-info-recommended").on("click", function (elem) {
elem.preventDefault();
$("#DerivationScheme").val(recommendedPubKey);
$("#DerivationSchemeFormat").val("BTCPay");
var account = elem.currentTarget.getAttribute("data-ledgeraccount");
var cryptoCode = GetSelectedCryptoCode();
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode + "&account=" + account)
.then(function (result) {
if (cryptoCode !== GetSelectedCryptoCode())
return;
$("#DerivationScheme").val(result.extPubKey);
$("#DerivationSchemeFormat").val("BTCPay");
})
.catch(function (reason) { Write('check', 'error', reason); });
return false;
});
$("#CryptoCurrency").on("change", function (elem) {
$("#no-ledger-info").css("display", "none");
$("#ledger-info").css("display", "none");
updateInfo();
});
var updateInfo = function () {
if (!ledgerDetected)
return false;
var cryptoCode = $("#CryptoCurrency").val();
var cryptoCode = GetSelectedCryptoCode();
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode)
.catch(function (reason) { Write('check', 'error', reason); })
.then(function (result) {
if (!result)
return;
if (cryptoCode !== GetSelectedCryptoCode())
return;
if (result.error) {
Write('check', 'error', result.error);
return;
}
else {
Write('check', 'success', 'This store is configured to use your ledger');
recommendedPubKey = result.extPubKey;
$("#no-ledger-info").css("display", "none");
$("#ledger-info").css("display", "block");
$(".ledger-info-cointype").text(result.coinType);
}
});
};

View File

@ -3,6 +3,7 @@
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
var recommendedFees = "";
var recommendedBalance = "";
var cryptoCode = $("#cryptoCode").val();
function WriteAlert(type, message) {
$(".alert").removeClass("alert-danger");
@ -37,7 +38,7 @@
}
var args = "";
args += "cryptoCode=" + $("#cryptoCurrencies").val();
args += "cryptoCode=" + cryptoCode;
args += "&destination=" + $("#destination-textbox").val();
args += "&amount=" + $("#amount-textbox").val();
args += "&feeRate=" + $("#fee-textbox").val();
@ -64,6 +65,10 @@
WriteAlert("danger", result.error);
} else {
WriteAlert("success", 'Transaction broadcasted (' + result.transactionId + ')');
$("#fee-textbox").val("");
$("#amount-textbox").val("");
$("#destination-textbox").val("");
$("#substract-checkbox").prop("checked", false);
updateInfo();
}
});
@ -85,15 +90,10 @@
return false;
});
$("#cryptoCurrencies").on("change", function (elem) {
updateInfo();
});
var updateInfo = function () {
if (!ledgerDetected)
return false;
$(".crypto-info").css("display", "none");
var cryptoCode = $("#cryptoCurrencies").val();
bridge.sendCommand("getinfo", "cryptoCode=" + cryptoCode)
.catch(function (reason) { Write('check', 'error', reason); })
.then(function (result) {
@ -125,7 +125,7 @@
.catch(function (reason)
{
if (reason.message === "Sign failed")
reason = "Have you forgot to activate browser support in your ledger app?";
reason = "Are you running the ledger app with version equals or above 1.2.4?";
Write('hw', 'error', reason);
})
.then(function (result) {

View File

@ -1,5 +1,28 @@
// TODO: Refactor... switch from jQuery to Vue.js
// public methods
function resetTabsSlider() {
$("#tabsSlider").removeClass("slide-copy");
$("#tabsSlider").removeClass("slide-altcoins");
$("#scan-tab").removeClass("active");
$("#copy-tab").removeClass("active");
$("#altcoins-tab").removeClass("active");
$("#copy").hide();
$("#copy").removeClass("active");
$("#scan").hide();
$("#scan").removeClass("active");
$("#altcoins").hide();
$("#altcoins").removeClass("active");
}
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;
if (newStatus === "complete" ||
@ -20,27 +43,18 @@ function onDataCallback(jsonData) {
$(".modal-dialog").addClass("paid");
if ($("#scan").hasClass("active")) {
$("#scan").removeClass("active");
} else if ($("#copy").hasClass("active")) {
$("#copy").removeClass("active");
}
resetTabsSlider();
$("#paid").addClass("active");
}
if (newStatus === "expired" || newStatus === "invalid") { //TODO: different state if the invoice is invalid (failed to confirm after timeout)
$(".timer-row").removeClass("expiring-soon");
$(".timer-row__message span").html("Invoice expired.");
$(".timer-row__spinner").html("");
$("#emailAddressView").removeClass("active");
$(".modal-dialog").addClass("expired");
$("#expired").addClass("active");
if ($("#scan").hasClass("active")) {
$("#scan").removeClass("active");
} else if ($("#copy").hasClass("active")) {
$("#copy").removeClass("active");
}
resetTabsSlider();
}
if (checkoutCtrl.srvModel.status !== newStatus) {
@ -48,7 +62,7 @@ function onDataCallback(jsonData) {
}
// restoring qr code view only when currency is switched
if (jsonData.paymentMethodId == srvModel.paymentMethodId) {
if (jsonData.paymentMethodId === srvModel.paymentMethodId) {
$(".payment__currencies").show();
$(".payment__spinner").hide();
}
@ -58,7 +72,7 @@ function onDataCallback(jsonData) {
}
function changeCurrency(currency) {
if (srvModel.paymentMethodId != currency) {
if (srvModel.paymentMethodId !== currency) {
$(".payment__currencies").hide();
$(".payment__spinner").show();
srvModel.paymentMethodId = currency;
@ -114,7 +128,6 @@ $(document).ready(function () {
function hideEmailForm() {
$("[role=document]").removeClass("enter-purchaser-email");
$("#emailAddressView").removeClass("active");
$("placeholder-refundEmail").html(srvModel.customerEmail);
@ -143,21 +156,26 @@ $(document).ready(function () {
contentType: "application/json; charset=utf-8"
}).done(function () {
hideEmailForm();
})
.fail(function (jqXHR, textStatus, errorThrown) {
}).fail(function (jqXHR, textStatus, errorThrown) {
})
})
.always(function () {
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").removeClass("loading");
});
} else {
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
}
return false;
});
}
// Validate Email address
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
/* =============== Even listeners =============== */
// Email
@ -171,47 +189,33 @@ $(document).ready(function () {
// Scan/Copy Transitions
// Scan Tab
$("#scan-tab").click(function () {
if (!$(this).is(".active")) {
$(this).addClass("active");
}
if ($("#copy-tab").is(".active")) {
$("#copy-tab").removeClass("active");
}
$(".payment-tabs__slider").removeClass("slide-right");
if (!$("#scan").is(".active")) {
$("#copy").hide();
$("#copy").removeClass("active");
$("#scan").show();
$("#scan").addClass("active");
}
resetTabsSlider();
activateTab("#scan");
});
// Main Copy tab
// Copy tab
$("#copy-tab").click(function () {
if (!$(this).is(".active")) {
$(this).addClass("active");
}
resetTabsSlider();
activateTab("#copy");
if ($("#scan-tab").is(".active")) {
$("#scan-tab").removeClass("active");
}
if (!$(".payment-tabs__slider").is("slide-right")) {
$(".payment-tabs__slider").addClass("slide-right");
}
if (!$("#copy").is(".active")) {
$("#copy").show();
$("#copy").addClass("active");
$("#scan").hide();
$("#scan").removeClass("active");
}
$("#tabsSlider").addClass("slide-copy");
});
// Altcoins tab
$("#altcoins-tab").click(function () {
resetTabsSlider();
activateTab("#altcoins");
$("#tabsSlider").addClass("slide-altcoins");
});
function activateTab(senderName) {
$(senderName + "-tab").addClass("active");
$(senderName).show();
$(senderName).addClass("active");
}
// Payment received
// Should connect using webhook ?
// If notification received
@ -244,12 +248,6 @@ $(document).ready(function () {
// function to load contents in different language should go there
});
// Validate Email address
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
// Expand Line-Items
$(".buyerTotalLine").click(function () {
$("line-items").toggleClass("expanded");
@ -288,46 +286,59 @@ $(document).ready(function () {
}
function animateUpdate() {
var now = new Date();
var timeDiff = end.getTime() - now.getTime();
var perc = 100 - Math.round(timeDiff / timerMax * 100);
var status = checkoutCtrl.srvModel.status;
if (perc === 75 && (status === "paidPartial" || status === "new")) {
$(".timer-row").addClass("expiring-soon");
$(".timer-row__message span").html("Invoice expiring soon ...");
checkoutCtrl.expiringSoon = true;
updateProgress(perc);
}
if (perc <= 100) {
updateProgress(perc);
setTimeout(animateUpdate, timeoutVal);
}
if (perc >= 100 && status === "expired") {
onDataCallback(status);
}
//if (perc >= 100 && status === "expired") {
// onDataCallback(status);
//}
}
}
// Manual Copy
// Amount
var copyAmount = new Clipboard('.manual-box__amount__value', {
target: function () {
var $el = $(".manual-box__amount__value");
$el.removeClass("copy-cursor").addClass("copied");
setTimeout(function () { $el.removeClass("copied").addClass("copy-cursor"); }, 500);
return document.querySelector('.manual-box__amount__value span');
// Clipboard Copy
var copyAmount = new Clipboard('._copySpan', {
target: function (trigger) {
return copyElement(trigger, 0, 65).firstChild;
}
});
// Address
var copyAddress = new Clipboard('.manual-box__address__value', {
target: function () {
var $elm = $(".manual-box__address__value");
$elm.removeClass("copy-cursor").addClass("copied");
setTimeout(function () { $elm.removeClass("copied").addClass("copy-cursor"); }, 500);
return document.querySelector('.manual-box__address__value .manual-box__address__wrapper .manual-box__address__wrapper__value');
var copyAmount = new Clipboard('._copyInput', {
target: function (trigger) {
return copyElement(trigger, 4, 65).firstChild;
}
});
function copyElement(trigger, popupLeftModifier, popupTopModifier) {
var elm = $(trigger);
var position = elm.offset();
position.top -= popupLeftModifier;
position.left += (elm.width() / 2) - popupTopModifier;
$(".copyLabelPopup").css(position).addClass("copied");
elm.removeClass("copy-cursor").addClass("clipboardCopied");
setTimeout(clearSelection, 100);
setTimeout(function () {
elm.removeClass("clipboardCopied").addClass("copy-cursor");
$(".copyLabelPopup").removeClass("copied");
}, 1000);
return trigger;
}
function clearSelection() {
if (window.getSelection) { window.getSelection().removeAllRanges(); }
else if (document.selection) { document.selection.empty(); }
}
// EOF Copy
// Disable enter key
$(document).keypress(
function (event) {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,47 @@
const locales_es = {
nested: {
lang: 'Lenguaje'
},
"Awaiting Payment...": "En espera de pago...",
"Pay with": "Pagar con",
"Contact and Refund Email": "Contacto y correo electrónico de reembolso",
"Contact_Body": "Por favor provea una dirección de correo electrónico a continuación. Nos pondremos en contacto con usted en esta dirección si hay un problema con su pago.",
"Your email": "Tu correo electrónico",
"Continue": "Continuar",
"Please enter a valid email address": "Por favor entre un correo electrónico valido",
"Order Amount": "Total de el pedido",
"Network Cost": "Costo de la red",
"Already Paid": "Ya pagado",
"Due": "Debido",
// Tabs
"Scan": "Escaniar",
"Copy": "Copiar",
"Conversion": "Conversión",
// Scan tab
"Open in wallet": "Abrir en billetera",
// Copy tab
"CompletePay_Body": "Para completar su pago, envíe {{btcDue}} {{cryptoCode}} a la dirección siguiente.",
"Amount": "Cantidad",
"Address": "Direccón",
"Copied": "Copiado",
// Conversion tab
"ConversionTab_BodyTop": "Puede pagar {{btcDue}} {{cryptoCode}} usando altcoins que no sean los que el comerciante soporta directamente.",
"ConversionTab_BodyDesc": "Este servicio es proveído por terceros. Tenga en cuenta que no tenemos control sobre cómo los proveedores enviarán sus fondos. La factura solo se marcará como abonada una vez que se reciban los fondos en el bloque de cadenas de {{cryptoCode}} .",
"Shapeshift_Button_Text": "Pagar con Altcoins",
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos de Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "La factura expira pronto...",
"Invoice expired": "La factura expiro",
"What happened?": "¿Qué sucedió?",
"InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. \ Puede volver a {{storeName}} si desea volver a enviar su pago.",
"InvoiceExpired_Body_2": "Si intentó enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido sus fondos.",
"InvoiceExpired_Body_3": "Si la transacción no es aceptada por la red de Bitcoin, los fondos se podrán gastar nuevamente en su billetera. Dependiendo de su billetera, esto puede tomar 48-72 horas.",
"Invoice ID": "ID de factura",
"Order ID": "ID de pedido",
"Return to StoreName": "Regresar a {{storeName}}",
// Invoice paid
"This invoice has been paid": "Esta factura ha sido pagada",
// Invoice archived
"This invoice has been archived": "Esta factura ha sido archivada",
"Archived_Body": "Por favor, comuníquese con la tienda para obtener información de su pedido o asistencia"
};

View File

@ -0,0 +1,48 @@
const locales_fr = {
nested: {
lang: 'Langue'
},
"Awaiting Payment...": "En attente du paiement...",
"Pay with": "Payer avec",
"Contact and Refund Email": "Adresse de contact et de remboursement",
"Contact_Body": "Merci de renseigner l'adresse email ci-dessous. Nous vous contacterons à cette adresse si il y a un problème avec votre paiement.",
"Your email": "Votre email",
"Continue": "Continuer",
"Please enter a valid email address": "Merci de rentrer une addrese email valide",
"Order Amount": "Montant de la commande",
"Network Cost": "Coût réseau",
"Already Paid": "Déjà payé",
"Due": "Dûe",
// Tabs
"Scan": "Scanner",
"Copy": "Copier",
"Conversion": "Convertir",
// Scan tab
"Open in wallet": "Ouvrir le portefeuille",
// Copy tab
"CompletePay_Body": "Pour terminer le paiement, merci d'envoyer {{btcDue}} {{cryptoCode}} à l'adresse ci-dessous.",
"Amount": "Montant",
"Address": "Adresse",
"Copied": "Copié",
// Conversion tab
"ConversionTab_BodyTop": "Vous pouvez payer {{btcDue}} {{cryptoCode}} en utilisant d'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.",
// Invoice expired
"Invoice expiring soon...": "La facture va bientôt expirer...",
"Invoice expired": "Facture expiré",
"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.",
"Invoice ID": "Numéro de facture",
"Order ID": "Numéro de commande",
"Return to StoreName": "Retourner sur {{storeName}}",
// Invoice paid
"This invoice has been paid": "Cette facture a été payée",
// Invoice archived
"This invoice has been archived": "Cette facture a été archivée",
"Archived_Body": "Merci de contacter le marchand pour plus d'assistance ou d'information sur cette commande."
};

View File

@ -0,0 +1,48 @@
const locales_is = {
nested: {
lang: 'Tungumál'
},
"Awaiting Payment...": "Bíð eftir greiðslu...",
"Pay with": "Borga með",
"Contact and Refund Email": "Netfang",
"Contact_Body": "Við munum hafa samband við þig á þessu netfangi ef það er vandamál með greiðsluna þína.",
"Your email": "Þitt netfang",
"Continue": "Áfram",
"Please enter a valid email address": "Þú verður að nota gilt netfang",
"Order Amount": "Upphæð",
"Network Cost": "Auka gjöld",
"Already Paid": "Nú þegar greitt",
"Due": "Due",
// Tabs
"Scan": "Skanna",
"Copy": "Afrita",
"Conversion": "Umbreyting",
// Scan tab
"Open in wallet": "Opna með veski",
// Copy tab
"CompletePay_Body": "Til að klára greiðsluna skaltu senda {{btcDue}} {{cryptoCode}} á lykilinn fyrir neðan.",
"Amount": "Magn",
"Address": "Lykill",
"Copied": "Afritað",
// Conversion tab
"ConversionTab_BodyTop": "Þú getur borgað {{btcDue}} {{cryptoCode}} með altcoins.",
"ConversionTab_BodyDesc": "Þessi þjónusta er veitt af þriðja aðila. Mundu að við höfum ekki stjórn á því hvernig birgja vilja senda peningana þína. Reikningur verður aðeins merktur þegar móttekinir eru á {{cryptoCode}} Blockchain.",
"Shapeshift_Button_Text": "Borga með Altcoins",
"ConversionTab_Lightning": "Engir viðskiptaveitendur eru í boði fyrir Lightning Network greiðslur.",
// Invoice expired
"Invoice expiring soon...": "Innheimt rennur út fljótlega...",
"Invoice expired": "Innheimt útrunnin",
"What happened?": "Hvað gerðist?",
"InvoiceExpired_Body_1": "Þessi innheimtun er útrunnin. Innheimtun er aðeins gild í {{maxTimeMinutes}} mínútur. \
Þú getur farið aftur á {{storeName}} ef þú vilt reyna aftur.",
"InvoiceExpired_Body_2": "Ef þú reyndir að senda greiðslu, þá hefur hún ekki verið samþykkt.",
"InvoiceExpired_Body_3": "Ef viðskiptin eru ekki samþykkt af Bitcoin netinu verða fjármunirnir aðgengilegar aftur í veskinu þínu. Það fer eftir veskinu þínu og getur tekið 48-72 klukkustundir.",
"Invoice ID": "Innheimtu ID",
"Order ID": "Pöntun ID",
"Return to StoreName": "Fara aftur á {{storeName}}",
// Invoice paid
"This invoice has been paid": "Þetta hefur verið greitt",
// Invoice archived
"This invoice has been archived": "Þessi innheimta hefur verið gerð ógild",
"Archived_Body": "Vinsamlega hafðu samband fyrir upplýsingar eða aðstoð."
};

View File

@ -0,0 +1,48 @@
const locales_ja = {
nested: {
lang: 'Language'
},
"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": "ライトニングのペイメントでは現在変換サービスが存在しないためご利用いただけません。ご了承ください。",
// Invoice expired
"Invoice expiring soon...": "お支払いの期限が迫っています...",
"Invoice expired": "お支払いの期限が切れました",
"What happened?": "え!?ナニコレ!?",
"InvoiceExpired_Body_1": "当件のお支払いの有効期限が過ぎてしまいました。最大 {{maxTimeMinutes}} 分以内に支払うことが義務付けられています。 \
まだお支払いのご希望の場合 {{storeName}} に一旦戻っていただき、もう一度お支払いの手続きを最初からやり直してみてください。",
"InvoiceExpired_Body_2": "送金手続きを完了したつもりでも、ネットワークにて取り込まれて処理されるまでは決済となりません。現時点ではまだ着金しておりません。",
"InvoiceExpired_Body_3": "ネットワークにて取り込まれなかった送金はいずれ送金元のウォレットに戻りますが、ウォレットソフトによっては2〜3日かかる場合もございますのでご了承ください。",
"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": "ご注文に関わる詳細などでお困りの場合はお店の担当窓口へお問い合わせください。"
};

View File

@ -0,0 +1,48 @@
const locales_nl = {
nested: {
lang: 'Taal'
},
"Awaiting Payment...": "Wachtende op de betaling...",
"Pay with": "Betalen met",
"Contact and Refund Email": "Email adres voor opvolging en terugbetaling",
"Contact_Body": "Bedankt om je email adres in te vullen voor een mogelijke opvolging. We contacteren je indien er een probleem optreedt.",
"Your email": "Je email adres",
"Continue": "Verdergaan",
"Please enter a valid email address": "Bedankt om een geldig email adres in te vullen",
"Order Amount": "Bedrag van je bestelling",
"Network Cost": "Netwerk kosten",
"Already Paid": "Reeds betaald",
"Due": "Verschuldigd",
// Tabs
"Scan": "Scannen",
"Copy": "Kopiëren",
"Conversion": "Omzetting",
// Scan tab
"Open in wallet": "Wallet openen",
// Copy tab
"CompletePay_Body": "Om de betaling te vervoledigen, bedankt om {{btcDue}} {{cryptoCode}} naar het hieronder vemelde adres op te sturen.",
"Amount": "Bedrag",
"Address": "Adres",
"Copied": "Gekopieerd",
// Conversion tab
"ConversionTab_BodyTop": "Je kan alternatieve cryptocurrencies gebruiken die niet ondersteund zijn door de verkoper, om {{btcDue}} {{cryptoCode}} te betalen.",
"ConversionTab_BodyDesc": "Deze dienst wordt door een externe partij geleverd. Bijgevolg, hebben we geen zicht over jouw fondsen. De factuur wordt pas als betaald beschouwd, wanneer de fondsen door de blockchain aanvaard zijn {{ cryptoCode }}.",
"Shapeshift_Button_Text": "Betalen met een alternatieve cryptocurrency",
"ConversionTab_Lightning": "Geen leverancier beschikbaar voor de betalingen op het Lightning Network",
// Invoice expired
"Invoice expiring soon...": "De factuur zal weldra vervallen...",
"Invoice expired": "Vervallen factuur",
"What happened?": "Wat gebeurde er?",
"InvoiceExpired_Body_1": "De factuur is vervallen. Een factuur is geldig voor {{maxTimeMinutes}} minuten. \
Je kan terug komen naar {{storeName}} indien je nog eens je betaling wilt proberen uit te voeren.",
"InvoiceExpired_Body_2": "Indien je een betaling uitvoerde, werd deze nog niet aanvaard door de blockchain. We hebben je fondsen nog niet ontvangen.",
"InvoiceExpired_Body_3": "Indien je transactie niet door de blockchain werd aanvaard, zullen je fondsen terug in wallet verschijnen. Volgens de wallet, kan dit 48 to 72 uren duren.",
"Invoice ID": "Factuurnummer",
"Order ID": "Bestllingsnummer",
"Return to StoreName": "Terug naar {{storeName}}",
// Invoice paid
"This invoice has been paid": "Deze factuur werd betaald",
// Invoice archived
"This invoice has been archived": "Deze factuur werd geactiveerd",
"Archived_Body": "Bedankt om de winkel te contacteren voor bijstand met of informatie over deze bestelling."
};

View File

@ -0,0 +1,48 @@
const locales_pt_br = {
nested: {
lang: 'Idioma'
},
"Awaiting Payment...": "Esperando o pagamento...",
"Pay with": "Pague com",
"Contact and Refund Email": "Email de contato e reembolso",
"Contact_Body": "Por favor, forneça um email abaixo. Nós iremos contactar você se algum problema ocorrer com o seu pagamento.",
"Your email": "Seu email",
"Continue": "Continue",
"Please enter a valid email address": "Por favor, entre com um email válido",
"Order Amount": "Valor do pedido",
"Network Cost": "Custo da rede",
"Already Paid": "Já foi pago",
"Due": "Devido",
// Tabs
"Scan": "Escaneie",
"Copy": "Copie",
"Conversion": "Conversão",
// Scan tab
"Open in wallet": "Abra na carteira",
// Copy tab
"CompletePay_Body": "Para completar seu pagamento, por favor envie {{btcDue}} {{cryptoCode}} para o endereço abaixo.",
"Amount": "Quantia",
"Address": "Endereço",
"Copied": "Copiado",
// Conversion tab
"ConversionTab_BodyTop": "Você pode pagar {{btcDue}} {{cryptoCode}} utilizando outras altcoins além das que a loja aceita diretamente.",
"ConversionTab_BodyDesc": "Esse serviço é oferecido por terceiros. Por favor, tenha em mente que não temos nenhum controle sobre como seus fundos serão utilizados. A fatura apenas será marcada como paga quando os fundos forem recebidos na Blockchain {{cryptoCode}}.",
"Shapeshift_Button_Text": "Pague com Altcoins",
"ConversionTab_Lightning": "Não há provedores de conversão disponíveis para pagamentos via Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "A fatura está vencendo...",
"Invoice expired": "Fatura vencida",
"What happened?": "O que aconteceu?",
"InvoiceExpired_Body_1": "Essa fatura vence. Uma fatura é válida apenas por {{maxTimeMinutes}} minutos. \
Você pode retornar à {{storeName}} se desejar enviar seu pagamento novamente.",
"InvoiceExpired_Body_2": "Se você tentou enviar um pagamento, o mesmo não foi aceito pela rede Bitcoin. Nós não recebemos ainda o valor enviado.",
"InvoiceExpired_Body_3": "Se a transação não for aceita pela rede Bitcoin, o valor retornará à sua carteira. Dependendo da sua carteira, isso pode demorar de 48 a 72 horas.",
"Invoice ID": "Nº da Fatura",
"Order ID": "Nº do Pedido",
"Return to StoreName": "Retornar à {{storeName}}",
// Invoice paid
"This invoice has been paid": "Essa fatura foi paga",
// Invoice archived
"This invoice has been archived": "Essa fatura foi arquivada",
"Archived_Body": "Por favor, contate o estabelecimento para informações e suporte"
};

View File

@ -0,0 +1,12 @@
var urlParams;
(window.onpopstate = function () {
var match,
pl = /\+/g, // Regex for replacing addition symbol with a space
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
query = window.location.search.substring(1);
urlParams = {};
while (match = search.exec(query))
urlParams[decode(match[1])] = decode(match[2]);
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("VueI18next",[],t):"object"==typeof exports?exports.VueI18next=t():e.VueI18next=t()}(this,function(){return function(e){function t(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=2)}([function(e,t,n){"use strict";function i(e){i.installed||(i.installed=!0,t.Vue=u=e,u.mixin({computed:{$t:function(){var e=this;return function(t,n){return e.$i18n.t(t,n,e.$i18n.i18nLoadedAt)}}},beforeCreate:function(){var e=this.$options;e.i18n?this.$i18n=e.i18n:e.parent&&e.parent.$i18n&&(this.$i18n=e.parent.$i18n)}}),u.component(r.default.name,r.default))}Object.defineProperty(t,"__esModule",{value:!0}),t.Vue=void 0,t.install=i;var o=n(1),r=function(e){return e&&e.__esModule?e:{default:e}}(o),u=t.Vue=void 0},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={name:"i18next",functional:!0,props:{tag:{type:String,default:"span"},path:{type:String,required:!0}},render:function(e,t){var n=t.props,i=t.data,o=t.children,r=t.parent,u=r.$i18n;if(!u)return o;var a=n.path,s=u.i18next.services.interpolator.regexp,f=u.t(a,{interpolation:{prefix:"#$?",suffix:"?$#"}}),d=[],c={};return o.forEach(function(e){e.data&&e.data.attrs&&e.data.attrs.tkey&&(c[e.data.attrs.tkey]=e)}),f.split(s).reduce(function(e,t,n){var i=void 0;if(n%2==0){if(0===t.length)return e;i=t}else i=o[parseInt(t,10)];return e.push(i),e},d),e(n.tag,i,d)}},e.exports=t.default},function(e,t,n){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(0),a=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};i(this,e);var o=n.bindI18n,r=void 0===o?"languageChanged loaded":o,u=n.bindStore,a=void 0===u?"added removed":u;this._vm=null,this.i18next=t,this.onI18nChanged=this.onI18nChanged.bind(this),r&&this.i18next.on(r,this.onI18nChanged),a&&this.i18next.store&&this.i18next.store.on(a,this.onI18nChanged),this.resetVM({i18nLoadedAt:new Date})}return r(e,[{key:"resetVM",value:function(e){var t=this._vm,n=u.Vue.config.silent;u.Vue.config.silent=!0,this._vm=new u.Vue({data:e}),u.Vue.config.silent=n,t&&u.Vue.nextTick(function(){return t.$destroy()})}},{key:"t",value:function(e,t){return this.i18next.t(e,t)}},{key:"onI18nChanged",value:function(){this.i18nLoadedAt=new Date}},{key:"i18nLoadedAt",get:function(){return this._vm.$data.i18nLoadedAt},set:function(e){this._vm.$set(this._vm,"i18nLoadedAt",e)}}]),e}();t.default=a,a.install=u.install,a.version="0.4.0",("undefined"==typeof window?"undefined":o(window))&&window.Vue&&window.Vue.use(a),e.exports=t.default}])});
//# sourceMappingURL=vue-i18next.js.map

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