Compare commits

...

34 Commits

Author SHA1 Message Date
2d551b9fc5 bump 2019-01-16 00:13:03 +09:00
884acdde32 Disabled POLIS and Bitcoin because default exchange (cryptopedia) is down 2019-01-16 00:08:17 +09:00
8f896de794 Merge pull request #516 from Kukks/feature/crowdfund
Crowdfund Bug fixes
2019-01-15 23:57:29 +09:00
5e4e26d2fd Merge pull request #529 from Kukks/bugfix/app-proper-redirect
fix app redirect to app instead of root url
2019-01-15 23:54:54 +09:00
ae688e6615 Merge pull request #530 from Horndev/patch-1
Grammatical corrections in exception messages
2019-01-15 23:54:20 +09:00
c4c812bdf6 Remove cryptopia from directly queried exchanges 2019-01-15 23:53:32 +09:00
e620fc0283 Add expert mode to BTCPay with No Change UTXO option 2019-01-15 23:50:45 +09:00
c333902468 Round up invoice price 2019-01-15 22:12:29 +09:00
4c83ecd06a Remove unused code 2019-01-15 21:56:33 +09:00
b28a547dc4 Grammatical corrections in exception messages
Fixed a few grammatical errors in LightningLikePaymentHandler.
2019-01-15 12:21:31 +00:00
6bc17e05bd add ids for better styling possibilities 2019-01-15 13:12:19 +01:00
0903350d30 add more log 2019-01-15 12:12:17 +01:00
6c0f19b457 Merge branch 'master' into feature/crowdfund 2019-01-15 09:48:16 +01:00
e119dc823f fix app redirect to app instead of root url 2019-01-15 09:46:07 +01:00
43295c9c57 Merge pull request #528 from bolatovumar/master
Update .NET Core SDK version in documentation
2019-01-15 16:42:40 +09:00
ded8b54042 Merge pull request #524 from Kukks/escapedstorename
Fix Store name character escaping on paid invoice #522
2019-01-15 16:41:51 +09:00
50a3178d51 Update .NET Core SDK version in documentation
Address #523
2019-01-14 16:49:12 -08:00
393c226032 fix escaped store name in return 2019-01-14 09:32:22 +01:00
f2630df387 dispose streamer properly 2019-01-14 08:21:27 +01:00
abcd2c1750 add padding when disqus enabled 2019-01-14 08:01:07 +01:00
cc95f3b5b5 fix exponent numbers in contribution amounts 2019-01-14 07:53:03 +01:00
a08ee93b43 fix issue with perk ordering 2019-01-14 07:45:21 +01:00
4b90f873d5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-11 10:52:31 +01:00
419ab8e0b1 add loader and fix perk badge zindex 2019-01-11 10:52:21 +01:00
c95ef27998 bump 2019-01-11 00:17:58 +09:00
63dfd93834 Merge pull request #511 from Kukks/feature/crowdfund
crowdfund: change text
2019-01-11 00:17:37 +09:00
57610881de change text 2019-01-10 16:12:00 +01:00
7469faf296 Merge pull request #510 from Kukks/feature/crowdfund
fix notif number
2019-01-10 23:58:57 +09:00
55a884a559 fix js number 2019-01-10 15:57:30 +01:00
ee2b3c3d10 bump 2019-01-10 23:41:08 +09:00
e5819a260b Merge pull request #509 from Kukks/feature/crowdfund
missed commit crowdfund
2019-01-10 23:40:52 +09:00
a3ecf48702 fix pos update too 2019-01-10 15:37:50 +01:00
1c0b904cd2 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-10 15:35:19 +01:00
072d8a1728 fix exponents in js product editor 2019-01-10 15:35:03 +01:00
26 changed files with 321 additions and 173 deletions

View File

@ -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;

View File

@ -1856,6 +1856,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()

View File

@ -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();

View File

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

View File

@ -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());
@ -255,6 +257,7 @@ namespace BTCPayServer.Controllers
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 });

View File

@ -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;

View File

@ -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
)
{
@ -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>();

View File

@ -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>());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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;

View File

@ -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; }
}
}

View File

@ -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; }

View File

@ -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))
{

View File

@ -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)
{

View File

@ -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());

View File

@ -1,4 +1,5 @@
@using BTCPayServer.Hubs
@using BTCPayServer.Crowdfund
@using BTCPayServer.Hubs
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@model UpdateCrowdfundViewModel
@{

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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 />

View File

@ -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",

View File

@ -52,4 +52,5 @@ canvas#fireworks {
left: -15px;
top: -15px;
padding-top: 5px;
z-index: 1
}

View File

@ -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);

View File

@ -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, '&lt;')
.replace(/>/g, '&gt;')
;
}
}
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;
};

View File

@ -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:
```