Compare commits
59 Commits
Author | SHA1 | Date | |
---|---|---|---|
fd4f4e6aff | |||
cedf8f75e8 | |||
cd0a650df4 | |||
6d6b9e2ba6 | |||
fd915fdc5c | |||
59be813fe9 | |||
465fbdd47f | |||
f220abb716 | |||
db46ca87d7 | |||
d873a1a545 | |||
a464a8702b | |||
63722b932a | |||
698b3c46cd | |||
df81051d07 | |||
ac70a77361 | |||
59a2432af9 | |||
ea4fa8d5d4 | |||
ade3eff75c | |||
db2a2a2b6c | |||
579dcb5af8 | |||
69247dee8a | |||
7b9541b8e9 | |||
a12e4d7f64 | |||
897da9b07a | |||
293525d480 | |||
198e810355 | |||
fe25e00c94 | |||
8b129ab2e5 | |||
770bed54d1 | |||
774817d4ac | |||
b8068b2ae8 | |||
3007a6bbc8 | |||
1c0c8fece2 | |||
c52eee47f0 | |||
f88c98b9d9 | |||
b0e9e10f7e | |||
39d47e33f6 | |||
4b7b6c6327 | |||
a59edc5e8c | |||
5ba322ea6a | |||
b47b4b10cb | |||
c88f391935 | |||
26d3178e93 | |||
1ad27c7827 | |||
4200a8eed5 | |||
daceb7af8e | |||
f703b53bce | |||
2762224f0f | |||
86fc64d184 | |||
726cd6fd49 | |||
be1c4666e0 | |||
b75dfc4191 | |||
97815f8daf | |||
af16e1db69 | |||
5f6913b3a2 | |||
2b31af80cb | |||
3f9889d374 | |||
f8189c64a4 | |||
c9b5f89f17 |
@ -4,6 +4,7 @@
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -61,16 +61,16 @@ namespace BTCPayServer.Tests
|
||||
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 = cryptoCode,
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, "Save");
|
||||
return store;
|
||||
}
|
||||
@ -91,6 +91,7 @@ namespace BTCPayServer.Tests
|
||||
CryptoCurrency = crytoCode,
|
||||
DerivationSchemeFormat = crytoCode,
|
||||
DerivationScheme = derivation.ToString(),
|
||||
Confirmation = true
|
||||
}, "Save");
|
||||
}
|
||||
|
||||
|
@ -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,50 @@ 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()
|
||||
{
|
||||
@ -472,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);
|
||||
@ -506,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);
|
||||
@ -519,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);
|
||||
@ -704,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)
|
||||
@ -714,7 +767,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
private void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
|
@ -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]
|
||||
|
@ -37,7 +37,7 @@ services:
|
||||
- postgres
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.70
|
||||
image: nicolasdorier/nbxplorer:1.0.1.12
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
|
@ -65,6 +65,8 @@ namespace BTCPayServer
|
||||
|
||||
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
|
@ -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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,8 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.82</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>
|
||||
|
@ -48,6 +48,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
@ -161,6 +162,7 @@ namespace BTCPayServer.Controllers
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = FormatCurrency(cryptoData),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
@ -237,8 +239,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoicePaymentEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceStatusChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
@ -249,7 +250,7 @@ namespace BTCPayServer.Controllers
|
||||
finally
|
||||
{
|
||||
leases.Dispose();
|
||||
await CloseSocket(webSocket);
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
@ -268,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)
|
||||
@ -414,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));
|
||||
}
|
||||
|
||||
|
@ -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,7 +164,7 @@ 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();
|
||||
@ -178,7 +178,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.SetCryptoData(cryptoDatas);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
_EventAggregator.Publish(new Events.InvoiceCreatedEvent(entity.Id));
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
@ -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());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceCreatedEvent
|
||||
{
|
||||
public InvoiceCreatedEvent(string id)
|
||||
{
|
||||
InvoiceId = id;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} created";
|
||||
}
|
||||
}
|
||||
}
|
31
BTCPayServer/Events/InvoiceEvent.cs
Normal file
31
BTCPayServer/Events/InvoiceEvent.cs
Normal 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})";
|
||||
}
|
||||
}
|
||||
}
|
@ -7,19 +7,29 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceIPNEvent
|
||||
{
|
||||
public InvoiceIPNEvent(string invoiceId)
|
||||
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
EventCode = eventCode;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public string Error { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string ipnType = "IPN";
|
||||
if(EventCode.HasValue)
|
||||
{
|
||||
ipnType = $"IPN ({EventCode.Value} {Name})";
|
||||
}
|
||||
if (Error == null)
|
||||
return $"IPN sent for invoice {InvoiceId}";
|
||||
return $"Error while sending IPN: {Error}";
|
||||
return $"{ipnType} sent for invoice {InvoiceId}";
|
||||
return $"Error while sending {ipnType}: {Error}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoicePaymentEvent
|
||||
{
|
||||
|
||||
public InvoicePaymentEvent(string invoiceId, string cryptoCode, string address)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
Address = address;
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
|
||||
public string Address { get; set; }
|
||||
public string CryptoCode { get; private set; }
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{CryptoCode}: Invoice {InvoiceId} received a payment on {Address}";
|
||||
}
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -37,6 +37,9 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public ILogger Logger
|
||||
@ -63,32 +66,32 @@ namespace BTCPayServer.HostedServices
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice)
|
||||
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
return;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id));
|
||||
await SendNotification(invoice, cts.Token);
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
|
||||
await SendNotification(invoice, eventCode, name, cts.Token);
|
||||
return;
|
||||
}
|
||||
catch(OperationCanceledException) when(cts.IsCancellationRequested)
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id)
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
}
|
||||
catch(Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id)
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
}
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
|
||||
if (!string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
|
||||
}
|
||||
@ -107,14 +110,18 @@ namespace BTCPayServer.HostedServices
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id));
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token);
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
|
||||
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
|
||||
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id)
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
@ -123,12 +130,25 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id)
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
reschedule = true;
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
|
||||
|
||||
List<string> messages = new List<string>();
|
||||
while(ex != null)
|
||||
{
|
||||
messages.Add(ex.Message);
|
||||
ex = ex.InnerException;
|
||||
}
|
||||
string message = String.Join(',', messages.ToArray());
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + message);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = $"Unexpected error: {message}"
|
||||
});
|
||||
}
|
||||
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
|
||||
|
||||
@ -143,8 +163,23 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoicePaymentNotificationEvent
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
public class InvoicePaymentNotificationEventWrapper
|
||||
{
|
||||
[JsonProperty("event")]
|
||||
public InvoicePaymentNotificationEvent Event { get; set; }
|
||||
[JsonProperty("data")]
|
||||
public InvoicePaymentNotification Data { get; set; }
|
||||
}
|
||||
|
||||
Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, CancellationToken cancellation)
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Method = HttpMethod.Post;
|
||||
@ -167,7 +202,7 @@ namespace BTCPayServer.HostedServices
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
|
||||
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC");
|
||||
if(btcCryptoInfo != null)
|
||||
if (btcCryptoInfo != null)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
notification.Rate = (double)dto.Rate;
|
||||
@ -177,8 +212,22 @@ namespace BTCPayServer.HostedServices
|
||||
notification.BTCPrice = dto.BTCPrice;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
string notificationString = null;
|
||||
if (eventCode.HasValue)
|
||||
{
|
||||
var wrapper = new InvoicePaymentNotificationEventWrapper();
|
||||
wrapper.Data = notification;
|
||||
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
|
||||
notificationString = JsonConvert.SerializeObject(wrapper);
|
||||
}
|
||||
else
|
||||
{
|
||||
notificationString = JsonConvert.SerializeObject(notification);
|
||||
}
|
||||
|
||||
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
|
||||
request.Content = new StringContent(JsonConvert.SerializeObject(notification), UTF8, "application/json");
|
||||
request.Content = new StringContent(notificationString, UTF8, "application/json");
|
||||
var response = await _Client.SendAsync(request, cancellation);
|
||||
return response;
|
||||
}
|
||||
@ -193,42 +242,41 @@ namespace BTCPayServer.HostedServices
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceStatusChangedEvent>(async e =>
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
||||
await SaveEvent(invoice.Id, e);
|
||||
|
||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||
if (invoice.FullNotifications)
|
||||
{
|
||||
if (e.NewState == "expired" ||
|
||||
e.NewState == "paid" ||
|
||||
e.NewState == "invalid" ||
|
||||
e.NewState == "complete"
|
||||
if (e.Name == "invoice_expired" ||
|
||||
e.Name == "invoice_paidInFull" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed"
|
||||
)
|
||||
await Notify(invoice);
|
||||
}
|
||||
|
||||
if(e.NewState == "confirmed")
|
||||
|
||||
if (e.Name == "invoice_confirmed")
|
||||
{
|
||||
await Notify(invoice);
|
||||
}
|
||||
await SaveEvent(invoice.Id, e);
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
await Notify(invoice, e.EventCode, e.Name);
|
||||
}
|
||||
}));
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceCreatedEvent>(async e =>
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoicePaymentEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
|
||||
{
|
||||
|
@ -68,7 +68,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey, network.CryptoCode);
|
||||
if (invoice != null)
|
||||
{
|
||||
String address = scriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? scriptPubKey.ToString();
|
||||
Logs.PayServer.LogInformation($"{address} is mapping to invoice {invoice}");
|
||||
_WatchRequests.Add(invoice);
|
||||
}
|
||||
}
|
||||
|
||||
async Task NotifyBlock()
|
||||
@ -82,8 +86,11 @@ namespace BTCPayServer.HostedServices
|
||||
private async Task UpdateInvoice(string invoiceId, CancellationToken cancellation)
|
||||
{
|
||||
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
|
||||
{
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
|
||||
@ -122,7 +129,7 @@ namespace BTCPayServer.HostedServices
|
||||
break;
|
||||
}
|
||||
|
||||
if (!changed || cancellation.IsCancellationRequested)
|
||||
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
@ -146,7 +153,7 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired"));
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
@ -169,7 +176,7 @@ namespace BTCPayServer.HostedServices
|
||||
invoice.Payments.Add(payment);
|
||||
#pragma warning restore CS0618
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
context.Events.Add(new InvoicePaymentEvent(invoice.Id, coins.Wallet.Network.CryptoCode, coin.Coin.ScriptPubKey.GetDestinationAddress(coins.Wallet.Network.NBitcoinNetwork).ToString()));
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1002, "invoice_receivedPayment"));
|
||||
dirtyAddress = true;
|
||||
}
|
||||
if (dirtyAddress)
|
||||
@ -188,26 +195,20 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = null;
|
||||
invoice.ExceptionStatus = totalPaid > accounting.TotalDue ? "paidOver" : null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired")
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
@ -246,14 +247,14 @@ namespace BTCPayServer.HostedServices
|
||||
(totalConfirmed < accounting.TotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -266,7 +267,7 @@ namespace BTCPayServer.HostedServices
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -277,7 +278,7 @@ namespace BTCPayServer.HostedServices
|
||||
private IEnumerable<Task<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
|
||||
{
|
||||
return strategies
|
||||
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
.Select(d => (Wallet: _WalletProvider.IsAvailable(d.Network) ? _WalletProvider.GetWallet(d.Network) : null,
|
||||
Network: d.Network,
|
||||
Strategy: d.DerivationStrategyBase))
|
||||
.Where(d => d.Wallet != null)
|
||||
@ -408,11 +409,23 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private void Watch(string invoiceId)
|
||||
private async Task Watch(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
_WatchRequests.Add(invoiceId);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
try
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (invoice.ExpirationTime > now)
|
||||
{
|
||||
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
|
||||
_WatchRequests.Add(invoiceId);
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{ }
|
||||
}
|
||||
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
@ -430,7 +443,13 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); }));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceCreatedEvent>(b => { Watch(b.InvoiceId); }));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
{
|
||||
await Watch(b.InvoiceId);
|
||||
}
|
||||
}));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@ -464,24 +483,32 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
|
||||
ConcurrentDictionary<string, Task> executing = new ConcurrentDictionary<string, Task>();
|
||||
ConcurrentDictionary<string, Lazy<Task>> executing = new ConcurrentDictionary<string, Lazy<Task>>();
|
||||
try
|
||||
{
|
||||
// This loop just make sure an invoice will not be updated at the same time by two tasks.
|
||||
// If an update is happening while a request come, then the update is deferred when the executing task is over
|
||||
foreach (var item in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
{
|
||||
var task = executing.GetOrAdd(item, async i =>
|
||||
var localItem = item;
|
||||
var toExecute = new Lazy<Task>(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await UpdateInvoice(i, cancellation);
|
||||
await UpdateInvoice(localItem, cancellation);
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
|
||||
await Task.Delay(2000, cancellation);
|
||||
executing.TryRemove(localItem, out Lazy<Task> unused);
|
||||
}
|
||||
finally { executing.TryRemove(item, out Task useless); }
|
||||
});
|
||||
}, false);
|
||||
var executingTask = executing.GetOrAdd(item, toExecute);
|
||||
executingTask.Value.GetAwaiter(); // Make sure it run
|
||||
if (executingTask != toExecute)
|
||||
{
|
||||
// What was planned can't run for now, rebook it when the executingTask finish
|
||||
var unused = executingTask.Value.ContinueWith(t => _WatchRequests.Add(localItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested)
|
||||
@ -489,7 +516,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Task.WhenAll(executing.Values);
|
||||
await Task.WhenAll(executing.Values.Select(v => v.Value).ToArray());
|
||||
}
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -97,6 +97,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string TransactionSpeed { get; set; }
|
||||
public object StoreName
|
||||
{
|
||||
get;
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
{
|
||||
|
34
BTCPayServer/Models/StoreViewModels/WalletModel.cs
Normal file
34
BTCPayServer/Models/StoreViewModels/WalletModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
|
@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public interface IRateProviderFactory
|
||||
{
|
||||
IRateProvider GetRateProvider(BTCPayNetwork network);
|
||||
IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
53
BTCPayServer/Services/Rates/TweakRateProvider.cs
Normal file
53
BTCPayServer/Services/Rates/TweakRateProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
94
BTCPayServer/Views/Shared/SyncModal.cshtml
Normal file
94
BTCPayServer/Views/Shared/SyncModal.cshtml
Normal 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()">×</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>
|
@ -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">×</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)
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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" />
|
||||
|
74
BTCPayServer/Views/Stores/Wallet.cshtml
Normal file
74
BTCPayServer/Views/Stores/Wallet.cshtml
Normal 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">×</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>
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -43,7 +43,7 @@ h6 {
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
/*font-size: 16px;*/
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
77
BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js
Normal file
77
BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
132
BTCPayServer/wwwroot/js/StoreWallet.js
Normal file
132
BTCPayServer/wwwroot/js/StoreWallet.js
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
@ -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");
|
||||
|
7
BTCPayServer/wwwroot/js/ledgerwebsocket.js
Normal file
7
BTCPayServer/wwwroot/js/ledgerwebsocket.js
Normal file
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
6
BTCPayServer/wwwroot/js/vue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1579
BTCPayServer/wwwroot/vendor/animatecss/animate.css
vendored
Normal file
1579
BTCPayServer/wwwroot/vendor/animatecss/animate.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -9890,4 +9890,3 @@ a.text-dark:focus, a.text-dark:hover {
|
||||
.invisible {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap.css.map */
|
||||
|
@ -8593,4 +8593,3 @@ a.text-dark:focus, a.text-dark:hover {
|
||||
.invisible {
|
||||
visibility: hidden !important
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap.min.css.map */
|
||||
|
@ -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
|
||||
|
2
LICENSE
2
LICENSE
@ -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
|
||||
|
10
README.md
10
README.md
@ -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
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
{ "sdk": { "version": "2.0.0" } }
|
Reference in New Issue
Block a user