Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
a52a1901c4 | |||
45aee607e3 | |||
c263016939 | |||
741915b1f8 | |||
6f2534ba82 | |||
43635071d9 | |||
22f06ecd4e | |||
7efe83eba8 | |||
a5b732e197 | |||
f404aaf768 | |||
e1f8177834 | |||
cff391a7a9 | |||
9cd7608a53 | |||
6950a06532 | |||
0e6c2ec556 | |||
479fc50d9a | |||
a29a8f7ed9 | |||
83cf637f9d | |||
5dbb4bf6be | |||
f1f227b746 | |||
b96cac16c6 | |||
f58fdafdcd | |||
b7b39f8284 | |||
7a173a6692 | |||
0bb260bec9 |
@ -251,6 +251,17 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseFilter()
|
||||
{
|
||||
var filter = "storeid:abc status:abed blabhbalh ";
|
||||
var search = new SearchString(filter);
|
||||
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
|
||||
Assert.Equal("blabhbalh", search.TextSearch);
|
||||
Assert.Equal("abc", search.Filters["storeid"]);
|
||||
Assert.Equal("abed", search.Filters["status"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
@ -274,19 +285,23 @@ namespace BTCPayServer.Tests
|
||||
}, Facade.Merchant);
|
||||
var repo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.OrderId
|
||||
}).GetAwaiter().GetResult();
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.Id
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
Eventually(() =>
|
||||
{
|
||||
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.OrderId
|
||||
}).GetAwaiter().GetResult();
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.Id
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
});
|
||||
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
|
||||
|
@ -21,7 +21,7 @@ services:
|
||||
- "tests:127.0.0.1"
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.27
|
||||
image: nicolasdorier/nbxplorer:1.0.0.29
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -39,6 +39,9 @@ services:
|
||||
- bitcoind
|
||||
- postgres
|
||||
|
||||
eclair:
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
|
||||
bitcoind:
|
||||
container_name: btcpayserver_dev_bitcoind
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
@ -46,7 +49,13 @@ services:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: "rpcuser=ceiwHEbqWI83\nrpcpassword=DwubwWsoo3\nregtest=1\nrpcport=43782\nport=39388\nwhitelist=0.0.0.0/0"
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
expose:
|
||||
- "43782"
|
||||
- "39388"
|
||||
|
@ -69,7 +69,7 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15);
|
||||
await ctx.PairingCodes.AddAsync(new PairingCodeData()
|
||||
ctx.PairingCodes.Add(new PairingCodeData()
|
||||
{
|
||||
Id = pairingCodeId,
|
||||
DateCreated = now,
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.28</Version>
|
||||
<Version>1.0.0.37</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
@ -21,8 +21,8 @@
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.1" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.41" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.12" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.48" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.13" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.18" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
|
@ -57,6 +57,7 @@ namespace BTCPayServer.Configuration
|
||||
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
|
||||
RequireHttps = conf.GetOrDefault<bool>("requirehttps", false);
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
}
|
||||
|
||||
public bool RequireHttps
|
||||
@ -68,5 +69,10 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri ExternalUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,11 +49,6 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}");
|
||||
}
|
||||
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
|
||||
_Resources.Add(db);
|
||||
|
||||
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
|
||||
_Resources.Add(db);
|
||||
|
||||
ApplicationDbContextFactory dbContext = null;
|
||||
if (opts.PostgresConnectionString == null)
|
||||
@ -68,7 +63,9 @@ namespace BTCPayServer.Configuration
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
|
||||
}
|
||||
DBFactory = dbContext;
|
||||
InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
|
||||
|
||||
InvoiceRepository = new InvoiceRepository(dbContext, CreateDBPath(opts, "InvoiceDB"), Network);
|
||||
_Resources.Add(InvoiceRepository);
|
||||
}
|
||||
|
||||
private static string CreateDBPath(BTCPayServerOptions opts, string name)
|
||||
|
@ -19,8 +19,8 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets",
|
||||
Name = "NBXplorer"
|
||||
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
|
||||
Name = "BTCPay"
|
||||
};
|
||||
app.HelpOption("-? | -h | --help");
|
||||
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
|
||||
@ -30,7 +30,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
|
||||
app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
|
||||
app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
|
||||
|
||||
app.Option("--externalurl", $"The expected external url of this service, use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
return app;
|
||||
}
|
||||
|
||||
|
@ -235,8 +235,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Register(string returnUrl = null)
|
||||
public async Task<IActionResult> Register(string returnUrl = null)
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription)
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
@ -247,9 +250,11 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription)
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
|
@ -15,6 +15,7 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -23,11 +24,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> Invoice(string invoiceId, string command)
|
||||
public IActionResult Invoice(string invoiceId, string command)
|
||||
{
|
||||
if (command == "refresh")
|
||||
{
|
||||
await _Watcher.WatchAsync(invoiceId, true);
|
||||
_Watcher.Watch(invoiceId);
|
||||
}
|
||||
StatusMessage = "Invoice is state is being refreshed, please refresh the page soon...";
|
||||
return RedirectToAction(nameof(Invoice), new
|
||||
@ -118,8 +119,7 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var dto = invoice.EntityToDTO();
|
||||
|
||||
var cryptoFormat = _CurrencyNameTable.GetCurrencyProvider("BTC");
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
@ -133,7 +133,7 @@ namespace BTCPayServer.Controllers
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(invoice.ProductInformation.Currency)),
|
||||
Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})",
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
TxFees = invoice.TxFee.ToString(),
|
||||
@ -188,12 +188,15 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
|
||||
{
|
||||
var model = new InvoicesModel();
|
||||
var filterString = new SearchString(searchTerm);
|
||||
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
TextSearch = searchTerm,
|
||||
TextSearch = filterString.TextSearch,
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId()
|
||||
UserId = GetUserId(),
|
||||
Status = filterString.Filters.TryGet("status"),
|
||||
StoreId = filterString.Filters.TryGet("storeid")
|
||||
}))
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
@ -246,21 +249,30 @@ namespace BTCPayServer.Controllers
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
var result = await CreateInvoiceCore(new Invoice()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = "USD",
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
try
|
||||
{
|
||||
var result = await CreateInvoiceCore(new Invoice()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = model.Currency,
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
catch (RateUnavailableException)
|
||||
{
|
||||
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SelectList> GetStores(string userId, string storeId = null)
|
||||
@ -281,6 +293,16 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
|
@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
|
||||
}
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
|
||||
{
|
||||
//TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level
|
||||
var derivationStrategy = store.DerivationStrategy;
|
||||
@ -87,12 +87,13 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceTime = DateTimeOffset.UtcNow,
|
||||
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
|
||||
};
|
||||
var storeBlob = store.GetStoreBlob(_Network);
|
||||
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
|
||||
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
|
||||
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications;
|
||||
@ -110,13 +111,16 @@ namespace BTCPayServer.Controllers
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
entity.TxFee = store.GetStoreBlob(_Network).NetworkFeeDisabled ? Money.Zero : (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
|
||||
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
|
||||
|
||||
var getFeeRate = _FeeProvider.GetFeeRateAsync();
|
||||
var getRate = _RateProvider.GetRateAsync(invoice.Currency);
|
||||
var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
|
||||
entity.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
|
||||
entity.Rate = (double)await getRate;
|
||||
entity.PosData = invoice.PosData;
|
||||
entity.DepositAddress = await getAddress;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
|
||||
await _Watcher.WatchAsync(entity.Id);
|
||||
_Watcher.Watch(entity.Id);
|
||||
var resp = entity.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
@ -32,15 +32,50 @@ namespace BTCPayServer.Controllers
|
||||
public IActionResult ListUsers()
|
||||
{
|
||||
var users = new UsersViewModel();
|
||||
users.StatusMessage = StatusMessage;
|
||||
users.Users
|
||||
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
|
||||
{
|
||||
Name = u.UserName,
|
||||
Email = u.Email
|
||||
Email = u.Email,
|
||||
Id = u.Id
|
||||
}).ToList();
|
||||
return View(users);
|
||||
}
|
||||
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteUser(string userId)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete user " + user.Email,
|
||||
Description = "This user will be permanently deleted",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> DeleteUserPost(string userId)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
await _UserManager.DeleteAsync(user);
|
||||
StatusMessage = "User deleted";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
|
@ -144,13 +144,16 @@ namespace BTCPayServer.Controllers
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob(_Network);
|
||||
var vm = new StoreViewModel();
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !store.GetStoreBlob(_Network).NetworkFeeDisabled;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.DerivationScheme = store.DerivationStrategy;
|
||||
vm.StatusMessage = StatusMessage;
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -205,11 +208,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
if (store.GetStoreBlob(_Network).NetworkFeeDisabled != !model.NetworkFee)
|
||||
var blob = store.GetStoreBlob(_Network);
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
|
||||
if (store.SetStoreBlob(blob, _Network))
|
||||
{
|
||||
var blob = store.GetStoreBlob(_Network);
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
store.SetStoreBlob(blob, _Network);
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
|
98
BTCPayServer/CustomThreadPool.cs
Normal file
98
BTCPayServer/CustomThreadPool.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
class CustomThreadPool : IDisposable
|
||||
{
|
||||
CancellationTokenSource _Cancel = new CancellationTokenSource();
|
||||
TaskCompletionSource<bool> _Exited;
|
||||
int _ExitedCount = 0;
|
||||
Thread[] _Threads;
|
||||
Exception _UnhandledException;
|
||||
BlockingCollection<(Action, TaskCompletionSource<object>)> _Actions = new BlockingCollection<(Action, TaskCompletionSource<object>)>(new ConcurrentQueue<(Action, TaskCompletionSource<object>)>());
|
||||
|
||||
public CustomThreadPool(int threadCount, string threadName)
|
||||
{
|
||||
if (threadCount <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(threadCount));
|
||||
_Exited = new TaskCompletionSource<bool>();
|
||||
_Threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(RunLoop) { Name = threadName }).ToArray();
|
||||
foreach (var t in _Threads)
|
||||
t.Start();
|
||||
}
|
||||
|
||||
public void Do(Action act)
|
||||
{
|
||||
DoAsync(act).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public T Do<T>(Func<T> act)
|
||||
{
|
||||
return DoAsync(act).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<T> DoAsync<T>(Func<T> act)
|
||||
{
|
||||
TaskCompletionSource<object> done = new TaskCompletionSource<object>();
|
||||
_Actions.Add((() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
done.TrySetResult(act());
|
||||
}
|
||||
catch (Exception ex) { done.TrySetException(ex); }
|
||||
}
|
||||
, done));
|
||||
return (T)(await done.Task.ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public Task DoAsync(Action act)
|
||||
{
|
||||
return DoAsync<object>(() =>
|
||||
{
|
||||
act();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
void RunLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var act in _Actions.GetConsumingEnumerable(_Cancel.Token))
|
||||
{
|
||||
act.Item1();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cancel.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_Cancel.Cancel();
|
||||
_UnhandledException = ex;
|
||||
}
|
||||
if (Interlocked.Increment(ref _ExitedCount) == _Threads.Length)
|
||||
{
|
||||
foreach (var action in _Actions)
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Item2.TrySetCanceled();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
_Exited.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Cancel.Cancel();
|
||||
_Exited.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -65,17 +67,33 @@ namespace BTCPayServer.Data
|
||||
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
}
|
||||
|
||||
public void SetStoreBlob(StoreBlob storeBlob, Network network)
|
||||
public bool SetStoreBlob(StoreBlob storeBlob, Network network)
|
||||
{
|
||||
StoreBlob = Encoding.UTF8.GetBytes(new Serializer(network).ToString(storeBlob));
|
||||
var original = new Serializer(network).ToString(GetStoreBlob(network));
|
||||
var newBlob = new Serializer(network).ToString(storeBlob);
|
||||
if (original == newBlob)
|
||||
return false;
|
||||
StoreBlob = Encoding.UTF8.GetBytes(newBlob);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class StoreBlob
|
||||
{
|
||||
public StoreBlob()
|
||||
{
|
||||
MonitoringExpiration = 60;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int MonitoringExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,13 +30,16 @@ namespace BTCPayServer.Hosting
|
||||
TokenRepository _TokenRepository;
|
||||
RequestDelegate _Next;
|
||||
CallbackController _CallbackController;
|
||||
BTCPayServerOptions _Options;
|
||||
public BTCPayMiddleware(RequestDelegate next,
|
||||
TokenRepository tokenRepo,
|
||||
BTCPayServerOptions options,
|
||||
CallbackController callbackController)
|
||||
{
|
||||
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
|
||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_CallbackController = callbackController;
|
||||
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
|
||||
@ -50,6 +53,16 @@ namespace BTCPayServer.Hosting
|
||||
_Registered = true;
|
||||
}
|
||||
|
||||
// Make sure that code executing after this point think that the external url has been hit.
|
||||
if(_Options.ExternalUrl != null)
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
if(_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
}
|
||||
|
||||
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
|
||||
var sig = values.FirstOrDefault();
|
||||
httpContext.Request.Headers.TryGetValue("x-identity", out values);
|
||||
|
@ -9,12 +9,22 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class CreateInvoiceModel
|
||||
{
|
||||
public CreateInvoiceModel()
|
||||
{
|
||||
Currency = "USD";
|
||||
}
|
||||
[Required]
|
||||
public double? Amount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string StoreId
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class UserViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name
|
||||
{
|
||||
get; set;
|
||||
|
@ -10,6 +10,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class StoreViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
[Display(Name = "Store Name")]
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
@ -34,6 +35,14 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
|
||||
[Range(10, 60 * 24 * 31)]
|
||||
public int MonitoringExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Consider the invoice confirmed when the payment transaction...")]
|
||||
public SpeedPolicy SpeedPolicy
|
||||
{
|
||||
|
49
BTCPayServer/SearchString.cs
Normal file
49
BTCPayServer/SearchString.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class SearchString
|
||||
{
|
||||
string _OriginalString;
|
||||
public SearchString(string str)
|
||||
{
|
||||
str = str ?? string.Empty;
|
||||
str = str.Trim();
|
||||
_OriginalString = str.Trim();
|
||||
TextSearch = _OriginalString;
|
||||
var splitted = str.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Filters
|
||||
= splitted
|
||||
.Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
.Where(kv => kv.Length == 2)
|
||||
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1]))
|
||||
.ToDictionary(o => o.Key, o => o.Value);
|
||||
|
||||
foreach(var filter in splitted)
|
||||
{
|
||||
if(filter.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries).Length == 2)
|
||||
{
|
||||
TextSearch = TextSearch.Replace(filter, string.Empty);
|
||||
}
|
||||
}
|
||||
TextSearch = TextSearch.Trim();
|
||||
}
|
||||
|
||||
public string TextSearch
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Dictionary<string, string> Filters { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _OriginalString;
|
||||
}
|
||||
}
|
||||
}
|
@ -251,7 +251,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset? MonitoringExpiration
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public DateTimeOffset MonitoringExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
@ -19,7 +19,7 @@ using BTCPayServer.Models.InvoicingModels;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class InvoiceRepository
|
||||
public class InvoiceRepository : IDisposable
|
||||
{
|
||||
|
||||
|
||||
@ -45,24 +45,17 @@ namespace BTCPayServer.Services.Invoices
|
||||
_Network = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, DBreezeEngine engine, Network network)
|
||||
private CustomThreadPool _IndexerThread;
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, Network network)
|
||||
{
|
||||
_Engine = engine;
|
||||
_Engine = new DBreezeEngine(dbreezePath);
|
||||
_IndexerThread = new CustomThreadPool(1, "Invoice Indexer");
|
||||
_Network = network;
|
||||
_ContextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public async Task AddPendingInvoice(string invoiceId)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId });
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePendingInvoice(string invoiceId)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
@ -117,7 +110,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
invoice.StoreId = storeId;
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
await context.AddAsync(new InvoiceData()
|
||||
context.Invoices.Add(new InvoiceData()
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Id = invoice.Id,
|
||||
@ -127,21 +120,20 @@ namespace BTCPayServer.Services.Invoices
|
||||
Status = invoice.Status,
|
||||
ItemCode = invoice.ProductInformation.ItemCode,
|
||||
CustomerEmail = invoice.RefundMail
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
});
|
||||
context.AddressInvoices.Add(new AddressInvoiceData()
|
||||
{
|
||||
Address = invoice.DepositAddress.ScriptPubKey.Hash.ToString(),
|
||||
InvoiceDataId = invoice.Id,
|
||||
CreatedTime = DateTimeOffset.UtcNow,
|
||||
});
|
||||
|
||||
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
Address = invoice.DepositAddress.ToString(),
|
||||
Assigned = DateTimeOffset.UtcNow
|
||||
});
|
||||
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -231,11 +223,14 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
void AddToTextSearch(string invoiceId, params string[] terms)
|
||||
{
|
||||
using (var tx = _Engine.GetTransaction())
|
||||
_IndexerThread.DoAsync(() =>
|
||||
{
|
||||
tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
|
||||
tx.Commit();
|
||||
}
|
||||
using (var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
|
||||
tx.Commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task UpdateInvoiceStatus(string invoiceId, string status, string exceptionStatus)
|
||||
@ -251,6 +246,17 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePaidInvoiceToInvalid(string invoiceId)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData?.Status != "paid")
|
||||
return;
|
||||
invoiceData.Status = "invalid";
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
public async Task<InvoiceEntity> GetInvoice(string storeId, string id, bool inludeAddressData = false)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
@ -373,12 +379,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
int i = 0;
|
||||
foreach (var output in outputs)
|
||||
{
|
||||
await context.RefundAddresses.AddAsync(new RefundAddressesData()
|
||||
context.RefundAddresses.Add(new RefundAddressesData()
|
||||
{
|
||||
Id = invoiceId + "-" + i,
|
||||
InvoiceDataId = invoiceId,
|
||||
Blob = ToBytes(output)
|
||||
}).ConfigureAwait(false);
|
||||
});
|
||||
i++;
|
||||
}
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
@ -406,7 +412,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
InvoiceDataId = invoiceId
|
||||
};
|
||||
|
||||
await context.Payments.AddAsync(data).ConfigureAwait(false);
|
||||
context.Payments.Add(data);
|
||||
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
AddToTextSearch(invoiceId, receivedCoin.Outpoint.Hash.ToString());
|
||||
@ -451,6 +457,14 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
return NBitcoin.JsonConverters.Serializer.ToString(data, Network);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_Engine != null)
|
||||
_Engine.Dispose();
|
||||
if (_IndexerThread != null)
|
||||
_IndexerThread.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoiceQuery
|
||||
|
@ -80,9 +80,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
|
||||
}
|
||||
|
||||
var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60);
|
||||
if (invoice.Status == "complete" ||
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow))
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
|
||||
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
|
||||
@ -133,6 +132,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
needSave = true;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
invoice.Status = "expired";
|
||||
if (invoice.FullNotifications)
|
||||
{
|
||||
_NotificationManager.Notify(invoice);
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
@ -181,22 +184,39 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
if (!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow)
|
||||
var transactions = await GetPaymentsWithTransaction(invoice);
|
||||
var chainConfirmedTransactions = transactions.Where(t => t.Confirmations >= 1);
|
||||
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(invoice);
|
||||
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => !t.Transaction.RBF);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
}
|
||||
transactions = transactions.Where(t => !t.Transaction.RBF);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
}
|
||||
|
||||
var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.Output.Value).Sum();
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(chainTotalConfirmed < invoice.GetTotalCryptoDue()))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
invoice.Status = "invalid";
|
||||
needSave = true;
|
||||
if (invoice.FullNotifications)
|
||||
{
|
||||
_NotificationManager.Notify(invoice);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
|
||||
if (totalConfirmed >= invoice.GetTotalCryptoDue())
|
||||
{
|
||||
@ -206,12 +226,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
invoice.Status = "invalid";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "confirmed")
|
||||
@ -302,12 +316,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WatchAsync(string invoiceId, bool singleShot = false)
|
||||
public void Watch(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
if (!singleShot)
|
||||
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
|
||||
_WatchRequests.Add(invoiceId);
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
@ -11,5 +12,8 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool LockSubscription { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
var rateJson = new RateJson();
|
||||
rateJson.Code = rate.Name;
|
||||
rateJson.Rate = rate.Value["rate"].Value<decimal>();
|
||||
rateJson.Rate = decimal.Parse(rate.Value["rate"].Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
|
||||
RatesByCurrency.Add(rate.Name, rateJson.Rate);
|
||||
Rates.Add(rateJson);
|
||||
}
|
||||
|
@ -76,8 +76,8 @@ namespace BTCPayServer.Services.Stores
|
||||
ApplicationUserId = ownerId,
|
||||
Role = "Owner"
|
||||
};
|
||||
await ctx.AddAsync(store).ConfigureAwait(false);
|
||||
await ctx.AddAsync(userStore).ConfigureAwait(false);
|
||||
ctx.Add(store);
|
||||
ctx.Add(userStore);
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
return store;
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
<!-- base href="/" -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>BitPay Invoice</title>
|
||||
<title>BTCPay Invoice</title>
|
||||
|
||||
<environment include="Development">
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
|
||||
|
@ -19,6 +19,11 @@
|
||||
<input asp-for="Amount" class="form-control" />
|
||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Currency" class="control-label"></label>*
|
||||
<input asp-for="Currency" class="form-control" />
|
||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="OrderId" class="control-label"></label>
|
||||
<input asp-for="OrderId" class="form-control" />
|
||||
|
@ -16,13 +16,21 @@
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||
<hr class="primary">
|
||||
<p>Create, search or pay an invoice.</p>
|
||||
<p>Create, search or pay an invoice. (<a href="#help" data-toggle="collapse">Help</a>)</p>
|
||||
<div id="help" class="collapse text-left">
|
||||
<p>You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.</br>
|
||||
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters</p>
|
||||
<ul>
|
||||
<li><b>storeid:id</b> for filtering a specific store</li>
|
||||
<li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<form asp-action="SearchInvoice" method="post">
|
||||
<input asp-for="SearchTerm" class="form-control" />
|
||||
<input type="hidden" asp-for="Count" />
|
||||
<span asp-validation-for="SearchTerm" class="text-danger"></span>
|
||||
<button type="button" class="btn btn-default" title="Search invoice">
|
||||
<button type="submit" class="btn btn-default" title="Search invoice">
|
||||
<span class="glyphicon glyphicon-search"></span> Search
|
||||
</button>
|
||||
</form>
|
||||
@ -48,7 +56,23 @@
|
||||
<tr>
|
||||
<td>@invoice.Date</td>
|
||||
<td>@invoice.InvoiceId</td>
|
||||
<td>@invoice.Status</td>
|
||||
@if(invoice.Status == "paid")
|
||||
{
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a class="dropdown-toggle dropdown-toggle-split" style="cursor: pointer;" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@invoice.Status <span class="sr-only">Toggle Dropdown</span>
|
||||
</a>
|
||||
<div class="dropdown-menu pull-right">
|
||||
<button class="dropdown-item small" data-toggle="modal" data-target="#myModal" onclick="$('#invoiceId').val('@invoice.InvoiceId')">Make Invalid</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td>@invoice.Status</td>
|
||||
}
|
||||
<td>@invoice.AmountCurrency</td>
|
||||
<td><a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> - <a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a></td>
|
||||
</tr>
|
||||
@ -59,20 +83,45 @@
|
||||
@if(Model.Skip != 0)
|
||||
{
|
||||
<a href="@Url.Action("ListInvoices", new
|
||||
{
|
||||
searchTerm = Model.SearchTerm,
|
||||
skip = Math.Max(0, Model.Skip - Model.Count),
|
||||
count = Model.Count,
|
||||
})"><<</a><span> - </span>
|
||||
{
|
||||
searchTerm = Model.SearchTerm,
|
||||
skip = Math.Max(0, Model.Skip - Model.Count),
|
||||
count = Model.Count,
|
||||
})"><<</a><span> - </span>
|
||||
}
|
||||
<a href="@Url.Action("ListInvoices", new
|
||||
{
|
||||
searchTerm = Model.SearchTerm,
|
||||
skip = Model.Skip + Model.Count,
|
||||
count = Model.Count,
|
||||
})">>></a>
|
||||
{
|
||||
searchTerm = Model.SearchTerm,
|
||||
skip = Model.Skip + Model.Count,
|
||||
count = Model.Count,
|
||||
})">>></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal -->
|
||||
<div id="myModal" class="modal fade" role="dialog">
|
||||
<form method="post" action="/invoices/invalidatepaid">
|
||||
<input id="invoiceId" name="invoiceId" type="hidden" />
|
||||
<div class="modal-dialog">
|
||||
|
||||
<!-- Modal content-->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Set Invoice status to Invalid</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to invalidate this transaction? This action is NOT undoable!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger">Yes, make invoice Invalid</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -14,6 +14,7 @@
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -22,6 +23,7 @@
|
||||
<tr>
|
||||
<td>@user.Name</td>
|
||||
<td>@user.Email</td>
|
||||
<td><a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -19,6 +19,10 @@
|
||||
<label asp-for="RequiresConfirmedEmail"></label>
|
||||
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-inline" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="LockSubscription"></label>
|
||||
<input asp-for="LockSubscription" type="checkbox" class="form-check-inline" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success" name="command" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -16,6 +16,15 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="Id"></label>
|
||||
<input asp-for="Id" readonly class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="StoreName"></label>
|
||||
<input asp-for="StoreName" class="form-control" />
|
||||
<span asp-validation-for="StoreName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="StoreName"></label>
|
||||
<input asp-for="StoreName" class="form-control" />
|
||||
@ -30,6 +39,11 @@
|
||||
<label asp-for="NetworkFee"></label>
|
||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="MonitoringExpiration"></label>
|
||||
<input asp-for="MonitoringExpiration" class="form-control" />
|
||||
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SpeedPolicy"></label>
|
||||
<select asp-for="SpeedPolicy" class="form-control">
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26730.16
|
||||
VisualStudioVersion = 15.0.27004.2005
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer", "BTCPayServer\BTCPayServer.csproj", "{949A0870-8D8C-4DE5-8845-DDD560489177}"
|
||||
EndProject
|
||||
|
Reference in New Issue
Block a user