Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
686ae029e0 | |||
f3fd2e7d0f | |||
7efd9ba0a5 | |||
1c2a6bb8a1 | |||
7bcf1cbdd5 | |||
2aaa2544bd | |||
d85f03ba20 | |||
cfb51a6be4 | |||
c9d778c94b | |||
fd62f882de | |||
adc050f190 | |||
2d551b9fc5 | |||
884acdde32 | |||
8f896de794 | |||
5e4e26d2fd | |||
ae688e6615 | |||
c4c812bdf6 | |||
e620fc0283 | |||
c333902468 | |||
4c83ecd06a | |||
b28a547dc4 | |||
6bc17e05bd | |||
0903350d30 | |||
6c0f19b457 | |||
e119dc823f | |||
43295c9c57 | |||
ded8b54042 | |||
50a3178d51 | |||
393c226032 | |||
f2630df387 | |||
abcd2c1750 | |||
cc95f3b5b5 | |||
a08ee93b43 | |||
4b90f873d5 | |||
419ab8e0b1 | |||
c95ef27998 | |||
63dfd93834 | |||
57610881de | |||
7469faf296 | |||
55a884a559 | |||
ee2b3c3d10 | |||
e5819a260b | |||
a3ecf48702 | |||
1c0b904cd2 | |||
072d8a1728 |
@ -5,6 +5,7 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Crowdfund;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Hubs;
|
||||
|
64
BTCPayServer.Tests/MockDelay.cs
Normal file
64
BTCPayServer.Tests/MockDelay.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class MockDelay : IDelay
|
||||
{
|
||||
class WaitObj
|
||||
{
|
||||
public DateTimeOffset Expiration;
|
||||
public TaskCompletionSource<bool> CTS;
|
||||
}
|
||||
|
||||
List<WaitObj> waits = new List<WaitObj>();
|
||||
DateTimeOffset _Now = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
public async Task Wait(TimeSpan delay, CancellationToken cancellation)
|
||||
{
|
||||
WaitObj w = new WaitObj();
|
||||
w.Expiration = _Now + delay;
|
||||
w.CTS = new TaskCompletionSource<bool>();
|
||||
using (cancellation.Register(() =>
|
||||
{
|
||||
w.CTS.TrySetCanceled();
|
||||
}))
|
||||
{
|
||||
lock (waits)
|
||||
{
|
||||
waits.Add(w);
|
||||
}
|
||||
await w.CTS.Task;
|
||||
}
|
||||
}
|
||||
|
||||
public void Advance(TimeSpan time)
|
||||
{
|
||||
_Now += time;
|
||||
lock (waits)
|
||||
{
|
||||
foreach (var wait in waits.ToArray())
|
||||
{
|
||||
if (_Now >= wait.Expiration)
|
||||
{
|
||||
wait.CTS.TrySetResult(true);
|
||||
waits.Remove(wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AdvanceMilliseconds(long milli)
|
||||
{
|
||||
Advance(TimeSpan.FromMilliseconds(milli));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
@ -1549,6 +1549,81 @@ donation:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanScheduleBackgroundTasks()
|
||||
{
|
||||
BackgroundJobClient client = new BackgroundJobClient();
|
||||
MockDelay mockDelay = new MockDelay();
|
||||
client.Delay = mockDelay;
|
||||
bool[] jobs = new bool[4];
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
Logs.Tester.LogInformation("Start Job[0] in 5 sec");
|
||||
client.Schedule(async () => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; }, TimeSpan.FromSeconds(5.0));
|
||||
Logs.Tester.LogInformation("Start Job[1] in 2 sec");
|
||||
client.Schedule(async () => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; }, TimeSpan.FromSeconds(2.0));
|
||||
Logs.Tester.LogInformation("Start Job[2] fails in 6 sec");
|
||||
client.Schedule(async () => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
|
||||
Logs.Tester.LogInformation("Start Job[3] starts in in 7 sec");
|
||||
client.Schedule(async () => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; }, TimeSpan.FromSeconds(7.0));
|
||||
|
||||
Assert.True(new[] { false, false, false, false }.SequenceEqual(jobs));
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
var processing = client.ProcessJobs(cts.Token);
|
||||
|
||||
Assert.Equal(4, client.GetExecutingCount());
|
||||
|
||||
var waitJobsFinish = client.WaitAllRunning(default);
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(2.0));
|
||||
Assert.True(new[] { false, true, false, false }.SequenceEqual(jobs));
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(3.0));
|
||||
Assert.True(new[] { true, true, false, false }.SequenceEqual(jobs));
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
Assert.True(new[] { true, true, true, false }.SequenceEqual(jobs));
|
||||
Assert.Equal(1, client.GetExecutingCount());
|
||||
|
||||
Assert.False(waitJobsFinish.Wait(100));
|
||||
Assert.False(waitJobsFinish.IsCompletedSuccessfully);
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
Assert.True(new[] { true, true, true, true }.SequenceEqual(jobs));
|
||||
|
||||
Assert.True(waitJobsFinish.Wait(100));
|
||||
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
|
||||
Assert.True(!waitJobsFinish.IsFaulted);
|
||||
Assert.Equal(0, client.GetExecutingCount());
|
||||
|
||||
bool jobExecuted = false;
|
||||
Logs.Tester.LogInformation("This job will be cancelled");
|
||||
client.Schedule(async () => { jobExecuted = true; }, TimeSpan.FromSeconds(1.0));
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(0.5));
|
||||
Assert.False(jobExecuted);
|
||||
Thread.Sleep(100);
|
||||
Assert.Equal(1, client.GetExecutingCount());
|
||||
|
||||
|
||||
waitJobsFinish = client.WaitAllRunning(default);
|
||||
Assert.False(waitJobsFinish.Wait(100));
|
||||
cts.Cancel();
|
||||
Assert.True(waitJobsFinish.Wait(100));
|
||||
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
|
||||
Assert.True(!waitJobsFinish.IsFaulted);
|
||||
Assert.False(jobExecuted);
|
||||
|
||||
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
|
||||
Thread.Sleep(100); // Make sure it get cancelled
|
||||
|
||||
Assert.False(jobExecuted);
|
||||
Assert.Equal(0, client.GetExecutingCount());
|
||||
Assert.True(processing.IsCanceled);
|
||||
Assert.True(client.WaitAllRunning(default).Wait(100));
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void PosDataParser_ParsesCorrectly()
|
||||
@ -1856,6 +1931,50 @@ donation:
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateStrangeInvoice()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.000000012m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.000000019m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0.00000001m, invoice1.Price);
|
||||
Assert.Equal(0.00000002m, invoice2.Price);
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = -0.1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0.0m, invoice.Price);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
|
@ -69,7 +69,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.2
|
||||
image: nicolasdorier/nbxplorer:2.0.0.8
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -47,12 +47,14 @@ namespace BTCPayServer
|
||||
NetworkType = networkType;
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitBitcore();
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
//InitBitcore();
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitDash();
|
||||
InitPolis();
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
//InitPolis();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.40</Version>
|
||||
<Version>1.0.3.44</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -36,8 +36,6 @@
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.4" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.20" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
@ -47,10 +45,10 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.73" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.77" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
|
||||
<PackageReference Include="DBreeze" Version="1.92.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.1" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
|
@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Crowdfund;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Hubs;
|
||||
@ -19,6 +20,7 @@ using BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -183,7 +185,7 @@ namespace BTCPayServer.Controllers
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl,
|
||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(),
|
||||
|
||||
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
@ -254,7 +256,7 @@ namespace BTCPayServer.Controllers
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
|
||||
|
@ -71,7 +71,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
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 ?
|
||||
@ -95,7 +94,16 @@ namespace BTCPayServer.Controllers
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
|
||||
}
|
||||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
|
||||
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
|
||||
WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (Exception ex) { model.RateError = ex.Message; }
|
||||
}
|
||||
model.AdvancedMode = advancedMode;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendModel vm)
|
||||
WalletId walletId, WalletSendModel vm, string command = null)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
@ -212,6 +213,14 @@ namespace BTCPayServer.Controllers
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
if (command == "noob" || command == "expert")
|
||||
{
|
||||
ModelState.Clear();
|
||||
vm.AdvancedMode = command == "expert";
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
|
||||
if (destination == null)
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
|
||||
@ -231,7 +240,8 @@ namespace BTCPayServer.Controllers
|
||||
Destination = vm.Destination,
|
||||
Amount = vm.Amount.Value,
|
||||
SubstractFees = vm.SubstractFees,
|
||||
FeeSatoshiPerByte = vm.FeeSatoshiPerByte
|
||||
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
|
||||
NoChange = vm.NoChange
|
||||
});
|
||||
}
|
||||
|
||||
@ -403,6 +413,7 @@ namespace BTCPayServer.Controllers
|
||||
// getxpub
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
bool noChange = false,
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
{
|
||||
@ -436,7 +447,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
|
||||
destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
@ -487,24 +498,16 @@ namespace BTCPayServer.Controllers
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(derivationScheme);
|
||||
|
||||
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
|
||||
var changeAddress = await change;
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
foreach (var element in send)
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
List<Coin> availableCoins = new List<Coin>();
|
||||
foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
|
||||
{
|
||||
if (element.destination == null)
|
||||
throw new ArgumentNullException(nameof(element.destination));
|
||||
if (element.amount == null)
|
||||
throw new ArgumentNullException(nameof(element.amount));
|
||||
if (element.amount <= Money.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
availableCoins.Add(c.Coin);
|
||||
}
|
||||
|
||||
var changeAddress = await change;
|
||||
|
||||
var storeBlob = storeData.GetStoreBlob();
|
||||
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
|
||||
@ -520,10 +523,25 @@ namespace BTCPayServer.Controllers
|
||||
storeData.SetStoreBlob(storeBlob);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
retry:
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
if (element.destination == null)
|
||||
throw new ArgumentNullException(nameof(element.destination));
|
||||
if (element.amount == null)
|
||||
throw new ArgumentNullException(nameof(element.amount));
|
||||
if (element.amount <= Money.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||
builder.AddCoins(availableCoins);
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
@ -531,6 +549,7 @@ namespace BTCPayServer.Controllers
|
||||
if (element.substractFees)
|
||||
builder.SubtractFees();
|
||||
}
|
||||
|
||||
builder.SetChange(changeAddress.Item1);
|
||||
|
||||
if (network.MinFee == null)
|
||||
@ -547,13 +566,15 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
foreach (var c in unspentCoins)
|
||||
var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
|
||||
if (noChange && hasChange)
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().ToList();
|
||||
amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum();
|
||||
subsctractFeesValue = true;
|
||||
goto retry;
|
||||
}
|
||||
|
||||
var hasChange = unsigned.Outputs.Count == 2;
|
||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||
|
||||
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
@ -35,20 +36,30 @@ namespace BTCPayServer.Hubs
|
||||
{
|
||||
model.RedirectToCheckout = false;
|
||||
_AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext();
|
||||
var result = await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
|
||||
switch (result)
|
||||
try
|
||||
{
|
||||
case OkObjectResult okObjectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
|
||||
break;
|
||||
case ObjectResult objectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
|
||||
break;
|
||||
default:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
|
||||
break;
|
||||
|
||||
var result =
|
||||
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
|
||||
switch (result)
|
||||
{
|
||||
case OkObjectResult okObjectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
|
||||
break;
|
||||
case ObjectResult objectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
|
||||
break;
|
||||
default:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception)
|
||||
{
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Hubs;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
@ -16,14 +16,11 @@ using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin;
|
||||
using YamlDotNet.Core;
|
||||
|
||||
namespace BTCPayServer.Hubs
|
||||
namespace BTCPayServer.Crowdfund
|
||||
{
|
||||
public class
|
||||
CrowdfundHubStreamer
|
||||
public class CrowdfundHubStreamer: IDisposable
|
||||
{
|
||||
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app_";
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
@ -37,7 +34,9 @@ namespace BTCPayServer.Hubs
|
||||
|
||||
private readonly ConcurrentDictionary<string,(string appId, bool useAllStoreInvoices,bool useInvoiceAmount)> _QuickAppInvoiceLookup =
|
||||
new ConcurrentDictionary<string, (string appId, bool useAllStoreInvoices, bool useInvoiceAmount)>();
|
||||
|
||||
|
||||
private List<IEventAggregatorSubscription> _Subscriptions;
|
||||
|
||||
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<CrowdfundHub> hubContext,
|
||||
IMemoryCache memoryCache,
|
||||
@ -116,13 +115,15 @@ namespace BTCPayServer.Hubs
|
||||
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(OnInvoiceEvent);
|
||||
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
||||
_Subscriptions = new List<IEventAggregatorSubscription>()
|
||||
{
|
||||
UpdateLookup(updated.AppId, updated.StoreId, updated.Settings);
|
||||
InvalidateCacheForApp(updated.AppId);
|
||||
});
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(OnInvoiceEvent),
|
||||
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
||||
{
|
||||
UpdateLookup(updated.AppId, updated.StoreId, updated.Settings);
|
||||
InvalidateCacheForApp(updated.AppId);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private string GetCacheKey(string appId)
|
||||
@ -152,7 +153,7 @@ namespace BTCPayServer.Hubs
|
||||
Enum.GetName(typeof(PaymentTypes),
|
||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||
} );
|
||||
|
||||
_Logger.LogInformation($"App {quickLookup.appId}: Received Payment");
|
||||
InvalidateCacheForApp(quickLookup.appId);
|
||||
break;
|
||||
case InvoiceEvent.Created:
|
||||
@ -361,5 +362,10 @@ namespace BTCPayServer.Hubs
|
||||
StartDate = startDate
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Subscriptions.ForEach(subscription => subscription.Dispose());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using JetBrains.Annotations;
|
||||
@ -98,15 +96,5 @@ namespace BTCPayServer.Data
|
||||
else if (_Type == DatabaseType.MySQL)
|
||||
builder.UseMySql(_ConnectionString);
|
||||
}
|
||||
|
||||
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
||||
{
|
||||
builder.UseMemoryStorage();
|
||||
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
|
||||
//if (_Type == DatabaseType.Sqlite)
|
||||
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
|
||||
//else if (_Type == DatabaseType.Postgres)
|
||||
// builder.UsePostgreSqlStorage(_ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class BackgroundJobSchedulerHostedService : IHostedService
|
||||
{
|
||||
public BackgroundJobSchedulerHostedService(IBackgroundJobClient backgroundJobClient)
|
||||
{
|
||||
BackgroundJobClient = (BackgroundJobClient)backgroundJobClient;
|
||||
}
|
||||
|
||||
public BackgroundJobClient BackgroundJobClient { get; }
|
||||
|
||||
Task _Loop;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Stop = new CancellationTokenSource();
|
||||
_Loop = BackgroundJobClient.ProcessJobs(_Stop.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
CancellationTokenSource _Stop;
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Stop.Cancel();
|
||||
try
|
||||
{
|
||||
await _Loop;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
await BackgroundJobClient.WaitAllRunning(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class BackgroundJobClient : IBackgroundJobClient
|
||||
{
|
||||
class BackgroundJob
|
||||
{
|
||||
public Func<Task> Action;
|
||||
public TimeSpan Delay;
|
||||
public IDelay DelayImplementation;
|
||||
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
|
||||
{
|
||||
this.Action = action;
|
||||
this.Delay = delay;
|
||||
this.DelayImplementation = delayImplementation;
|
||||
}
|
||||
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
{
|
||||
await DelayImplementation.Wait(Delay, cancellationToken);
|
||||
await Action();
|
||||
}
|
||||
}
|
||||
|
||||
public IDelay Delay { get; set; } = TaskDelay.Instance;
|
||||
public int GetExecutingCount()
|
||||
{
|
||||
lock (_Processing)
|
||||
{
|
||||
return _Processing.Count();
|
||||
}
|
||||
}
|
||||
|
||||
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
|
||||
HashSet<Task> _Processing = new HashSet<Task>();
|
||||
public void Schedule(Func<Task> action, TimeSpan delay)
|
||||
{
|
||||
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
|
||||
}
|
||||
|
||||
public async Task WaitAllRunning(CancellationToken cancellationToken)
|
||||
{
|
||||
Task[] processing = null;
|
||||
lock (_Processing)
|
||||
{
|
||||
processing = _Processing.ToArray();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(processing).WithCancellation(cancellationToken);
|
||||
}
|
||||
catch (Exception) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessJobs(CancellationToken cancellationToken)
|
||||
{
|
||||
while (await _Jobs.Reader.WaitToReadAsync(cancellationToken))
|
||||
{
|
||||
if (_Jobs.Reader.TryRead(out var job))
|
||||
{
|
||||
var processing = job.Run(cancellationToken);
|
||||
lock (_Processing)
|
||||
{
|
||||
_Processing.Add(processing);
|
||||
}
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
processing.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Logs.PayServer.LogWarning(t.Exception, "Unhandled exception while job running");
|
||||
}
|
||||
lock (_Processing)
|
||||
{
|
||||
_Processing.Remove(processing);
|
||||
}
|
||||
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
using Hangfire;
|
||||
using Hangfire.Common;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire.Annotations;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@ -21,6 +18,7 @@ using NBXplorer;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -44,11 +42,6 @@ namespace BTCPayServer.HostedServices
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public ILogger Logger
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
IBackgroundJobClient _JobClient;
|
||||
EventAggregator _EventAggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
@ -60,10 +53,8 @@ namespace BTCPayServer.HostedServices
|
||||
EventAggregator eventAggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ILogger<InvoiceNotificationManager> logger,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
Logger = logger as ILogger ?? NullLogger.Instance;
|
||||
_JobClient = jobClient;
|
||||
_EventAggregator = eventAggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
@ -71,7 +62,7 @@ namespace BTCPayServer.HostedServices
|
||||
_EmailSender = emailSender;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
void Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
|
||||
@ -87,56 +78,26 @@ namespace BTCPayServer.HostedServices
|
||||
// TODO: Consider adding info on ItemDesc and payment info (amount)
|
||||
|
||||
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
|
||||
await _EmailSender.SendEmailAsync(
|
||||
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
_EmailSender.SendEmailAsync(invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
return;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
|
||||
var response = await SendNotification(invoice, eventCode, name, cts.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
}
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
|
||||
if (!string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
|
||||
}
|
||||
|
||||
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
|
||||
public async Task NotifyHttp(string invoiceData)
|
||||
{
|
||||
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
|
||||
var jobId = GetHttpJobId(job.Invoice);
|
||||
|
||||
if (!_Executing.TryAdd(jobId, jobId))
|
||||
return; //For some reason, Hangfire fire the job several time
|
||||
|
||||
Logger.LogInformation("Running " + jobId);
|
||||
bool reschedule = false;
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
|
||||
reschedule = !response.IsSuccessStatusCode;
|
||||
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
|
||||
@ -149,9 +110,8 @@ namespace BTCPayServer.HostedServices
|
||||
Error = "Timeout"
|
||||
});
|
||||
reschedule = true;
|
||||
Logger.LogInformation("Job " + jobId + " timed out");
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
catch (Exception ex)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
@ -166,21 +126,18 @@ namespace BTCPayServer.HostedServices
|
||||
ex = ex.InnerException;
|
||||
}
|
||||
string message = String.Join(',', messages.ToArray());
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + message);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = $"Unexpected error: {message}"
|
||||
});
|
||||
}
|
||||
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
|
||||
finally { cts?.Dispose(); }
|
||||
|
||||
job.TryCount++;
|
||||
|
||||
if (job.TryCount < MaxTry && reschedule)
|
||||
{
|
||||
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
|
||||
|
||||
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
|
||||
}
|
||||
@ -320,11 +277,6 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
int MaxTry = 6;
|
||||
|
||||
private static string GetHttpJobId(InvoiceEntity invoice)
|
||||
{
|
||||
return $"{invoice.Id}-{invoice.Status}-HTTP";
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@ -350,19 +302,18 @@ namespace BTCPayServer.HostedServices
|
||||
e.Name == InvoiceEvent.Completed ||
|
||||
e.Name == InvoiceEvent.ExpiredPaidPartial
|
||||
)
|
||||
tasks.Add(Notify(invoice));
|
||||
Notify(invoice);
|
||||
}
|
||||
|
||||
if (e.Name == "invoice_confirmed")
|
||||
{
|
||||
tasks.Add(Notify(invoice));
|
||||
Notify(invoice);
|
||||
}
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
tasks.Add(Notify(invoice, e.EventCode, e.Name));
|
||||
Notify(invoice, e.EventCode, e.Name);
|
||||
}
|
||||
await Task.WhenAll(tasks.ToArray());
|
||||
}));
|
||||
|
||||
|
||||
|
@ -11,7 +11,6 @@ using BTCPayServer.Logging;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Collections.Concurrent;
|
||||
using Hangfire;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
|
@ -38,6 +38,7 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Crowdfund;
|
||||
using BTCPayServer.Hubs;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
@ -116,7 +117,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
{
|
||||
Fallback = new FeeRate(100, 1),
|
||||
Fallback = new FeeRate(100L, 1),
|
||||
BlockTarget = 20
|
||||
});
|
||||
|
||||
@ -144,6 +145,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
|
@ -19,7 +19,6 @@ using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Hangfire;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Threading.Tasks;
|
||||
@ -27,11 +26,8 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Hangfire.AspNetCore;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.IO;
|
||||
using Hangfire.Dashboard;
|
||||
using Hangfire.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
@ -46,18 +42,6 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
class NeedRole : IDashboardAuthorizationFilter
|
||||
{
|
||||
string _Role;
|
||||
public NeedRole(string role)
|
||||
{
|
||||
_Role = role;
|
||||
}
|
||||
public bool Authorize([NotNull] DashboardContext context)
|
||||
{
|
||||
return context.GetHttpContext().User.IsInRole(_Role);
|
||||
}
|
||||
}
|
||||
public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory)
|
||||
{
|
||||
Configuration = conf;
|
||||
@ -108,13 +92,6 @@ namespace BTCPayServer.Hosting
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
});
|
||||
|
||||
services.AddHangfire((o) =>
|
||||
{
|
||||
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
|
||||
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
|
||||
options.ConfigureHangfireBuilder(o);
|
||||
});
|
||||
services.AddCors(o =>
|
||||
{
|
||||
o.AddPolicy("BitpayAPI", b =>
|
||||
@ -193,12 +170,6 @@ namespace BTCPayServer.Hosting
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
app.UseHangfireServer();
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
|
||||
{
|
||||
AppPath = options.GetRootUri(),
|
||||
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
|
||||
});
|
||||
app.UseSignalR(route =>
|
||||
{
|
||||
route.MapHub<CrowdfundHub>("/apps/crowdfund/hub");
|
||||
|
33
BTCPayServer/IDelay.cs
Normal file
33
BTCPayServer/IDelay.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public interface IDelay
|
||||
{
|
||||
Task Wait(TimeSpan delay, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class TaskDelay : IDelay
|
||||
{
|
||||
TaskDelay()
|
||||
{
|
||||
|
||||
}
|
||||
private static readonly TaskDelay _Instance = new TaskDelay();
|
||||
public static TaskDelay Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Instance;
|
||||
}
|
||||
}
|
||||
public Task Wait(TimeSpan delay, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Delay(delay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,5 +11,6 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public bool SubstractFees { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public bool NoChange { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,10 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
[Display(Name = "Fee rate (satoshi per byte)")]
|
||||
[Required]
|
||||
public int FeeSatoshiPerByte { get; set; }
|
||||
|
||||
[Display(Name = "Make sure no change UTXO is created")]
|
||||
public bool NoChange { get; set; }
|
||||
public bool AdvancedMode { get; set; }
|
||||
public decimal? Rate { get; set; }
|
||||
public int Divisibility { get; set; }
|
||||
public string Fiat { get; set; }
|
||||
|
@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
|
||||
throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely maner");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -78,7 +78,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely manner");
|
||||
throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely manner");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -115,7 +115,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
|
||||
if (address == null)
|
||||
throw new PaymentMethodUnavailableException($"DNS did not resolved {nodeInfo.Host}");
|
||||
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
|
||||
|
||||
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
|
||||
{
|
||||
|
@ -198,44 +198,6 @@ namespace BTCPayServer.Security
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
|
||||
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
|
||||
{
|
||||
if (!httpContext.Request.Path.HasValue)
|
||||
return false;
|
||||
|
||||
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
isJson)
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET" &&
|
||||
(isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||
return true;
|
||||
|
||||
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
|
||||
{
|
||||
|
12
BTCPayServer/Services/IBackgroundJobClient.cs
Normal file
12
BTCPayServer/Services/IBackgroundJobClient.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public interface IBackgroundJobClient
|
||||
{
|
||||
void Schedule(Func<Task> act, TimeSpan zero);
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Hangfire;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
@ -104,7 +104,8 @@ namespace BTCPayServer.Services.Rates
|
||||
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
|
||||
|
||||
// Cryptopia is often not available
|
||||
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
|
||||
|
||||
// Handmade providers
|
||||
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||
|
@ -1,4 +1,5 @@
|
||||
@using BTCPayServer.Hubs
|
||||
@using BTCPayServer.Crowdfund
|
||||
@using BTCPayServer.Hubs
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@model UpdateCrowdfundViewModel
|
||||
@{
|
||||
|
@ -159,7 +159,7 @@
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<input type="text" class="js-product-price form-control mb-2" value="{price}" />
|
||||
<input type="number" class="js-product-price form-control mb-2" value="{price}" />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
|
@ -1,73 +1,78 @@
|
||||
<div class="container p-0" id="app" v-cloak>
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl">
|
||||
<div class="d-flex justify-content-between px-2">
|
||||
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl" id="crowdfund-main-image">
|
||||
<div class="d-flex justify-content-between px-2" id="crowdfund-header-container">
|
||||
<h1>
|
||||
{{srvModel.title}}
|
||||
<span class="h6 text-muted" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate">
|
||||
{{srvModel.title}}
|
||||
<span class="h6 text-muted" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" id="crowdfund-header-start-date">
|
||||
Starts {{startDateRelativeTime}}
|
||||
</span>
|
||||
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate">
|
||||
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" id="crowdfund-header-end-date">
|
||||
Ends {{endDateRelativeTime}}
|
||||
</span>
|
||||
<span class="h6 text-muted" v-else-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date">
|
||||
<span class="h6 text-muted" v-else-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" id="crowdfund-header-active">
|
||||
Currently Active!
|
||||
</span>
|
||||
|
||||
</h1>
|
||||
|
||||
<span v-if="srvModel.targetAmount" class="mt-3">
|
||||
<span class="h5">{{srvModel.targetAmount}} {{targetCurrency}}</span>
|
||||
<span v-if="srvModel.resetEvery !== 'Never'" v-b-tooltip
|
||||
<span v-if="srvModel.targetAmount" class="mt-3" id="crowdfund-header-target">
|
||||
<span class="h5" id="crowdfund-header-target-amount">{{srvModel.targetAmount}} {{targetCurrency}}</span>
|
||||
<span v-if="srvModel.resetEvery !== 'Never'"
|
||||
id="crowdfund-header-target-dynamic"
|
||||
v-b-tooltip
|
||||
:title="'Goal resets every ' + srvModel.resetEveryAmount + ' ' + srvModel.resetEvery + ((srvModel.resetEveryAmount>1)?'s': '')" >Dynamic </span>
|
||||
<span v-if="srvModel.enforceTargetAmount">Hardcap Goal <span class="fa fa-question-circle" v-b-tooltip title="No contributions allowed after the goal has been reached"></span></span>
|
||||
<span v-else>Softcap Goal <span class="fa fa-question-circle" v-b-tooltip title="Contributions allowed even after goal is reached"></span> </span>
|
||||
<span v-if="srvModel.enforceTargetAmount"
|
||||
id="crowdfund-header-target-softcap">Hardcap Goal <span class="fa fa-question-circle" v-b-tooltip title="No contributions allowed after the goal has been reached"></span></span>
|
||||
<span v-else
|
||||
id="crowdfund-header-target-hardcap">Softcap Goal <span class="fa fa-question-circle" v-b-tooltip title="Contributions allowed even after goal is reached"></span> </span>
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount">
|
||||
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount"
|
||||
id="crowdfund-progress-bar">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
:aria-valuenow="srvModel.info.progressPercentage"
|
||||
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
|
||||
aria-valuemin="0"
|
||||
v-b-tooltip :title="srvModel.info.progressPercentage + '% Confirmed Contributions'"
|
||||
id="crowdfund-progress-bar-confirmed-bar"
|
||||
v-b-tooltip :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% contributions'"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
id="crowdfund-progress-bar-pending-bar"
|
||||
:aria-valuenow="srvModel.info.pendingProgressPercentage"
|
||||
v-bind:style="{ width: srvModel.info.pendingProgressPercentage + '%' }"
|
||||
v-b-tooltip :title="srvModel.info.pendingProgressPercentage + '% Pending Contributions'"
|
||||
v-b-tooltip :title="parseFloat(srvModel.info.pendingProgressPercentage).toFixed(2) + '% contributions pending confirmation'"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
|
||||
<div class="row py-2 text-center">
|
||||
<div class="col-sm border-right" id="raised-amount">
|
||||
<div class="col-sm border-right" id="crowdfund-body-raised-amount">
|
||||
<h5>{{ raisedAmount }} {{targetCurrency}} </h5>
|
||||
<h5 class="text-muted">Raised</h5>
|
||||
</div>
|
||||
<div class="col-sm border-right" id="goal-raised">
|
||||
<div class="col-sm border-right" id="crowdfund-body-goal-raised">
|
||||
<h5>{{ percentageRaisedAmount }}%</h5>
|
||||
<h5 class="text-muted">Of Goal</h5>
|
||||
</div>
|
||||
<div class="col-sm border-right">
|
||||
<div class="col-sm border-right" id="crowdfund-body-total-contributors">
|
||||
<h5>
|
||||
{{srvModel.info.totalContributors}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Contributors</h5>
|
||||
</div>
|
||||
<div class="col-sm" v-if="endDiff" id="campaign-dates-started">
|
||||
<div class="col-sm" v-if="endDiff" id="crowdfund-body-campaign-dates-started">
|
||||
<h5>
|
||||
{{endDiff}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Left</h5>
|
||||
<b-tooltip target="campaign-dates-started" >
|
||||
<b-tooltip target="crowdfund-body-campaign-dates-started" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
@ -78,13 +83,13 @@
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="col-sm" v-if="startDiff" id="campaign-dates-not-started">
|
||||
<div class="col-sm" v-if="startDiff" id="crowdfund-body-campaign-dates-not-started">
|
||||
<h5>
|
||||
{{startDiff}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Left to start</h5>
|
||||
|
||||
<b-tooltip target="campaign-dates-ended" >
|
||||
<b-tooltip target="crowdfund-body-campaign-dates-not-started" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
@ -95,13 +100,13 @@
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="col-sm" v-if="ended" id="campaign-dates-ended">
|
||||
<div class="col-sm" v-if="ended" id="crowdfund-body-campaign-dates-not-active">
|
||||
<h5>
|
||||
Campaign
|
||||
</h5>
|
||||
<h5 >not active</h5>
|
||||
|
||||
<b-tooltip target="campaign-dates-not-started" >
|
||||
<b-tooltip target="crowdfund-body-campaign-dates-not-active" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
@ -111,14 +116,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-tooltip target="raised-amount" v-if="paymentStats && paymentStats.length > 0">
|
||||
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0">
|
||||
<ul class="p-0 text-uppercase">
|
||||
<li v-for="stat of paymentStats" class="list-unstyled">
|
||||
|
||||
@ -126,37 +127,34 @@
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
<b-tooltip target="goal-raised" v-if="srvModel.resetEvery !== 'Never'">
|
||||
<b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'">
|
||||
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
|
||||
</b-tooltip>
|
||||
|
||||
|
||||
<div class="card-title">
|
||||
<div class="card-title" id="crowdfund-body-header">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 col-lg-9">
|
||||
|
||||
|
||||
<h2 class="text-muted" v-if="srvModel.tagline">{{srvModel.tagline}}</h2>
|
||||
<div class="col-sm-12 col-md-8 col-lg-9" id="crowdfund-body-header-tagline-container">
|
||||
<h2 class="text-muted" v-if="srvModel.tagline" id="crowdfund-body-header-tagline">{{srvModel.tagline}}</h2>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4 col-lg-3">
|
||||
<button v-if="active" class="btn btn-lg btn-primary w-100 font-weight-bold" v-on:click="contributeModalOpen = true">Contribute</button>
|
||||
|
||||
|
||||
<div class="col-sm-12 col-md-4 col-lg-3" id="crowdfund-body-header-cta-container">
|
||||
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary w-100 font-weight-bold" v-on:click="contributeModalOpen = true">Contribute</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="srvModel.disqusEnabled">
|
||||
<b-tabs>
|
||||
<b-tab title="Details"active>
|
||||
<div class="row ">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description"></div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description"></div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:active="active"
|
||||
:loading="loading"
|
||||
:in-modal="false"
|
||||
:perks="perks">
|
||||
|
||||
@ -173,11 +171,12 @@
|
||||
<template v-else>
|
||||
<hr/>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description"></div>
|
||||
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description"></div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:loading="loading"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:active="active"
|
||||
:in-modal="false"
|
||||
@ -207,11 +206,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
|
||||
|
||||
<contribute v-if="contributeModalOpen"
|
||||
:target-currency="srvModel.targetCurrency"
|
||||
:active="active"
|
||||
:perks="srvModel.perks"
|
||||
:loading="loading"
|
||||
:in-modal="true">
|
||||
</contribute>
|
||||
</b-modal>
|
||||
@ -219,11 +218,20 @@
|
||||
</div>
|
||||
|
||||
<script type="text/x-template" id="perks-template">
|
||||
<div>
|
||||
<perk v-if="!perks || perks.length ===0" :perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}" :target-currency="targetCurrency" :active="active"
|
||||
<div class="perks-container">
|
||||
<perk v-if="!perks || perks.length ===0"
|
||||
:perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}"
|
||||
:target-currency="targetCurrency"
|
||||
:active="active"
|
||||
:loading="loading"
|
||||
:in-modal="inModal">
|
||||
</perk>
|
||||
<perk v-for="(perk, index) in perks" :perk="perk" :target-currency="targetCurrency" :active="active" :display-perks-ranking="displayPerksRanking" :index="index"
|
||||
<perk v-for="(perk, index) in perks" :perk="perk" :key="perk.id"
|
||||
:target-currency="targetCurrency"
|
||||
:active="active"
|
||||
:display-perks-ranking="displayPerksRanking"
|
||||
:index="index"
|
||||
:loading="loading"
|
||||
:in-modal="inModal">
|
||||
</perk>
|
||||
</div>
|
||||
@ -276,8 +284,12 @@
|
||||
<span class='input-group-text'>{{targetCurrency}}</span>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!active"
|
||||
v-bind:class="{ 'btn-disabled': loading}"
|
||||
:disabled="!active || loading"
|
||||
type="submit">
|
||||
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
@ -297,6 +309,7 @@
|
||||
<h3 v-if="!inModal" class="mb-3">Contribute</h3>
|
||||
<perks
|
||||
:perks="perks"
|
||||
:loading="loading"
|
||||
:in-modal="inModal"
|
||||
:display-perks-ranking="displayPerksRanking"
|
||||
:target-currency="targetCurrency"
|
||||
|
@ -387,10 +387,10 @@
|
||||
</div>
|
||||
<div class="success-message">{{$t("This invoice has been paid")}}</div>
|
||||
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
|
||||
<span>{{$t("Return to StoreName", srvModel)}}</span>
|
||||
<span v-html="$t('Return to StoreName', srvModel)"></span>
|
||||
</a>
|
||||
<button class="action-button close-action" v-show="isModal">
|
||||
<span>{{$t("Return to StoreName", srvModel)}}</span>
|
||||
<span v-html="$t('home.header.title')">{{$t("Return to StoreName", srvModel)}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
|
||||
{
|
||||
public enum ServerNavPages
|
||||
{
|
||||
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintenance, Logs
|
||||
Index, Users, Rates, Emails, Policies, Theme, Services, Maintenance, Logs
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,5 @@
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="Logs">Logs</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
|
||||
</div>
|
||||
|
||||
|
@ -55,7 +55,23 @@
|
||||
<label asp-for="SubstractFees"></label>
|
||||
<input asp-for="SubstractFees" class="form-check" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Confirm</button>
|
||||
@if (Model.AdvancedMode)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="NoChange"></label>
|
||||
<a href="https://docs.btcpayserver.org/features/wallet#make-sure-no-change-utxo-is-created-expert-mode" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<input asp-for="NoChange" class="form-check" />
|
||||
</div>
|
||||
}
|
||||
<button name="command" type="submit" class="btn btn-primary">Confirm</button>
|
||||
@if (Model.AdvancedMode)
|
||||
{
|
||||
<button name="command" type="submit" value="noob" class="btn btn-secondary">Use noob mode</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button name="command" type="submit" value="expert" class="btn btn-secondary">Use expert mode</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@
|
||||
<input type="hidden" asp-for="Amount" />
|
||||
<input type="hidden" asp-for="FeeSatoshiPerByte" />
|
||||
<input type="hidden" asp-for="SubstractFees" />
|
||||
<input type="hidden" asp-for="NoChange" />
|
||||
<p>
|
||||
You can send money received by this store to an address with the help of your Ledger Wallet. <br />
|
||||
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br />
|
||||
|
@ -18,17 +18,17 @@ addLoadEvent(function (ev) {
|
||||
Vue.use(Toasted);
|
||||
|
||||
Vue.component('contribute', {
|
||||
props: ["targetCurrency", "active", "perks", "inModal", "displayPerksRanking"],
|
||||
props: ["targetCurrency", "active", "perks", "inModal", "displayPerksRanking", "loading"],
|
||||
template: "#contribute-template"
|
||||
});
|
||||
|
||||
Vue.component('perks', {
|
||||
props: ["perks", "targetCurrency", "active", "inModal","displayPerksRanking"],
|
||||
props: ["perks", "targetCurrency", "active", "inModal","displayPerksRanking", "loading"],
|
||||
template: "#perks-template"
|
||||
});
|
||||
|
||||
Vue.component('perk', {
|
||||
props: ["perk", "targetCurrency", "active", "inModal", "displayPerksRanking", "index"],
|
||||
props: ["perk", "targetCurrency", "active", "inModal", "displayPerksRanking", "index", "loading"],
|
||||
template: "#perk-template",
|
||||
data: function () {
|
||||
return {
|
||||
@ -46,24 +46,35 @@ addLoadEvent(function (ev) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if(!this.active){
|
||||
if(!this.active || this.loading){
|
||||
return;
|
||||
}
|
||||
|
||||
eventAggregator.$emit("contribute", {amount: this.amount, choiceKey: this.perk.id});
|
||||
eventAggregator.$emit("contribute", {amount: parseFloat(this.amount), choiceKey: this.perk.id});
|
||||
},
|
||||
expand: function(){
|
||||
if(this.canExpand){
|
||||
this.expanded = true;
|
||||
}
|
||||
},
|
||||
setAmount: function (amount) {
|
||||
this.amount = (amount || 0).noExponents();
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
this.amount = this.perk.price.value;
|
||||
|
||||
mounted: function () {
|
||||
this.setAmount(this.perk.price.value);
|
||||
},
|
||||
watch: {
|
||||
perk: function (newValue, oldValue) {
|
||||
if (newValue.price.value != oldValue.price.value) {
|
||||
this.setAmount(newValue.price.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app = new Vue({
|
||||
@ -84,8 +95,9 @@ addLoadEvent(function (ev) {
|
||||
active: true,
|
||||
animation: true,
|
||||
sound: true,
|
||||
lastUpdated:""
|
||||
|
||||
lastUpdated:"",
|
||||
loading: false,
|
||||
timeoutState: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -191,9 +203,11 @@ addLoadEvent(function (ev) {
|
||||
this.active = this.started && !this.ended;
|
||||
setTimeout(this.updateComputed, 1000);
|
||||
},
|
||||
submitModalContribute: function(e){
|
||||
debugger;
|
||||
this.$refs.modalContribute.onContributeFormSubmit(e);
|
||||
setLoading: function(val){
|
||||
this.loading = val;
|
||||
if(this.timeoutState){
|
||||
clearTimeout(this.timeoutState);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
@ -207,8 +221,19 @@ addLoadEvent(function (ev) {
|
||||
btcpay.showFrame();
|
||||
|
||||
self.contributeModalOpen = false;
|
||||
self.setLoading(false);
|
||||
});
|
||||
|
||||
eventAggregator.$on("contribute", function () {
|
||||
self.setLoading(true);
|
||||
|
||||
self.timeoutState = setTimeout(function(){
|
||||
self.setLoading(false);
|
||||
},5000);
|
||||
});
|
||||
eventAggregator.$on("invoice-error", function(error){
|
||||
|
||||
self.setLoading(false);
|
||||
var msg = "";
|
||||
if(typeof error === "string"){
|
||||
msg = error;
|
||||
@ -236,6 +261,7 @@ addLoadEvent(function (ev) {
|
||||
if(self.animation) {
|
||||
fireworks();
|
||||
}
|
||||
amount = parseFloat(amount).noExponents();
|
||||
if(onChain){
|
||||
Vue.toasted.show('New payment of ' + amount+ " "+ cryptoCode + " " + (onChain? "On Chain": "LN "), {
|
||||
iconPack: "fontawesome",
|
||||
|
@ -52,4 +52,5 @@ canvas#fireworks {
|
||||
left: -15px;
|
||||
top: -15px;
|
||||
padding-top: 5px;
|
||||
z-index: 1
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
var amount = $("#Amount").val();
|
||||
var fee = $("#FeeSatoshiPerByte").val();
|
||||
var substractFee = $("#SubstractFees").val();
|
||||
var noChange = $("#NoChange").val();
|
||||
|
||||
var loc = window.location, ws_uri;
|
||||
if (loc.protocol === "https:") {
|
||||
@ -48,8 +49,14 @@
|
||||
args += "&amount=" + amount;
|
||||
args += "&feeRate=" + fee;
|
||||
args += "&substractFees=" + substractFee;
|
||||
args += "&noChange=" + noChange;
|
||||
|
||||
WriteAlert("warning", 'Please validate the transaction on your ledger');
|
||||
if (noChange === "True") {
|
||||
WriteAlert("warning", 'WARNING: Because you want to make sure no change UTXO is created, you will end up sending more than the chosen amount to your destination. Please validate the transaction on your ledger');
|
||||
}
|
||||
else {
|
||||
WriteAlert("warning", 'Please validate the transaction on your ledger');
|
||||
}
|
||||
|
||||
var confirmButton = $("#confirm-button");
|
||||
confirmButton.prop("disabled", true);
|
||||
|
File diff suppressed because one or more lines are too long
@ -26,7 +26,7 @@
|
||||
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros re-enviarán tus fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
|
||||
"ConversionTab_CalculateAmount_Error": "Reintentar",
|
||||
"ConversionTab_LoadCurrencies_Error": "Reintentar",
|
||||
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos de Lightning Network.",
|
||||
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos con Lightning Network.",
|
||||
"ConversionTab_CurrencyList_Select_Option": "Selecciona la moneda a convertir",
|
||||
"Invoice expiring soon...": "La factura expira pronto...",
|
||||
"Invoice expired": "La factura expiró",
|
||||
|
49
BTCPayServer/wwwroot/locales/sl-SI.json
Normal file
49
BTCPayServer/wwwroot/locales/sl-SI.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
|
||||
"code": "sl-SI",
|
||||
"currentLanguage": "Slovenščina",
|
||||
"lang": "Jezik",
|
||||
"Awaiting Payment...": "Čakam na plačilo...",
|
||||
"Pay with": "Plačaj z",
|
||||
"Contact and Refund Email": "E-pošta za kontakt in vračilo",
|
||||
"Contact_Body": "Prosimo, vpišite vaš e-poštni naslov. Kontaktirali vas bomo v primeru težav ali vračila.",
|
||||
"Your email": "E-poštni naslov",
|
||||
"Continue": "Nadaljuj",
|
||||
"Please enter a valid email address": "Prosimo, vnesite veljaven e-poštni naslov",
|
||||
"Order Amount": "Znesek",
|
||||
"Network Cost": "Strošek omrežja",
|
||||
"Already Paid": "Že plačano",
|
||||
"Due": "Ostane za plačilo",
|
||||
"Scan": "Skeniraj",
|
||||
"Copy": "Kopiraj",
|
||||
"Conversion": "Menjava",
|
||||
"Open in wallet": "Odpri v denarnici",
|
||||
"CompletePay_Body": "Za dokončanje plačila, prosimo pošljite {{btcDue}} {{cryptoCode}} na spodnji naslov.",
|
||||
"Amount": "Znesek",
|
||||
"Address": "Naslov",
|
||||
"Copied": "Kopirano",
|
||||
"ConversionTab_BodyTop": "Plačilo {{btcDue}} {{cryptoCode}} lahko izvedete z uporabo altcoina, ki ga prodajalec neposredno ne podpira.",
|
||||
"ConversionTab_BodyDesc": "To storitev ponuja tretja oseba. Ne pozabite, da nimamo nadzora nad tem, kako bodo ponudniki posredovali vaša sredstva. Račun bo označen kot plačan šele po prejemu sredstev na {{cryptoCode}} Blockchain.",
|
||||
"ConversionTab_CalculateAmount_Error": "Poskusite znova",
|
||||
"ConversionTab_LoadCurrencies_Error": "Poskusite znova",
|
||||
"ConversionTab_Lightning": "Ne obstaja ponudnik za menjavo valut pri plačilih v Lightning omrežju.",
|
||||
"ConversionTab_CurrencyList_Select_Option": "Prosimo, izberite valuto iz katere želite menjavo",
|
||||
"Invoice expiring soon...": "Račun bo kmalu potekel...",
|
||||
"Invoice expired": "Račun je potekel",
|
||||
"What happened?": "Kaj se je zgodilo?",
|
||||
"InvoiceExpired_Body_1": "Račun je potekel in ni več veljaven. Račun je veljaven samo {{maxTimeMinutes}} minut. \nVrnete se lahko na {{storeName}}, kjer lahko ponovno izvedete plačilo.",
|
||||
"InvoiceExpired_Body_2": "Če ste poskušali poslati plačilo, to še ni bilo sprejeto v omrežju. Plačila še nismo prejeli.",
|
||||
"InvoiceExpired_Body_3": "Če ga bomo prejeli kasneje, bomo obdelali naročilo ali vas kontaktirali glede vračila...",
|
||||
"Invoice ID": "Račun št.",
|
||||
"Order ID": "Naročilo št.",
|
||||
"Return to StoreName": "Vrni se na {{storeName}}",
|
||||
"This invoice has been paid": "Račun je plačan.",
|
||||
"This invoice has been archived": "Račun je arhiviran.",
|
||||
"Archived_Body": "Prosimo, kontaktirajte trgovino za informacije o naročilu ali pomoč.",
|
||||
"BOLT 11 Invoice": "BOLT 11 Invoice",
|
||||
"Node Info": "Node Info",
|
||||
"txCount": "{{count}} transakcija",
|
||||
"txCount_plural": "{{count}} transakcij/e",
|
||||
"Pay with CoinSwitch": "Plačilo z CoinSwitch",
|
||||
"Pay with Changelly": "Plačilo z Changelly"
|
||||
}
|
@ -29,7 +29,7 @@ Products.prototype.loadFromTemplate = function() {
|
||||
}
|
||||
|
||||
if (productProperty.indexOf('price:') !== -1) {
|
||||
price = parseFloat(productProperty.replace('price:', '').trim());
|
||||
price = parseFloat(productProperty.replace('price:', '').trim()).noExponents();
|
||||
}
|
||||
if (productProperty.indexOf('title:') !== -1) {
|
||||
title = productProperty.replace('title:', '').trim();
|
||||
@ -68,13 +68,13 @@ Products.prototype.saveTemplate = function() {
|
||||
var product = this.products[key],
|
||||
id = product.id,
|
||||
title = product.title,
|
||||
price = product.price,
|
||||
price = product.price? product.price : 0,
|
||||
image = product.image
|
||||
description = product.description,
|
||||
custom = product.custom;
|
||||
|
||||
template += id + '\n' +
|
||||
' price: ' + price + '\n' +
|
||||
' price: ' + parseFloat(price).noExponents() + '\n' +
|
||||
' title: ' + title + '\n';
|
||||
|
||||
if (description) {
|
||||
@ -158,7 +158,7 @@ Products.prototype.itemContent = function(index) {
|
||||
var template = this.template($('#template-product-content'), {
|
||||
'id': product != null ? this.escape(product.id) : '',
|
||||
'index': isNaN(index) ? '' : this.escape(index),
|
||||
'price': product != null ? this.escape(product.price) : '',
|
||||
'price': product != null ? parseFloat(this.escape(product.price)).noExponents() : '',
|
||||
'title': product != null ? this.escape(product.title) : '',
|
||||
'description': product != null ? this.escape(product.description) : '',
|
||||
'image': product != null ? this.escape(product.image) : '',
|
||||
@ -183,4 +183,22 @@ Products.prototype.escape = function(input) {
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
mag= Number(data[1])+ 1;
|
||||
|
||||
if(mag<0){
|
||||
z= sign + '0.';
|
||||
while(mag++) z += '0';
|
||||
return z + str.replace(/^\-/,'');
|
||||
}
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
||||
|
@ -64,7 +64,7 @@ You can also read the [BTCPay Merchants Guide](https://www.reddit.com/r/Bitcoin/
|
||||
|
||||
While the documentation advises to use docker-compose, you may want to build BTCPay yourself.
|
||||
|
||||
First install .NET Core SDK v2.1.4 (with patch version >= 403) as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/2.1).
|
||||
First install .NET Core SDK v2.1.6 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/2.1).
|
||||
|
||||
On Powershell:
|
||||
```
|
||||
|
Reference in New Issue
Block a user