Compare commits

...

54 Commits

Author SHA1 Message Date
9b404e330d bump 2019-01-08 23:03:53 +09:00
1667f9b2ef Merge pull request #504 from Kukks/bugfix/general
Fix Coinswitch Issues, Fix LN Node Info Clipboard, Fix Vue-Cloak Styles
2019-01-08 23:02:34 +09:00
8b686f0b12 fix coin switch issues 2019-01-08 11:27:37 +01:00
def8d1e0cb fix ln node clipboard 2019-01-08 10:54:02 +01:00
b15267be4d Merge pull request #453 from 2pac1/master
Update Anyone can enable invoices text so its much more clear
2019-01-08 12:56:04 +09:00
5c074f6f5f Update translations 2019-01-07 22:51:26 +09:00
679942159e bump 2019-01-07 22:37:55 +09:00
63c309bd12 Merge pull request #499 from Kukks/node-info-page
Add Node Info Page
2019-01-07 22:04:08 +09:00
3cefd7bd1e Merge pull request #467 from Kukks/feature/coinswitch
CoinSwitch Integration
2019-01-07 19:11:55 +09:00
12c418d84d Add Node Info Page 2019-01-07 09:52:27 +01:00
4b982f815c Renaming 2019-01-07 15:35:18 +09:00
d4d3346b6d Merge pull request #463 from sipsorcery/455-disablereg
Set disable registration as default true
2019-01-07 15:20:04 +09:00
6010a103e0 Added new disable-registration command line option. 2019-01-06 16:43:55 +01:00
5dc1da2af0 Don't disable user registrations if debug for unit tests. 2019-01-06 14:55:18 +01:00
5fd77d9fcc Merge pull request #492 from Kukks/crowdfund-part-1
Crowdfund Part 1: JS Dependencies
2019-01-06 21:59:52 +09:00
540414d8f5 Merge pull request #495 from Kukks/crowdfund-part-3
Enhance Invoice Events
2019-01-06 18:14:56 +09:00
abcdb8ced0 Merge pull request #494 from Kukks/crowdfund-part-2
Crowdfund Part 2: Expand Invoice Searching
2019-01-06 18:14:23 +09:00
5076d73695 Enhance Invoice Events 2019-01-06 10:12:45 +01:00
d88735f84e Merge remote-tracking branch 'upstream/master' into 455-disablereg 2019-01-06 10:05:33 +01:00
2244f0ab76 Merge pull request #493 from btcpayserver/feature/fastertests
[WIP] Make tests fast to execute
2019-01-06 18:04:38 +09:00
40c85d6104 Expand Invoice Searching 2019-01-06 10:00:55 +01:00
42892e24f4 Remove uneeded database call during derivation scheme registration 2019-01-06 17:58:11 +09:00
c27557826b add vendors 2019-01-06 09:08:05 +01:00
88150b6535 Improve IPN tests 2019-01-06 15:04:30 +09:00
b2aebcc5d3 Merge pull request #480 from britttttk/fix/PaymentButton
Fix payment button size
2019-01-05 22:32:11 +09:00
ae9ad0fa65 Merge pull request #489 from btcpayserver/feature/networkfee
Add support for removing network fee on first payment
2019-01-05 22:09:10 +09:00
a05cd5678b Add support for removing network fee on first payment 2019-01-05 17:45:49 +09:00
0f175174f6 Rename TxFee to NetworkFee and save the Network Fee of each payment under PaymentEntity 2019-01-05 13:31:05 +09:00
fa8993191e Update coinswitch.html 2019-01-03 15:31:56 +01:00
239ce28575 Update coinswitch.html 2019-01-02 21:50:43 +01:00
1e26926350 Fix payment button size 2018-12-30 00:27:22 -07:00
fff39d9879 Merge remote-tracking branch 'origin/feature/coinswitch' into feature/coinswitch 2018-12-28 12:34:01 +01:00
444e761d41 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-28 12:33:55 +01:00
68a3def35a save state for coinswitch started order 2018-12-28 12:33:47 +01:00
3effdf0f4d Update UpdateCoinSwitchSettings.cshtml 2018-12-28 10:49:12 +01:00
3c122bcf53 Merge pull request #478 from mariodian/pos-fix-search-btn
PoS: Fix z-index of search cancel button that overlaps modal confirmation
2018-12-28 16:12:59 +09:00
037ff52f4f Fix z-index of search cancel button that overlaps modal confirmation 2018-12-28 11:13:04 +08:00
8b71556425 Merge branch 'master' into 455-disablereg 2018-12-20 21:58:07 +01:00
ae6e1bfd85 Update UpdateCoinSwitchSettings.cshtml 2018-12-20 21:01:10 +01:00
6d1f3b73ef update link 2018-12-20 20:40:33 +01:00
0dcaf80c7f Changed disable register mechanism to apply policy setting after admin user created rather than using DB user count checks. 2018-12-20 20:39:48 +01:00
fc9cd5bdf0 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-20 17:57:04 +01:00
a434c45196 fix function names 2018-12-20 17:56:57 +01:00
30a3a84ec9 fix final issues with integration 2018-12-20 14:33:31 +01:00
cfaa5766ed Always allow user registration if there are no user records. 2018-12-19 20:03:27 +01:00
8b08db308b Merge branch 'master' into 455-disablereg 2018-12-19 19:40:13 +01:00
3e2ff55954 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-19 10:41:36 +01:00
a34d1641b3 Set disable registration as default true. 2018-12-18 20:16:48 +01:00
40c645e433 coinswitch integration 2018-12-18 20:14:59 +01:00
b2e5415a35 coinswitch integration 2018-12-18 20:00:30 +01:00
2b4603a234 coinswitch integration 2018-12-18 19:01:58 +01:00
e3833914b3 Update AddDerivationScheme.cshtml 2018-12-13 16:18:01 +01:00
49cdec6961 Update PayButtonEnable.cshtml 2018-12-13 12:26:30 +01:00
c00c95efcf initial coinswitch work 2018-12-11 12:47:38 +01:00
71 changed files with 22377 additions and 228 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,9 @@ using BTCPayServer.Security;
using NBXplorer.Models;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
using NBitpayClient.Extensions;
using BTCPayServer.Services;
using System.Text.RegularExpressions;
using BTCPayServer.Events;
namespace BTCPayServer.Tests
{
@ -98,7 +101,7 @@ namespace BTCPayServer.Tests
Rate = 10513.44m,
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
TxFee = Money.Coins(0.00000100m),
NextNetworkFee = Money.Coins(0.00000100m),
DepositAddress = dummy
}));
paymentMethods.Add(new PaymentMethod()
@ -107,7 +110,7 @@ namespace BTCPayServer.Tests
Rate = 216.79m
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
TxFee = Money.Coins(0.00010000m),
NextNetworkFee = Money.Coins(0.00010000m),
DepositAddress = dummy
}));
invoiceEntity.SetPaymentMethods(paymentMethods);
@ -115,12 +118,12 @@ namespace BTCPayServer.Tests
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
var accounting = btc.Calculate();
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
}));
accounting = btc.Calculate();
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() { Value = accounting.Due }
}));
@ -147,7 +150,7 @@ namespace BTCPayServer.Tests
var entity = new InvoiceEntity();
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) });
entity.ProductInformation = new ProductInformation() { Price = 5000 };
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
@ -155,20 +158,20 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true, NetworkFee = 0.1m });
accounting = paymentMethod.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true, NetworkFee = 0.1m });
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true, NetworkFee = 0.1m });
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
@ -187,13 +190,13 @@ namespace BTCPayServer.Tests
{
CryptoCode = "BTC",
Rate = 1000,
TxFee = Money.Coins(0.1m)
NextNetworkFee = Money.Coins(0.1m)
});
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 500,
TxFee = Money.Coins(0.01m)
NextNetworkFee = Money.Coins(0.01m)
});
entity.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>();
@ -205,7 +208,7 @@ namespace BTCPayServer.Tests
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.1m });
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
@ -222,7 +225,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.01m });
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
@ -242,7 +245,7 @@ namespace BTCPayServer.Tests
Assert.Equal(2, accounting.TxRequired);
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true, NetworkFee = 0.1m });
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
@ -272,7 +275,7 @@ namespace BTCPayServer.Tests
var entity = new InvoiceEntity();
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) });
entity.ProductInformation = new ProductInformation() { Price = 5000 };
entity.PaymentTolerance = 0;
@ -349,7 +352,7 @@ namespace BTCPayServer.Tests
(1000.0001m, "₹ 1,000.00 (INR)", "INR")
})
{
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3);
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3);
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}
@ -484,7 +487,7 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
public void CanSendIPN()
public async Task CanSendIPN()
{
using (var callbackServer = new CustomServer())
{
@ -494,6 +497,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 5.0m,
@ -506,13 +510,43 @@ namespace BTCPayServer.Tests
ExtendedNotifications = true
});
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
Thread.Sleep(5000);
callbackServer.ProcessNextRequest((ctx) =>
bool receivedPayment = false;
bool paid = false;
bool confirmed = false;
bool completed = false;
while (!completed || !confirmed)
{
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
});
var request = await callbackServer.GetNextRequest();
if (request.ContainsKey("event"))
{
var evtName = request["event"]["name"].Value<string>();
switch (evtName)
{
case InvoiceEvent.Created:
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
break;
case InvoiceEvent.ReceivedPayment:
receivedPayment = true;
break;
case InvoiceEvent.PaidInFull:
Assert.True(receivedPayment);
tester.ExplorerNode.Generate(6);
paid = true;
break;
case InvoiceEvent.Confirmed:
Assert.True(paid);
confirmed = true;
break;
case InvoiceEvent.Completed:
Assert.True(paid); //TODO: Fix, out of order event mean we can receive invoice_confirmed after invoice_complete
completed = true;
break;
default:
Assert.False(true, $"{evtName} was not expected");
break;
}
}
}
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
Assert.NotNull(invoice2);
}
@ -726,6 +760,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
@ -1591,13 +1626,19 @@ donation:
[Trait("Integration", "Integration")]
public void CanExportInvoicesJson()
{
decimal GetFieldValue(string input, string fieldName)
{
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
Assert.True(match.Success);
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
}
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 10,
@ -1608,8 +1649,7 @@ donation:
FullNotifications = true
}, Facade.Merchant);
var networkFee = Money.Satoshis(10000);
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
// ensure 0 invoices exported because there are no payments yet
var jsonResult = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
var result = Assert.IsType<ContentResult>(jsonResult);
@ -1619,7 +1659,7 @@ donation:
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
//
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3*networkFee;
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
cashCow.SendToAddress(invoiceAddress, firstPayment);
Thread.Sleep(1000); // prevent race conditions, ordering payments
// look if you can reduce thread sleep, this was min value for me
@ -1629,7 +1669,7 @@ donation:
Thread.Sleep(1000);
// pay remaining amount
cashCow.SendToAddress(invoiceAddress, 4*networkFee);
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
Thread.Sleep(1000);
Eventually(() =>
@ -1641,21 +1681,102 @@ donation:
var parsedJson = JsonConvert.DeserializeObject<object[]>(paidresult.Content);
Assert.Equal(3, parsedJson.Length);
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
var pay1str = parsedJson[0].ToString();
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
Assert.Contains("\"InvoiceDue\": 1.5", pay1str);
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
var pay2str = parsedJson[1].ToString();
Assert.Contains("\"InvoiceDue\": 1.5", pay2str);
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
var pay3str = parsedJson[2].ToString();
Assert.Contains("\"InvoiceDue\": 0", pay3str);
});
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanChangeNetworkFeeMode()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
{
Logs.Tester.LogInformation($"Trying with {nameof(networkFeeMode)}={networkFeeMode}");
user.SetNetworkFeeMode(networkFeeMode);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 10,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var networkFee = Money.Satoshis(10000).ToDecimal(MoneyUnit.BTC);
var missingMoney = Money.Satoshis(5000).ToDecimal(MoneyUnit.BTC);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
// Check that for the first payment, no network fee are included
var due = Money.Parse(invoice.CryptoInfo[0].Due);
var productPartDue = (invoice.Price / invoice.Rate);
switch (networkFeeMode)
{
case NetworkFeeMode.MultiplePaymentsOnly:
case NetworkFeeMode.Never:
Assert.Equal(productPartDue, due.ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Always:
Assert.Equal(productPartDue + networkFee, due.ToDecimal(MoneyUnit.BTC));
break;
default:
throw new NotSupportedException(networkFeeMode.ToString());
}
var firstPayment = productPartDue - missingMoney;
cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment));
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
// Check that for the second payment, network fee are included
due = Money.Parse(invoice.CryptoInfo[0].Due);
Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid));
switch (networkFeeMode)
{
case NetworkFeeMode.MultiplePaymentsOnly:
Assert.Equal(missingMoney + networkFee, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney + networkFee, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Always:
Assert.Equal(missingMoney + 2 * networkFee, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney + 2 * networkFee, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Never:
Assert.Equal(missingMoney, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
default:
throw new NotSupportedException(networkFeeMode.ToString());
}
});
cashCow.SendToAddress(invoiceAddress, due);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
});
}
}
}
[Fact]
[Trait("Integration", "Integration")]
@ -1667,7 +1788,7 @@ donation:
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.36</Version>
<Version>1.0.3.39</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>

View File

@ -61,6 +61,12 @@ namespace BTCPayServer.Configuration
set;
} = new List<NBXplorerConnectionSetting>();
public bool DisableRegistration
{
get;
private set;
}
public static string GetDebugLog(IConfiguration configuration)
{
return configuration.GetValue<string>("debuglog", null);
@ -237,6 +243,8 @@ namespace BTCPayServer.Configuration
Logs.Configuration.LogInformation("LogFile: " + LogFile);
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
}
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
}
private SSHSettings ParseSSHConfiguration(IConfiguration conf)

View File

@ -43,6 +43,7 @@ namespace BTCPayServer.Configuration
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();

View File

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

View File

@ -73,8 +73,8 @@ namespace BTCPayServer.Controllers
Skip = offset,
EndDate = dateEnd,
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
OrderId = orderId == null ? null : new[] { orderId },
ItemCode = itemCode == null ? null : new[] { itemCode },
Status = status == null ? null : new[] { status },
StoreId = new[] { this.HttpContext.GetStoreData().Id }
};

View File

@ -13,6 +13,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
@ -258,6 +259,11 @@ namespace BTCPayServer.Controllers
storeBlob.ChangellySettings.IsConfigured())
? storeBlob.ChangellySettings
: null;
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
storeBlob.CoinSwitchSettings.IsConfigured())
? storeBlob.CoinSwitchSettings
: null;
var changellyAmountDue = changelly != null
@ -304,11 +310,14 @@ namespace BTCPayServer.Controllers
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
NetworkFee = paymentMethodDetails.GetTxFee(),
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
ChangellyEnabled = changelly != null,
ChangellyMerchantId = changelly?.ChangellyMerchantId,
ChangellyAmountDue = changellyAmountDue,
CoinSwitchEnabled = coinswitch != null,
CoinSwitchMerchantId = coinswitch?.MerchantId,
CoinSwitchMode = coinswitch?.Mode,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
@ -638,13 +647,13 @@ namespace BTCPayServer.Controllers
if (newState == "invalid")
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, InvoiceEvent.MarkedInvalid));
StatusMessage = "Invoice marked invalid";
}
else if(newState == "complete")
{
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, "invoice_markedComplete"));
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, InvoiceEvent.MarkedCompleted));
StatusMessage = "Invoice marked complete";
}
return RedirectToAction(nameof(ListInvoices));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Security;
using BTCPayServer.Rating;
@ -249,6 +250,12 @@ namespace BTCPayServer.Data
}
}
public enum NetworkFeeMode
{
MultiplePaymentsOnly,
Always,
Never
}
public class StoreBlob
{
public StoreBlob()
@ -258,10 +265,21 @@ namespace BTCPayServer.Data
PaymentTolerance = 0;
RequiresRefundEmail = true;
}
public bool NetworkFeeDisabled
[Obsolete("Use NetworkFeeMode instead")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool? NetworkFeeDisabled
{
get; set;
}
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public NetworkFeeMode NetworkFeeMode
{
get;
set;
}
public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; }
@ -305,6 +323,7 @@ namespace BTCPayServer.Data
public bool AnyoneCanInvoice { get; set; }
public ChangellySettings ChangellySettings { get; set; }
public CoinSwitchSettings CoinSwitchSettings { get; set; }
string _LightningDescriptionTemplate;

View File

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

View File

@ -341,14 +341,14 @@ namespace BTCPayServer.HostedServices
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
if (invoice.FullNotifications)
{
if (e.Name == "invoice_expired" ||
e.Name == "invoice_paidInFull" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_markedInvalid" ||
e.Name == "invoice_markedComplete" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_completed" ||
e.Name == "invoice_expiredPaidPartial"
if (e.Name == InvoiceEvent.Expired ||
e.Name == InvoiceEvent.PaidInFull ||
e.Name == InvoiceEvent.FailedToConfirm ||
e.Name == InvoiceEvent.MarkedInvalid ||
e.Name == InvoiceEvent.MarkedCompleted ||
e.Name == InvoiceEvent.FailedToConfirm ||
e.Name == InvoiceEvent.Completed ||
e.Name == InvoiceEvent.ExpiredPaidPartial
)
tasks.Add(Notify(invoice));
}

View File

@ -66,10 +66,10 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, InvoiceEvent.Expired));
invoice.Status = InvoiceStatus.Expired;
if(invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, InvoiceEvent.ExpiredPaidPartial));
}
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
@ -84,7 +84,7 @@ namespace BTCPayServer.HostedServices
{
if (invoice.Status == InvoiceStatus.New)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, InvoiceEvent.PaidInFull));
invoice.Status = InvoiceStatus.Paid;
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
@ -93,7 +93,7 @@ namespace BTCPayServer.HostedServices
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
{
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, InvoiceEvent.PaidAfterExpiration));
context.MarkDirty();
}
}
@ -139,14 +139,14 @@ namespace BTCPayServer.HostedServices
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, InvoiceEvent.FailedToConfirm));
invoice.Status = InvoiceStatus.Invalid;
context.MarkDirty();
}
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, InvoiceEvent.Confirmed));
invoice.Status = InvoiceStatus.Confirmed;
context.MarkDirty();
}
@ -157,7 +157,7 @@ namespace BTCPayServer.HostedServices
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, InvoiceEvent.Completed));
invoice.Status = InvoiceStatus.Complete;
context.MarkDirty();
}
@ -247,13 +247,13 @@ namespace BTCPayServer.HostedServices
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == "invoice_created")
if (b.Name == InvoiceEvent.Created)
{
Watch(b.Invoice.Id);
await Wait(b.Invoice.Id);
}
if (b.Name == "invoice_receivedPayment")
if (b.Name == InvoiceEvent.ReceivedPayment)
{
Watch(b.Invoice.Id);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,7 @@ namespace BTCPayServer.Payments.Bitcoin
public TxOut Output { get; set; }
public int ConfirmationCount { get; set; }
public bool RBF { get; set; }
public decimal NetworkFee { get; set; }
/// <summary>
/// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer

View File

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

View File

@ -158,7 +158,7 @@ namespace BTCPayServer.Payments.Bitcoin
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
if (!alreadyExist)
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network);
if(payment != null)
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
}
@ -332,7 +332,7 @@ namespace BTCPayServer.Payments.Bitcoin
{
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint);
if (payment != null)
{
@ -373,7 +373,7 @@ namespace BTCPayServer.Payments.Bitcoin
invoice.SetPaymentMethod(paymentMethod);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
return invoice;
}
public Task StopAsync(CancellationToken cancellationToken)

View File

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

View File

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

View File

@ -22,6 +22,8 @@ namespace BTCPayServer.Payments.Lightning
return GetPaymentId();
}
public decimal NetworkFee { get; set; }
public string GetPaymentId()
{
return BOLT11;

View File

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

View File

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

View File

@ -42,7 +42,7 @@ namespace BTCPayServer.Payments.Lightning
{
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
{
if (inv.Name == "invoice_created")
if (inv.Name == InvoiceEvent.Created)
{
await EnsureListening(inv.Invoice.Id, false);
}
@ -192,12 +192,12 @@ namespace BTCPayServer.Payments.Lightning
{
BOLT11 = notification.BOLT11,
Amount = notification.Amount
}, network.CryptoCode, accounted: true);
}, network, accounted: true);
if (payment != null)
{
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
if (invoice != null)
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
}
}

View File

@ -65,8 +65,7 @@ namespace BTCPayServer.Services.Invoices.Export
var pdata = payment.GetCryptoPaymentData();
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks);
var paymentFee = pmethod.GetPaymentMethodDetails().GetTxFee();
var paidAfterNetworkFees = pdata.GetValue() - paymentFee;
var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee;
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
var target = new ExportInvoiceHolder
@ -83,7 +82,7 @@ namespace BTCPayServer.Services.Invoices.Export
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
// and merchant effectivelly would receive 0 BTC, invoice won't be paid
// while looking just at export you could sum Paid and assume merchant "received payments"
NetworkFee = paymentFee.ToString(CultureInfo.InvariantCulture),
NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture),
InvoiceDue = invoiceDue,
OrderId = invoice.OrderId,
StoreId = invoice.StoreId,

View File

@ -485,7 +485,7 @@ namespace BTCPayServer.Services.Invoices
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider)
{
PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider);
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
var serializer = new Serializer(Dummy);
#pragma warning disable CS0618
if (PaymentMethod != null)
@ -499,11 +499,11 @@ namespace BTCPayServer.Services.Invoices
r.ParentEntity = this;
r.Network = networkProvider?.GetNetwork(r.CryptoCode);
if (r.Network != null || networkProvider == null)
rates.Add(r);
paymentMethods.Add(r);
}
}
#pragma warning restore CS0618
return rates;
return paymentMethods;
}
Network Dummy = Network.Main;
@ -517,8 +517,6 @@ namespace BTCPayServer.Services.Invoices
public void SetPaymentMethods(PaymentMethodDictionary paymentMethods)
{
if (paymentMethods.NetworkProvider != null)
throw new InvalidOperationException($"{nameof(paymentMethods)} should have NetworkProvider to null");
var obj = new JObject();
var serializer = new Serializer(Dummy);
#pragma warning disable CS0618
@ -731,7 +729,7 @@ namespace BTCPayServer.Services.Invoices
{
FeeRate = FeeRate,
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress,
TxFee = TxFee
NextNetworkFee = NextNetworkFee
};
}
else
@ -739,7 +737,7 @@ namespace BTCPayServer.Services.Invoices
var details = PaymentMethodExtensions.DeserializePaymentMethodDetails(GetId(), PaymentMethodDetails);
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
{
btcLike.TxFee = TxFee;
btcLike.NextNetworkFee = NextNetworkFee;
btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress;
btcLike.FeeRate = FeeRate;
}
@ -761,7 +759,7 @@ namespace BTCPayServer.Services.Invoices
if (paymentMethod is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod)
{
TxFee = bitcoinPaymentMethod.TxFee;
NextNetworkFee = bitcoinPaymentMethod.NextNetworkFee;
FeeRate = bitcoinPaymentMethod.FeeRate;
DepositAddress = bitcoinPaymentMethod.DepositAddress;
}
@ -776,8 +774,8 @@ namespace BTCPayServer.Services.Invoices
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).FeeRate")]
public FeeRate FeeRate { get; set; }
[JsonProperty(PropertyName = "txFee")]
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).TxFee")]
public Money TxFee { get; set; }
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).NextNetworkFee")]
public Money NextNetworkFee { get; set; }
[JsonProperty(PropertyName = "depositAddress")]
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
public string DepositAddress { get; set; }
@ -801,7 +799,7 @@ namespace BTCPayServer.Services.Invoices
.OrderBy(p => p.ReceivedTime)
.Select(_ =>
{
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetPaymentMethodId()].GetTxFee());
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee);
paid += _.GetValue(paymentMethods, GetId());
if (!paidEnough)
{
@ -842,17 +840,18 @@ namespace BTCPayServer.Services.Invoices
var method = GetPaymentMethodDetails();
if (method == null)
return 0.0m;
return method.GetTxFee();
return method.GetNextNetworkFee();
}
}
public class PaymentEntity
{
public int Version { get; set; }
public DateTimeOffset ReceivedTime
{
get; set;
}
public decimal NetworkFee { get; set; }
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")]
public OutPoint Outpoint
{
@ -889,7 +888,7 @@ namespace BTCPayServer.Services.Invoices
#pragma warning disable CS0618
if (string.IsNullOrEmpty(CryptoPaymentDataType))
{
// In case this is a payment done before this update, consider it unconfirmed with RBF for safety
// For invoices created when CryptoPaymentDataType was not existing, we just consider that it is a RBFed payment for safety
var paymentData = new Payments.Bitcoin.BitcoinLikePaymentData();
paymentData.Outpoint = Outpoint;
paymentData.Output = Output;

View File

@ -193,7 +193,7 @@ retry:
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
}
public async Task<bool> NewAddress(string invoiceId, IPaymentMethodDetails paymentMethod, BTCPayNetwork network)
public async Task<bool> NewAddress(string invoiceId, Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod paymentMethod, BTCPayNetwork network)
{
using (var context = _ContextFactory.CreateContext())
{
@ -206,14 +206,13 @@ retry:
if (currencyData == null)
return false;
var existingPaymentMethod = currencyData.GetPaymentMethodDetails();
var existingPaymentMethod = (Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)currencyData.GetPaymentMethodDetails();
if (existingPaymentMethod.GetPaymentDestination() != null)
{
MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId());
}
existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination());
currencyData.SetPaymentMethodDetails(existingPaymentMethod);
#pragma warning disable CS0618
if (network.IsBTC)
@ -379,11 +378,24 @@ retry:
private InvoiceEntity ToEntity(Data.InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
PaymentMethodDictionary paymentMethods = null;
#pragma warning disable CS0618
entity.Payments = invoice.Payments.Select(p =>
{
var paymentEntity = ToObject<PaymentEntity>(p.Blob, null);
paymentEntity.Accounted = p.Accounted;
// PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee.
// We want to hide this legacy detail in InvoiceRepository, so we fetch the fee from the PaymentMethod and assign it to the PaymentEntity.
if (paymentEntity.Version == 0)
{
if (paymentMethods == null)
paymentMethods = entity.GetPaymentMethods(null);
var paymentMethodDetails = paymentMethods.TryGet(paymentEntity.GetPaymentMethodId())?.GetPaymentMethodDetails();
if (paymentMethodDetails != null) // == null should never happen, but we never know.
paymentEntity.NetworkFee = paymentMethodDetails.GetNextNetworkFee();
}
return paymentEntity;
})
.OrderBy(a => a.ReceivedTime).ToList();
@ -451,11 +463,16 @@ retry:
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.ItemCode != null)
query = query.Where(i => i.ItemCode == queryObject.ItemCode);
if (queryObject.OrderId != null)
query = query.Where(i => i.OrderId == queryObject.OrderId);
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
{
var statusSet = queryObject.OrderId.ToHashSet();
query = query.Where(i => statusSet.Contains(i.OrderId));
}
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
{
var statusSet = queryObject.ItemCode.ToHashSet();
query = query.Where(i => statusSet.Contains(i.ItemCode));
}
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
@ -547,21 +564,37 @@ retry:
/// <param name="cryptoCode"></param>
/// <param name="accounted"></param>
/// <returns>The PaymentEntity or null if already added</returns>
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode, bool accounted = false)
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetwork network, bool accounted = false)
{
using (var context = _ContextFactory.CreateContext())
{
var invoice = context.Invoices.Find(invoiceId);
if (invoice == null)
return null;
InvoiceEntity invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob, network.NBitcoinNetwork);
PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType()), null);
IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
PaymentEntity entity = new PaymentEntity
{
Version = 1,
#pragma warning disable CS0618
CryptoCode = cryptoCode,
CryptoCode = network.CryptoCode,
#pragma warning restore CS0618
ReceivedTime = date.UtcDateTime,
Accounted = accounted
Accounted = accounted,
NetworkFee = paymentMethodDetails.GetNextNetworkFee()
};
entity.SetCryptoPaymentData(paymentData);
if (paymentMethodDetails is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod &&
bitcoinPaymentMethod.NetworkFeeMode == NetworkFeeMode.MultiplePaymentsOnly &&
bitcoinPaymentMethod.NextNetworkFee == Money.Zero)
{
bitcoinPaymentMethod.NextNetworkFee = bitcoinPaymentMethod.FeeRate.GetFee(100); // assume price for 100 bytes
paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod);
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork);
}
PaymentData data = new PaymentData
{
Id = paymentData.GetPaymentId(),
@ -666,12 +699,12 @@ retry:
get; set;
}
public string OrderId
public string[] OrderId
{
get; set;
}
public string ItemCode
public string[] ItemCode
{
get; set;
}

View File

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

View File

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

View File

@ -212,7 +212,7 @@
<div class="col-sm-8 col-lg-9 mb-2">
<div class="input-group mb-2">
<input type="text" class="js-search form-control" placeholder="Find product">
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 9999; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 1049; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
</div>
</div>
</div>

View File

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

View File

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

View File

@ -29,6 +29,8 @@
</p>
<ul>
<li><code>storeid:id</code> for filtering a specific store</li>
<li><code>orderid:id</code> for filtering a specific order</li>
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>

View File

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

View File

@ -37,7 +37,7 @@
No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page.
</p>
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use?</span>
<span>A ledger wallet is detected, which account do you want to use? No need to paste manually xpub if your ledger device was detected. Just select derivation scheme from the list bellow and xpub will automatically populate.</span>
<ul>
@for (int i = 0; i < 4; i++)
{

View File

@ -85,6 +85,15 @@
</div>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
<button name="command" type="submit" value="test" class="btn btn-secondary">Test connection</button>
<a
class="btn btn-secondary"
asp-controller="PublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.StoreId"
target="_blank">
Open Public Node Info Page
</a>
</form>
</div>
</div>

View File

@ -43,15 +43,15 @@
<div class="form-group">
<label>Button Size</label>
<div style="vertical-align:top; font-size:12px; display:flex;">
<button class="btn" style="width:95px;height:40px;margin-right:40px;"
<button class="btn" style="width:146px;height:40px;margin-right:40px;"
v-on:click="inputChanges($event, 0)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 0) }">
146 x 40 px
</button>
<button class="btn btn-default" style="width:126px;height:46px;margin-right:40px;"
<button class="btn btn-default" style="width:168px;height:46px;margin-right:40px;"
v-on:click="inputChanges($event, 1)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 1) }">
168 x 46 px
</button>
<button class="btn btn-default" style="width:146px;height:57px;"
<button class="btn btn-default" style="width:209px;height:57px;"
v-on:click="inputChanges($event, 2)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 2) }">
209 x 57 px
</button>

View File

@ -1,6 +1,6 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Please confirm you want to allow anyone to create invoices in your store");
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Please confirm you want to allow outside world to create invoices in your store (via API).");
ViewBag.MainTitle = "Pay Button";
}
@ -12,10 +12,10 @@
<div class="form-group">
<p>
To start using Pay Buttons you need to explicitly turn on this feature.
Once you do so, any source will be able to create an invoice on your instance store.
Once you do so, any 3rd party entity will be able to create an invoice on your instance store (via API for example).
</p>
@Html.Hidden("EnableStore", true)
<button name="command" type="submit" value="save" class="btn btn-primary">Allow anyone to create invoices</button>
<button name="command" type="submit" value="save" class="btn btn-primary">Allow outside world to create invoices via API (POS app is preauthorised and does not need this enabled).</button>
</div>
</form>
</div>

View File

@ -0,0 +1,42 @@
@using Microsoft.AspNetCore.Mvc.Rendering
@model UpdateCoinSwitchSettingsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store CoinSwitch Settings");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage"/>
<div class="row">
<div class="col-md-10">
<form method="post">
<p>
You can obtain a merchant id at
<a href="https://coinswitch.co/switch/setup/btcpay" target="_blank">
https://coinswitch.co/switch/setup/btcpay
</a>
</p>
<div class="form-group">
<label asp-for="MerchantId"></label>
<input asp-for="MerchantId" class="form-control"/>
<span asp-validation-for="MerchantId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Mode"></label>
<select asp-for="Mode" asp-items="Model.Modes" class="form-control" >
</select>
</div>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>
</div>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -43,9 +43,13 @@
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NetworkFee"></label>
<label asp-for="NetworkFeeMode"></label>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
<select asp-for="NetworkFeeMode" class="form-control">
<option value="MultiplePaymentsOnly">... only if the customer makes more than one payment for the invoice</option>
<option value="Always">... on every payment</option>
<option value="Never">Never add network fee</option>
</select>
</div>
<div class="form-group">
<label asp-for="AnyoneCanCreateInvoice"></label>

View File

@ -53,6 +53,16 @@
"wwwroot/checkout/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/lightning-node-info-bundle.min.js",
"inputFiles": [
"wwwroot/vendor/clipboard.js/clipboard.js",
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vuejs/vue-qrcode.js",
"wwwroot/vendor/vue-toasted/vue-toasted.min.js"
]
},
{
"outputFileName": "wwwroot/bundles/cart-bundle.min.js",
"inputFiles": [

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CoinSwitch</title>
<script>
var script = document.createElement('script'),
head = document.head || document.getElementsByTagName('head')[0];
script.src = 'https://files.coinswitch.co/public/js/cs_switch.js';
head.insertBefore(script, head.firstChild);
script.addEventListener('load', function () {
var qs = (function (a) {
if (a == "") return {};
var b = {};
for (var i = 0; i < a.length; ++i) {
var p = a[i].split('=', 2);
if (p.length == 1)
b[p[0]] = "";
else
b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
}
return b;
})(window.location.search.substr(1).split('&'));
var merchantId = qs["merchant_id"];
var toCurrencyDue = qs["toCurrencyDue"];
var toCurrencyAddress = qs["toCurrencyAddress"];
var toCurrency = qs["toCurrency"];
var mode = qs["mode"];
document.body.classList.add(mode);
var orderId = null;
var payment = new Coinswitch(merchantId);
if(window.localStorage){
orderId= window.localStorage.getItem(toCurrencyAddress);
}
var config = {
to_currency: toCurrency.toLowerCase(),
to_currency_address: toCurrencyAddress,
to_amount: parseFloat(toCurrencyDue),
state: orderId
};
waitForCoinSwitch();
function waitForCoinSwitch() {
if (typeof payment.open !== "function") {
setTimeout(waitForCoinSwitch, 1000);
return;
}
payment.open(config);
payment.on("Exchange:Complete", function () {
if(window.localStorage){
window.localStorage.removeItem(toCurrencyAddress);
}
window.close();
});
payment.on("Exchange:Initiated", function(orderId) {
if(window.localStorage){
window.localStorage.setItem(toCurrencyAddress,orderId);
}
});
payment.on("Exchange:Closed", function () {
window.close();
window.postMessage("popup-closed", "*");
})
}
});
</script>
<style>
body.popup #CoinSwitchPayment .cs-pay {
height: 100%;
}
body.popup #CoinSwitchPayment .cs-pay__main {
width: 100%;
min-height: 90vh;
padding-bottom: 20px;
}
body.popup #CoinSwitchPayment .cs-pay__dismiss-row {
display: none;
}
</style>
</head>
<body>
</body>
</html>

View File

@ -11504,3 +11504,15 @@ low-fee-timeline {
#prettydropdown-DefaultLang ul li{
width:100% !important;
}
[v-cloak] > * { display:none }
[v-cloak]::before { content: "loading…" }
.btn-link {
color: #329F80;
}
.btn-link:hover {
color: #24725B;
}

View File

@ -0,0 +1,66 @@
var CoinSwitchComponent =
{
props: ["toCurrency", "toCurrencyDue", "toCurrencyAddress", "merchantId", "autoload", "mode"],
data: function () {
return {
opened: false
};
},
computed: {
showInlineIFrame: function () {
return this.url && this.opened;
},
url: function () {
return window.location.origin + "/checkout/coinswitch.html?" +
"&toCurrency=" +
this.toCurrency +
"&toCurrencyAddress=" +
this.toCurrencyAddress +
"&toCurrencyDue=" +
this.toCurrencyDue +
"&mode=" +
this.mode +
(this.merchantId ? "&merchant_id=" + this.merchantId : "");
}
},
methods: {
openDialog: function (e) {
if (e && e.preventDefault) {
e.preventDefault();
}
if (this.mode === 'inline') {
this.opened = true;
} else if (this.mode === "popup") {
var coinSwitchWindow = window.open(
this.url,
'CoinSwitch',
'width=360,height=650,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0');
coinSwitchWindow.opener = null;
coinSwitchWindow.focus();
}
},
closeDialog: function () {
if (this.mode === 'inline') {
this.opened = false;
}
},
onLoadIframe: function (event) {
$("#prettydropdown-DefaultLang").hide();
var c = this.closeDialog.bind(this);
event.currentTarget.contentWindow.addEventListener("message", function (evt) {
if (evt && evt.data == "popup-closed") {
c();
$("#prettydropdown-DefaultLang").show();
}
});
}
},
mounted: function () {
if (this.autoload) {
this.openDialog();
}
}
};

View File

@ -6,10 +6,10 @@
"Awaiting Payment...": "お支払いをお待ちしております…",
"Pay with": "お支払い方法",
"Contact and Refund Email": "問題発生時の連絡先",
"Contact_Body": "決済において、何問題が発生したこちらのメールアドレスに対して連絡差し上げることもございますので、ご記入ください",
"Contact_Body": "決済において、何らかの問題が発生した場合こちらのメールアドレスに対して連絡差し上げることもございますので、ご記入お願い致します。",
"Your email": "ご自分のメールアドレス",
"Continue": "続ける",
"Please enter a valid email address": "正常なメールアドレスをご記入ください",
"Please enter a valid email address": "有効なメールアドレスをご記入ください",
"Order Amount": "注文金額",
"Network Cost": "ネットワーク手数料",
"Already Paid": "支払い済み金額",
@ -24,16 +24,16 @@
"Copied": "コピーしました",
"ConversionTab_BodyTop": "代わりに、お店が受け付けていなくても {{btcDue}} {{cryptoCode}} での支払いもできます。",
"ConversionTab_BodyDesc": "ただし、この変換は第三者サービスによるものですので、お店が受け付けている通貨で着金するまでの間の処理に関しては何の保証もいたしません。変換後に受付中の通貨 ({{cryptoCode}}) がお店に着金してから支払い済みとなりますのでご了承ください。",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_CalculateAmount_Error": "リトライ",
"ConversionTab_LoadCurrencies_Error": "リトライ",
"ConversionTab_Lightning": "ライトニングのペイメントでは現在変換サービスが存在しないためご利用いただけません。ご了承ください。",
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
"ConversionTab_CurrencyList_Select_Option": "変換する通貨を選択してください",
"Invoice expiring soon...": "お支払いの期限が迫っています...",
"Invoice expired": "お支払いの期限が切れました",
"What happened?": "え!?ナニコレ!?",
"InvoiceExpired_Body_1": "当件のお支払いの有効期限が過ぎてしまいました。最大 {{maxTimeMinutes}} 分以内に支払うことが義務付けられています。 \nまだお支払いのご希望の場合 {{storeName}} に一旦戻っていただき、もう一度お支払いの手続きを最初からやり直してみてください。",
"InvoiceExpired_Body_2": "送金手続きを完了したつもりでも、ネットワークにて取り込まれて処理されるまでは決済となりません。現時点ではまだ着金しておりません。",
"InvoiceExpired_Body_3": "",
"InvoiceExpired_Body_3": "後日着金を確認した場合、ご注文をそのまま処理させていただくか、払い戻しの為に連絡させていただく場合がございます...",
"Invoice ID": "お支払い ID",
"Order ID": "ご注文 ID",
"Return to StoreName": "{{storeName}} に戻る",

View File

@ -4,42 +4,42 @@
"currentLanguage": "Türkçe",
"lang": "Dil",
"Awaiting Payment...": "Ödeme Bekleniyor...",
"Pay with": "Aracılığıyla Öde",
"Contact and Refund Email": "İletişim ve Geri Ödeme Email",
"Contact_Body": "Lütfen aşağıda bir email belirtin. Ödemede sorun olması halinde bu adresten sizinle iletişime geçeceğiz.",
"Your email": "Email Adresiniz",
"Pay with": "Şu para ile öde",
"Contact and Refund Email": "İletişim ve Geri Ödeme E-postası",
"Contact_Body": "Lütfen aşağıda bir e-posta belirtin. Ödemede sorun olması halinde bu adresten sizinle iletişime geçeceğiz.",
"Your email": "E-posta Adresiniz",
"Continue": "Devam Et",
"Please enter a valid email address": "Lütfen geçerli bir email adresi giriniz",
"Order Amount": "Ödeme Tutarı",
"Please enter a valid email address": "Lütfen geçerli bir e-posta adresi giriniz",
"Order Amount": "Sipariş Tutarı",
"Network Cost": "Ağ Ücreti",
"Already Paid": "Zaten Ödendi",
"Due": "Bitiş Tarihi",
"Already Paid": "Ödenmiş Olan",
"Due": "Borç Bakiyesi",
"Scan": "Tara",
"Copy": "Kopyala",
"Conversion": "Dönüştürme",
"Conversion": "Dönüşüm Oranı",
"Open in wallet": "Cüzdanda Aç",
"CompletePay_Body": "Ödemenizi tamamlamak için, lütfen {{btcDue}} {{cryptoCode}} bilgilerini aşağıdaki adrese gönderin.",
"CompletePay_Body": "Ödemenizi tamamlamak için, lütfen aşağıdaki adrese {{btcDue}} {{cryptoCode}} gönderin.",
"Amount": "Tutar",
"Address": "Adres",
"Copied": "Kopyalandı",
"ConversionTab_BodyTop": "{{btcDue}} {{cryptoCode}} ödemenizi satıcının desteklemediği altcoinler ile de yapabilirsiniz.",
"ConversionTab_BodyDesc": "Bu servis 3. kişiler tarafından sağlanmaktadır. Ödemelerinizin iletildiği sağlayıcılar üzerinde bizim herhangi bir yetkimiz bulunmamaktadır. Faturanın ödendi olarak işaretlenmesi için ödemenin {{cryptoCode}} Blockchain ağından gelmesi gerekmektedir.",
"ConversionTab_BodyDesc": "Bu servis 3. kişiler tarafından sağlanmaktadır. Ödemenizi ileten sağlayıcıların bunu nasıl yapacağı üzerinde bizim herhangi bir yetkimiz bulunmamaktadır. Fatura ancak ödeme {{cryptoCode}} Blok Zincirinde alındığında ödenmiş olarak kabul edilecektir.",
"ConversionTab_CalculateAmount_Error": "Tekar dene",
"ConversionTab_LoadCurrencies_Error": "Tekrar dene",
"ConversionTab_Lightning": "Lightning Network ödemesi için dönüşüm sağlayıcı mevcut değil.",
"ConversionTab_Lightning": "Lightning ı ödemesi için dönüşüm sağlayıcı mevcut değil.",
"ConversionTab_CurrencyList_Select_Option": "Lütfen çevrim için bir para birimi seçin",
"Invoice expiring soon...": "Fatura son kullanım tarihi yakın...",
"Invoice expired": "Fatura son kullanım tarihi geçti",
"What happened?": "Ne oldu?",
"InvoiceExpired_Body_1": "Bu Fatura'nın son kullanım tarihi doldu. Bir Fatura sadece {{maxTimeMinutes}}  dakika geçerlidir.\nÖdemenizi tekrar göndermek istiyorsanız {{storeName}} mağazasına dönebilirsiniz.",
"InvoiceExpired_Body_1": "Bu Fatura'nın son kullanım tarihi doldu. Bir Fatura sadece {{maxTimeMinutes}} dakika geçerlidir.\nÖdemenizi tekrar göndermek istiyorsanız {{storeName}} mağazasına dönebilirsiniz.",
"InvoiceExpired_Body_2": "Ödemenizi göndermeyi denediyseniz, ağ tarafından henüz onaylanmadı. Ödemeniz bize ulaşmadı.",
"InvoiceExpired_Body_3": "Eğer daha sonra bize ulaşırsa, ya siparişinizi onaylarız ya da iade için sizinle iletişime geçeriz.",
"Invoice ID": "Fatura ID",
"Order ID": "Sipariş ID",
"Return to StoreName": "{{storeName}} dön",
"This invoice has been paid": "Bu Fatura ödendi",
"This invoice has been archived": "Bu Fatura arşivlendi",
"Archived_Body": "Bilgi ve yardım için lütfen mağaza ile iletişime geçin",
"This invoice has been paid": "Bu fatura ödendi",
"This invoice has been archived": "Bu fatura arşivlendi",
"Archived_Body": "Sipariş bilgisi veya yardım için lütfen mağaza ile iletişime geçin",
"BOLT 11 Invoice": "BOLT 11 Faturası",
"Node Info": "Düğüm Bilgisi",
"txCount": "{{count}} işlem",

View File

@ -0,0 +1,33 @@
/*
2017 Julian Garnier
Released under the MIT license
*/
var $jscomp={scope:{}};$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(e,r,p){if(p.get||p.set)throw new TypeError("ES3 does not support getters and setters.");e!=Array.prototype&&e!=Object.prototype&&(e[r]=p.value)};$jscomp.getGlobal=function(e){return"undefined"!=typeof window&&window===e?e:"undefined"!=typeof global&&null!=global?global:e};$jscomp.global=$jscomp.getGlobal(this);$jscomp.SYMBOL_PREFIX="jscomp_symbol_";
$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(e){return $jscomp.SYMBOL_PREFIX+(e||"")+$jscomp.symbolCounter_++};
$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var e=$jscomp.global.Symbol.iterator;e||(e=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[e]&&$jscomp.defineProperty(Array.prototype,e,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};$jscomp.arrayIterator=function(e){var r=0;return $jscomp.iteratorPrototype(function(){return r<e.length?{done:!1,value:e[r++]}:{done:!0}})};
$jscomp.iteratorPrototype=function(e){$jscomp.initSymbolIterator();e={next:e};e[$jscomp.global.Symbol.iterator]=function(){return this};return e};$jscomp.array=$jscomp.array||{};$jscomp.iteratorFromArray=function(e,r){$jscomp.initSymbolIterator();e instanceof String&&(e+="");var p=0,m={next:function(){if(p<e.length){var u=p++;return{value:r(u,e[u]),done:!1}}m.next=function(){return{done:!0,value:void 0}};return m.next()}};m[Symbol.iterator]=function(){return m};return m};
$jscomp.polyfill=function(e,r,p,m){if(r){p=$jscomp.global;e=e.split(".");for(m=0;m<e.length-1;m++){var u=e[m];u in p||(p[u]={});p=p[u]}e=e[e.length-1];m=p[e];r=r(m);r!=m&&null!=r&&$jscomp.defineProperty(p,e,{configurable:!0,writable:!0,value:r})}};$jscomp.polyfill("Array.prototype.keys",function(e){return e?e:function(){return $jscomp.iteratorFromArray(this,function(e){return e})}},"es6-impl","es3");var $jscomp$this=this;
(function(e,r){"function"===typeof define&&define.amd?define([],r):"object"===typeof module&&module.exports?module.exports=r():e.anime=r()})(this,function(){function e(a){if(!h.col(a))try{return document.querySelectorAll(a)}catch(c){}}function r(a,c){for(var d=a.length,b=2<=arguments.length?arguments[1]:void 0,f=[],n=0;n<d;n++)if(n in a){var k=a[n];c.call(b,k,n,a)&&f.push(k)}return f}function p(a){return a.reduce(function(a,d){return a.concat(h.arr(d)?p(d):d)},[])}function m(a){if(h.arr(a))return a;
h.str(a)&&(a=e(a)||a);return a instanceof NodeList||a instanceof HTMLCollection?[].slice.call(a):[a]}function u(a,c){return a.some(function(a){return a===c})}function C(a){var c={},d;for(d in a)c[d]=a[d];return c}function D(a,c){var d=C(a),b;for(b in a)d[b]=c.hasOwnProperty(b)?c[b]:a[b];return d}function z(a,c){var d=C(a),b;for(b in c)d[b]=h.und(a[b])?c[b]:a[b];return d}function T(a){a=a.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,function(a,c,d,k){return c+c+d+d+k+k});var c=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(a);
a=parseInt(c[1],16);var d=parseInt(c[2],16),c=parseInt(c[3],16);return"rgba("+a+","+d+","+c+",1)"}function U(a){function c(a,c,b){0>b&&(b+=1);1<b&&--b;return b<1/6?a+6*(c-a)*b:.5>b?c:b<2/3?a+(c-a)*(2/3-b)*6:a}var d=/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(a)||/hsla\((\d+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)/g.exec(a);a=parseInt(d[1])/360;var b=parseInt(d[2])/100,f=parseInt(d[3])/100,d=d[4]||1;if(0==b)f=b=a=f;else{var n=.5>f?f*(1+b):f+b-f*b,k=2*f-n,f=c(k,n,a+1/3),b=c(k,n,a);a=c(k,n,a-1/3)}return"rgba("+
255*f+","+255*b+","+255*a+","+d+")"}function y(a){if(a=/([\+\-]?[0-9#\.]+)(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(a))return a[2]}function V(a){if(-1<a.indexOf("translate")||"perspective"===a)return"px";if(-1<a.indexOf("rotate")||-1<a.indexOf("skew"))return"deg"}function I(a,c){return h.fnc(a)?a(c.target,c.id,c.total):a}function E(a,c){if(c in a.style)return getComputedStyle(a).getPropertyValue(c.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase())||"0"}function J(a,c){if(h.dom(a)&&
u(W,c))return"transform";if(h.dom(a)&&(a.getAttribute(c)||h.svg(a)&&a[c]))return"attribute";if(h.dom(a)&&"transform"!==c&&E(a,c))return"css";if(null!=a[c])return"object"}function X(a,c){var d=V(c),d=-1<c.indexOf("scale")?1:0+d;a=a.style.transform;if(!a)return d;for(var b=[],f=[],n=[],k=/(\w+)\((.+?)\)/g;b=k.exec(a);)f.push(b[1]),n.push(b[2]);a=r(n,function(a,b){return f[b]===c});return a.length?a[0]:d}function K(a,c){switch(J(a,c)){case "transform":return X(a,c);case "css":return E(a,c);case "attribute":return a.getAttribute(c)}return a[c]||
0}function L(a,c){var d=/^(\*=|\+=|-=)/.exec(a);if(!d)return a;var b=y(a)||0;c=parseFloat(c);a=parseFloat(a.replace(d[0],""));switch(d[0][0]){case "+":return c+a+b;case "-":return c-a+b;case "*":return c*a+b}}function F(a,c){return Math.sqrt(Math.pow(c.x-a.x,2)+Math.pow(c.y-a.y,2))}function M(a){a=a.points;for(var c=0,d,b=0;b<a.numberOfItems;b++){var f=a.getItem(b);0<b&&(c+=F(d,f));d=f}return c}function N(a){if(a.getTotalLength)return a.getTotalLength();switch(a.tagName.toLowerCase()){case "circle":return 2*
Math.PI*a.getAttribute("r");case "rect":return 2*a.getAttribute("width")+2*a.getAttribute("height");case "line":return F({x:a.getAttribute("x1"),y:a.getAttribute("y1")},{x:a.getAttribute("x2"),y:a.getAttribute("y2")});case "polyline":return M(a);case "polygon":var c=a.points;return M(a)+F(c.getItem(c.numberOfItems-1),c.getItem(0))}}function Y(a,c){function d(b){b=void 0===b?0:b;return a.el.getPointAtLength(1<=c+b?c+b:0)}var b=d(),f=d(-1),n=d(1);switch(a.property){case "x":return b.x;case "y":return b.y;
case "angle":return 180*Math.atan2(n.y-f.y,n.x-f.x)/Math.PI}}function O(a,c){var d=/-?\d*\.?\d+/g,b;b=h.pth(a)?a.totalLength:a;if(h.col(b))if(h.rgb(b)){var f=/rgb\((\d+,\s*[\d]+,\s*[\d]+)\)/g.exec(b);b=f?"rgba("+f[1]+",1)":b}else b=h.hex(b)?T(b):h.hsl(b)?U(b):void 0;else f=(f=y(b))?b.substr(0,b.length-f.length):b,b=c&&!/\s/g.test(b)?f+c:f;b+="";return{original:b,numbers:b.match(d)?b.match(d).map(Number):[0],strings:h.str(a)||c?b.split(d):[]}}function P(a){a=a?p(h.arr(a)?a.map(m):m(a)):[];return r(a,
function(a,d,b){return b.indexOf(a)===d})}function Z(a){var c=P(a);return c.map(function(a,b){return{target:a,id:b,total:c.length}})}function aa(a,c){var d=C(c);if(h.arr(a)){var b=a.length;2!==b||h.obj(a[0])?h.fnc(c.duration)||(d.duration=c.duration/b):a={value:a}}return m(a).map(function(a,b){b=b?0:c.delay;a=h.obj(a)&&!h.pth(a)?a:{value:a};h.und(a.delay)&&(a.delay=b);return a}).map(function(a){return z(a,d)})}function ba(a,c){var d={},b;for(b in a){var f=I(a[b],c);h.arr(f)&&(f=f.map(function(a){return I(a,
c)}),1===f.length&&(f=f[0]));d[b]=f}d.duration=parseFloat(d.duration);d.delay=parseFloat(d.delay);return d}function ca(a){return h.arr(a)?A.apply(this,a):Q[a]}function da(a,c){var d;return a.tweens.map(function(b){b=ba(b,c);var f=b.value,e=K(c.target,a.name),k=d?d.to.original:e,k=h.arr(f)?f[0]:k,w=L(h.arr(f)?f[1]:f,k),e=y(w)||y(k)||y(e);b.from=O(k,e);b.to=O(w,e);b.start=d?d.end:a.offset;b.end=b.start+b.delay+b.duration;b.easing=ca(b.easing);b.elasticity=(1E3-Math.min(Math.max(b.elasticity,1),999))/
1E3;b.isPath=h.pth(f);b.isColor=h.col(b.from.original);b.isColor&&(b.round=1);return d=b})}function ea(a,c){return r(p(a.map(function(a){return c.map(function(b){var c=J(a.target,b.name);if(c){var d=da(b,a);b={type:c,property:b.name,animatable:a,tweens:d,duration:d[d.length-1].end,delay:d[0].delay}}else b=void 0;return b})})),function(a){return!h.und(a)})}function R(a,c,d,b){var f="delay"===a;return c.length?(f?Math.min:Math.max).apply(Math,c.map(function(b){return b[a]})):f?b.delay:d.offset+b.delay+
b.duration}function fa(a){var c=D(ga,a),d=D(S,a),b=Z(a.targets),f=[],e=z(c,d),k;for(k in a)e.hasOwnProperty(k)||"targets"===k||f.push({name:k,offset:e.offset,tweens:aa(a[k],d)});a=ea(b,f);return z(c,{children:[],animatables:b,animations:a,duration:R("duration",a,c,d),delay:R("delay",a,c,d)})}function q(a){function c(){return window.Promise&&new Promise(function(a){return p=a})}function d(a){return g.reversed?g.duration-a:a}function b(a){for(var b=0,c={},d=g.animations,f=d.length;b<f;){var e=d[b],
k=e.animatable,h=e.tweens,n=h.length-1,l=h[n];n&&(l=r(h,function(b){return a<b.end})[0]||l);for(var h=Math.min(Math.max(a-l.start-l.delay,0),l.duration)/l.duration,w=isNaN(h)?1:l.easing(h,l.elasticity),h=l.to.strings,p=l.round,n=[],m=void 0,m=l.to.numbers.length,t=0;t<m;t++){var x=void 0,x=l.to.numbers[t],q=l.from.numbers[t],x=l.isPath?Y(l.value,w*x):q+w*(x-q);p&&(l.isColor&&2<t||(x=Math.round(x*p)/p));n.push(x)}if(l=h.length)for(m=h[0],w=0;w<l;w++)p=h[w+1],t=n[w],isNaN(t)||(m=p?m+(t+p):m+(t+" "));
else m=n[0];ha[e.type](k.target,e.property,m,c,k.id);e.currentValue=m;b++}if(b=Object.keys(c).length)for(d=0;d<b;d++)H||(H=E(document.body,"transform")?"transform":"-webkit-transform"),g.animatables[d].target.style[H]=c[d].join(" ");g.currentTime=a;g.progress=a/g.duration*100}function f(a){if(g[a])g[a](g)}function e(){g.remaining&&!0!==g.remaining&&g.remaining--}function k(a){var k=g.duration,n=g.offset,w=n+g.delay,r=g.currentTime,x=g.reversed,q=d(a);if(g.children.length){var u=g.children,v=u.length;
if(q>=g.currentTime)for(var G=0;G<v;G++)u[G].seek(q);else for(;v--;)u[v].seek(q)}if(q>=w||!k)g.began||(g.began=!0,f("begin")),f("run");if(q>n&&q<k)b(q);else if(q<=n&&0!==r&&(b(0),x&&e()),q>=k&&r!==k||!k)b(k),x||e();f("update");a>=k&&(g.remaining?(t=h,"alternate"===g.direction&&(g.reversed=!g.reversed)):(g.pause(),g.completed||(g.completed=!0,f("complete"),"Promise"in window&&(p(),m=c()))),l=0)}a=void 0===a?{}:a;var h,t,l=0,p=null,m=c(),g=fa(a);g.reset=function(){var a=g.direction,c=g.loop;g.currentTime=
0;g.progress=0;g.paused=!0;g.began=!1;g.completed=!1;g.reversed="reverse"===a;g.remaining="alternate"===a&&1===c?2:c;b(0);for(a=g.children.length;a--;)g.children[a].reset()};g.tick=function(a){h=a;t||(t=h);k((l+h-t)*q.speed)};g.seek=function(a){k(d(a))};g.pause=function(){var a=v.indexOf(g);-1<a&&v.splice(a,1);g.paused=!0};g.play=function(){g.paused&&(g.paused=!1,t=0,l=d(g.currentTime),v.push(g),B||ia())};g.reverse=function(){g.reversed=!g.reversed;t=0;l=d(g.currentTime)};g.restart=function(){g.pause();
g.reset();g.play()};g.finished=m;g.reset();g.autoplay&&g.play();return g}var ga={update:void 0,begin:void 0,run:void 0,complete:void 0,loop:1,direction:"normal",autoplay:!0,offset:0},S={duration:1E3,delay:0,easing:"easeOutElastic",elasticity:500,round:0},W="translateX translateY translateZ rotate rotateX rotateY rotateZ scale scaleX scaleY scaleZ skewX skewY perspective".split(" "),H,h={arr:function(a){return Array.isArray(a)},obj:function(a){return-1<Object.prototype.toString.call(a).indexOf("Object")},
pth:function(a){return h.obj(a)&&a.hasOwnProperty("totalLength")},svg:function(a){return a instanceof SVGElement},dom:function(a){return a.nodeType||h.svg(a)},str:function(a){return"string"===typeof a},fnc:function(a){return"function"===typeof a},und:function(a){return"undefined"===typeof a},hex:function(a){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a)},rgb:function(a){return/^rgb/.test(a)},hsl:function(a){return/^hsl/.test(a)},col:function(a){return h.hex(a)||h.rgb(a)||h.hsl(a)}},A=function(){function a(a,
d,b){return(((1-3*b+3*d)*a+(3*b-6*d))*a+3*d)*a}return function(c,d,b,f){if(0<=c&&1>=c&&0<=b&&1>=b){var e=new Float32Array(11);if(c!==d||b!==f)for(var k=0;11>k;++k)e[k]=a(.1*k,c,b);return function(k){if(c===d&&b===f)return k;if(0===k)return 0;if(1===k)return 1;for(var h=0,l=1;10!==l&&e[l]<=k;++l)h+=.1;--l;var l=h+(k-e[l])/(e[l+1]-e[l])*.1,n=3*(1-3*b+3*c)*l*l+2*(3*b-6*c)*l+3*c;if(.001<=n){for(h=0;4>h;++h){n=3*(1-3*b+3*c)*l*l+2*(3*b-6*c)*l+3*c;if(0===n)break;var m=a(l,c,b)-k,l=l-m/n}k=l}else if(0===
n)k=l;else{var l=h,h=h+.1,g=0;do m=l+(h-l)/2,n=a(m,c,b)-k,0<n?h=m:l=m;while(1e-7<Math.abs(n)&&10>++g);k=m}return a(k,d,f)}}}}(),Q=function(){function a(a,b){return 0===a||1===a?a:-Math.pow(2,10*(a-1))*Math.sin(2*(a-1-b/(2*Math.PI)*Math.asin(1))*Math.PI/b)}var c="Quad Cubic Quart Quint Sine Expo Circ Back Elastic".split(" "),d={In:[[.55,.085,.68,.53],[.55,.055,.675,.19],[.895,.03,.685,.22],[.755,.05,.855,.06],[.47,0,.745,.715],[.95,.05,.795,.035],[.6,.04,.98,.335],[.6,-.28,.735,.045],a],Out:[[.25,
.46,.45,.94],[.215,.61,.355,1],[.165,.84,.44,1],[.23,1,.32,1],[.39,.575,.565,1],[.19,1,.22,1],[.075,.82,.165,1],[.175,.885,.32,1.275],function(b,c){return 1-a(1-b,c)}],InOut:[[.455,.03,.515,.955],[.645,.045,.355,1],[.77,0,.175,1],[.86,0,.07,1],[.445,.05,.55,.95],[1,0,0,1],[.785,.135,.15,.86],[.68,-.55,.265,1.55],function(b,c){return.5>b?a(2*b,c)/2:1-a(-2*b+2,c)/2}]},b={linear:A(.25,.25,.75,.75)},f={},e;for(e in d)f.type=e,d[f.type].forEach(function(a){return function(d,f){b["ease"+a.type+c[f]]=h.fnc(d)?
d:A.apply($jscomp$this,d)}}(f)),f={type:f.type};return b}(),ha={css:function(a,c,d){return a.style[c]=d},attribute:function(a,c,d){return a.setAttribute(c,d)},object:function(a,c,d){return a[c]=d},transform:function(a,c,d,b,f){b[f]||(b[f]=[]);b[f].push(c+"("+d+")")}},v=[],B=0,ia=function(){function a(){B=requestAnimationFrame(c)}function c(c){var b=v.length;if(b){for(var d=0;d<b;)v[d]&&v[d].tick(c),d++;a()}else cancelAnimationFrame(B),B=0}return a}();q.version="2.2.0";q.speed=1;q.running=v;q.remove=
function(a){a=P(a);for(var c=v.length;c--;)for(var d=v[c],b=d.animations,f=b.length;f--;)u(a,b[f].animatable.target)&&(b.splice(f,1),b.length||d.pause())};q.getValue=K;q.path=function(a,c){var d=h.str(a)?e(a)[0]:a,b=c||100;return function(a){return{el:d,property:a,totalLength:N(d)*(b/100)}}};q.setDashoffset=function(a){var c=N(a);a.setAttribute("stroke-dasharray",c);return c};q.bezier=A;q.easings=Q;q.timeline=function(a){var c=q(a);c.pause();c.duration=0;c.add=function(d){c.children.forEach(function(a){a.began=
!0;a.completed=!0});m(d).forEach(function(b){var d=z(b,D(S,a||{}));d.targets=d.targets||a.targets;b=c.duration;var e=d.offset;d.autoplay=!1;d.direction=c.direction;d.offset=h.und(e)?b:L(e,b);c.began=!0;c.completed=!0;c.seek(d.offset);d=q(d);d.began=!0;d.completed=!0;d.duration>b&&(c.duration=d.duration);c.children.push(d)});c.seek(0);c.reset();c.autoplay&&c.restart();return c};return c};q.random=function(a,c){return Math.floor(Math.random()*(c-a+1))+a};return q});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,311 @@
.fade-enter-active, .fade-leave-active {
transition: opacity .15s linear;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
/* workaround for https://github.com/bootstrap-vue/bootstrap-vue/issues/1560 */
/* source: _input-group.scss */
.input-group > .input-group-prepend > .b-dropdown > .btn,
.input-group > .input-group-append:not(:last-child) > .b-dropdown > .btn,
.input-group > .input-group-append:last-child > .b-dropdown:not(:last-child):not(.dropdown-toggle) > .btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > .input-group-append > .b-dropdown > .btn,
.input-group > .input-group-prepend:not(:first-child) > .b-dropdown > .btn,
.input-group > .input-group-prepend:first-child > .b-dropdown:not(:first-child) > .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Special styling for type=range and type=color input */
input.form-control[type="range"],
input.form-control[type="color"] {
height: 2.25rem;
}
input.form-control.form-control-sm[type="range"],
input.form-control.form-control-sm[type="color"] {
height: 1.9375rem;
}
input.form-control.form-control-lg[type="range"],
input.form-control.form-control-lg[type="color"] {
height: 3rem;
}
/* Less padding on type=color */
input.form-control[type="color"] {
padding: 0.25rem 0.25rem;
}
input.form-control.form-control-sm[type="color"] {
padding: 0.125rem 0.125rem;
}
/* Add support for fixed layout table */
table.b-table.b-table-fixed {
table-layout: fixed;
}
/* Busy table styling */
table.b-table[aria-busy='false'] {
opacity: 1;
}
table.b-table[aria-busy='true'] {
opacity: 0.6;
}
/* Sort styling */
table.b-table > thead > tr > th,
table.b-table > tfoot > tr > th {
position: relative;
}
table.b-table > thead > tr > th.sorting,
table.b-table > tfoot > tr > th.sorting {
padding-right: 1.5em;
cursor: pointer;
}
table.b-table > thead > tr > th.sorting::before,
table.b-table > thead > tr > th.sorting::after,
table.b-table > tfoot > tr > th.sorting::before,
table.b-table > tfoot > tr > th.sorting::after {
position: absolute;
bottom: 0;
display: block;
opacity: 0.4;
padding-bottom: inherit;
font-size: inherit;
line-height: 180%;
}
table.b-table > thead > tr > th.sorting::before,
table.b-table > tfoot > tr > th.sorting::before {
right: 0.75em;
content: '\2191';
}
table.b-table > thead > tr > th.sorting::after,
table.b-table > tfoot > tr > th.sorting::after {
right: 0.25em;
content: '\2193';
}
table.b-table > thead > tr > th.sorting_asc::after,
table.b-table > thead > tr > th.sorting_desc::before,
table.b-table > tfoot > tr > th.sorting_asc::after,
table.b-table > tfoot > tr > th.sorting_desc::before {
opacity: 1;
}
/* Stacked table layout */
/* Derived from http://blog.adrianroselli.com/2017/11/a-responsive-accessible-table.html */
/* Always stacked */
table.b-table.b-table-stacked {
width: 100%;
}
table.b-table.b-table-stacked,
table.b-table.b-table-stacked > tbody,
table.b-table.b-table-stacked > tbody > tr,
table.b-table.b-table-stacked > tbody > tr > td,
table.b-table.b-table-stacked > tbody > tr > th,
table.b-table.b-table-stacked > caption {
display: block;
}
/* Hide stuff we can't deal with, or shouldn't show */
table.b-table.b-table-stacked > thead,
table.b-table.b-table-stacked > tfoot,
table.b-table.b-table-stacked > tbody > tr.b-table-top-row,
table.b-table.b-table-stacked > tbody > tr.b-table-bottom-row {
display: none;
}
/* inter-row top border */
table.b-table.b-table-stacked > tbody > tr > :first-child {
border-top-width: 0.4rem;
}
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
table.b-table.b-table-stacked > tbody > tr > [data-label] {
display: grid;
grid-template-columns: 40% auto;
grid-gap: 0.25rem 1rem;
}
/* generate row cell "heading" */
table.b-table.b-table-stacked > tbody > tr > [data-label]::before {
content: attr(data-label);
display: inline;
text-align: right;
overflow-wrap: break-word;
font-weight: bold;
font-style: normal;
}
@media all and (max-width: 575.99px) {
/* Under SM */
table.b-table.b-table-stacked-sm {
width: 100%;
}
table.b-table.b-table-stacked-sm,
table.b-table.b-table-stacked-sm > tbody,
table.b-table.b-table-stacked-sm > tbody > tr,
table.b-table.b-table-stacked-sm > tbody > tr > td,
table.b-table.b-table-stacked-sm > tbody > tr > th,
table.b-table.b-table-stacked-sm > caption {
display: block;
}
/* hide stuff we can't deal with, or shouldn't show */
table.b-table.b-table-stacked-sm > thead,
table.b-table.b-table-stacked-sm > tfoot,
table.b-table.b-table-stacked-sm > tbody > tr.b-table-top-row,
table.b-table.b-table-stacked-sm > tbody > tr.b-table-bottom-row {
display: none;
}
/* inter-row top border */
table.b-table.b-table-stacked-sm > tbody > tr > :first-child {
border-top-width: 0.4rem;
}
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
table.b-table.b-table-stacked-sm > tbody > tr > [data-label] {
display: grid;
grid-template-columns: 40% auto;
grid-gap: 0.25rem 1rem;
}
/* generate row cell "heading" */
table.b-table.b-table-stacked-sm > tbody > tr > [data-label]::before {
content: attr(data-label);
display: inline;
text-align: right;
overflow-wrap: break-word;
font-weight: bold;
font-style: normal;
}
}
@media all and (max-width: 767.99px) {
/* under MD */
table.b-table.b-table-stacked-md {
width: 100%;
}
table.b-table.b-table-stacked-md,
table.b-table.b-table-stacked-md > tbody,
table.b-table.b-table-stacked-md > tbody > tr,
table.b-table.b-table-stacked-md > tbody > tr > td,
table.b-table.b-table-stacked-md > tbody > tr > th,
table.b-table.b-table-stacked-md > caption {
display: block;
}
/* hide stuff we can't deal with, or shouldn't show */
table.b-table.b-table-stacked-md > thead,
table.b-table.b-table-stacked-md > tfoot,
table.b-table.b-table-stacked-md > tbody > tr.b-table-top-row,
table.b-table.b-table-stacked-md > tbody > tr.b-table-bottom-row {
display: none;
}
/* inter-row top border */
table.b-table.b-table-stacked-md > tbody > tr > :first-child {
border-top-width: 0.4rem;
}
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
table.b-table.b-table-stacked-md > tbody > tr > [data-label] {
display: grid;
grid-template-columns: 40% auto;
grid-gap: 0.25rem 1rem;
}
/* generate row cell "heading" */
table.b-table.b-table-stacked-md > tbody > tr > [data-label]::before {
content: attr(data-label);
display: inline;
text-align: right;
overflow-wrap: break-word;
font-weight: bold;
font-style: normal;
}
}
@media all and (max-width: 991.99px) {
/* under LG */
table.b-table.b-table-stacked-lg {
width: 100%;
}
table.b-table.b-table-stacked-lg,
table.b-table.b-table-stacked-lg > tbody,
table.b-table.b-table-stacked-lg > tbody > tr,
table.b-table.b-table-stacked-lg > tbody > tr > td,
table.b-table.b-table-stacked-lg > tbody > tr > th,
table.b-table.b-table-stacked-lg > caption {
display: block;
}
/* hide stuff we can't deal with, or shouldn't show */
table.b-table.b-table-stacked-lg > thead,
table.b-table.b-table-stacked-lg > tfoot,
table.b-table.b-table-stacked-lg > tbody > tr.b-table-top-row,
table.b-table.b-table-stacked-lg > tbody > tr.b-table-bottom-row {
display: none;
}
/* inter-row top border */
table.b-table.b-table-stacked-lg > tbody > tr > :first-child {
border-top-width: 0.4rem;
}
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
table.b-table.b-table-stacked-lg > tbody > tr > [data-label] {
display: grid;
grid-template-columns: 40% auto;
grid-gap: 0.25rem 1rem;
}
/* generate row cell "heading" */
table.b-table.b-table-stacked-lg > tbody > tr > [data-label]::before {
content: attr(data-label);
display: inline;
text-align: right;
overflow-wrap: break-word;
font-weight: bold;
font-style: normal;
}
}
@media all and (max-width: 1199.99px) {
/* under XL */
table.b-table.b-table-stacked-xl {
width: 100%;
}
table.b-table.b-table-stacked-xl,
table.b-table.b-table-stacked-xl > tbody,
table.b-table.b-table-stacked-xl > tbody > tr,
table.b-table.b-table-stacked-xl > tbody > tr > td,
table.b-table.b-table-stacked-xl > tbody > tr > th,
table.b-table.b-table-stacked-xl > caption {
display: block;
}
/* hide stuff we can't deal with, or shouldn't show */
table.b-table.b-table-stacked-xl > thead,
table.b-table.b-table-stacked-xl > tfoot,
table.b-table.b-table-stacked-xl > tbody > tr.b-table-top-row,
table.b-table.b-table-stacked-xl > tbody > tr.b-table-bottom-row {
display: none;
}
/* inter-row top border */
table.b-table.b-table-stacked-xl > tbody > tr > :first-child {
border-top-width: 0.4rem;
}
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
table.b-table.b-table-stacked-xl > tbody > tr > [data-label] {
display: grid;
grid-template-columns: 40% auto;
grid-gap: 0.25rem 1rem;
}
/* generate row cell "heading" */
table.b-table.b-table-stacked-xl > tbody > tr > [data-label]::before {
content: attr(data-label);
display: inline;
text-align: right;
overflow-wrap: break-word;
font-weight: bold;
font-style: normal;
}
}
/* Details row styling */
table.b-table > tbody > tr.b-table-details > td {
border-top: none;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long