Compare commits

...

67 Commits

Author SHA1 Message Date
fd4f4e6aff better feedback if forgot to activate browser support 2018-02-13 12:20:56 +09:00
cedf8f75e8 Small UI adjustements 2018-02-13 11:41:21 +09:00
cd0a650df4 Ledger wallet support 2018-02-13 03:27:36 +09:00
6d6b9e2ba6 disable test not passing because of bitpay 2018-02-10 22:24:50 +09:00
fd915fdc5c fix test 2018-02-10 22:21:52 +09:00
59be813fe9 bump 2018-02-10 22:10:36 +09:00
465fbdd47f Fix bug which can happen if parsing of CoinAverage decimal is on another culture 2018-02-10 22:03:33 +09:00
f220abb716 Make the address verification step mandatory 2018-02-07 21:59:16 +09:00
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
86fc64d184 Bump 2018-01-17 16:34:24 +09:00
726cd6fd49 Add badge if not on mainnet in the top bar 2018-01-17 16:34:01 +09:00
be1c4666e0 resize videos 2018-01-17 16:28:09 +09:00
b75dfc4191 Merge branch 'lepipele-dev-lepi' 2018-01-17 16:23:21 +09:00
97815f8daf Merge branch 'dev-lepi' of https://github.com/lepipele/btcpayserver into lepipele-dev-lepi 2018-01-17 16:18:54 +09:00
af16e1db69 update docker test 2018-01-17 16:17:27 +09:00
5f6913b3a2 Can tweak the rate at store level 2018-01-17 15:59:31 +09:00
2b31af80cb Can configure invoice expiration 2018-01-17 15:14:53 +09:00
3f9889d374 Update docker, remove tx cache, use new nbxplorer 2018-01-17 15:02:53 +09:00
f8189c64a4 Non blocking modal that shows sync progress
Ref: https://forkbitpay.slack.com/archives/C7M093Z55/p1515557792000053
2018-01-16 10:37:06 -06:00
c9b5f89f17 Make sure BTCPayServer exit cleanly in case of crash during startup, see https://github.com/aspnet/Hosting/issues/1194 2018-01-15 14:42:51 +09:00
ecb82f2cc9 do not send IPN if not set 2018-01-15 00:22:40 +09:00
f340c6eb7f Add more events to invoice 2018-01-14 22:06:06 +09:00
ba0e080816 Invoices has events recorded 2018-01-14 21:48:23 +09:00
bb3d107309 Fix legacy rate not being correct for alt only payment 2018-01-14 15:26:14 +09:00
8517b222bf Add redirect url to the invoice page 2018-01-14 15:01:09 +09:00
aed32204b5 Fix crashing ManageController 2018-01-14 14:52:15 +09:00
6b4eeff3f1 add tests, do not returns bitcoinAddress field if not supported by the invoice 2018-01-13 22:01:09 +09:00
e3cc589ebb fix nbxplorer 2018-01-13 21:07:04 +09:00
81 changed files with 14676 additions and 10770 deletions

View File

@ -4,6 +4,7 @@
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>

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

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.0.0-sdk
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

View File

@ -46,28 +46,31 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public StoresController CreateStore(string cryptoCode = "BTC")
public StoresController CreateStore(string cryptoCode = null)
{
return CreateStoreAsync(cryptoCode).GetAwaiter().GetResult();
}
public async Task<StoresController> CreateStoreAsync(string cryptoCode = "BTC")
public string CryptoCode { get; set; } = "BTC";
public async Task<StoresController> CreateStoreAsync(string cryptoCode = null)
{
cryptoCode = cryptoCode ?? CryptoCode;
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
var store = parent.PayTester.GetController<StoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
await store.UpdateStore(StoreId, new StoreViewModel()
{
SpeedPolicy = SpeedPolicy.MediumSpeed
});
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(StoreId, vm);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = "BTC",
CryptoCurrency = cryptoCode,
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, "Save");
return store;
}
@ -88,6 +91,7 @@ namespace BTCPayServer.Tests
CryptoCurrency = crytoCode,
DerivationSchemeFormat = crytoCode,
DerivationScheme = derivation.ToString(),
Confirmation = true
}, "Save");
}

View File

@ -24,6 +24,8 @@ using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Eclair;
using System.Collections.Generic;
using BTCPayServer.Models.StoreViewModels;
using System.Threading.Tasks;
namespace BTCPayServer.Tests
{
@ -49,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));
@ -278,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);
@ -359,7 +362,7 @@ namespace BTCPayServer.Tests
change.Value -= (payment2 - payment1) * 2; //Add more fees
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
tester.ExplorerNode.SendRawTransaction(replaced);
var test = tester.ExplorerClient.Sync(user.DerivationScheme, null);
var test = tester.ExplorerClient.GetUTXOs(user.DerivationScheme, null);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -392,6 +395,109 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanTweakRate()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
Assert.Equal(1.0, vm.RateMultiplier);
vm.RateMultiplier = 0.5;
storeController.UpdateStore(user.StoreId, vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
}
}
[Fact]
public void CanHaveLTCOnlyStore()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.CryptoCode = "LTC";
user.GrantAccess();
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
var cashCow = tester.LTCExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
});
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
var controller = tester.PayTester.GetController<InvoiceController>(null);
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("LTC", checkout.CryptoCode);
//////////////////////
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
Assert.Null(invoice.BitcoinAddress);
Assert.NotEqual(1.0, invoice.Rate);
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Equal("paid", checkout.Status);
});
}
}
[Fact]
public void CanPayWithTwoCurrencies()
{
@ -413,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);
@ -447,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);
@ -460,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);
@ -645,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)
@ -655,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

@ -13,25 +13,27 @@ namespace BTCPayServer.Tests
{
// Unit test that generates temorary checkout Bitpay page
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
[Fact]
public void BitpayCheckout()
{
var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
var url = new Uri("https://test.bitpay.com/");
var btcpay = new Bitpay(key, url);
var invoice = btcpay.CreateInvoice(new Invoice()
{
Price = 5.0,
Currency = "USD",
PosData = "posData",
OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
ItemDesc = "Hello from the otherside"
}, Facade.Merchant);
// Testnet of Bitpay down
//[Fact]
//public void BitpayCheckout()
//{
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
// var url = new Uri("https://test.bitpay.com/");
// var btcpay = new Bitpay(key, url);
// var invoice = btcpay.CreateInvoice(new Invoice()
// {
// go to invoice.Url
Console.WriteLine(invoice.Url);
}
// Price = 5.0,
// Currency = "USD",
// PosData = "posData",
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
// ItemDesc = "Hello from the otherside"
// }, Facade.Merchant);
// // go to invoice.Url
// Console.WriteLine(invoice.Url);
//}
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
[Fact]

View File

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

View File

@ -65,6 +65,8 @@ namespace BTCPayServer
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public override string ToString()
{
return CryptoCode;

View File

@ -20,13 +20,14 @@ 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",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType)
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
});
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
@ -18,13 +19,14 @@ 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",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType)
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
});
}
}

View File

@ -2,7 +2,8 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.77</Version>
<Version>1.0.1.24</Version>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -20,23 +21,24 @@
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.51" />
<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.0.33" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
<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" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.2" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@ -99,4 +101,10 @@
<ItemGroup>
<Folder Include="Build\" />
</ItemGroup>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -15,6 +15,7 @@ using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
namespace BTCPayServer.Controllers
{
@ -25,10 +26,10 @@ namespace BTCPayServer.Controllers
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
ILogger _logger;
public AccountController(
UserManager<ApplicationUser> userManager,
@ -36,16 +37,15 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository,
ILogger<AccountController> logger)
SettingsRepository settingsRepository)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_logger = Logs.PayServer;
}
[TempData]
@ -259,12 +259,10 @@ namespace BTCPayServer.Controllers
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
if (admin.Count == 0)
{
_logger.LogInformation("Admin created.");
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
}
@ -291,7 +289,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
/// <summary>
/// <summary>
/// Test property
/// </summary>
public string RegisteredUserId

View File

@ -33,7 +33,8 @@ namespace BTCPayServer.Controllers
{
UserId = GetUserId(),
InvoiceId = invoiceId,
IncludeAddresses = true
IncludeAddresses = true,
IncludeEvents = true
})).FirstOrDefault();
if (invoice == null)
return NotFound();
@ -47,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,
@ -55,8 +57,10 @@ namespace BTCPayServer.Controllers
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus
StatusException = invoice.ExceptionStatus,
Events = invoice.Events
};
foreach (var data in invoice.GetCryptoData(null))
@ -158,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 ?? "/",
@ -234,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));
@ -246,7 +250,7 @@ namespace BTCPayServer.Controllers
finally
{
leases.Dispose();
await CloseSocket(webSocket);
await webSocket.CloseSocket();
}
return new EmptyResult();
}
@ -265,21 +269,6 @@ namespace BTCPayServer.Controllers
catch { try { webSocket.Dispose(); } catch { } }
}
private static async Task CloseSocket(WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
catch { }
finally { try { webSocket.Dispose(); } catch { } }
}
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
@ -411,6 +400,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

@ -78,11 +78,11 @@ namespace BTCPayServer.Controllers
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
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
@ -94,7 +94,7 @@ namespace BTCPayServer.Controllers
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl;
@ -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 &&
@ -130,7 +130,7 @@ namespace BTCPayServer.Controllers
{
network = _.Network,
getFeeRate = _.FeeRateProvider.GetFeeRateAsync(),
getRate = _.RateProvider.GetRateAsync(invoice.Currency),
getRate = storeBlob.ApplyRateRules(_.Network, _.RateProvider).GetRateAsync(invoice.Currency),
getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy)
};
});
@ -164,23 +164,21 @@ namespace BTCPayServer.Controllers
#pragma warning disable CS0618
var btc = _NetworkProvider.BTC;
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
var rateProvider = _RateProviders.GetRateProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
var gettingRate = rateProvider.GetRateAsync("BTC");
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
entity.Rate = await gettingRate;
}
// So users does not crash if they check depositAddress is not set
entity.DepositAddress = cryptoDatas.First().Value.DepositAddress;
#pragma warning restore CS0618
}
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

@ -33,7 +33,6 @@ namespace BTCPayServer.Controllers
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
private readonly BTCPayWallet _Wallet;
IHostingEnvironment _Env;
StoreRepository _StoreRepository;
@ -47,7 +46,7 @@ namespace BTCPayServer.Controllers
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
BTCPayWallet wallet,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository,
IHostingEnvironment env)
{
@ -57,7 +56,6 @@ namespace BTCPayServer.Controllers
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
_Wallet = wallet;
_Env = env;
_StoreRepository = storeRepository;
}

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

@ -2,21 +2,30 @@
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
@ -28,6 +37,7 @@ namespace BTCPayServer.Controllers
public class StoresController : Controller
{
public StoresController(
IOptions<MvcJsonOptions> mvcJsonOptions,
StoreRepository repo,
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
@ -35,6 +45,7 @@ namespace BTCPayServer.Controllers
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
IHostingEnvironment env)
{
_Repo = repo;
@ -45,9 +56,13 @@ namespace BTCPayServer.Controllers
_Env = env;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_MvcJsonOptions = mvcJsonOptions.Value;
_FeeRateProvider = feeRateProvider;
}
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
private MvcJsonOptions _MvcJsonOptions;
private IFeeProviderFactory _FeeRateProvider;
BTCPayWalletProvider _WalletProvider;
AccessTokenController _TokenController;
StoreRepository _Repo;
@ -87,6 +102,338 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("{storeId}/wallet")]
public async Task<IActionResult> Wallet(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(storeId);
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
return View(model);
}
private string GetStoreUrl(string storeId)
{
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
}
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport
{
private readonly WebSocket webSocket;
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
{
if (webSocket == null)
throw new ArgumentNullException(nameof(webSocket));
this.webSocket = webSocket;
}
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
public async Task<byte[][]> Exchange(byte[][] apdus)
{
List<byte[]> responses = new List<byte[]>();
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
{
foreach (var apdu in apdus)
{
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
}
foreach (var apdu in apdus)
{
byte[] response = new byte[300];
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
Array.Resize(ref response, result.Count);
responses.Add(response);
}
}
return responses.ToArray();
}
}
class LedgerTestResult
{
public bool Success { get; set; }
public string Error { get; set; }
}
class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
class SendToAddressResult
{
public string TransactionId { get; set; }
}
class GetXPubResult
{
public string ExtPubKey { get; set; }
}
[HttpGet]
[Route("{storeId}/ws/ledger")]
public async Task<IActionResult> LedgerConnection(
string storeId,
string command,
// getinfo
string cryptoCode = null,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var ledgerTransport = new WebSocketTransport(webSocket);
var ledger = new LedgerWallet.LedgerClient(ledgerTransport);
try
{
if (command == "test")
{
var version = await ledger.GetFirmwareVersionAsync();
await Send(webSocket, new LedgerTestResult() { Success = true });
}
if (command == "getxpub")
{
var network = _NetworkProvider.GetNetwork(cryptoCode);
try
{
var pubkey = await GetExtPubKey(ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true), false);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = true,
Legacy = false
});
await Send(webSocket, new GetXPubResult() { ExtPubKey = derivation.ToString() });
}
catch(FormatException)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Unsupported ledger app" });
}
}
if (command == "getinfo")
{
var network = _NetworkProvider.GetNetwork(cryptoCode);
var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Derivation strategy for {cryptoCode} is not set" });
return new EmptyResult();
}
DirectDerivationStrategy directStrategy = GetDirectStrategy(strategy);
if (directStrategy == null)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"The feature does not work for multi-sig wallets" });
return new EmptyResult();
}
var foundKeyPath = await GetKeyPath(ledger, network, directStrategy);
if (foundKeyPath == null)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"This store is not configured to use this ledger" });
return new EmptyResult();
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategy.DerivationStrategyBase);
await Send(webSocket, new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi });
}
if (command == "sendtoaddress")
{
var network = _NetworkProvider.GetNetwork(cryptoCode);
var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Derivation strategy for {cryptoCode} is not set" });
return new EmptyResult();
}
DirectDerivationStrategy directStrategy = GetDirectStrategy(strategy);
if (directStrategy == null)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"The feature does not work for multi-sig wallets" });
return new EmptyResult();
}
var foundKeyPath = await GetKeyPath(ledger, network, directStrategy);
if (foundKeyPath == null)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"This store is not configured to use this ledger" });
return new EmptyResult();
}
BitcoinAddress destinationAddress = null;
try
{
destinationAddress = BitcoinAddress.Create(destination.Trim());
}
catch
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Invalid destination address" });
return new EmptyResult();
}
Money amountBTC = null;
try
{
amountBTC = Money.Parse(amount);
}
catch
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Invalid amount" });
return new EmptyResult();
}
if (amount <= Money.Zero)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "The amount should be above zero" });
return new EmptyResult();
}
FeeRate feeRateValue = null;
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate)), 1);
}
catch
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Invalid fee rate" });
return new EmptyResult();
}
if (feeRateValue.FeePerK <= Money.Zero)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "The fee rate should be above zero" });
return new EmptyResult();
}
bool substractFeeBool = bool.Parse(substractFees);
var wallet = _WalletProvider.GetWallet(network);
var unspentCoins = await wallet.GetUnspentCoins(strategy.DerivationStrategyBase);
TransactionBuilder builder = new TransactionBuilder();
builder.AddCoins(unspentCoins.Item1);
builder.Send(destinationAddress, amountBTC);
if (substractFeeBool)
builder.SubtractFees();
var change = await wallet.GetChangeAddressAsync(strategy.DerivationStrategyBase);
builder.SetChange(change.Item1);
builder.SendEstimatedFees(feeRateValue);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
Dictionary<OutPoint, KeyPath> keyPaths = unspentCoins.Item2;
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
ledgerTransport.Timeout = TimeSpan.FromMinutes(5);
var fullySigned = await ledger.SignTransactionAsync(
usedCoins.Select(c => new SignatureRequest
{
InputCoin = c,
KeyPath = foundKeyPath.Derive(keyPaths[c.Outpoint]),
PubKey = directStrategy.Root.Derive(keyPaths[c.Outpoint]).PubKey
}).ToArray(),
unsigned,
hasChange ? foundKeyPath.Derive(change.Item2) : null);
try
{
var result = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { fullySigned });
if (!result[0].Success)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"RPC Error while broadcasting: {result[0].RPCCode} {result[0].RPCCodeMessage} {result[0].RPCMessage}" });
return new EmptyResult();
}
}
catch (Exception ex)
{
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Error while broadcasting: " + ex.Message });
return new EmptyResult();
}
await Send(webSocket, new SendToAddressResult() { TransactionId = fullySigned.GetHash().ToString() });
}
}
catch (LedgerWallet.LedgerWalletException ex)
{ try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = ex.Message }); } catch { } }
catch (OperationCanceledException)
{ try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = "timeout" }); } catch { } }
catch (Exception ex)
{ try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = ex.Message }); } catch { } }
finally
{
await webSocket.CloseSocket();
}
return new EmptyResult();
}
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
{
KeyPath foundKeyPath = null;
foreach (var account in
new[] { new KeyPath("49'"), new KeyPath("44'") }
.Select(purpose => purpose.Derive(network.CoinType))
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
{
try
{
var extpubkey = await GetExtPubKey(ledger, network, account, true);
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
{
foundKeyPath = account;
break;
}
}
catch (FormatException)
{
throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}");
}
}
return foundKeyPath;
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
{
var pubKey = await ledger.GetWalletPubKeyAsync(account);
if (pubKey.Address.Network != network.NBitcoinNetwork)
{
if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main)
throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}");
}
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
return extpubkey;
}
private static DirectDerivationStrategy GetDirectStrategy(DerivationStrategy strategy)
{
var directStrategy = strategy.DerivationStrategyBase as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy.DerivationStrategyBase as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
return directStrategy;
}
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
private async Task Send(WebSocket webSocket, object result)
{
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
[HttpGet]
public async Task<IActionResult> ListStores()
{
@ -95,7 +442,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))
@ -163,6 +510,9 @@ namespace BTCPayServer.Controllers
AddDerivationSchemes(store, vm);
vm.StatusMessage = StatusMessage;
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
return View(vm);
}
@ -193,15 +543,17 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
return View(vm);
}
[HttpPost]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string command, string selectedScheme = null)
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string selectedScheme = null)
{
selectedScheme = selectedScheme ?? "BTC";
vm.ServerUrl = GetStoreUrl(storeId);
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
@ -220,16 +572,31 @@ namespace BTCPayServer.Controllers
return View(vm);
}
if (command == "Save")
DerivationStrategyBase strategy = null;
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
vm.DerivationScheme = strategy.ToString();
}
store.SetDerivationStrategy(network, vm.DerivationScheme);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(vm);
}
if (strategy == null || vm.Confirmation)
{
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
if (strategy != null)
await wallet.TrackAsync(strategy);
vm.DerivationScheme = strategy.ToString();
}
store.SetDerivationStrategy(network, vm.DerivationScheme);
}
catch
@ -239,29 +606,22 @@ 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
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
try
{
var scheme = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
var line = scheme.GetLineFor(DerivationFeature.Deposit);
var line = strategy.GetLineFor(DerivationFeature.Deposit);
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()));
}
}
catch
for (int i = 0; i < 10; i++)
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
var address = line.Derive((uint)i);
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
}
}
vm.Confirmation = true;
return View(vm);
}
}
@ -274,6 +634,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();
@ -306,12 +668,31 @@ namespace BTCPayServer.Controllers
var blob = store.GetStoreBlob();
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))
{
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);
@ -346,7 +727,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

@ -26,6 +26,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<InvoiceEventData> InvoiceEvents
{
get; set;
}
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;
@ -132,6 +137,15 @@ namespace BTCPayServer.Data
o.InvoiceDataId,
#pragma warning disable CS0618
o.Address
#pragma warning restore CS0618
});
builder.Entity<InvoiceEventData>()
.HasKey(o => new
{
o.InvoiceDataId,
#pragma warning disable CS0618
o.UniqueId
#pragma warning restore CS0618
});
}

View File

@ -32,6 +32,11 @@ namespace BTCPayServer.Data
get; set;
}
public List<InvoiceEventData> Events
{
get; set;
}
public List<RefundAddressesData> RefundAddresses
{
get; set;

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class InvoiceEventData
{
public string InvoiceDataId
{
get; set;
}
public string UniqueId { get; internal set; }
public DateTimeOffset Timestamp
{
get; set;
}
public string Message { get; set; }
}
}

View File

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Data
{
@ -99,10 +100,10 @@ namespace BTCPayServer.Data
if (!existing && string.IsNullOrEmpty(derivationScheme))
{
if(network.IsBTC)
if (network.IsBTC)
DerivationStrategy = null;
}
else if(!existing)
else if (!existing)
strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme)));
// This is deprecated so we don't have to set anymore
//if (network.IsBTC)
@ -173,10 +174,27 @@ namespace BTCPayServer.Data
}
}
public class RateRule
{
public RateRule()
{
RuleName = "Multiplier";
}
public string RuleName { get; set; }
public double Multiplier { get; set; }
public decimal Apply(BTCPayNetwork network, decimal rate)
{
return rate * (decimal)Multiplier;
}
}
public class StoreBlob
{
public StoreBlob()
{
InvoiceExpiration = 15;
MonitoringExpiration = 60;
}
public bool NetworkFeeDisabled
@ -190,5 +208,59 @@ namespace BTCPayServer.Data
get;
set;
}
[DefaultValue(15)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int InvoiceExpiration
{
get;
set;
}
public void SetRateMultiplier(double rate)
{
RateRules = new List<RateRule>();
RateRules.Add(new RateRule() { Multiplier = rate });
}
public decimal GetRateMultiplier()
{
decimal rate = 1.0m;
if (RateRules == null || RateRules.Count == 0)
return rate;
foreach (var rule in RateRules)
{
rate = rule.Apply(null, rate);
}
return rate;
}
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

@ -8,10 +8,19 @@ namespace BTCPayServer.Events
public class InvoiceDataChangedEvent
{
public string InvoiceId { get; set; }
public string Status { get; internal set; }
public string ExceptionStatus { get; internal set; }
public override string ToString()
{
return $"Invoice {InvoiceId} data changed";
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
{
return $"Invoice status is {Status}";
}
else
{
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
}
}
}
}

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

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceIPNEvent
{
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 $"{ipnType} sent for invoice {InvoiceId}";
return $"Error while sending {ipnType}: {Error}";
}
}
}

View File

@ -1,24 +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)
{
InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} received a payment";
}
}
}

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

@ -5,18 +5,12 @@ using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceCreatedEvent
public class InvoiceStopWatchedEvent
{
public InvoiceCreatedEvent(string id)
{
InvoiceId = id;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} created";
return $"Invoice {InvoiceId} is not monitored anymore.";
}
}
}

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;
@ -38,11 +40,15 @@ namespace BTCPayServer
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
{
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
if (cookieFile == null || !explorer.SetCookieAuth(cookieFile))
if (cookieFile == null)
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
explorer.SetNoAuth();
}
if(!explorer.SetCookieAuth(cookieFile))
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
}
return explorer;
}
@ -64,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

@ -19,16 +19,39 @@ using System.Linq;
using System.Threading;
using BTCPayServer.Services.Wallets;
using System.IO;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using System.Net.WebSockets;
namespace BTCPayServer
{
public static class Extensions
{
public static async Task CloseSocket(this WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
catch { }
finally { try { webSocket.Dispose(); } catch { } }
}
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
{
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,18 +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
{
await SendNotification(invoice, cts.Token);
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
await SendNotification(invoice, eventCode, name, cts.Token);
return;
}
catch // It fails, it is OK because we try with hangfire after
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = "Timeout"
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = ex.Message
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
@ -93,14 +110,45 @@ namespace BTCPayServer.HostedServices
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
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 (Exception ex)
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = "Timeout"
});
reschedule = true;
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
Logger.LogInformation("Job " + jobId + " timed out");
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = ex.Message
});
reschedule = true;
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); }
@ -115,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;
@ -139,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;
@ -149,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;
}
@ -165,29 +242,60 @@ 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);
}
if (invoice.ExtendedNotifications)
{
await Notify(invoice, e.EventCode, e.Name);
}
}));
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceIPNEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
return Task.CompletedTask;
}
private Task SaveEvent(string invoiceId, object evt)
{
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt);
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();

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);
@ -99,7 +106,7 @@ namespace BTCPayServer.HostedServices
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
updateContext.Events.Add(new InvoiceDataChangedEvent() { Status = invoice.Status, ExceptionStatus = invoice.ExceptionStatus, InvoiceId = invoice.Id });
}
var changed = stateBefore != invoice.Status;
@ -118,11 +125,11 @@ namespace BTCPayServer.HostedServices
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
_EventAggregator.Publish<InvoiceStopWatchedEvent>(new InvoiceStopWatchedEvent() { InvoiceId = invoice.Id });
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));
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

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
@ -36,6 +37,8 @@ using System.Threading;
using Microsoft.Extensions.Options;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
namespace BTCPayServer.Hosting
{
@ -88,6 +91,15 @@ namespace BTCPayServer.Hosting
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
// {
// l.UseHttps("devtest.pfx", "toto");
// });
//});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run

View File

@ -12,7 +12,11 @@ namespace BTCPayServer.Logging
{
public class CustomConsoleLogProvider : ILoggerProvider
{
ConsoleLoggerProcessor _Processor = new ConsoleLoggerProcessor();
ConsoleLoggerProcessor _Processor;
public CustomConsoleLogProvider(ConsoleLoggerProcessor processor)
{
_Processor = processor;
}
public ILogger CreateLogger(string categoryName)
{
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);

View File

@ -0,0 +1,508 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20180114123253_events")]
partial class events
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class events : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "InvoiceEvents",
columns: table => new
{
InvoiceDataId = table.Column<string>(nullable: false),
UniqueId = table.Column<string>(nullable: false),
Message = table.Column<string>(nullable: true),
Timestamp = table.Column<DateTimeOffset>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InvoiceEvents", x => new { x.InvoiceDataId, x.UniqueId });
table.ForeignKey(
name: "FK_InvoiceEvents_Invoices_InvoiceDataId",
column: x => x.InvoiceDataId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InvoiceEvents");
}
}
}

View File

@ -81,6 +81,21 @@ namespace BTCPayServer.Migrations
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
@ -407,6 +422,14 @@ namespace BTCPayServer.Migrations
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")

View File

@ -97,6 +97,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public string TransactionSpeed { get; set; }
public object StoreName
{
get;
@ -112,6 +114,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
internal set;
}
public string RedirectUrl { get; set; }
public string Fiat
{
get;
@ -124,5 +128,6 @@ namespace BTCPayServer.Models.InvoicingModels
}
public HistoricalAddressInvoiceData[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
}
}

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

@ -48,10 +48,13 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
public bool Confirmation { get; set; }
public SelectList CryptoCurrencies { get; set; }
public SelectList DerivationSchemeFormats { get; set; }
public string ServerUrl { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();

View File

@ -47,7 +47,34 @@ namespace BTCPayServer.Models.StoreViewModels
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
[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
{
get;
set;
}
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
[Range(1, 60 * 24 * 31)]
public int InvoiceExpiration
{
get;
set;
}
[Display(Name = "Payment invalid if transactions fails to confirm ... minutes after invoice expiration")]
[Range(10, 60 * 24 * 31)]
public int MonitoringExpiration
{

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class WalletModel
{
public string ServerUrl { get; set; }
public SelectList CryptoCurrencies { get; set; }
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

View File

@ -24,8 +24,8 @@ namespace BTCPayServer
{
ServicePointManager.DefaultConnectionLimit = 100;
IWebHost host = null;
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider();
var processor = new ConsoleLoggerProcessor();
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider(processor);
var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(loggerProvider);
var logger = loggerFactory.CreateLogger("Configuration");
@ -44,7 +44,8 @@ namespace BTCPayServer
.ConfigureLogging(l =>
{
l.AddFilter("Microsoft", LogLevel.Error);
l.AddProvider(new CustomConsoleLogProvider());
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
l.AddProvider(new CustomConsoleLogProvider(processor));
})
.UseStartup<Startup>()
.Build();
@ -61,13 +62,9 @@ namespace BTCPayServer
if (!string.IsNullOrEmpty(ex.Message))
Logs.Configuration.LogError(ex.Message);
}
catch (Exception exception)
{
logger.LogError("Exception thrown while running the server");
logger.LogError(exception.ToString());
}
finally
{
processor.Dispose();
if (host != null)
host.Dispose();
loggerProvider.Dispose();

View File

@ -5,12 +5,13 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Text;
using NBXplorer;
namespace BTCPayServer.Services
{
public class BTCPayServerEnvironment
{
public BTCPayServerEnvironment(IHostingEnvironment env)
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider)
{
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
#if DEBUG
@ -19,11 +20,13 @@ namespace BTCPayServer.Services
Build = "Release";
#endif
Environment = env;
ChainType = provider.NBXplorerNetworkProvider.ChainType;
}
public IHostingEnvironment Environment
{
get; set;
}
public ChainType ChainType { get; set; }
public string Version
{
get; set;

View File

@ -299,6 +299,7 @@ namespace BTCPayServer.Services.Invoices
set;
}
public bool ExtendedNotifications { get; set; }
public List<InvoiceEventData> Events { get; internal set; }
public bool IsExpired()
{

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 });
@ -173,8 +176,11 @@ namespace BTCPayServer.Services.Invoices
invoiceEntity.SetCryptoData(currencyData);
invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork);
context.AddressInvoices.Add(new AddressInvoiceData() {
InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow }
context.AddressInvoices.Add(new AddressInvoiceData()
{
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
.SetHash(bitcoinAddress.ScriptPubKey.Hash, network.CryptoCode));
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
@ -188,6 +194,21 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task AddInvoiceEvent(string invoiceId, object evt)
{
using (var context = _ContextFactory.CreateContext())
{
context.InvoiceEvents.Add(new InvoiceEventData()
{
InvoiceDataId = invoiceId,
Message = evt.ToString(),
Timestamp = DateTimeOffset.UtcNow,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
await context.SaveChangesAsync();
}
}
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode)
{
foreach (var address in entity.GetCryptoData(null))
@ -314,6 +335,10 @@ namespace BTCPayServer.Services.Invoices
{
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetHash() + a.GetCryptoCode()).ToHashSet();
}
if(invoice.Events != null)
{
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
}
return entity;
}
@ -326,8 +351,10 @@ namespace BTCPayServer.Services.Invoices
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if(queryObject.IncludeAddresses)
if (queryObject.IncludeAddresses)
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
if (queryObject.IncludeEvents)
query = query.Include(o => o.Events);
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
@ -536,5 +563,7 @@ namespace BTCPayServer.Services.Invoices
set;
}
public bool IncludeAddresses { get; set; }
public bool IncludeEvents { get; set; }
}
}

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

@ -2,6 +2,7 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
@ -24,6 +25,8 @@ namespace BTCPayServer.Services.Rates
CryptoCode = cryptoCode ?? "BTC";
}
public string Exchange { get; set; }
public string CryptoCode { get; set; }
public string Market
@ -45,7 +48,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,15 +68,24 @@ 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"]));
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase) && TryToDecimal(p, out decimal unused))
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
{
TryToDecimal(p, out decimal v);
return v;
});
}
}
private decimal ToDecimal(JToken token)
private bool TryToDecimal(JProperty p, out decimal v)
{
return decimal.Parse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
JToken token = p.Value[Exchange == null ? "last" : "bid"];
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
}
public async Task<ICollection<Rate>> GetRatesAsync()

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

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Services.Rates
{
public class TweakRateProvider : IRateProvider
{
private BTCPayNetwork network;
private IRateProvider rateProvider;
private List<RateRule> rateRules;
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List<RateRule> rateRules)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (rateProvider == null)
throw new ArgumentNullException(nameof(rateProvider));
if (rateRules == null)
throw new ArgumentNullException(nameof(rateRules));
this.network = network;
this.rateProvider = rateProvider;
this.rateRules = rateRules;
}
public async Task<decimal> GetRateAsync(string currency)
{
var rate = await rateProvider.GetRateAsync(currency);
foreach(var rule in rateRules)
{
rate = rule.Apply(network, rate);
}
return rate;
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
List<Rate> rates = new List<Rate>();
foreach (var rate in await rateProvider.GetRatesAsync())
{
var localRate = rate.Value;
foreach (var rule in rateRules)
{
localRate = rule.Apply(network, localRate);
}
rates.Add(new Rate(rate.Currency, localRate));
}
return rates;
}
}
}

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

@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@ -29,33 +30,33 @@ namespace BTCPayServer.Services
}
public class TransactionCache : IDisposable
{
IOptions<MemoryCacheOptions> _Options;
//IOptions<MemoryCacheOptions> _Options;
public TransactionCache(IOptions<MemoryCacheOptions> options, BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
_Options = options;
_MemoryCache = new MemoryCache(_Options);
Network = network;
//if (network == null)
// throw new ArgumentNullException(nameof(network));
//_Options = options;
//_MemoryCache = new MemoryCache(_Options);
//Network = network;
}
uint256 _LastHash;
int _ConfOffset;
IMemoryCache _MemoryCache;
//uint256 _LastHash;
//int _ConfOffset;
//IMemoryCache _MemoryCache;
public void NewBlock(uint256 newHash, uint256 previousHash)
{
if (_LastHash != previousHash)
{
var old = _MemoryCache;
_ConfOffset = 0;
_MemoryCache = new MemoryCache(_Options);
Thread.MemoryBarrier();
old.Dispose();
}
else
_ConfOffset++;
_LastHash = newHash;
//if (_LastHash != previousHash)
//{
// var old = _MemoryCache;
// _ConfOffset = 0;
// _MemoryCache = new MemoryCache(_Options);
// Thread.MemoryBarrier();
// old.Dispose();
//}
//else
// _ConfOffset++;
//_LastHash = newHash;
}
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(60);
@ -64,29 +65,32 @@ namespace BTCPayServer.Services
public void AddToCache(TransactionResult tx)
{
_MemoryCache.Set(tx.Transaction.GetHash(), tx, DateTimeOffset.UtcNow + CacheSpan);
//Logging.Logs.PayServer.LogInformation($"ADD CACHE: {tx.Transaction.GetHash()} ({tx.Confirmations} conf)");
//_MemoryCache.Set(tx.Transaction.GetHash(), tx, DateTimeOffset.UtcNow + CacheSpan);
}
public TransactionResult GetTransaction(uint256 txId)
{
_MemoryCache.TryGetValue(txId.ToString(), out object tx);
//var ok = _MemoryCache.TryGetValue(txId.ToString(), out object tx);
//Logging.Logs.PayServer.LogInformation($"GET CACHE: {txId} ({ok} plus {_ConfOffset})");
var result = tx as TransactionResult;
var confOffset = _ConfOffset;
if (result != null && result.Confirmations > 0 && confOffset > 0)
{
var serializer = new NBXplorer.Serializer(Network.NBitcoinNetwork);
result = serializer.ToObject<TransactionResult>(serializer.ToString(result));
result.Confirmations += confOffset;
result.Height += confOffset;
}
return result;
//var result = tx as TransactionResult;
//var confOffset = _ConfOffset;
//if (result != null && result.Confirmations > 0 && confOffset > 0)
//{
// var serializer = new NBXplorer.Serializer(Network.NBitcoinNetwork);
// result = serializer.ToObject<TransactionResult>(serializer.ToString(result));
// result.Confirmations += confOffset;
// result.Height += confOffset;
//}
//return result;
return null; // Does not work correctly yet
}
public void Dispose()
{
_MemoryCache.Dispose();
//_MemoryCache.Dispose();
}
}
}

View File

@ -15,8 +15,7 @@ namespace BTCPayServer.Services.Wallets
{
public class KnownState
{
public uint256 UnconfirmedHash { get; set; }
public uint256 ConfirmedHash { get; set; }
public UTXOChanges PreviousCall { get; set; }
}
public class NetworkCoins
{
@ -69,6 +68,20 @@ namespace BTCPayServer.Services.Wallets
return pathInfo.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork);
}
public async Task<(BitcoinAddress, KeyPath)> GetChangeAddressAsync(DerivationStrategyBase derivationStrategy)
{
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Change, 0, false).ConfigureAwait(false);
// Might happen on some broken install
if (pathInfo == null)
{
await _Client.TrackAsync(derivationStrategy).ConfigureAwait(false);
pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Change, 0, false).ConfigureAwait(false);
}
return (pathInfo.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork), pathInfo.KeyPath);
}
public async Task TrackAsync(DerivationStrategyBase derivationStrategy)
{
await _Client.TrackAsync(derivationStrategy);
@ -88,37 +101,37 @@ namespace BTCPayServer.Services.Wallets
public async Task<NetworkCoins> GetCoins(DerivationStrategyBase strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
{
var changes = await _Client.SyncAsync(strategy, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false);
var changes = await _Client.GetUTXOsAsync(strategy, state?.PreviousCall, false, cancellation).ConfigureAwait(false);
return new NetworkCoins()
{
TimestampedCoins = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => new NetworkCoins.TimestampedCoin() { Coin = c.AsCoin(), DateTime = c.Timestamp }).ToArray(),
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
State = new KnownState() { PreviousCall = changes },
Strategy = strategy,
Wallet = this
};
}
public Task BroadcastTransactionsAsync(List<Transaction> transactions)
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
{
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
return Task.WhenAll(tasks);
}
public async Task<(Coin[], Dictionary<OutPoint, KeyPath>)> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
{
var changes = await _Client.GetUTXOsAsync(derivationStrategy, null, false, cancellation).ConfigureAwait(false);
var keyPaths = new Dictionary<OutPoint, KeyPath>();
foreach (var coin in changes.GetUnspentUTXOs())
{
keyPaths.TryAdd(coin.Outpoint, coin.KeyPath);
}
return (changes.GetUnspentCoins(), keyPaths);
}
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
{
var result = await _Client.SyncAsync(derivationStrategy, null, true);
Dictionary<OutPoint, UTXO> received = new Dictionary<OutPoint, UTXO>();
foreach(var utxo in result.Confirmed.UTXOs.Concat(result.Unconfirmed.UTXOs))
{
received.TryAdd(utxo.Outpoint, utxo);
}
foreach (var utxo in result.Confirmed.SpentOutpoints.Concat(result.Unconfirmed.SpentOutpoints))
{
received.Remove(utxo);
}
return received.Values.Select(c => c.Value).Sum();
var result = await _Client.GetUTXOsAsync(derivationStrategy, null, true);
return result.GetUnspentUTXOs().Select(c => c.Value).Sum();
}
}
}

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

@ -57,10 +57,10 @@
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
<iframe width="400" height="225" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
</div>
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
<iframe width="400" height="225" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
</div>
</div>
</div>

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>
@ -77,6 +81,10 @@
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
<tr>
<th>Redirect Url</th>
<td>@Model.RedirectUrl</td>
</tr>
</table>
</div>
@ -221,5 +229,28 @@
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Events</h3>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Date</th>
<th>Message</th>
</tr>
</thead>
<tbody>
@foreach(var evt in Model.Events)
{
<tr>
<td>@evt.Timestamp</td>
<td>@evt.Message</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,94 @@
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
<!-- Modal -->
<div id="modalDialog" class="modal-dialog animated bounceInRight">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Your nodes are synching...</h4>
<button type="button" class="close" onclick="dismissSyncModal()">&times;</button>
</div>
<div class="modal-body">
<p>
Some of your nodes are still synching...<br />
BTCPay Server will not work correctly until it is over.
</p>
@foreach (var line in dashboard.GetAll())
{
<h4>@line.Network.CryptoCode</h4>
@if (line.Status == null)
{
<ul>
<li>The node is offline</li>
@if (line.Error != null)
{
<li>Last error: @line.Error</li>
}
</ul>
}
else
{
<ul>
<li>NBXplorer headers height: @line.Status.ChainHeight</li>
@if (line.Status.BitcoinStatus == null)
{
if (line.State == BTCPayServer.HostedServices.NBXplorerState.Synching)
{
<li>The node is starting...</li>
}
else
{
<li>The node is offline</li>
@if (line.Error != null)
{
<li>Last error: @line.Error</li>
}
}
}
else if (line.Status.BitcoinStatus.IsSynched)
{
<li>The node is synched (Height: @line.Status.BitcoinStatus.Headers)</li>
@if (line.Status.BitcoinStatus.IsSynched &&
line.Status.SyncHeight.HasValue &&
line.Status.SyncHeight.Value < line.Status.BitcoinStatus.Headers)
{
<li>NBXplorer is synching... (Height: @line.Status.SyncHeight.Value)</li>
}
}
else
{
<li>Node headers height: @line.Status.BitcoinStatus.Headers</li>
<li>Validated blocks: @line.Status.BitcoinStatus.Blocks</li>
}
</ul>
@if (!line.Status.IsFullySynched && line.Status.BitcoinStatus != null)
{
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))"
aria-valuemin="0" aria-valuemax="100" style="width:@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%">
@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%
</div>
</div>
}
}
}
</div>
</div>
</div>
<link href="~/vendor/animatecss/animate.css" rel="stylesheet" />
<script type="text/javascript">
function dismissSyncModal() {
$("#modalDialog").addClass('animated bounceOutRight')
}
</script>
<style type="text/css">
#modalDialog {
position: fixed;
bottom: 20px;
right: 20px;
margin: 0px;
}
</style>

View File

@ -38,7 +38,7 @@
<body id="page-top">
@{
if(ViewBag.AlwaysShrinkNavBar == null)
if (ViewBag.AlwaysShrinkNavBar == null)
{
ViewBag.AlwaysShrinkNavBar = true;
}
@ -48,30 +48,36 @@
<!-- Navigation -->
<nav class='navbar navbar-expand-lg navbar-light fixed-top @additionalStyle' id="mainNav">
<div class="container">
<a class="navbar-brand js-scroll-trigger" href="~/"><img src="~/img/logo.png" height="45"></a>
<a class="navbar-brand js-scroll-trigger" href="~/">
<img src="~/img/logo.png" height="45">
@if(env.ChainType != NBXplorer.ChainType.Main)
{
<span class="badge badge-warning" style="font-size:10px;">@env.ChainType.ToString()</span>
}
</a>
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
@if(SignInManager.IsSignedIn(User))
{
@if(User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
</li>}
else
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
{
@if(User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
</li>}
else
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
</ul>
</div>
@ -80,93 +86,6 @@
@RenderBody()
@if(!dashboard.IsFullySynched())
{
<!-- Modal -->
<div id="synching-modal" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Your nodes are synching...</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>
Some of your nodes are still synching...<br />
BTCPay Server will not work correctly until it is over.
</p>
@foreach(var line in dashboard.GetAll())
{
<h4>@line.Network.CryptoCode</h4>
@if(line.Status == null)
{
<ul>
<li>The node is offline</li>
@if(line.Error != null)
{
<li>Last error: @line.Error</li>
}
</ul>
}
else
{
<ul>
<li>NBXplorer headers height: @line.Status.ChainHeight</li>
@if(line.Status.BitcoinStatus == null)
{
if(line.State == BTCPayServer.HostedServices.NBXplorerState.Synching)
{
<li>The node is starting...</li>
}
else
{
<li>The node is offline</li>
@if(line.Error != null)
{
<li>Last error: @line.Error</li>
}
}
}
else if(line.Status.BitcoinStatus.IsSynched)
{
<li>The node is synched (Height: @line.Status.BitcoinStatus.Headers)</li>
@if(line.Status.BitcoinStatus.IsSynched &&
line.Status.SyncHeight.HasValue &&
line.Status.SyncHeight.Value < line.Status.BitcoinStatus.Headers)
{
<li>NBXplorer is synching... (Height: @line.Status.SyncHeight.Value)</li>
}
}
else
{
<li>Node headers height: @line.Status.BitcoinStatus.Headers</li>
<li>Validated blocks: @line.Status.BitcoinStatus.Blocks</li>
}
</ul>
@if(!line.Status.IsFullySynched && line.Status.BitcoinStatus != null)
{
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))"
aria-valuemin="0" aria-valuemax="100" style="width:@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%">
@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%
</div>
</div>
}
}
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
}
<footer class="bg-dark">
<div class="container text-right"><span style="font-size:8px;">@env.ToString()</span></div>
</footer>
@ -184,13 +103,10 @@
<!-- Custom scripts for this template -->
<script src="~/js/creative.js"></script>
@if(!dashboard.IsFullySynched())
@if (!dashboard.IsFullySynched())
{
<script type="text/javascript">
$(function () {
$("#synching-modal").modal();
});
</script>
@Html.Partial("SyncModal")
}
@RenderSection("Scripts", required: false)

View File

@ -13,94 +13,112 @@
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="col-md-8">
<form method="post">
<div class="form-group">
<h5>Derivation Scheme</h5>
@if(Model.AddressSamples.Count == 0)
{
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
}
</div>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
@if(!Model.Confirmation)
{
<div class="form-group">
<h5>Derivation Scheme</h5>
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
</div>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
@if(Model.AddressSamples.Count == 0)
{
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
</tr>
</tbody>
</table>
}
else
{
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
}
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
<button name="command" type="submit" class="btn btn-default" value="Check">Check ExtPubKey</button>
<div class="form-group">
<label asp-for="DerivationScheme"></label>
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
No ledger wallet detected. If you own one, use chrome, open the app, activate the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a>, and refresh this page.
</p>
<p id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, please use our <a id="ledger-info-recommended" href="#">recommended choice</a></span>
</p>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
</tr>
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-info">Continue</button>
}
else
{
<div class="form-group">
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
</div>
<input type="hidden" asp-for="CryptoCurrency" />
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-success">Confirm</button>
}
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script type="text/javascript">
@Model.ServerUrl.ToJSVariableModel("srvModel");
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
}

View File

@ -14,8 +14,10 @@ namespace BTCPayServer.Views.Stores
public static string Tokens => "Tokens";
public static string Wallet => "Wallet";
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
public static string WalletNavClass(ViewContext viewContext) => PageNavClass(viewContext, Wallet);
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);

View File

@ -38,6 +38,24 @@
<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" />
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="InvoiceExpiration"></label>
<input asp-for="InvoiceExpiration" class="form-control" />
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MonitoringExpiration"></label>
<input asp-for="MonitoringExpiration" class="form-control" />

View File

@ -0,0 +1,74 @@
@model WalletModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.AddActivePage(StoreNavPages.Wallet);
}
<h4>@ViewData["Title"]</h4>
<div class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span id="alertMessage"></span>
</div>
<div class="row">
<div class="col-md-10">
<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 />
If your Ledger wallet is not detected:
</p>
<ul>
<li>Activate <i class="icon-upload icon-large"></i> the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a> and refresh this page</li>
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></li>
</ul>
<p id="hw-loading"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
<p id="hw-error" style="display:none;"><span class="glyphicon glyphicon-remove-sign" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
<p id="hw-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
<p id="check-loading" style="display:none;"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
<p id="check-error" style="display:none;"><span class="glyphicon glyphicon-remove-sign" style="color:red;"></span> <span class="check-label">An error happened</span></p>
<p id="check-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
</div>
</div>
<div class="col-md-6">
<form id="sendform" style="display:none;">
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select id="cryptoCurrencies" asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label>Destination</label>
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
<span id="Destination-Error" class="text-danger"></span>
</div>
<div class="form-group">
<label>Amount</label>
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
</p>
</div>
<div class="form-group">
<label>Fee rate (satoshi per byte)</label>
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
<span id="FeeRate-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per bytes.
</p>
</div>
<div class="form-group">
<label>Substract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
</form>
</div>
@section Scripts
{
<script type="text/javascript">
@Model.ToJSVariableModel("srvModel")
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}

View File

@ -4,5 +4,6 @@
<ul class="nav nav-pills nav-stacked">
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
<li class="@StoreNavPages.WalletNavClass(ViewContext)"><a asp-action="Wallet">Wallet</a></li>
</ul>

View File

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

View File

@ -0,0 +1,77 @@
$(function () {
var ledgerDetected = false;
var recommendedPubKey = "";
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
function WriteAlert(type, message) {
}
function Write(prefix, type, message) {
if (type === "error") {
$("#no-ledger-info").css("display", "block");
$("#ledger-in fo").css("display", "none");
}
}
$("#ledger-info-recommended").on("click", function (elem) {
elem.preventDefault();
$("#DerivationScheme").val(recommendedPubKey);
$("#DerivationSchemeFormat").val("BTCPay");
return false;
});
$("#CryptoCurrency").on("change", function (elem) {
$("#no-ledger-info").css("display", "none");
$("#ledger-info").css("display", "none");
updateInfo();
});
var updateInfo = function () {
if (!ledgerDetected)
return false;
var cryptoCode = $("#CryptoCurrency").val();
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode)
.catch(function (reason) { Write('check', 'error', reason); })
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('check', 'error', result.error);
return;
}
else {
Write('check', 'success', 'This store is configured to use your ledger');
recommendedPubKey = result.extPubKey;
$("#no-ledger-info").css("display", "none");
$("#ledger-info").css("display", "block");
}
});
};
bridge.isSupported()
.then(function (supported) {
if (!supported) {
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
}
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason) {
if (reason.message === "Sign failed")
reason = "Have you forgot to activate browser support in your ledger app?";
Write('hw', 'error', reason);
})
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('hw', 'error', result.error);
} else {
Write('hw', 'success', 'Ledger detected');
ledgerDetected = true;
updateInfo();
}
});
}
});
});

View File

@ -0,0 +1,132 @@
$(function () {
var ledgerDetected = false;
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
function WriteAlert(type, message) {
$(".alert").removeClass("alert-danger");
$(".alert").removeClass("alert-warning");
$(".alert").removeClass("alert-success");
$(".alert").addClass("alert-" + type);
$(".alert").css("display", "block");
$("#alertMessage").text(message);
}
function Write(prefix, type, message) {
$("#" + prefix + "-loading").css("display", "none");
$("#" + prefix + "-error").css("display", "none");
$("#" + prefix + "-success").css("display", "none");
$("#" + prefix+"-" + type).css("display", "block");
$("." + prefix +"-label").text(message);
}
$("#sendform").on("submit", function (elem) {
elem.preventDefault();
var args = "";
args += "cryptoCode=" + $("#cryptoCurrencies").val();
args += "&destination=" + $("#destination-textbox").val();
args += "&amount=" + $("#amount-textbox").val();
args += "&feeRate=" + $("#fee-textbox").val();
args += "&substractFees=" + $("#substract-checkbox").prop("checked");
WriteAlert("warning", 'Please validate the transaction on your ledger');
var confirmButton = $("#confirm-button");
confirmButton.prop("disabled", true);
confirmButton.addClass("disabled");
bridge.sendCommand('sendtoaddress', args, 60 * 5 /* timeout */)
.catch(function (reason) {
WriteAlert("danger", reason);
confirmButton.prop("disabled", false);
confirmButton.removeClass("disabled");
})
.then(function (result) {
if (!result)
return;
confirmButton.prop("disabled", false);
confirmButton.removeClass("disabled");
if (result.error) {
WriteAlert("danger", result.error);
} else {
WriteAlert("success", 'Transaction broadcasted (' + result.transactionId + ')');
updateInfo();
}
});
return false;
});
$("#crypto-balance-link").on("click", function (elem) {
elem.preventDefault();
var val = $("#crypto-balance-link").text();
$("#amount-textbox").val(val);
$("#substract-checkbox").prop('checked', true);
return false;
});
$("#crypto-fee-link").on("click", function (elem) {
elem.preventDefault();
var val = $("#crypto-fee-link").text();
$("#fee-textbox").val(val);
return false;
});
$("#cryptoCurrencies").on("change", function (elem) {
updateInfo();
});
var updateInfo = function () {
if (!ledgerDetected)
return false;
$(".crypto-info").css("display", "none");
var cryptoCode = $("#cryptoCurrencies").val();
bridge.sendCommand("getinfo", "cryptoCode=" + cryptoCode)
.catch(function (reason) { Write('check', 'error', reason); })
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('check', 'error', result.error);
return;
}
else {
Write('check', 'success', 'This store is configured to use your ledger');
$(".crypto-info").css("display", "block");
$("#crypto-fee").text(result.recommendedSatoshiPerByte);
$("#crypto-balance").text(result.balance);
$("#crypto-code").text(cryptoCode);
}
});
};
bridge.isSupported()
.then(function (supported) {
if (!supported) {
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
}
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason)
{
if (reason.message === "Sign failed")
reason = "Have you forgot to activate browser support in your ledger app?";
Write('hw', 'error', reason);
})
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('hw', 'error', result.error);
} else {
Write('hw', 'success', 'Ledger detected');
$("#sendform").css("display", "block");
ledgerDetected = true;
updateInfo();
}
});
}
});
});

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 one or more lines are too long

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

File diff suppressed because it is too large Load Diff

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,4 +1,4 @@
FROM microsoft/aspnetcore-build AS builder
FROM microsoft/aspnetcore-build:2.0.5-2.1.4-stretch AS builder
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
# Cache some dependencies
@ -6,7 +6,7 @@ RUN dotnet restore
COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release
FROM microsoft/aspnetcore:2.0.3
FROM microsoft/aspnetcore:2.0.5-stretch
WORKDIR /app
RUN mkdir /datadir

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

View File

@ -1 +0,0 @@
{ "sdk": { "version": "2.0.0" } }