Compare commits
42 Commits
v1.0.3.125
...
v1.0.3.127
Author | SHA1 | Date | |
---|---|---|---|
63df6ac5eb | |||
039bee5b65 | |||
be5597085b | |||
6b355cbe1b | |||
dec5d19a2f | |||
ff533994d8 | |||
221e2c7898 | |||
fb77fddcc3 | |||
c37086e000 | |||
3d6783b743 | |||
c479e6aae5 | |||
59a770e0d7 | |||
140259e737 | |||
59a391dcc9 | |||
3a1cdefa09 | |||
7be104f486 | |||
d90a65975c | |||
4e53f59a9c | |||
8e58fc128d | |||
756b6e9692 | |||
23d546c559 | |||
6d4ea6a951 | |||
f9b5dcd4a6 | |||
eab679cb2b | |||
ddf8b20091 | |||
f1457582fe | |||
7841f79f31 | |||
56e5acfb65 | |||
6b777878e3 | |||
428c7c5444 | |||
f8427eb801 | |||
2a53c056ca | |||
21d555ee6b | |||
d79fda166f | |||
c8025ebaac | |||
42d7ad02b0 | |||
21556d4c07 | |||
89a7166c1b | |||
5d6c28c997 | |||
717cadc64b | |||
3dac7ef3f3 | |||
056cb60d5d |
239
BTCPayServer.Tests/PaymentHandlerTest.cs
Normal file
239
BTCPayServer.Tests/PaymentHandlerTest.cs
Normal file
@ -0,0 +1,239 @@
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class PaymentHandlerTest
|
||||
{
|
||||
private BitcoinLikePaymentHandler handlerBTC;
|
||||
private LightningLikePaymentHandler handlerLN;
|
||||
private Dictionary<CurrencyPair, Task<RateResult>> currencyPairRateResult;
|
||||
|
||||
public PaymentHandlerTest(ITestOutputHelper helper)
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
|
||||
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
|
||||
var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
|
||||
currencyPairRateResult = new Dictionary<CurrencyPair, Task<RateResult>>();
|
||||
|
||||
var rateResultUSDBTC = new RateResult();
|
||||
rateResultUSDBTC.BidAsk= new BidAsk(1m);
|
||||
|
||||
var rateResultBTCUSD = new RateResult();
|
||||
rateResultBTCUSD.BidAsk= new BidAsk(1m);
|
||||
|
||||
currencyPairRateResult.Add(new CurrencyPair("USD", "BTC"), Task.FromResult(rateResultUSDBTC));
|
||||
currencyPairRateResult.Add(new CurrencyPair("BTC", "USD"), Task.FromResult(rateResultBTCUSD));
|
||||
|
||||
handlerBTC = new BitcoinLikePaymentHandler(null, networkProvider, null, null);
|
||||
handlerLN = new LightningLikePaymentHandler(null, null, networkProvider, null);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithLightningWhenInvoiceTotalUnderLightningMaxValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = null,
|
||||
LightningMaxValue = new CurrencyValue() {Value = 100.00m, Currency = "USD"}
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(98m, MoneyUnit.BTC);
|
||||
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerLN.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.Equal(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CannotPayWithLightningWhenInvoiceTotalAboveLightningMaxValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = null,
|
||||
LightningMaxValue = new CurrencyValue() {Value = 100.00m, Currency = "USD"}
|
||||
};
|
||||
var totalInvoiceAmount = new Money(102m, MoneyUnit.BTC);
|
||||
|
||||
//When
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerLN.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.NotEqual(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithLightningWhenInvoiceTotalEqualLightningMaxValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = null,
|
||||
LightningMaxValue = new CurrencyValue() {Value = 100.00m, Currency = "USD"}
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(100m, MoneyUnit.BTC);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerLN.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.Equal(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithBitcoinWhenInvoiceTotalAboveOnChainMinValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = new CurrencyValue() {Value = 100.00m, Currency = "USD"},
|
||||
LightningMaxValue = null
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(105m, MoneyUnit.BTC);
|
||||
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerBTC.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.Equal(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CannotPayWithBitcoinWhenInvoiceTotalUnderOnChainMinValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = new CurrencyValue() {Value = 100.00m, Currency = "USD"},
|
||||
LightningMaxValue = null
|
||||
};
|
||||
var totalInvoiceAmount = new Money(98m, MoneyUnit.BTC);
|
||||
|
||||
//When
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerBTC.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.NotEqual(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithBitcoinWhenInvoiceTotalEqualOnChainMinValue()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = new CurrencyValue() {Value = 100.00m, Currency = "USD"},
|
||||
LightningMaxValue = null
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(100m, MoneyUnit.BTC);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerBTC.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.Equal(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CannotPayWithBitcoinWhenInvoiceTotalUnderOnChainMinValueWhenLightningMaxValueIsGreater()
|
||||
{
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
//Given
|
||||
var store = new StoreBlob
|
||||
{
|
||||
OnChainMinValue = new CurrencyValue() {Value = 50.00m, Currency = "USD"},
|
||||
LightningMaxValue = new CurrencyValue() {Value = 100.00m, Currency = "USD"}
|
||||
};
|
||||
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
|
||||
//When
|
||||
var totalInvoiceAmount = new Money(45m, MoneyUnit.BTC);
|
||||
|
||||
//Then
|
||||
var errorMessage = handlerBTC.IsPaymentMethodAllowedBasedOnInvoiceAmount(store, currencyPairRateResult,
|
||||
totalInvoiceAmount, paymentMethodId);
|
||||
|
||||
Assert.NotEqual(errorMessage.Result, string.Empty);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -242,7 +242,9 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("input#EnableShoppingCart.form-check")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
|
||||
Assert.True(s.Driver.PageSource.Contains("App updated"), "Unable to create PoS");
|
||||
s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
@ -727,7 +727,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanRescanWallet()
|
||||
public async Task CanRescanWallet()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
@ -789,6 +789,32 @@ namespace BTCPayServer.Tests
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
var tx = Assert.Single(transactions.Transactions);
|
||||
Assert.Equal(tx.Id, txId.ToString());
|
||||
|
||||
// Hijack the test to see if we can add label and comments
|
||||
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addlabel: "test"));
|
||||
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addlabelclick: "test2"));
|
||||
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
|
||||
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
Assert.Contains("test", tx.Labels.Select(l => l.Value));
|
||||
Assert.Contains("test2", tx.Labels.Select(l => l.Value));
|
||||
Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count());
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
||||
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
Assert.Contains("test", tx.Labels.Select(l => l.Value));
|
||||
Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Value));
|
||||
Assert.Single(tx.Labels.GroupBy(l => l.Color));
|
||||
|
||||
var walletInfo = await tester.PayTester.GetService<WalletRepository>().GetWalletInfo(walletId);
|
||||
Assert.Single(walletInfo.LabelColors); // the test2 color should have been removed
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.217" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
@ -47,7 +47,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.7" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.9" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
|
||||
<PackageReference Include="OpenIddict" Version="2.0.0" />
|
||||
|
@ -183,7 +183,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
Version = u2fChallenge[0].version,
|
||||
Challenge = u2fChallenge[0].challenge,
|
||||
Challenges = JsonConvert.SerializeObject(u2fChallenge),
|
||||
Challenges = u2fChallenge,
|
||||
AppId = u2fChallenge[0].appId,
|
||||
UserId = user.Id,
|
||||
RememberMe = rememberMe
|
||||
|
@ -132,7 +132,7 @@ namespace BTCPayServer.Controllers
|
||||
EnforceTargetAmount = vm.EnforceTargetAmount,
|
||||
StartDate = vm.StartDate?.ToUniversalTime(),
|
||||
TargetCurrency = vm.TargetCurrency,
|
||||
Description = _htmlSanitizer.Sanitize( vm.Description),
|
||||
Description = vm.Description,
|
||||
EndDate = vm.EndDate?.ToUniversalTime(),
|
||||
TargetAmount = vm.TargetAmount,
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
|
@ -192,7 +192,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId });
|
||||
}
|
||||
|
||||
private async Task UpdateAppSettings(AppData app)
|
||||
|
@ -30,7 +30,6 @@ namespace BTCPayServer.Controllers
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
AppService AppService)
|
||||
{
|
||||
@ -39,7 +38,6 @@ namespace BTCPayServer.Controllers
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_AppService = AppService;
|
||||
}
|
||||
@ -49,7 +47,6 @@ namespace BTCPayServer.Controllers
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private AppService _AppService;
|
||||
|
||||
|
@ -269,7 +269,7 @@ namespace BTCPayServer.Controllers
|
||||
handler
|
||||
.IsPaymentMethodAllowedBasedOnInvoiceAmount(storeBlob, fetchingByCurrencyPair,
|
||||
paymentMethod.Calculate().Due, supportedPaymentMethod.PaymentId);
|
||||
if (errorMessage != null)
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
logs.Write($"{logPrefix} {errorMessage}");
|
||||
return null;
|
||||
|
@ -3,6 +3,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ManageViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -11,6 +12,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ManageController
|
||||
{
|
||||
private const string RecoveryCodesKey = nameof(RecoveryCodesKey);
|
||||
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
[HttpGet]
|
||||
@ -80,18 +82,8 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
var model = new EnableAuthenticatorViewModel
|
||||
{
|
||||
SharedKey = FormatKey(unformattedKey),
|
||||
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
|
||||
};
|
||||
var model = new EnableAuthenticatorViewModel();
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user, model);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
@ -100,32 +92,36 @@ namespace BTCPayServer.Controllers
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user, model);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture)
|
||||
.Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
|
||||
ModelState.AddModelError("Code", "Verification code is invalid.");
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user, model);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
TempData[RecoveryCodesKey] = recoveryCodes.ToArray();
|
||||
|
||||
return RedirectToAction(nameof(GenerateRecoveryCodes));
|
||||
}
|
||||
|
||||
@ -153,7 +149,20 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateRecoveryCodes()
|
||||
public IActionResult GenerateRecoveryCodes()
|
||||
{
|
||||
var recoveryCodes = (string[])TempData[RecoveryCodesKey];
|
||||
if (recoveryCodes == null)
|
||||
{
|
||||
return RedirectToAction(nameof(TwoFactorAuthentication));
|
||||
}
|
||||
|
||||
var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateRecoveryCodesWarning()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
@ -163,16 +172,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException(
|
||||
$"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
|
||||
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' because they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes.ToArray()};
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
|
||||
|
||||
return View(model);
|
||||
return View(nameof(GenerateRecoveryCodesWarning));
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
@ -201,5 +204,19 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
|
||||
private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user, EnableAuthenticatorViewModel model)
|
||||
{
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
model.SharedKey = FormatKey(unformattedKey);
|
||||
model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ namespace BTCPayServer.Controllers
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly CurrencyNameTable _Currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
|
||||
public PaymentRequestController(
|
||||
@ -52,7 +51,6 @@ namespace BTCPayServer.Controllers
|
||||
PaymentRequestService paymentRequestService,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer,
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
@ -62,7 +60,6 @@ namespace BTCPayServer.Controllers
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_Currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
@ -152,7 +149,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
blob.Title = viewModel.Title;
|
||||
blob.Email = viewModel.Email;
|
||||
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
|
||||
blob.Description = viewModel.Description;
|
||||
blob.Amount = viewModel.Amount;
|
||||
blob.ExpiryDate = viewModel.ExpiryDate?.ToUniversalTime();
|
||||
blob.Currency = viewModel.Currency;
|
||||
|
@ -44,12 +44,11 @@ namespace BTCPayServer.Controllers
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private readonly NBXplorerDashboard _dashBoard;
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
private readonly TorServices _torServices;
|
||||
BTCPayServerOptions _Options;
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
private BTCPayServerOptions _Options;
|
||||
private readonly AppService _AppService;
|
||||
private readonly StoredFileRepository _StoredFileRepository;
|
||||
private readonly FileService _FileService;
|
||||
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
|
||||
@ -59,14 +58,13 @@ namespace BTCPayServer.Controllers
|
||||
FileService fileService,
|
||||
IEnumerable<IStorageProviderService> storageProviderServices,
|
||||
BTCPayServerOptions options,
|
||||
RateFetcher rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
NBXplorerDashboard dashBoard,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
TorServices torServices,
|
||||
StoreRepository storeRepository,
|
||||
ApplicationDbContextFactory contextFactory)
|
||||
AppService appService)
|
||||
{
|
||||
_Options = options;
|
||||
_StoredFileRepository = storedFileRepository;
|
||||
@ -76,11 +74,10 @@ namespace BTCPayServer.Controllers
|
||||
_SettingsRepository = settingsRepository;
|
||||
_dashBoard = dashBoard;
|
||||
HttpClientFactory = httpClientFactory;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
_torServices = torServices;
|
||||
_ContextFactory = contextFactory;
|
||||
_AppService = appService;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -503,19 +500,16 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (appIdsToFetch.Any())
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
var apps = (await _AppService.GetApps(appIdsToFetch.ToArray()))
|
||||
.ToDictionary(data => data.Id, data => Enum.Parse<AppType>(data.AppType));;
|
||||
if (!string.IsNullOrEmpty(settings.RootAppId))
|
||||
{
|
||||
var apps = await ctx.Apps.Where(data => appIdsToFetch.Contains(data.Id))
|
||||
.ToDictionaryAsync(data => data.Id, data => Enum.Parse<AppType>(data.AppType));
|
||||
if (!string.IsNullOrEmpty(settings.RootAppId))
|
||||
{
|
||||
settings.RootAppType = apps[settings.RootAppId];
|
||||
}
|
||||
settings.RootAppType = apps[settings.RootAppId];
|
||||
}
|
||||
|
||||
foreach (var domainToAppMappingItem in settings.DomainToAppMapping)
|
||||
{
|
||||
domainToAppMappingItem.AppType = apps[domainToAppMappingItem.AppId];
|
||||
}
|
||||
foreach (var domainToAppMappingItem in settings.DomainToAppMapping)
|
||||
{
|
||||
domainToAppMappingItem.AppType = apps[domainToAppMappingItem.AppId];
|
||||
}
|
||||
}
|
||||
|
||||
@ -585,18 +579,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private async Task<List<SelectListItem>> GetAppSelectList()
|
||||
{
|
||||
// load display app dropdown
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var userId = _UserManager.GetUserId(base.User);
|
||||
var selectList = await ctx.Users.Where(user => user.Id == userId)
|
||||
.SelectMany(s => s.UserStores)
|
||||
.Select(s => s.StoreData)
|
||||
.SelectMany(s => s.Apps)
|
||||
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToListAsync();
|
||||
selectList.Insert(0, new SelectListItem("(None)", null));
|
||||
return selectList;
|
||||
}
|
||||
var apps = (await _AppService.GetAllApps(null, true))
|
||||
.Select(a => new SelectListItem($"{a.AppType} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
|
||||
apps.Insert(0, new SelectListItem("(None)", null));
|
||||
return apps;
|
||||
}
|
||||
|
||||
private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService)
|
||||
@ -871,6 +857,10 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> DynamicDnsService(DynamicDnsViewModel viewModel, string hostname, string command = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();
|
||||
|
||||
var i = settings.Services.FindIndex(d => d.Hostname.Equals(hostname, StringComparison.OrdinalIgnoreCase));
|
||||
|
@ -486,7 +486,7 @@ namespace BTCPayServer.Controllers
|
||||
Crypto = paymentMethodId.CryptoCode,
|
||||
Value = strategy?.ToPrettyString() ?? string.Empty,
|
||||
WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(paymentMethodId)
|
||||
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null
|
||||
});
|
||||
break;
|
||||
case LightningPaymentType _:
|
||||
@ -495,7 +495,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode,
|
||||
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
|
||||
Enabled = !excludeFilters.Match(paymentMethodId)
|
||||
Enabled = !excludeFilters.Match(paymentMethodId) && lightning?.GetLightningUrl() != null
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ namespace BTCPayServer.Controllers
|
||||
public partial class WalletsController : Controller
|
||||
{
|
||||
public StoreRepository Repository { get; }
|
||||
public WalletRepository WalletRepository { get; }
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
public ExplorerClientProvider ExplorerClientProvider { get; }
|
||||
|
||||
@ -54,6 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
CurrencyNameTable _currencyTable;
|
||||
public WalletsController(StoreRepository repo,
|
||||
WalletRepository walletRepository,
|
||||
CurrencyNameTable currencyTable,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
@ -66,6 +68,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
Repository = repo;
|
||||
WalletRepository = walletRepository;
|
||||
RateFetcher = rateProvider;
|
||||
NetworkProvider = networkProvider;
|
||||
_userManager = userManager;
|
||||
@ -76,6 +79,112 @@ namespace BTCPayServer.Controllers
|
||||
_walletProvider = walletProvider;
|
||||
}
|
||||
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
string[] LabelColorScheme = new string[]
|
||||
{
|
||||
"#fbca04",
|
||||
"#0e8a16",
|
||||
"#ff7619",
|
||||
"#84b6eb",
|
||||
"#5319e7",
|
||||
"#000000",
|
||||
"#cc317c",
|
||||
};
|
||||
|
||||
const int MaxLabelSize = 20;
|
||||
const int MaxCommentSize = 200;
|
||||
[HttpPost]
|
||||
[Route("{walletId}")]
|
||||
public async Task<IActionResult> ModifyTransaction(
|
||||
// We need addlabel and addlabelclick. addlabel is the + button if the label does not exists,
|
||||
// addlabelclick is if the user click on existing label. For some reason, reusing the same name attribute for both
|
||||
// does not work
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string transactionId,
|
||||
string addlabel = null,
|
||||
string addlabelclick = null,
|
||||
string addcomment = null,
|
||||
string removelabel = null)
|
||||
{
|
||||
addlabel = addlabel ?? addlabelclick;
|
||||
// Hack necessary when the user enter a empty comment and submit.
|
||||
// For some reason asp.net consider addcomment null instead of empty string...
|
||||
try
|
||||
{
|
||||
if (addcomment == null && Request?.Form?.TryGetValue(nameof(addcomment), out _) is true)
|
||||
{
|
||||
addcomment = string.Empty;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
/////////
|
||||
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var walletBlobInfoAsync = WalletRepository.GetWalletInfo(walletId);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletBlobInfo = await walletBlobInfoAsync;
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
if (addlabel != null)
|
||||
{
|
||||
addlabel = addlabel.Trim().ToLowerInvariant().Replace(',',' ').Truncate(MaxLabelSize);
|
||||
var labels = walletBlobInfo.GetLabels();
|
||||
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
if (!labels.Any(l => l.Value.Equals(addlabel, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
List<string> allColors = new List<string>();
|
||||
allColors.AddRange(LabelColorScheme);
|
||||
allColors.AddRange(labels.Select(l => l.Color));
|
||||
var chosenColor =
|
||||
allColors
|
||||
.GroupBy(k => k)
|
||||
.OrderBy(k => k.Count())
|
||||
.ThenBy(k => Array.IndexOf(LabelColorScheme, k.Key))
|
||||
.First().Key;
|
||||
walletBlobInfo.LabelColors.Add(addlabel, chosenColor);
|
||||
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
|
||||
}
|
||||
if (walletTransactionInfo.Labels.Add(addlabel))
|
||||
{
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
}
|
||||
}
|
||||
else if (removelabel != null)
|
||||
{
|
||||
removelabel = removelabel.Trim().ToLowerInvariant().Truncate(MaxLabelSize);
|
||||
if (walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
if (walletTransactionInfo.Labels.Remove(removelabel))
|
||||
{
|
||||
var canDelete = !walletTransactionsInfo.SelectMany(txi => txi.Value.Labels).Any(l => l == removelabel);
|
||||
if (canDelete)
|
||||
{
|
||||
walletBlobInfo.LabelColors.Remove(removelabel);
|
||||
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
|
||||
}
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (addcomment != null)
|
||||
{
|
||||
addcomment = addcomment.Trim().Truncate(MaxCommentSize);
|
||||
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
walletTransactionInfo.Comment = addcomment;
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
}
|
||||
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ListWallets()
|
||||
{
|
||||
var wallets = new ListWalletsViewModel();
|
||||
@ -118,31 +227,48 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{walletId}")]
|
||||
public async Task<IActionResult> WalletTransactions(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
WalletId walletId, string labelFilter = null)
|
||||
{
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletBlobAsync = WalletRepository.GetWalletInfo(walletId);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation);
|
||||
|
||||
var walletBlob = await walletBlobAsync;
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
var model = new ListTransactionsViewModel();
|
||||
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
|
||||
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions).ToArray())
|
||||
{
|
||||
var vm = new ListTransactionsViewModel.TransactionViewModel();
|
||||
model.Transactions.Add(vm);
|
||||
vm.Id = tx.TransactionId.ToString();
|
||||
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink, vm.Id);
|
||||
vm.Timestamp = tx.Timestamp;
|
||||
vm.Positive = tx.BalanceChange >= Money.Zero;
|
||||
vm.Balance = tx.BalanceChange.ToString();
|
||||
vm.IsConfirmed = tx.Confirmations != 0;
|
||||
|
||||
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
|
||||
{
|
||||
var labels = walletBlob.GetLabels(transactionInfo);
|
||||
vm.Labels.AddRange(labels);
|
||||
model.Labels.AddRange(labels);
|
||||
vm.Comment = transactionInfo.Comment;
|
||||
}
|
||||
|
||||
if (labelFilter == null || vm.Labels.Any(l => l.Value.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
model.Transactions.Add(vm);
|
||||
}
|
||||
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private static string GetLabelTarget(WalletId walletId, uint256 txId)
|
||||
{
|
||||
return $"{walletId}:{txId}";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send")]
|
||||
|
@ -61,6 +61,9 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<WalletData> Wallets { get; set; }
|
||||
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
|
||||
|
||||
public DbSet<StoreData> Stores
|
||||
{
|
||||
get; set;
|
||||
@ -231,6 +234,18 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasIndex(o => o.Status);
|
||||
|
||||
builder.Entity<WalletTransactionData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
o.WalletDataId,
|
||||
#pragma warning disable CS0618
|
||||
o.TransactionId
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
builder.Entity<WalletTransactionData>()
|
||||
.HasOne(o => o.WalletData)
|
||||
.WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
|
||||
|
||||
}
|
||||
|
103
BTCPayServer/Data/WalletData.cs
Normal file
103
BTCPayServer/Data/WalletData.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletData
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
public string Id { get; set; }
|
||||
|
||||
public List<WalletTransactionData> WalletTransactions { get; set; }
|
||||
|
||||
public byte[] Blob { get; set; }
|
||||
|
||||
public WalletBlobInfo GetBlobInfo()
|
||||
{
|
||||
if (Blob == null || Blob.Length == 0)
|
||||
{
|
||||
return new WalletBlobInfo();
|
||||
}
|
||||
var blobInfo = JsonConvert.DeserializeObject<WalletBlobInfo>(ZipUtils.Unzip(Blob));
|
||||
return blobInfo;
|
||||
}
|
||||
public void SetBlobInfo(WalletBlobInfo blobInfo)
|
||||
{
|
||||
if (blobInfo == null)
|
||||
{
|
||||
Blob = Array.Empty<byte>();
|
||||
return;
|
||||
}
|
||||
Blob = ZipUtils.Zip(JsonConvert.SerializeObject(blobInfo));
|
||||
}
|
||||
}
|
||||
|
||||
public class Label
|
||||
{
|
||||
public Label(string value, string color)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
if (color == null)
|
||||
throw new ArgumentNullException(nameof(color));
|
||||
Value = value;
|
||||
Color = color;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
public string Color { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
Label item = obj as Label;
|
||||
if (item == null)
|
||||
return false;
|
||||
return Value.Equals(item.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public static bool operator ==(Label a, Label b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.Value == b.Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(Label a, Label b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode(StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class WalletBlobInfo
|
||||
{
|
||||
public Dictionary<string, string> LabelColors { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public IEnumerable<Label> GetLabels(WalletTransactionInfo transactionInfo)
|
||||
{
|
||||
foreach (var label in transactionInfo.Labels)
|
||||
{
|
||||
if (LabelColors.TryGetValue(label, out var color))
|
||||
{
|
||||
yield return new Label(label, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Label> GetLabels()
|
||||
{
|
||||
foreach (var kv in LabelColors)
|
||||
{
|
||||
yield return new Label(kv.Key, kv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
BTCPayServer/Data/WalletTransactionData.cs
Normal file
51
BTCPayServer/Data/WalletTransactionData.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletTransactionData
|
||||
{
|
||||
public string WalletDataId { get; set; }
|
||||
public WalletData WalletData { get; set; }
|
||||
public string TransactionId { get; set; }
|
||||
public string Labels { get; set; }
|
||||
public byte[] Blob { get; set; }
|
||||
|
||||
public WalletTransactionInfo GetBlobInfo()
|
||||
{
|
||||
if (Blob == null || Blob.Length == 0)
|
||||
{
|
||||
return new WalletTransactionInfo();
|
||||
}
|
||||
var blobInfo = JsonConvert.DeserializeObject<WalletTransactionInfo>(ZipUtils.Unzip(Blob));
|
||||
if (!string.IsNullOrEmpty(Labels))
|
||||
{
|
||||
blobInfo.Labels.AddRange(Labels.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
return blobInfo;
|
||||
}
|
||||
public void SetBlobInfo(WalletTransactionInfo blobInfo)
|
||||
{
|
||||
if (blobInfo == null)
|
||||
{
|
||||
Labels = string.Empty;
|
||||
Blob = Array.Empty<byte>();
|
||||
return;
|
||||
}
|
||||
if (blobInfo.Labels.Any(l => l.Contains(',', StringComparison.OrdinalIgnoreCase)))
|
||||
throw new ArgumentException(paramName: nameof(blobInfo), message: "Labels must not contains ','");
|
||||
Labels = String.Join(',', blobInfo.Labels);
|
||||
Blob = ZipUtils.Zip(JsonConvert.SerializeObject(blobInfo));
|
||||
}
|
||||
}
|
||||
|
||||
public class WalletTransactionInfo
|
||||
{
|
||||
public string Comment { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public HashSet<string> Labels { get; set; } = new HashSet<string>();
|
||||
}
|
||||
}
|
@ -40,6 +40,12 @@ namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string Truncate(this string value, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return value;
|
||||
return value.Length <= maxLength ? value : value.Substring(0, maxLength);
|
||||
}
|
||||
public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
|
||||
where T : class, IStartupTask
|
||||
=> services.AddTransient<IStartupTask, T>();
|
||||
|
@ -34,7 +34,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
|
||||
{
|
||||
var settings = await SettingsRepository.GetSettingAsync<DynamicDnsSettings>();
|
||||
var settings = await SettingsRepository.GetSettingAsync<DynamicDnsSettings>() ?? new DynamicDnsSettings();
|
||||
foreach (var service in settings.Services)
|
||||
{
|
||||
if (service?.Enabled is true && (service.LastUpdated is null ||
|
||||
|
@ -90,6 +90,7 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<WalletRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<U2FService>();
|
||||
@ -125,6 +126,7 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
|
||||
services.TryAddSingleton<AppService>();
|
||||
services.TryAddTransient<Safe>();
|
||||
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
||||
{
|
||||
|
||||
|
885
BTCPayServer/Migrations/20190802142637_WalletData.Designer.cs
generated
Normal file
885
BTCPayServer/Migrations/20190802142637_WalletData.Designer.cs
generated
Normal file
@ -0,0 +1,885 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20190802142637_WalletData")]
|
||||
partial class WalletData
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.11-servicing-32099");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ApplicationId");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("Properties");
|
||||
|
||||
b.Property<string>("Scopes");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasMaxLength(450);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
|
||||
|
||||
b.ToTable("OpenIddictAuthorizations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<string>("ClientSecret");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("ConsentType");
|
||||
|
||||
b.Property<string>("DisplayName");
|
||||
|
||||
b.Property<string>("Permissions");
|
||||
|
||||
b.Property<string>("PostLogoutRedirectUris");
|
||||
|
||||
b.Property<string>("Properties");
|
||||
|
||||
b.Property<string>("RedirectUris");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.HasIndex("ClientId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OpenIddictApplications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdToken", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ApplicationId");
|
||||
|
||||
b.Property<string>("AuthorizationId");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<DateTimeOffset?>("CreationDate");
|
||||
|
||||
b.Property<DateTimeOffset?>("ExpirationDate");
|
||||
|
||||
b.Property<string>("Payload");
|
||||
|
||||
b.Property<string>("Properties");
|
||||
|
||||
b.Property<string>("ReferenceId")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasMaxLength(450);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorizationId");
|
||||
|
||||
b.HasIndex("ReferenceId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
|
||||
|
||||
b.ToTable("OpenIddictTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("AppType");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<bool>("TagAllInvoices");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Apps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Wallets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.Property<string>("WalletDataId");
|
||||
|
||||
b.Property<string>("TransactionId");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("Labels");
|
||||
|
||||
b.HasKey("WalletDataId", "TransactionId");
|
||||
|
||||
b.ToTable("WalletTransactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
|
||||
b.Property<int>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PaymentRequests");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<byte[]>("AttestationCert")
|
||||
.IsRequired();
|
||||
|
||||
b.Property<int>("Counter");
|
||||
|
||||
b.Property<byte[]>("KeyHandle")
|
||||
.IsRequired();
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<byte[]>("PublicKey")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.ToTable("U2FDevices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("FileName");
|
||||
|
||||
b.Property<string>("StorageFileName");
|
||||
|
||||
b.Property<DateTime>("Timestamp");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope<string>", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("Description");
|
||||
|
||||
b.Property<string>("DisplayName");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200);
|
||||
|
||||
b.Property<string>("Properties");
|
||||
|
||||
b.Property<string>("Resources");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OpenIddictScopes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", "Application")
|
||||
.WithMany("Authorizations")
|
||||
.HasForeignKey("ApplicationId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("OpenIdClients")
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdToken", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", "Application")
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("ApplicationId");
|
||||
|
||||
b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", "Authorization")
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("AuthorizationId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Invoices")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PairedSINs")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletData", "WalletData")
|
||||
.WithMany("WalletTransactions")
|
||||
.HasForeignKey("WalletDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PaymentRequests")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("U2FDevices")
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("StoredFiles")
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
52
BTCPayServer/Migrations/20190802142637_WalletData.cs
Normal file
52
BTCPayServer/Migrations/20190802142637_WalletData.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class WalletData : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Wallets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Wallets", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WalletTransactions",
|
||||
columns: table => new
|
||||
{
|
||||
WalletDataId = table.Column<string>(nullable: false),
|
||||
TransactionId = table.Column<string>(nullable: false),
|
||||
Labels = table.Column<string>(nullable: true),
|
||||
Blob = table.Column<byte[]>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WalletTransactions", x => new { x.WalletDataId, x.TransactionId });
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletTransactions_Wallets_WalletDataId",
|
||||
column: x => x.WalletDataId,
|
||||
principalTable: "Wallets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "WalletTransactions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Wallets");
|
||||
}
|
||||
}
|
||||
}
|
@ -399,6 +399,33 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Wallets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.Property<string>("WalletDataId");
|
||||
|
||||
b.Property<string>("TransactionId");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("Labels");
|
||||
|
||||
b.HasKey("WalletDataId", "TransactionId");
|
||||
|
||||
b.ToTable("WalletTransactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -776,6 +803,14 @@ namespace BTCPayServer.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletData", "WalletData")
|
||||
.WithMany("WalletTransactions")
|
||||
.HasForeignKey("WalletDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
|
@ -61,6 +61,9 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
|
||||
public string RedirectAutomatically { get; set; } = string.Empty;
|
||||
|
||||
public string AppId { get; set; }
|
||||
public string SearchTerm { get; set; }
|
||||
|
||||
public SelectList RedirectAutomaticallySelectList =>
|
||||
new SelectList(new List< SelectListItem>()
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
@ -15,9 +16,10 @@ namespace BTCPayServer.Models.ManageViewModels
|
||||
[Display(Name = "Verification Code")]
|
||||
public string Code { get; set; }
|
||||
|
||||
[ReadOnly(true)]
|
||||
[BindNever]
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
[BindNever]
|
||||
public string AuthenticatorUri { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +88,8 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
EmbeddedCSS = blob.EmbeddedCSS;
|
||||
CustomCSSLink = blob.CustomCSSLink;
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||
if (!string.IsNullOrEmpty(EmbeddedCSS))
|
||||
EmbeddedCSS = $"<style>{EmbeddedCSS}</style>";
|
||||
switch (data.Status)
|
||||
{
|
||||
case PaymentRequestData.PaymentRequestStatus.Pending:
|
||||
|
@ -22,6 +22,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public decimal Max { get; set; }
|
||||
public decimal Step { get; set; }
|
||||
|
||||
// Custom Amount properties (ButtonType = 1)
|
||||
public bool SimpleInput { get; set; }
|
||||
public bool FitButtonInline { get; set; }
|
||||
|
||||
[Url]
|
||||
public string ServerIpn { get; set; }
|
||||
[Url]
|
||||
|
@ -2,6 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
@ -11,11 +13,14 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public bool IsConfirmed { get; set; }
|
||||
public string Comment { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Link { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public HashSet<Label> Labels { get; set; } = new HashSet<Label>();
|
||||
}
|
||||
public HashSet<Label> Labels { get; set; } = new HashSet<Label>();
|
||||
public List<TransactionViewModel> Transactions { get; set; } = new List<TransactionViewModel>();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
@ -10,9 +10,6 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
@ -72,25 +69,19 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
public override async Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob,
|
||||
Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (storeBlob.OnChainMinValue == null)
|
||||
if (storeBlob.OnChainMinValue != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var limitValueRate =
|
||||
await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.OnChainMinValue.Currency)];
|
||||
|
||||
if (limitValueRate.BidAsk != null)
|
||||
{
|
||||
var limitValueCrypto = Money.Coins(storeBlob.OnChainMinValue.Value / limitValueRate.BidAsk.Bid);
|
||||
|
||||
if (amount > limitValueCrypto)
|
||||
var currentRateToCrypto = await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.OnChainMinValue.Currency)];
|
||||
if (currentRateToCrypto?.BidAsk != null)
|
||||
{
|
||||
return null;
|
||||
var limitValueCrypto = Money.Coins(storeBlob.OnChainMinValue.Value / currentRateToCrypto.BidAsk.Bid);
|
||||
if (amount < limitValueCrypto)
|
||||
{
|
||||
return "The amount of the invoice is too low to be paid on chain";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "The amount of the invoice is too low to be paid on chain";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
||||
|
@ -7,8 +7,6 @@ using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using InvoiceResponse = BTCPayServer.Models.InvoiceResponse;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
|
@ -13,9 +13,6 @@ using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -149,23 +146,20 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public override async Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob,
|
||||
Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (storeBlob.OnChainMinValue == null)
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var currentRateToCrypto = await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.LightningMaxValue.Currency)];
|
||||
|
||||
var limitValueRate = await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.OnChainMinValue.Currency)];
|
||||
|
||||
if (limitValueRate.BidAsk != null)
|
||||
{
|
||||
var limitValueCrypto = Money.Coins(storeBlob.OnChainMinValue.Value / limitValueRate.BidAsk.Bid);
|
||||
|
||||
if (amount < limitValueCrypto)
|
||||
if (currentRateToCrypto?.BidAsk != null)
|
||||
{
|
||||
return null;
|
||||
var limitValueCrypto = Money.Coins(storeBlob.LightningMaxValue.Value / currentRateToCrypto.BidAsk.Bid);
|
||||
if (amount > limitValueCrypto)
|
||||
{
|
||||
return "The amount of the invoice is too high to be paid with lightning";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "The amount of the invoice is too high to be paid with lightning";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse)
|
||||
|
@ -231,7 +231,21 @@ namespace BTCPayServer.Services.Apps
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<AppData>> GetApps(string[] appIds, bool includeStore = false)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var query = ctx.Apps
|
||||
.Where(us => appIds.Contains(us.Id));
|
||||
|
||||
if (includeStore)
|
||||
{
|
||||
query = query.Include(data => data.StoreData);
|
||||
}
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AppData> GetApp(string appId, AppType appType, bool includeStore = false)
|
||||
{
|
||||
@ -269,10 +283,10 @@ namespace BTCPayServer.Services.Apps
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Description = _HtmlSanitizer.Sanitize(c.GetDetailString("description")),
|
||||
Description = c.GetDetailString("description"),
|
||||
Id = c.Key,
|
||||
Image = _HtmlSanitizer.Sanitize(c.GetDetailString("image")),
|
||||
Title = _HtmlSanitizer.Sanitize(c.GetDetailString("title") ?? c.Key),
|
||||
Image = c.GetDetailString("image"),
|
||||
Title = c.GetDetailString("title") ?? c.Key,
|
||||
Price = c.GetDetail("price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
|
@ -77,6 +77,8 @@ namespace BTCPayServer.Services
|
||||
webRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(new UTF8Encoding(false).GetBytes($"{builder.UserName}:{builder.Password}")));
|
||||
webRequest.Headers.TryAddWithoutValidation("User-Agent", $"BTCPayServer/{GetVersion()} btcpayserver@gmail.com");
|
||||
webRequest.Method = HttpMethod.Get;
|
||||
builder.UserName = string.Empty;
|
||||
builder.Password = string.Empty;
|
||||
webRequest.RequestUri = builder.Uri;
|
||||
return webRequest;
|
||||
}
|
||||
|
@ -126,11 +126,11 @@ namespace BTCPayServer.Services
|
||||
.Inputs
|
||||
.HDKeysFor(accountKey, accountKeyPath)
|
||||
.Where(hd => !hd.Coin.PartialSigs.ContainsKey(hd.PubKey)) // Don't want to sign something twice
|
||||
.GroupBy(hd => hd.Coin)
|
||||
.GroupBy(hd => hd.Coin.PrevOut, hd => hd)
|
||||
.Select(i => new SignatureRequest()
|
||||
{
|
||||
InputCoin = i.Key.GetSignableCoin(),
|
||||
InputTransaction = i.Key.NonWitnessUtxo,
|
||||
InputCoin = i.First().Coin.GetSignableCoin(),
|
||||
InputTransaction = i.First().Coin.NonWitnessUtxo,
|
||||
KeyPath = i.First().RootedKeyPath.KeyPath,
|
||||
PubKey = i.First().PubKey
|
||||
}).ToArray();
|
||||
|
@ -10,6 +10,7 @@ namespace BTCPayServer.Services.Mails
|
||||
{
|
||||
public class EmailSettings
|
||||
{
|
||||
[Display(Name = "SMTP Server")]
|
||||
public string Server
|
||||
{
|
||||
get; set;
|
||||
@ -24,7 +25,7 @@ namespace BTCPayServer.Services.Mails
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[DataType(DataType.Password)]
|
||||
|
||||
public string Password
|
||||
{
|
||||
get; set;
|
||||
|
34
BTCPayServer/Services/Safe.cs
Normal file
34
BTCPayServer/Services/Safe.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Safe
|
||||
{
|
||||
private readonly IHtmlHelper _htmlHelper;
|
||||
private readonly IJsonHelper _jsonHelper;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
|
||||
public Safe(IHtmlHelper htmlHelper, IJsonHelper jsonHelper, HtmlSanitizer htmlSanitizer)
|
||||
{
|
||||
_htmlHelper = htmlHelper;
|
||||
_jsonHelper = jsonHelper;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
}
|
||||
|
||||
public IHtmlContent Raw(string value)
|
||||
{
|
||||
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
|
||||
}
|
||||
|
||||
public IHtmlContent Json(object model)
|
||||
{
|
||||
return _htmlHelper.Raw(_jsonHelper.Serialize(model));
|
||||
}
|
||||
}
|
||||
}
|
93
BTCPayServer/Services/WalletRepository.cs
Normal file
93
BTCPayServer/Services/WalletRepository.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class WalletRepository
|
||||
{
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
|
||||
public WalletRepository(ApplicationDbContextFactory contextFactory)
|
||||
{
|
||||
_ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
|
||||
}
|
||||
|
||||
public async Task SetWalletInfo(WalletId walletId, WalletBlobInfo blob)
|
||||
{
|
||||
if (walletId == null)
|
||||
throw new ArgumentNullException(nameof(walletId));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var walletData = new WalletData() { Id = walletId.ToString() };
|
||||
walletData.SetBlobInfo(blob);
|
||||
var entity = await ctx.Wallets.AddAsync(walletData);
|
||||
entity.State = EntityState.Modified;
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // Does not exists
|
||||
{
|
||||
entity.State = EntityState.Added;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId)
|
||||
{
|
||||
if (walletId == null)
|
||||
throw new ArgumentNullException(nameof(walletId));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return (await ctx.WalletTransactions
|
||||
.Where(w => w.WalletDataId == walletId.ToString())
|
||||
.Select(w => w)
|
||||
.ToArrayAsync())
|
||||
.ToDictionary(w => w.TransactionId, w => w.GetBlobInfo());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<WalletBlobInfo> GetWalletInfo(WalletId walletId)
|
||||
{
|
||||
if (walletId == null)
|
||||
throw new ArgumentNullException(nameof(walletId));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var data = await ctx.Wallets
|
||||
.Where(w => w.Id == walletId.ToString())
|
||||
.Select(w => w)
|
||||
.FirstOrDefaultAsync();
|
||||
return data?.GetBlobInfo() ?? new WalletBlobInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetWalletTransactionInfo(WalletId walletId, string transactionId, WalletTransactionInfo walletTransactionInfo)
|
||||
{
|
||||
if (walletId == null)
|
||||
throw new ArgumentNullException(nameof(walletId));
|
||||
if (transactionId == null)
|
||||
throw new ArgumentNullException(nameof(transactionId));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var walletData = new WalletTransactionData() { WalletDataId = walletId.ToString(), TransactionId = transactionId };
|
||||
walletData.SetBlobInfo(walletTransactionInfo);
|
||||
var entity = await ctx.WalletTransactions.AddAsync(walletData);
|
||||
entity.State = EntityState.Modified;
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // Does not exists
|
||||
{
|
||||
entity.State = EntityState.Added;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Services.U2F.Models
|
||||
@ -18,7 +19,7 @@ namespace BTCPayServer.Services.U2F.Models
|
||||
public string DeviceResponse { get; set; }
|
||||
|
||||
[Display(Name = "Challenges")]
|
||||
public string Challenges { get; set; }
|
||||
public List<ServerChallenge> Challenges { get; set; }
|
||||
|
||||
[Display(Name = "Challenge")]
|
||||
public string Challenge { get; set; }
|
||||
|
@ -37,9 +37,9 @@
|
||||
};
|
||||
setTimeout(function() {
|
||||
window.u2f.sign(
|
||||
"@Model.AppId",
|
||||
"@Model.Challenge",
|
||||
@Html.Raw(@Model.Challenges), function (data) {
|
||||
@Safe.Json(Model.AppId),
|
||||
@Safe.Json(Model.Challenge),
|
||||
@Safe.Json(Model.Challenges), function (data) {
|
||||
if (data.errorCode) {
|
||||
$("#error-response").text(errorMap[data.errorCode]);
|
||||
return;
|
||||
|
@ -1,4 +1,6 @@
|
||||
@model UpdatePointOfSaleViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@using System.Globalization
|
||||
@model UpdatePointOfSaleViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Update Point of Sale";
|
||||
}
|
||||
@ -133,6 +135,9 @@
|
||||
<input type="hidden" asp-for="NotificationEmailWarning" />
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" id="SaveSettings" />
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ViewPointOfSale" asp-controller="AppsPublic" asp-route-appId="@Model.Id" id="ViewApp">View App</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="accordion" id="accordian-dev-info">
|
||||
@ -205,7 +210,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<a asp-action="ListApps">Back to the app list</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,17 +30,17 @@
|
||||
@item.Price.Value
|
||||
if (item.Custom)
|
||||
{
|
||||
Html.Raw("or more");
|
||||
Safe.Raw("or more");
|
||||
}
|
||||
}
|
||||
else if (item.Custom)
|
||||
{
|
||||
Html.Raw("Any amount");
|
||||
Safe.Raw("Any amount");
|
||||
}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<p class="card-text overflow-hidden">@Html.Raw(item.Description)</p>
|
||||
<p class="card-text overflow-hidden">@Safe.Raw(item.Description)</p>
|
||||
|
||||
</div>
|
||||
@if (Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
|
||||
|
@ -131,7 +131,7 @@
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden">@Html.Raw(Model.Description)</div>
|
||||
<div class="card-text overflow-hidden">@Safe.Raw(Model.Description)</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<partial
|
||||
|
@ -21,7 +21,7 @@
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
var srvModel = @Safe.Json(Model);
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
|
||||
@ -33,7 +33,7 @@
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
@Safe.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@
|
||||
{
|
||||
<link rel="stylesheet" href="~/cart/css/style.css">
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
var srvModel = @Safe.Json(Model);
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<p>You need to pay <b>@Model.Amount</b> to <b>@Model.Address</b></p>
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.BitcoinUri)" style="margin-bottom:20px;"></div>
|
||||
<div id="qrCodeData" data-url="@Model.BitcoinUri" style="margin-bottom:20px;"></div>
|
||||
<p>
|
||||
<a class="btn btn-primary" href="@Model.BitcoinUri">
|
||||
<span>Open in wallet</span>
|
||||
@ -52,7 +52,7 @@
|
||||
<script type="text/javascript">
|
||||
new QRCode(document.getElementById("qrCode"),
|
||||
{
|
||||
text: "@Html.Raw(Model.BitcoinUri)",
|
||||
text: @Safe.Json(Model.BitcoinUri),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
|
@ -19,7 +19,7 @@
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
|
||||
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
var srvModel = @Safe.Json(Model);
|
||||
</script>
|
||||
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
|
||||
@ -114,8 +114,8 @@
|
||||
</div>
|
||||
</invoice>
|
||||
<script type="text/javascript">
|
||||
var availableLanguages = @Html.Raw(Json.Serialize(langService.GetLanguages().Select((language) => language.Code)));;
|
||||
var storeDefaultLang = "@Model.DefaultLang";
|
||||
var availableLanguages = @Safe.Json(langService.GetLanguages().Select((language) => language.Code));;
|
||||
var storeDefaultLang = @Safe.Json(@Model.DefaultLang);
|
||||
var fallbackLanguage = "en";
|
||||
startingLanguage = computeStartingLanguage();
|
||||
// initialization
|
||||
@ -123,7 +123,7 @@
|
||||
.use(window.i18nextXHRBackend)
|
||||
.init({
|
||||
backend: {
|
||||
loadPath: '@(Model.RootPath)locales/{{lng}}.json'
|
||||
loadPath: @Safe.Json($"{Model.RootPath}locales/{{{{lng}}}}.json")
|
||||
},
|
||||
lng: startingLanguage,
|
||||
fallbackLng: fallbackLanguage,
|
||||
|
@ -15,7 +15,7 @@
|
||||
width: 140px;
|
||||
}
|
||||
</style>
|
||||
<section>
|
||||
<section class="invoice-details">
|
||||
<div class="container">
|
||||
@if (!string.IsNullOrEmpty(Model.StatusMessage))
|
||||
{
|
||||
|
@ -39,7 +39,7 @@
|
||||
};
|
||||
|
||||
setTimeout(function() {
|
||||
var request = { "challenge": "@Model.Challenge", "version": "@Model.Version", "appId": "@Model.AppId" };
|
||||
var request = { "challenge": @Safe.Json(Model.Challenge), "version": @Safe.Json(Model.Version), "appId": @Safe.Json(Model.AppId) };
|
||||
var registerRequests = [{version: request.version, challenge: request.challenge}];
|
||||
u2f.register(request.appId, registerRequests, [],
|
||||
function(data) {
|
||||
|
@ -20,7 +20,7 @@
|
||||
<li>
|
||||
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.AuthenticatorUri)"></div>
|
||||
<div id="qrCodeData" data-url="@Model.AuthenticatorUri"></div>
|
||||
<br />
|
||||
</li>
|
||||
<li>
|
||||
@ -53,7 +53,7 @@
|
||||
<script type="text/javascript">
|
||||
new QRCode(document.getElementById("qrCode"),
|
||||
{
|
||||
text: "@Html.Raw(Model.AuthenticatorUri)",
|
||||
text: @Safe.Json(Model.AuthenticatorUri),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
|
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="w-100 p-2">@Html.Raw(Model.Description)</div>
|
||||
<div class="w-100 p-2">@Safe.Raw(Model.Description)</div>
|
||||
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
|
@ -22,7 +22,7 @@
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
var srvModel = @Safe.Json(Model);
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
|
||||
@ -31,12 +31,8 @@
|
||||
}
|
||||
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle.min.css"></bundle>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
|
||||
@Safe.Raw(Model.EmbeddedCSS);
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" />
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
var srvModel = @Safe.Json(Model);
|
||||
|
||||
|
||||
window.onload = function() {
|
||||
|
@ -54,7 +54,7 @@
|
||||
{
|
||||
<div class="form-group">
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.ServiceLink)"></div>
|
||||
<div id="qrCodeData" data-url="@Model.ServiceLink"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -70,7 +70,7 @@
|
||||
<script type="text/javascript">
|
||||
new QRCode(document.getElementById("qrCode"),
|
||||
{
|
||||
text: "@Html.Raw(Model.ServiceLink)",
|
||||
text: @Safe.Json(Model.ServiceLink),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
|
@ -90,7 +90,7 @@
|
||||
{
|
||||
<div class="form-group">
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.QRCode)"></div>
|
||||
<div id="qrCodeData" data-url="@Model.QRCode"></div>
|
||||
</div>
|
||||
<p>See QR Code information by clicking <a href="#detailsQR" data-toggle="collapse">here</a></p>
|
||||
<div id="detailsQR" class="collapse">
|
||||
@ -184,7 +184,7 @@
|
||||
<script type="text/javascript">
|
||||
new QRCode(document.getElementById("qrCode"),
|
||||
{
|
||||
text: "@Html.Raw(Model.QRCode)",
|
||||
text: @Safe.Json(Model.QRCode),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
|
@ -78,7 +78,7 @@
|
||||
{
|
||||
<div class="form-group">
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.ServiceLink)"></div>
|
||||
<div id="qrCodeData" data-url="@Model.ServiceLink"></div>
|
||||
</div>
|
||||
<p>See QR Code information by clicking <a href="#detailsQR" data-toggle="collapse">here</a></p>
|
||||
<div id="detailsQR" class="collapse">
|
||||
@ -101,7 +101,7 @@
|
||||
<script type="text/javascript">
|
||||
new QRCode(document.getElementById("qrCode"),
|
||||
{
|
||||
text: "@Html.Raw(Model.ServiceLink)",
|
||||
text: @Safe.Json(Model.ServiceLink),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
|
@ -1,57 +1,114 @@
|
||||
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
|
||||
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<partial name="_StatusMessage" for="StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-quick-fill" style="display: none">
|
||||
<div class="col-sm-6">
|
||||
<div class="dropdown quick-fill">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Quick fill settings for...
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="" data-Server="smtp.gmail.com" data-Port="587" data-EnableSSL="true">Gmail.com</a>
|
||||
<a class="dropdown-item" href="" data-Server="mail.yahoo.com" data-Port="587" data-EnableSSL="true">Yahoo.com</a>
|
||||
</div>
|
||||
</div>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.row-quick-fill').show();
|
||||
|
||||
$('.dropdown.quick-fill a').click(function(e){
|
||||
e.preventDefault();
|
||||
|
||||
var aNode = $(this);
|
||||
var data = aNode.data();
|
||||
|
||||
for(var key in data){
|
||||
var value = data[key];
|
||||
var inputNodes = $('input[name*="Settings.'+key+'" i]');
|
||||
|
||||
if(inputNodes.length){
|
||||
inputNodes.each(function(i, input){
|
||||
input = $(input);
|
||||
var type = input.attr('type');
|
||||
if(type === 'checkbox'){
|
||||
input.prop('checked', value);
|
||||
|
||||
}else{
|
||||
input.val(value);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<form method="post" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Server"></label>
|
||||
<input asp-for="Settings.Server" class="form-control" />
|
||||
<input asp-for="Settings.Server" class="form-control"/>
|
||||
<span asp-validation-for="Settings.Server" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Port"></label>
|
||||
<input asp-for="Settings.Port" class="form-control" />
|
||||
<input asp-for="Settings.Port" class="form-control"/>
|
||||
<span asp-validation-for="Settings.Port" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.FromDisplay"></label>
|
||||
<input asp-for="Settings.FromDisplay" class="form-control" />
|
||||
<input asp-for="Settings.FromDisplay" class="form-control"/>
|
||||
<small class="form-text text-muted">
|
||||
Some email providers (like Gmail) don't allow you to set your display name, so this setting may not have any effect.
|
||||
</small>
|
||||
<span asp-validation-for="Settings.FromDisplay" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.From"></label>
|
||||
<input asp-for="Settings.From" class="form-control" />
|
||||
<input asp-for="Settings.From" class="form-control"/>
|
||||
<span asp-validation-for="Settings.From" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Login"></label>
|
||||
<input asp-for="Settings.Login" class="form-control" />
|
||||
<input asp-for="Settings.Login" class="form-control"/>
|
||||
<small class="form-text text-muted">
|
||||
For many email providers (like Gmail) your login is your email address.
|
||||
</small>
|
||||
<span asp-validation-for="Settings.Login" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Password"></label>
|
||||
<input asp-for="Settings.Password" value="@Model.Settings.Password" class="form-control" />
|
||||
<input asp-for="Settings.Password" value="@Model.Settings.Password" class="form-control"/>
|
||||
<span asp-validation-for="Settings.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.EnableSSL"></label>
|
||||
<input asp-for="Settings.EnableSSL" type="checkbox" class="form-check-inline" />
|
||||
<input asp-for="Settings.EnableSSL" type="checkbox" class="form-check-inline"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="TestEmail"></label>
|
||||
<input asp-for="TestEmail" class="form-control" />
|
||||
<input asp-for="TestEmail" class="form-control"/>
|
||||
<small class="form-text text-muted">
|
||||
If you want to test your settings, enter an email address here and click "Send Test Email". Your settings won't be saved, only a test email will be sent. After a successful test, you can click "Save".
|
||||
</small>
|
||||
<span asp-validation-for="TestEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Test">Test</button>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Test">Send Test Email</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -104,7 +104,7 @@
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible" style="position:absolute; top:75px;" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<span>Your access to BTCPay Server is over an unsecured network. If you are using docker deployment with NGINX and HTTPS is not available, you probably did not configured your DNS settings right. <br />
|
||||
<span>Your access to BTCPay Server is over an unsecured network. If you are using the docker deployment method with NGINX and HTTPS is not available, you probably did not configure your DNS settings correctly. <br />
|
||||
We disabled the register and login link so you don't leak your credentials.</span>
|
||||
</div>
|
||||
}
|
||||
@ -128,14 +128,14 @@
|
||||
|
||||
@RenderSection("Scripts", required: false)
|
||||
|
||||
<script type="text/javascript">
|
||||
var expectedDomain = @Html.Raw(Json.Serialize(env.ExpectedHost));
|
||||
var expectedProtocol = @Html.Raw(Json.Serialize(env.ExpectedProtocol));
|
||||
<script type="text/javascript">
|
||||
var expectedDomain = @Safe.Json(env.ExpectedHost);
|
||||
var expectedProtocol = @Safe.Json(env.ExpectedProtocol);
|
||||
if (window.location.host != expectedDomain || window.location.protocol != expectedProtocol + ":") {
|
||||
document.getElementById("badUrl").style.display = "block";
|
||||
document.getElementById("browserScheme").innerText = window.location.protocol.substr(0, window.location.protocol.length -1);
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -16,7 +16,7 @@
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(parsedModel.Html))
|
||||
{
|
||||
@Html.Raw(parsedModel.Html)
|
||||
@Safe.Raw(parsedModel.Html)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@ -72,7 +72,7 @@
|
||||
<label for="btn-slider">Slider</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" v-if="srvModel.buttonType == 2">
|
||||
<div class="form-row" v-if="srvModel.buttonType == 1 ||srvModel.buttonType == 2">
|
||||
<div class="form-group col-md-4">
|
||||
<label>Min</label>
|
||||
<input name="min" type="text" class="form-control"
|
||||
@ -95,6 +95,22 @@
|
||||
<small class="text-danger">{{ errors.first('step') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" v-if="srvModel.buttonType == 1">
|
||||
<div class="form-group col-md-6">
|
||||
<label>Use a simple input style</label>
|
||||
<input name="simpleInput" type="checkbox" class="form-check"
|
||||
v-model="srvModel.simpleInput" v-on:change="inputChanges"
|
||||
:class="{'is-invalid': errors.has('simpleInput') }">
|
||||
<small class="text-danger">{{ errors.first('simpleInput') }}</small>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label>Fit button inline</label>
|
||||
<input name="fitButtoninline" type="checkbox" class="form-check"
|
||||
v-model="srvModel.fitButtoninline" v-on:change="inputChanges"
|
||||
:class="{'is-invalid': errors.has('fitButtoninline') }">
|
||||
<small class="text-danger">{{ errors.first('fitButtoninline') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<br />
|
||||
@ -154,7 +170,7 @@
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<br />
|
||||
This parameter allows you to specify additional query string paramters that should be appended to the checkout page once the invoice is created. For example, <kbd>lang=da-DK</kbd> would load the checkout page in Danishby default.
|
||||
This parameter allows you to specify additional query string paramters that should be appended to the checkout page once the invoice is created. For example, <kbd>lang=da-DK</kbd> would load the checkout page in Danish by default.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -193,7 +209,7 @@
|
||||
|
||||
@section Scripts {
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
var srvModel = @Safe.Json(Model);
|
||||
|
||||
var payButtonCtrl = new Vue({
|
||||
el: '#payButtonCtrl',
|
||||
@ -208,9 +224,8 @@
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
<script id="template-select-currency" type="text/template">
|
||||
<select onchange="document.querySelector('input[type = hidden][name = currency]').value = event.target.value" style="-webkit-appearance: none; border: 0; display: block; padding: 0 3em; margin: auto auto 5px auto; font-size: 11px; background: 0 0; cursor: pointer;"><option value="USD">USD</option><option value="GBP">GBP</option><option value="EUR">EUR</option><option value="BTC">BTC</option></select>
|
||||
<select onmouseover="this.style.border='solid #ccc 1px'; this.style.padding='0px'" onmouseout="this.style.border='0'; this.style.padding='1px'" onchange="document.querySelector('input[type = hidden][name = currency]').value = event.target.value" style="border: 0; display: block; padding: 1px; margin: auto auto 5px auto; font-size: 11px; background: 0 0; cursor: pointer;"><option value="USD">USD</option><option value="GBP">GBP</option><option value="EUR">EUR</option><option value="BTC">BTC</option></select>
|
||||
</script>
|
||||
|
||||
<script id="template-button-plus-minus" type="text/template">
|
||||
@ -218,7 +233,7 @@
|
||||
</script>
|
||||
|
||||
<script id="template-input-price" type="text/template">
|
||||
<input type="text" id="btcpay-input-price" name="price" value="PRICEVALUE" style="border: none; background-image: none; background-color: transparent; -webkit-box-shadow: none ; -moz-box-shadow: none ; -webkit-appearance: none ; width: WIDTHINPUT; text-align: center; font-size: 25px; margin: auto; border-radius: 5px; line-height: 35px; background: #fff;" oninput="event.preventDefault();isNaN(event.target.value) || event.target.value <= 0 ? document.querySelector('#btcpay-input-price').value = PRICEVALUE : event.target.value" CUSTOM />
|
||||
<input type="TYPEVALUE" id="btcpay-input-price" name="price" min="MIN" max="MAX" step="STEP" value="PRICEVALUE" style="border: none; background-image: none; background-color: transparent; -webkit-box-shadow: none ; -moz-box-shadow: none ; -webkit-appearance: none ; width: WIDTHINPUT; text-align: center; font-size: 25px; margin: auto; border-radius: 5px; line-height: 35px; background: #fff;" oninput="event.preventDefault();isNaN(event.target.value) || event.target.value <= 0 ? document.querySelector('#btcpay-input-price').value = PRICEVALUE : event.target.value" CUSTOM />
|
||||
</script>
|
||||
|
||||
<script id="template-input-slider" type="text/template">
|
||||
|
@ -171,6 +171,6 @@
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script type="text/javascript">var defaultScript = @Html.Raw(Json.Serialize(Model.DefaultScript));</script>
|
||||
<script type="text/javascript">var defaultScript = @Safe.Json(Model.DefaultScript);</script>
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
}
|
||||
<style type="text/css">
|
||||
|
||||
|
||||
.smMaxWidth {
|
||||
max-width: 200px;
|
||||
}
|
||||
@ -45,20 +46,28 @@
|
||||
<span class="fa fa-clock-o" title="Switch date format"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th style="text-align:left">Label</th>
|
||||
<th>Transaction Id</th>
|
||||
<th style="text-align:right">Balance</th>
|
||||
<th style="text-align:right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var transaction in Model.Transactions)
|
||||
{
|
||||
<tr class="@(transaction.IsConfirmed ? "" : "unconf")">
|
||||
<tr>
|
||||
<td>
|
||||
<span class="switchTimeFormat" data-switch="@transaction.Timestamp.ToTimeAgo()">
|
||||
@transaction.Timestamp.ToBrowserDate()
|
||||
</span>
|
||||
</td>
|
||||
<td class="smMaxWidth text-truncate">
|
||||
<td style="text-align:left">
|
||||
@foreach (var label in transaction.Labels)
|
||||
{
|
||||
<a asp-route-labelFilter="@label.Value"><span class="badge" style="display:block;background-color:@label.Color;color:white;">@label.Value</span></a>
|
||||
}
|
||||
</td>
|
||||
<td class="smMaxWidth text-truncate @(transaction.IsConfirmed ? "" : "unconf")">
|
||||
<a href="@transaction.Link" target="_blank">
|
||||
@transaction.Id
|
||||
</a>
|
||||
@ -71,6 +80,51 @@
|
||||
{
|
||||
<td style="text-align:right; color:red;">@transaction.Balance</td>
|
||||
}
|
||||
<td style="text-align:right;">
|
||||
<div class="dropdown" style="display:inline-block;">
|
||||
<span class="fa fa-tags" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu">
|
||||
<form asp-action="ModifyTransaction" method="post">
|
||||
<input type="hidden" name="transactionId" value="@transaction.Id" />
|
||||
<div class="dropdown-item input-group">
|
||||
<input name="addlabel" placeholder="Label name" maxlength="20" type="text" class="form-control form-control-sm" />
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-primary btn-sm"><span class="fa fa-plus"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
@foreach (var label in Model.Labels)
|
||||
{
|
||||
@if (transaction.Labels.Contains(label))
|
||||
{
|
||||
<button name="removelabel" type="submit" class="dropdown-item" value="@label.Value"><span class="badge" style="display:block;background-color:@label.Color;color:white;text-align:left;"><span class="fa fa-check"></span> @label.Value</span></button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button name="addlabelclick" type="submit" class="dropdown-item" value="@label.Value"><span class="badge" style="display:block;background-color:@label.Color;color:white;text-align:left;">@label.Value</span></button>
|
||||
}
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown" style="display:inline-block;">
|
||||
@if (string.IsNullOrEmpty(transaction.Comment))
|
||||
{
|
||||
<span class="fa fa-comment" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="fa fa-commenting" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
}
|
||||
<div class="dropdown-menu">
|
||||
<form asp-action="ModifyTransaction" method="post">
|
||||
<input type="hidden" name="transactionId" value="@transaction.Id" />
|
||||
<textarea name="addcomment" class="dropdown-item" maxlength="200" rows="2" cols="20">@transaction.Comment</textarea>
|
||||
<div class="dropdown-item"><button type="submit" class="btn btn-primary">Save comment</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
@ -6,4 +6,5 @@
|
||||
@using BTCPayServer.Models.InvoicingModels
|
||||
@using BTCPayServer.Models.ManageViewModels
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@inject BTCPayServer.Services.Safe Safe
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
@ -43,6 +43,14 @@ namespace BTCPayServer
|
||||
return false;
|
||||
return ToString().Equals(item.ToString(), StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
public static WalletId Parse(string id)
|
||||
{
|
||||
if (TryParse(id, out var v))
|
||||
return v;
|
||||
throw new FormatException("Invalid WalletId");
|
||||
}
|
||||
|
||||
public static bool operator ==(WalletId a, WalletId b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
|
@ -47,5 +47,5 @@
|
||||
"Pay with CoinSwitch": "CoinSwitchでのお支払い",
|
||||
"Pay with Changelly": "Changellyでのお支払い",
|
||||
"Close": "閉じる",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due."
|
||||
"NotPaid_ExtraTransaction": "請求金額の全額が支払われていません。未払い分の別のトランザクションをお送りください。"
|
||||
}
|
@ -114,3 +114,8 @@ a.nav-link {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-details a{
|
||||
/* Prevent layout from breaking on hyperlinks with very long URLs as the visible text */
|
||||
word-break: break-word;
|
||||
}
|
@ -44,16 +44,25 @@ function inputChanges(event, buttonSize) {
|
||||
|
||||
var width = "209px";
|
||||
var widthInput = "3em";
|
||||
var formwidth = null;
|
||||
if (srvModel.buttonSize === 0) {
|
||||
width = "146px";
|
||||
widthInput = "2em";
|
||||
} else if (srvModel.buttonSize === 1) {
|
||||
if(srvModel.fitButtoninline){
|
||||
formwidth = "292px";
|
||||
}
|
||||
} else if (srvModel.buttonSize === 1 ) {
|
||||
width = "168px";
|
||||
if(srvModel.fitButtoninline){
|
||||
formwidth = "336px";
|
||||
}
|
||||
} else if (srvModel.buttonSize === 2) {
|
||||
width = "209px";
|
||||
if(srvModel.fitButtoninline){
|
||||
formwidth = "418px";
|
||||
}
|
||||
}
|
||||
|
||||
var html = '<form method="POST" action="' + esc(srvModel.urlRoot) + 'api/v1/invoices">';
|
||||
var html = '<form method="POST" action="' + esc(srvModel.urlRoot) + 'api/v1/invoices" '+(formwidth? 'style="width:'+formwidth+'"' : '')+'>';
|
||||
html += addinput("storeId", srvModel.storeId);
|
||||
|
||||
// Add price as hidden only if it's a fixed amount (srvModel.buttonType = 0)
|
||||
@ -61,11 +70,16 @@ function inputChanges(event, buttonSize) {
|
||||
html += addinput("price", srvModel.price);
|
||||
}
|
||||
else if (srvModel.buttonType == 1) {
|
||||
html += '\n <div style="text-align:center;display:inline;width:' + width + '">';
|
||||
html += '\n <div style="text-align:center;display:inline;'+ (srvModel.fitButtoninline? 'float:left':'width:'+ width) +';">';
|
||||
html += '<div>';
|
||||
html += addPlusMinusButton("-");
|
||||
html += addInputPrice(srvModel.price, widthInput, "");
|
||||
html += addPlusMinusButton("+");
|
||||
if(!srvModel.simpleInput) {
|
||||
html += addPlusMinusButton("-");
|
||||
}
|
||||
html += addInputPrice(srvModel.price, widthInput, "", srvModel.simpleInput? "number": null, srvModel.min, srvModel.max, srvModel.step);
|
||||
|
||||
if(!srvModel.simpleInput) {
|
||||
html += addPlusMinusButton("+");
|
||||
}
|
||||
html += '</div>';
|
||||
html += addSelectCurrency();
|
||||
html += '</div>';
|
||||
@ -130,12 +144,15 @@ function addPlusMinusButton(type) {
|
||||
}
|
||||
}
|
||||
|
||||
function addInputPrice(price, widthInput, customFn) {
|
||||
function addInputPrice(price, widthInput, customFn, type, min, max, step) {
|
||||
var input = document.getElementById('template-input-price').innerHTML.trim();
|
||||
|
||||
input = input.replace(/PRICEVALUE/g, price);
|
||||
input = input.replace("WIDTHINPUT", widthInput);
|
||||
|
||||
input = input.replace("TYPEVALUE", type || "text");
|
||||
input = input.replace("MIN", min || 0);
|
||||
input = input.replace("MAX", max|| "none");
|
||||
input = input.replace("STEP", step || "any");
|
||||
if (customFn) {
|
||||
return input.replace("CUSTOM", customFn);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.0.3.125</Version>
|
||||
<Version>1.0.3.127</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
Reference in New Issue
Block a user