Compare commits

...

40 Commits

Author SHA1 Message Date
db46ca87d7 do not share cache between long and short profile 2018-02-01 21:34:07 +01:00
d873a1a545 Set a longer timeout for the cache for /rates, update NBXPlorer, bump 2018-02-01 21:24:13 +01:00
a464a8702b Merge pull request #42 from Eskyee/patch-2
bootstrap.min.css deleted last line
2018-01-26 07:02:35 +01:00
63722b932a Merge pull request #41 from Eskyee/patch-1
bootstrap.css deleted last line
2018-01-26 07:02:22 +01:00
698b3c46cd bootstrap.min.css deleted last line
deleted this line
/*# sourceMappingURL=bootstrap.min.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404
(Not Found) (bootstrap.min.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.min.css.map
2018-01-26 01:19:58 +00:00
df81051d07 bootstrap.css deleted last line
deleted this line 
/*# sourceMappingURL=bootstrap.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404 
(Not Found) (bootstrap.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.css.map
2018-01-26 01:12:52 +00:00
ac70a77361 Fix #38 with paidOver + paidLate 2018-01-24 10:37:23 +01:00
59a2432af9 Better invoice loop, fix javascript 2018-01-20 14:09:57 +09:00
ea4fa8d5d4 Mock rate provider 2018-01-20 12:30:22 +09:00
ade3eff75c unwrap rates in api/rates 2018-01-20 12:11:24 +09:00
db2a2a2b6c Fix expiration message on checkout page 2018-01-20 00:33:37 +09:00
579dcb5af8 Fix confirmation message when changing altcoin derivation scheme 2018-01-20 00:29:20 +09:00
69247dee8a Fix api/rates allow to scope by cyrptoCode and storeId 2018-01-19 18:11:43 +09:00
7b9541b8e9 Do not crash if some of the altcoins are unavailable 2018-01-19 17:39:15 +09:00
a12e4d7f64 fix typo 2018-01-19 17:14:27 +09:00
897da9b07a Better explanation for the price source 2018-01-19 17:13:29 +09:00
293525d480 do not query the rate source if the preferred exchange did not changed 2018-01-19 16:19:13 +09:00
198e810355 Store can customize rate source 2018-01-19 16:00:20 +09:00
fe25e00c94 Fix https://github.com/btcpayserver/btcpayserver/issues/38 2018-01-19 10:52:44 +09:00
8b129ab2e5 Merge pull request #37 from lepipele/dev-lepi
Resolving problems with Vue console warnings
2018-01-19 01:41:01 +09:00
770bed54d1 bump 2018-01-19 00:52:38 +09:00
774817d4ac Add transaction speed on the invoice page 2018-01-19 00:52:17 +09:00
b8068b2ae8 Vue ignoring custom HTML5 elements
Ref: https://github.com/btcpayserver/btcpayserver/issues/34#issuecomment-358541767
2018-01-18 09:48:21 -06:00
3007a6bbc8 Upgrading Vue and linking production (min) version 2018-01-18 09:47:39 -06:00
1c0c8fece2 Change default speed to medium 2018-01-19 00:37:00 +09:00
c52eee47f0 bump 2018-01-19 00:13:40 +09:00
f88c98b9d9 fix block explorer link for mainnet 2018-01-18 23:57:41 +09:00
b0e9e10f7e Add extended notifications 2018-01-18 20:56:55 +09:00
39d47e33f6 Fix https://github.com/btcpayserver/btcpayserver/issues/31 2018-01-18 18:53:11 +09:00
4b7b6c6327 debug 2018-01-18 18:33:26 +09:00
a59edc5e8c bump 2018-01-18 18:12:44 +09:00
5ba322ea6a Add debug messages 2018-01-18 18:12:01 +09:00
b47b4b10cb should fix https://github.com/btcpayserver/btcpayserver/issues/31 2018-01-18 17:21:29 +09:00
c88f391935 Merge branch 'master' of https://github.com/btcpayserver/btcpayserver 2018-01-18 12:45:55 +09:00
26d3178e93 Fix expiration transitioning with a delay 2018-01-18 12:45:39 +09:00
1ad27c7827 Merge pull request #35 from thijstriemstra/patch-1
fix typos
2018-01-18 11:48:05 +09:00
4200a8eed5 add c# comment 2018-01-17 17:45:57 +01:00
daceb7af8e happy new year 2018-01-17 17:43:57 +01:00
f703b53bce fix typos 2018-01-17 17:42:06 +01:00
2762224f0f Fix parsing bug zpub LTC mainnet 2018-01-17 19:39:15 +09:00
45 changed files with 10994 additions and 10451 deletions

View File

@ -97,7 +97,12 @@ namespace BTCPayServer.Tests
.UseConfiguration(conf)
.ConfigureServices(s =>
{
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
var mockRates = new MockRateProviderFactory();
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m));
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
mockRates.AddMock(btc);
mockRates.AddMock(ltc);
s.AddSingleton<IRateProviderFactory>(mockRates);
s.AddLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)

View File

@ -25,6 +25,7 @@ using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Eclair;
using System.Collections.Generic;
using BTCPayServer.Models.StoreViewModels;
using System.Threading.Tasks;
namespace BTCPayServer.Tests
{
@ -50,7 +51,7 @@ namespace BTCPayServer.Tests
// Some check that handling legacy stuff does not break things
var cryptoData = entity.GetCryptoData("BTC", null, true);
cryptoData.Calculate();
Assert.NotNull(cryptoData);
Assert.NotNull(cryptoData);
Assert.Null(entity.GetCryptoData("BTC", null, false));
entity.SetCryptoData(new CryptoData() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
Assert.NotNull(entity.GetCryptoData("BTC", null, false));
@ -279,7 +280,8 @@ namespace BTCPayServer.Tests
OrderId = "orderId",
NotificationURL = callbackServer.GetUri().AbsoluteUri,
ItemDesc = "Some description",
FullNotifications = true
FullNotifications = true,
ExtendedNotifications = true
});
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
@ -517,6 +519,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
@ -551,6 +554,7 @@ namespace BTCPayServer.Tests
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -564,7 +568,7 @@ namespace BTCPayServer.Tests
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due));
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment);
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -749,6 +753,10 @@ namespace BTCPayServer.Tests
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
//Manually check that cache get hit after 10 sec
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" };
var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult();
Assert.Throws<RateUnavailableException>(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult());
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
@ -759,7 +767,7 @@ namespace BTCPayServer.Tests
private void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try

View File

@ -37,7 +37,7 @@ services:
- postgres
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.3
image: nicolasdorier/nbxplorer:1.0.1.10
ports:
- "32838:32838"
expose:

View File

@ -20,7 +20,7 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",

View File

@ -18,7 +18,7 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = "https://live.blockcypher.com/ltc/tx/{0}/",
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.1</Version>
<Version>1.0.1.20</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -21,10 +21,10 @@
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.52" />
<PackageReference Include="NBitcoin" Version="4.0.0.54" />
<PackageReference Include="NBitpayClient" Version="1.0.0.16" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.2" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.8" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />

View File

@ -48,6 +48,7 @@ namespace BTCPayServer.Controllers
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
Id = invoice.Id,
Status = invoice.Status,
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
@ -161,6 +162,7 @@ namespace BTCPayServer.Controllers
CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = FormatCurrency(cryptoData),
MerchantRefLink = invoice.RedirectURL ?? "/",
@ -237,8 +239,7 @@ namespace BTCPayServer.Controllers
try
{
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoicePaymentEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceStatusChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
@ -414,6 +415,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
return RedirectToAction(nameof(ListInvoices));
}

View File

@ -80,9 +80,9 @@ namespace BTCPayServer.Controllers
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).ToList();
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).Where(c => _ExplorerClients.IsAvailable(c.Network.CryptoCode)).ToList();
if (derivationStrategies.Count == 0)
throw new BitpayHttpException(400, "This store has not configured the derivation strategy");
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow
@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
.Select(derivationStrategy => (Wallet: _WalletProvider.GetWallet(derivationStrategy.Network),
DerivationStrategy: derivationStrategy.DerivationStrategyBase,
Network: derivationStrategy.Network,
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network),
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network, false),
FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network)))
.Where(_ => _.Wallet != null &&
_.FeeRateProvider != null &&
@ -164,7 +164,7 @@ namespace BTCPayServer.Controllers
#pragma warning disable CS0618
var btc = _NetworkProvider.BTC;
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc));
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
@ -178,7 +178,7 @@ namespace BTCPayServer.Controllers
entity.SetCryptoData(cryptoDatas);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceCreatedEvent(entity.Id));
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}

View File

@ -7,34 +7,68 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers
{
public class RateController : Controller
{
IRateProvider _RateProvider;
IRateProviderFactory _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
StoreRepository _StoreRepo;
public RateController(
IRateProviderFactory rateProviderFactory,
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
{
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_NetworkProvider = networkProvider;
_StoreRepo = storeRepo;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
{
var allRates = (await _RateProvider.GetRatesAsync());
return new DataWrapper<NBitpayClient.Rate[]>
(allRates.Select(r =>
var result = await GetRates2(cryptoCode, storeId);
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
if(rates == null)
return result;
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
{
cryptoCode = cryptoCode ?? "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
return NotFound();
var rateProvider = _RateProviderFactory.GetRateProvider(network, true);
if (rateProvider == null)
return NotFound();
if (storeId != null)
{
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return NotFound();
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
}
var allRates = (await rateProvider.GetRatesAsync());
return Json(allRates.Select(r =>
new NBitpayClient.Rate()
{
Code = r.Currency,
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
Value = r.Value
}).Where(n => n.Name != null).ToArray());
}
}
}

View File

@ -17,6 +17,7 @@ using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
@ -95,7 +96,7 @@ namespace BTCPayServer.Controllers
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetDerivationStrategies(_NetworkProvider)
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase))
.Where(_ => _.Wallet != null)
.Select(async _ => (await _.Wallet.GetBalance(_.DerivationStrategy)).ToString() + " " + _.Wallet.Network.CryptoCode))
@ -165,6 +166,7 @@ namespace BTCPayServer.Controllers
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
return View(vm);
}
@ -241,7 +243,7 @@ namespace BTCPayServer.Controllers
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {vm.CryptoCurrency} has been modified.";
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else
@ -256,7 +258,7 @@ namespace BTCPayServer.Controllers
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
}
}
catch
@ -276,6 +278,8 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
if (model.PreferredExchange != null)
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
@ -309,6 +313,10 @@ namespace BTCPayServer.Controllers
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
if (store.SetStoreBlob(blob))
@ -316,6 +324,19 @@ namespace BTCPayServer.Controllers
needUpdate = true;
}
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
{
using (HttpClient client = new HttpClient())
{
var rate = await client.GetAsync(model.RateSource);
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
{
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model);
}
}
}
if (needUpdate)
{
await _Repo.UpdateStore(store);
@ -350,7 +371,7 @@ namespace BTCPayServer.Controllers
var prefix = Utils.ToUInt32(data, false);
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
var standardPrefix = Utils.ToBytes(network.NBitcoinNetwork == Network.Main ? 0x0488b21eU : 0x043587cf, false);
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];

View File

@ -235,9 +235,29 @@ namespace BTCPayServer.Data
}
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
public string PreferredExchange { get; set; }
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
{
if (!PreferredExchange.IsCoinAverage())
{
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
if (rateProvider is CachedRateProvider cachedRateProvider)
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
cachedRateProvider.Inner
});
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
}
else
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
rateProvider
});
}
}
if (RateRules == null || RateRules.Count == 0)
return rateProvider;
return new TweakRateProvider(network, rateProvider, RateRules.ToList());

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceCreatedEvent
{
public InvoiceCreatedEvent(string id)
{
InvoiceId = id;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} created";
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceEvent
{
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
{
}
public InvoiceEvent(string invoiceId, int code, string name)
{
InvoiceId = invoiceId;
EventCode = code;
Name = name;
}
public string InvoiceId { get; set; }
public int EventCode { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
}
}
}

View File

@ -7,19 +7,29 @@ namespace BTCPayServer.Events
{
public class InvoiceIPNEvent
{
public InvoiceIPNEvent(string invoiceId)
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name)
{
InvoiceId = invoiceId;
EventCode = eventCode;
Name = name;
}
public int? EventCode { get; set; }
public string Name { get; set; }
public string InvoiceId { get; set; }
public string Error { get; set; }
public override string ToString()
{
string ipnType = "IPN";
if(EventCode.HasValue)
{
ipnType = $"IPN ({EventCode.Value} {Name})";
}
if (Error == null)
return $"IPN sent for invoice {InvoiceId}";
return $"Error while sending IPN: {Error}";
return $"{ipnType} sent for invoice {InvoiceId}";
return $"Error while sending {ipnType}: {Error}";
}
}
}

View File

@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoicePaymentEvent
{
public InvoicePaymentEvent(string invoiceId, string cryptoCode, string address)
{
InvoiceId = invoiceId;
Address = address;
CryptoCode = cryptoCode;
}
public string Address { get; set; }
public string CryptoCode { get; private set; }
public string InvoiceId { get; set; }
public override string ToString()
{
return $"{CryptoCode}: Invoice {InvoiceId} received a payment on {Address}";
}
}
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceStatusChangedEvent
{
public InvoiceStatusChangedEvent()
{
}
public InvoiceStatusChangedEvent(InvoiceEntity invoice, string newState)
{
OldState = invoice.Status;
InvoiceId = invoice.Id;
NewState = newState;
}
public string InvoiceId { get; set; }
public string OldState { get; set; }
public string NewState { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} changed status: {OldState} => {NewState}";
}
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using NBXplorer;
using BTCPayServer.HostedServices;
namespace BTCPayServer
{
@ -15,9 +16,10 @@ namespace BTCPayServer
BTCPayServerOptions _Options;
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options)
NBXplorerDashboard _Dashboard;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
{
_Dashboard = dashboard;
_NetworkProviders = networkProviders;
_Options = options;
@ -68,6 +70,16 @@ namespace BTCPayServer
return GetExplorerClient(network.CryptoCode);
}
public bool IsAvailable(BTCPayNetwork network)
{
return IsAvailable(network.CryptoCode);
}
public bool IsAvailable(string cryptoCode)
{
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
var network = _NetworkProviders.GetNetwork(cryptoCode);

View File

@ -31,6 +31,12 @@ namespace BTCPayServer
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static bool IsCoinAverage(this string exchangeName)
{
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();

View File

@ -37,6 +37,9 @@ namespace BTCPayServer.HostedServices
{
get; set;
}
public int? EventCode { get; set; }
public string Message { get; set; }
}
public ILogger Logger
@ -63,32 +66,32 @@ namespace BTCPayServer.HostedServices
_NetworkProvider = networkProvider;
}
async Task Notify(InvoiceEntity invoice)
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id));
await SendNotification(invoice, cts.Token);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
await SendNotification(invoice, eventCode, name, cts.Token);
return;
}
catch(OperationCanceledException) when(cts.IsCancellationRequested)
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id)
_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
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id)
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = ex.Message
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
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);
}
@ -107,14 +110,18 @@ namespace BTCPayServer.HostedServices
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id));
HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token);
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
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
});
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id)
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = "Timeout"
});
@ -123,12 +130,25 @@ namespace BTCPayServer.HostedServices
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id)
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = ex.Message
});
reschedule = true;
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
List<string> messages = new List<string>();
while(ex != null)
{
messages.Add(ex.Message);
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); }
@ -143,8 +163,23 @@ namespace BTCPayServer.HostedServices
}
}
public class InvoicePaymentNotificationEvent
{
[JsonProperty("code")]
public int Code { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
public class InvoicePaymentNotificationEventWrapper
{
[JsonProperty("event")]
public InvoicePaymentNotificationEvent Event { get; set; }
[JsonProperty("data")]
public InvoicePaymentNotification Data { get; set; }
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
@ -167,7 +202,7 @@ namespace BTCPayServer.HostedServices
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC");
if(btcCryptoInfo != null)
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Rate = (double)dto.Rate;
@ -177,8 +212,22 @@ namespace BTCPayServer.HostedServices
notification.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
string notificationString = null;
if (eventCode.HasValue)
{
var wrapper = new InvoicePaymentNotificationEventWrapper();
wrapper.Data = notification;
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
notificationString = JsonConvert.SerializeObject(wrapper);
}
else
{
notificationString = JsonConvert.SerializeObject(notification);
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(JsonConvert.SerializeObject(notification), UTF8, "application/json");
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await _Client.SendAsync(request, cancellation);
return response;
}
@ -193,42 +242,41 @@ namespace BTCPayServer.HostedServices
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
leases.Add(_EventAggregator.Subscribe<InvoiceStatusChangedEvent>(async e =>
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
await SaveEvent(invoice.Id, e);
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
if (invoice.FullNotifications)
{
if (e.NewState == "expired" ||
e.NewState == "paid" ||
e.NewState == "invalid" ||
e.NewState == "complete"
if (e.Name == "invoice_expired" ||
e.Name == "invoice_paidInFull" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_markedInvalid" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_completed"
)
await Notify(invoice);
}
if(e.NewState == "confirmed")
if (e.Name == "invoice_confirmed")
{
await Notify(invoice);
}
await SaveEvent(invoice.Id, e);
if (invoice.ExtendedNotifications)
{
await Notify(invoice, e.EventCode, e.Name);
}
}));
leases.Add(_EventAggregator.Subscribe<InvoiceCreatedEvent>(async e =>
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoicePaymentEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
{

View File

@ -68,7 +68,11 @@ namespace BTCPayServer.HostedServices
{
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey, network.CryptoCode);
if (invoice != null)
{
String address = scriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? scriptPubKey.ToString();
Logs.PayServer.LogInformation($"{address} is mapping to invoice {invoice}");
_WatchRequests.Add(invoice);
}
}
async Task NotifyBlock()
@ -82,8 +86,11 @@ namespace BTCPayServer.HostedServices
private async Task UpdateInvoice(string invoiceId, CancellationToken cancellation)
{
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
while (!cancellation.IsCancellationRequested)
int maxLoop = 5;
int loopCount = -1;
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
{
loopCount++;
try
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
@ -122,7 +129,7 @@ namespace BTCPayServer.HostedServices
break;
}
if (!changed || cancellation.IsCancellationRequested)
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
break;
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
@ -146,7 +153,7 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired"));
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
invoice.Status = "expired";
}
@ -169,7 +176,7 @@ namespace BTCPayServer.HostedServices
invoice.Payments.Add(payment);
#pragma warning restore CS0618
alreadyAccounted.Add(coin.Coin.Outpoint);
context.Events.Add(new InvoicePaymentEvent(invoice.Id, coins.Wallet.Network.CryptoCode, coin.Coin.ScriptPubKey.GetDestinationAddress(coins.Wallet.Network.NBitcoinNetwork).ToString()));
context.Events.Add(new InvoiceEvent(invoice, 1002, "invoice_receivedPayment"));
dirtyAddress = true;
}
if (dirtyAddress)
@ -188,26 +195,20 @@ namespace BTCPayServer.HostedServices
{
if (invoice.Status == "new")
{
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
invoice.Status = "paid";
invoice.ExceptionStatus = null;
invoice.ExceptionStatus = totalPaid > accounting.TotalDue ? "paidOver" : null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
else if (invoice.Status == "expired")
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
{
invoice.ExceptionStatus = "paidLate";
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
context.MarkDirty();
}
}
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
{
invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
invoice.ExceptionStatus = "paidPartial";
@ -246,14 +247,14 @@ namespace BTCPayServer.HostedServices
(totalConfirmed < accounting.TotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
invoice.Status = "invalid";
context.MarkDirty();
}
else if (totalConfirmed >= accounting.TotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
invoice.Status = "confirmed";
context.MarkDirty();
}
@ -266,7 +267,7 @@ namespace BTCPayServer.HostedServices
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue)
{
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
invoice.Status = "complete";
context.MarkDirty();
}
@ -277,7 +278,7 @@ namespace BTCPayServer.HostedServices
private IEnumerable<Task<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
{
return strategies
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
.Select(d => (Wallet: _WalletProvider.IsAvailable(d.Network) ? _WalletProvider.GetWallet(d.Network) : null,
Network: d.Network,
Strategy: d.DerivationStrategyBase))
.Where(d => d.Wallet != null)
@ -408,11 +409,23 @@ namespace BTCPayServer.HostedServices
}
}
private void Watch(string invoiceId)
private async Task Watch(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
_WatchRequests.Add(invoiceId);
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
try
{
var now = DateTimeOffset.UtcNow;
if (invoice.ExpirationTime > now)
{
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
_WatchRequests.Add(invoiceId);
}
}
catch when (_Cts.IsCancellationRequested)
{ }
}
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
@ -430,7 +443,13 @@ namespace BTCPayServer.HostedServices
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceCreatedEvent>(b => { Watch(b.InvoiceId); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == "invoice_created")
{
await Watch(b.InvoiceId);
}
}));
return Task.CompletedTask;
}
@ -464,24 +483,32 @@ namespace BTCPayServer.HostedServices
{
Logs.PayServer.LogInformation("Start watching invoices");
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
ConcurrentDictionary<string, Task> executing = new ConcurrentDictionary<string, Task>();
ConcurrentDictionary<string, Lazy<Task>> executing = new ConcurrentDictionary<string, Lazy<Task>>();
try
{
// This loop just make sure an invoice will not be updated at the same time by two tasks.
// If an update is happening while a request come, then the update is deferred when the executing task is over
foreach (var item in _WatchRequests.GetConsumingEnumerable(cancellation))
{
var task = executing.GetOrAdd(item, async i =>
var localItem = item;
var toExecute = new Lazy<Task>(async () =>
{
try
{
await UpdateInvoice(i, cancellation);
await UpdateInvoice(localItem, cancellation);
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
finally
{
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
await Task.Delay(2000, cancellation);
executing.TryRemove(localItem, out Lazy<Task> unused);
}
finally { executing.TryRemove(item, out Task useless); }
});
}, false);
var executingTask = executing.GetOrAdd(item, toExecute);
executingTask.Value.GetAwaiter(); // Make sure it run
if (executingTask != toExecute)
{
// What was planned can't run for now, rebook it when the executingTask finish
var unused = executingTask.Value.ContinueWith(t => _WatchRequests.Add(localItem));
}
}
}
catch when (cancellation.IsCancellationRequested)
@ -489,7 +516,7 @@ namespace BTCPayServer.HostedServices
}
finally
{
await Task.WhenAll(executing.Values);
await Task.WhenAll(executing.Values.Select(v => v.Value).ToArray());
}
Logs.PayServer.LogInformation("Stop watching invoices");
}

View File

@ -87,19 +87,22 @@ namespace BTCPayServer.HostedServices
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
leases.Add(_Aggregator.Subscribe<Events.InvoiceCreatedEvent>(async inv =>
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
List<Task> listeningDerivations = new List<Task>();
foreach (var notificationSessions in _Sessions)
if (inv.Name == "invoice_created")
{
var derivationStrategy = GetStrategy(notificationSessions.Key, invoice);
if (derivationStrategy != null)
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
List<Task> listeningDerivations = new List<Task>();
foreach (var notificationSessions in _Sessions)
{
listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token));
var derivationStrategy = GetStrategy(notificationSessions.Key, invoice);
if (derivationStrategy != null)
{
listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token));
}
}
await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false);
}
await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false);
}));
return Task.CompletedTask;
}

View File

@ -41,6 +41,11 @@ namespace BTCPayServer.HostedServices
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
}
public bool IsFullySynched(string cryptoCode)
{
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
}
public IEnumerable<NBXplorerSummary> GetAll()
{
return _Summaries.Values;

View File

@ -97,6 +97,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public string TransactionSpeed { get; set; }
public object StoreName
{
get;

View File

@ -39,5 +39,6 @@ namespace BTCPayServer.Models.InvoicingModels
public string OrderId { get; set; }
public string CryptoImage { get; set; }
public string NetworkFeeDescription { get; internal set; }
public int MaxTimeMinutes { get; internal set; }
}
}

View File

@ -47,6 +47,17 @@ namespace BTCPayServer.Models.StoreViewModels
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
public string PreferredExchange { get; set; }
public string RateSource
{
get
{
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
}
}
[Display(Name = "Multiply the original rate by ...")]
[Range(0.01, 10.0)]
public double RateMultiplier

View File

@ -45,6 +45,7 @@ namespace BTCPayServer
.ConfigureLogging(l =>
{
l.AddFilter("Microsoft", LogLevel.Error);
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
l.AddProvider(new CustomConsoleLogProvider(processor));
})
.UseStartup<Startup>()

View File

@ -1,4 +1,5 @@
using DBreeze;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
@ -16,6 +17,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using System.Globalization;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Logging;
namespace BTCPayServer.Services.Invoices
{
@ -43,6 +45,7 @@ namespace BTCPayServer.Services.Invoices
public async Task<bool> RemovePendingInvoice(string invoiceId)
{
Logs.PayServer.LogInformation($"Remove pending invoice {invoiceId}");
using (var ctx = _ContextFactory.CreateContext())
{
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });

View File

@ -11,6 +11,16 @@ namespace BTCPayServer.Services.Rates
{
IMemoryCache _Cache;
ConcurrentDictionary<string, IRateProvider> _Providers = new ConcurrentDictionary<string, IRateProvider>();
ConcurrentDictionary<string, IRateProvider> _LongCacheProviders = new ConcurrentDictionary<string, IRateProvider>();
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
public CachedDefaultRateProviderFactory(IMemoryCache cache)
{
if (cache == null)
@ -18,10 +28,13 @@ namespace BTCPayServer.Services.Rates
_Cache = cache;
}
public IRateProvider RateProvider { get; set; }
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0);
public IRateProvider GetRateProvider(BTCPayNetwork network)
public TimeSpan LongCacheSpan { get; set; } = TimeSpan.FromMinutes(15.0);
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
{
return _Providers.GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, network.DefaultRateProvider, _Cache) { CacheSpan = CacheSpan });
return (longCache ? _LongCacheProviders : _Providers).GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, RateProvider ?? network.DefaultRateProvider, _Cache) { CacheSpan = longCache ? LongCacheSpan : CacheSpan, AdditionalScope = longCache ? "LONG" : "SHORT" });
}
}
}

View File

@ -19,19 +19,28 @@ namespace BTCPayServer.Services.Rates
if (memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache));
this._Inner = inner;
this._MemoryCache = memoryCache;
this.MemoryCache = memoryCache;
this._CryptoCode = cryptoCode;
}
public IRateProvider Inner
{
get
{
return _Inner;
}
}
public TimeSpan CacheSpan
{
get;
set;
} = TimeSpan.FromMinutes(1.0);
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
public Task<decimal> GetRateAsync(string currency)
{
return _MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode, (ICacheEntry entry) =>
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRateAsync(currency);
@ -40,11 +49,13 @@ namespace BTCPayServer.Services.Rates
public Task<ICollection<Rate>> GetRatesAsync()
{
return _MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) =>
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync();
});
}
public string AdditionalScope { get; set; }
}
}

View File

@ -24,6 +24,8 @@ namespace BTCPayServer.Services.Rates
CryptoCode = cryptoCode ?? "BTC";
}
public string Exchange { get; set; }
public string CryptoCode { get; set; }
public string Market
@ -45,7 +47,15 @@ namespace BTCPayServer.Services.Rates
private async Task<Dictionary<string, decimal>> GetRatesCore()
{
var resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
HttpResponseMessage resp = null;
if (Exchange == null)
{
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
}
else
{
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}");
}
using (resp)
{
@ -57,9 +67,23 @@ namespace BTCPayServer.Services.Rates
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode();
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
if(Exchange != null)
{
rates = (JObject)rates["symbols"];
}
return rates.Properties()
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase))
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p => ToDecimal(p.Value["last"]));
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
{
if (Exchange == null)
{
return ToDecimal(p.Value["last"]);
}
else
{
return ToDecimal(p.Value["bid"]);
}
});
}
}

View File

@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Rates
{
public interface IRateProviderFactory
{
IRateProvider GetRateProvider(BTCPayNetwork network);
IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache);
}
}

View File

@ -6,17 +6,38 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class MockRateProviderFactory : IRateProviderFactory
{
List<MockRateProvider> _Mocks = new List<MockRateProvider>();
public MockRateProviderFactory()
{
}
public void AddMock(MockRateProvider mock)
{
_Mocks.Add(mock);
}
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
{
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
}
}
public class MockRateProvider : IRateProvider
{
List<Rate> _Rates;
public MockRateProvider(params Rate[] rates)
public string CryptoCode { get; }
public MockRateProvider(string cryptoCode, params Rate[] rates)
{
_Rates = new List<Rate>(rates);
CryptoCode = cryptoCode;
}
public MockRateProvider(List<Rate> rates)
public MockRateProvider(string cryptoCode, List<Rate> rates)
{
_Rates = rates;
CryptoCode = cryptoCode;
}
public Task<decimal> GetRateAsync(string currency)
{

View File

@ -68,7 +68,8 @@ namespace BTCPayServer.Services.Stores
StoreData store = new StoreData
{
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)),
StoreName = name
StoreName = name,
SpeedPolicy = Invoices.SpeedPolicy.MediumSpeed
};
var userStore = new UserStore
{

View File

@ -38,5 +38,10 @@ namespace BTCPayServer.Services.Wallets
return null;
return new BTCPayWallet(client, _TransactionCacheProvider.GetTransactionCache(network), network);
}
public bool IsAvailable(BTCPayNetwork network)
{
return _Client.IsAvailable(network);
}
}
}

View File

@ -21,7 +21,7 @@
@Model.ToJSVariableModel("srvModel")
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
<script src="~/js/vue.js" type="text/javascript" defer="defer"></script>
<script src="~/js/vue.min.js" type="text/javascript" defer="defer"></script>
<script src="~/js/vue-qrcode.js" type="text/javascript" defer="defer"></script>
<script src="~/js/core.js" type="text/javascript" defer="defer"></script>
<!-- <script src="img/Intl.js" type="text/javascript" defer="defer"></script>
@ -492,7 +492,7 @@
<div>
<div class="expired__body">
<div class="expired__header" i18n="">What happened?</div>
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for 15 minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for @Model.MaxTimeMinutes minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
<div class="expired__text" i18n="">If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.</div>
<div class="expired__text" i18n="">
If the transaction
@ -504,7 +504,7 @@
<span class="expired__text__bullet" i18n="">Invoice ID:</span> {{srvModel.invoiceId}}<br>
<!---->
<span>
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.OrderId}}
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.orderId}}
</span>
</div>
</div>

View File

@ -53,6 +53,10 @@
<th>Monitoring date</th>
<td>@Model.MonitoringDate</td>
</tr>
<tr>
<th>Transaction speed</th>
<td>@Model.TransactionSpeed</td>
</tr>
<tr>
<th>Status</th>
<td>@Model.Status</td>

View File

@ -38,6 +38,14 @@
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="PreferredExchange"></label>
<input asp-for="PreferredExchange" class="form-control" />
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.<small> (using 1 minute cache)</small>
</p>
</div>
<div class="form-group">
<label asp-for="RateMultiplier"></label>
<input asp-for="RateMultiplier" class="form-control" />

View File

@ -43,7 +43,7 @@ h6 {
}
p {
font-size: 16px;
/*font-size: 16px;*/
line-height: 1.5;
margin-bottom: 20px;
}

View File

@ -14,6 +14,12 @@
*/
// TODO: Vue controller... complete migrate to it for binding, animations can stay in jQuery
Vue.config.ignoredElements = [
'line-items',
'low-fee-timeline',
// Ignoring custom HTML5 elements, eg: bp-spinner
/^bp-/
];
var checkoutCtrl = new Vue({
el: '#checkoutCtrl',
components: {
@ -22,7 +28,7 @@ var checkoutCtrl = new Vue({
data: {
srvModel: srvModel
}
})
});
var display = $(".timer-row__time-left"); // Timer container
@ -82,7 +88,7 @@ function emailForm() {
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
}
})
});
}
/* =============== Even listeners =============== */
@ -130,7 +136,7 @@ $("#copy-tab").click(function () {
$(".payment-tabs__slider").addClass("slide-right");
}
if (!($("#copy").is(".active"))) {
if (!$("#copy").is(".active")) {
$("#copy").show();
$("#copy").addClass("active");
@ -278,7 +284,7 @@ function progressStart(timerMax) {
var now = new Date();
var timeDiff = end.getTime() - now.getTime();
var perc = 100 - Math.round((timeDiff / timerMax) * 100);
var perc = 100 - Math.round(timeDiff / timerMax * 100);
if (perc === 75 && (status === "paidPartial" || status === "new")) {
$(".timer-row").addClass("expiring-soon");

File diff suppressed because it is too large Load Diff

6
BTCPayServer/wwwroot/js/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -9890,4 +9890,3 @@ a.text-dark:focus, a.text-dark:hover {
.invisible {
visibility: hidden !important;
}
/*# sourceMappingURL=bootstrap.css.map */

View File

@ -8593,4 +8593,3 @@ a.text-dark:focus, a.text-dark:hover {
.invisible {
visibility: hidden !important
}
/*# sourceMappingURL=bootstrap.min.css.map */

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017 btcpayserver
Copyright (c) 2017-2018 btcpayserver
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -8,17 +8,17 @@
## Introduction
BTCPay Server is an Open Source payment processor conforms to the invoice API of Bitpay.
BTCPay Server is an Open Source payment processor, written in C#, that conforms to the invoice API of [Bitpay](https://bitpay.com/).
This allows easy migration of your code base to your own, self-hosted payment processor.
This solution is for you if:
* You currently use Bitpay as a payment processor but are worry about their commitment to Bitcoin in the future
* You are currently using Bitpay as a payment processor but are worried about their commitment to Bitcoin in the future
* You want to be in control of your own funds
* Bitpay compliance team decided to reject your application
* You want lower fee (we support Segwit)
* You want to become a payment processor yourself and offer BTCPay hosted solution to merchants
* You want to a way support other currency than those offered by Bitpay
* You want lower fees (we support Segwit)
* You want to become a payment processor yourself and offer a BTCPay hosted solution to merchants
* You want a way to support currencies other than those offered by Bitpay
## Documentation