Compare commits

..

31 Commits

Author SHA1 Message Date
335dd9e66d bump 2018-02-15 14:44:35 +09:00
cd1611dbcd make sure to not spam too much NBXplorer 2018-02-15 14:44:08 +09:00
c17793aca9 do not freeze the stores page 2018-02-15 13:33:29 +09:00
01d898b618 Caching GetCoins 2018-02-15 13:02:12 +09:00
17069c311b Remove transaction cache 2018-02-15 12:42:48 +09:00
921d072942 fix electrum format for add derivation 2018-02-15 11:47:45 +09:00
6181e8b3e4 Refactor the code to prepare the group to support of another hardware wallet 2018-02-13 16:57:40 +09:00
93fc12bb2e fix typo 2018-02-13 15:28:22 +09:00
8e73c1a2f0 error message if not using segwit 2018-02-13 13:22:37 +09:00
e97c15578d bump nbxplorer 2018-02-13 12:42:03 +09:00
fd4f4e6aff better feedback if forgot to activate browser support 2018-02-13 12:20:56 +09:00
cedf8f75e8 Small UI adjustements 2018-02-13 11:41:21 +09:00
cd0a650df4 Ledger wallet support 2018-02-13 03:27:36 +09:00
6d6b9e2ba6 disable test not passing because of bitpay 2018-02-10 22:24:50 +09:00
fd915fdc5c fix test 2018-02-10 22:21:52 +09:00
59be813fe9 bump 2018-02-10 22:10:36 +09:00
465fbdd47f Fix bug which can happen if parsing of CoinAverage decimal is on another culture 2018-02-10 22:03:33 +09:00
f220abb716 Make the address verification step mandatory 2018-02-07 21:59:16 +09:00
db46ca87d7 do not share cache between long and short profile 2018-02-01 21:34:07 +01:00
d873a1a545 Set a longer timeout for the cache for /rates, update NBXPlorer, bump 2018-02-01 21:24:13 +01:00
a464a8702b Merge pull request #42 from Eskyee/patch-2
bootstrap.min.css deleted last line
2018-01-26 07:02:35 +01:00
63722b932a Merge pull request #41 from Eskyee/patch-1
bootstrap.css deleted last line
2018-01-26 07:02:22 +01:00
698b3c46cd bootstrap.min.css deleted last line
deleted this line
/*# sourceMappingURL=bootstrap.min.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404
(Not Found) (bootstrap.min.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.min.css.map
2018-01-26 01:19:58 +00:00
df81051d07 bootstrap.css deleted last line
deleted this line 
/*# sourceMappingURL=bootstrap.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404 
(Not Found) (bootstrap.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.css.map
2018-01-26 01:12:52 +00:00
ac70a77361 Fix #38 with paidOver + paidLate 2018-01-24 10:37:23 +01:00
59a2432af9 Better invoice loop, fix javascript 2018-01-20 14:09:57 +09:00
ea4fa8d5d4 Mock rate provider 2018-01-20 12:30:22 +09:00
ade3eff75c unwrap rates in api/rates 2018-01-20 12:11:24 +09:00
db2a2a2b6c Fix expiration message on checkout page 2018-01-20 00:33:37 +09:00
579dcb5af8 Fix confirmation message when changing altcoin derivation scheme 2018-01-20 00:29:20 +09:00
69247dee8a Fix api/rates allow to scope by cyrptoCode and storeId 2018-01-19 18:11:43 +09:00
45 changed files with 1198 additions and 319 deletions

View File

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

View File

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

View File

@ -70,6 +70,7 @@ namespace BTCPayServer.Tests
CryptoCurrency = cryptoCode,
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, "Save");
return store;
}
@ -90,6 +91,7 @@ namespace BTCPayServer.Tests
CryptoCurrency = crytoCode,
DerivationSchemeFormat = crytoCode,
DerivationScheme = derivation.ToString(),
Confirmation = true
}, "Save");
}

View File

@ -519,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);
@ -553,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);
@ -566,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);
@ -765,7 +767,7 @@ namespace BTCPayServer.Tests
private void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try

View File

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

View File

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

View File

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

View File

@ -26,7 +26,8 @@ namespace BTCPayServer
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType)
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
});
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
@ -24,7 +25,8 @@ namespace BTCPayServer
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType)
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
});
}
}

View File

@ -2,7 +2,8 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.14</Version>
<Version>1.0.1.29</Version>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -20,11 +21,12 @@
<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.52" />
<PackageReference Include="NBitcoin" Version="4.0.0.54" />
<PackageReference Include="NBitpayClient" Version="1.0.0.16" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.2" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.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" />
@ -99,4 +101,10 @@
<ItemGroup>
<Folder Include="Build\" />
</ItemGroup>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -162,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 ?? "/",
@ -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)

View File

@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
.Select(derivationStrategy => (Wallet: _WalletProvider.GetWallet(derivationStrategy.Network),
DerivationStrategy: derivationStrategy.DerivationStrategyBase,
Network: derivationStrategy.Network,
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network),
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network, false),
FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network)))
.Where(_ => _.Wallet != null &&
_.FeeRateProvider != null &&
@ -164,7 +164,7 @@ namespace BTCPayServer.Controllers
#pragma warning disable CS0618
var btc = _NetworkProvider.BTC;
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc));
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();

View File

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

View File

@ -2,22 +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
@ -29,6 +37,7 @@ namespace BTCPayServer.Controllers
public class StoresController : Controller
{
public StoresController(
IOptions<MvcJsonOptions> mvcJsonOptions,
StoreRepository repo,
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
@ -36,6 +45,7 @@ namespace BTCPayServer.Controllers
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
IHostingEnvironment env)
{
_Repo = repo;
@ -46,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;
@ -88,6 +102,208 @@ 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 + "/";
}
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult
{
public string TransactionId { 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 hw = new HardwareWalletService(webSocket);
object result = null;
try
{
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for substract fees"); }
}
if (command == "test")
{
result = await hw.Test();
}
if (command == "getxpub")
{
result = await hw.GetExtPubKey(network);
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (!await hw.SupportDerivation(network, strategy))
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
unspentCoins.Item2.TryAdd(changeAddress.Item1.ScriptPubKey, changeAddress.Item2);
var transaction = await hw.SendToAddress(strategy, unspentCoins.Item1, network,
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
feeRateValue,
changeAddress.Item1,
unspentCoins.Item2);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = GetDerivationStrategy(store, network);
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
if (!directStrategy.Segwit)
return null;
return directStrategy;
}
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
}
return strategy.DerivationStrategyBase;
}
[HttpGet]
public async Task<IActionResult> ListStores()
{
@ -96,10 +312,10 @@ namespace BTCPayServer.Controllers
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetDerivationStrategies(_NetworkProvider)
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase))
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await _.Wallet.GetBalance(_.DerivationStrategy)).ToString() + " " + _.Wallet.Network.CryptoCode))
.Select(async _ => (await GetBalanceString(_)).ToString() + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
@ -117,6 +333,21 @@ namespace BTCPayServer.Controllers
return View(result);
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
@ -197,15 +428,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();
@ -224,16 +457,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
@ -243,29 +491,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);
}
}
@ -278,6 +519,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();

View File

@ -248,7 +248,7 @@ namespace BTCPayServer.Data
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
cachedRateProvider.Inner
});
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache);
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
}
else
{

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Events
{
@ -10,6 +11,7 @@ namespace BTCPayServer.Events
{
public BTCPayNetwork Network { get; set; }
public Script ScriptPubKey { get; set; }
public DerivationStrategyBase DerivationStrategy { get; set; }
public override string ToString()
{

View File

@ -21,11 +21,26 @@ 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";

View File

@ -29,9 +29,6 @@ namespace BTCPayServer.HostedServices
{
}
public Dictionary<BTCPayNetwork, KnownState> KnownStates { get; set; }
public Dictionary<BTCPayNetwork, KnownState> ModifiedKnownStates { get; set; } = new Dictionary<BTCPayNetwork, KnownState>();
public InvoiceEntity Invoice { get; set; }
public List<object> Events { get; set; } = new List<object>();
@ -64,11 +61,16 @@ namespace BTCPayServer.HostedServices
}
CompositeDisposable leases = new CompositeDisposable();
async Task NotifyReceived(Script scriptPubKey, BTCPayNetwork network)
async Task NotifyReceived(Events.TxOutReceivedEvent evt)
{
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey, network.CryptoCode);
if (invoice != null)
_WatchRequests.Add(invoice);
var invoiceId = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(evt.ScriptPubKey, evt.Network.CryptoCode);
if (invoiceId != null)
{
String address = evt.ScriptPubKey.GetDestinationAddress(evt.Network.NBitcoinNetwork)?.ToString() ?? evt.ScriptPubKey.ToString();
_WalletProvider.GetWallet(evt.Network).InvalidateCache(evt.DerivationStrategy);
Logs.PayServer.LogInformation($"{address} is mapping to invoice {invoiceId}");
_WatchRequests.Add(invoiceId);
}
}
async Task NotifyBlock()
@ -82,8 +84,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);
@ -92,8 +97,7 @@ namespace BTCPayServer.HostedServices
var stateBefore = invoice.Status;
var updateContext = new UpdateInvoiceContext()
{
Invoice = invoice,
KnownStates = changes
Invoice = invoice
};
await UpdateInvoice(updateContext).ConfigureAwait(false);
if (updateContext.Dirty)
@ -109,11 +113,6 @@ namespace BTCPayServer.HostedServices
_EventAggregator.Publish(evt, evt.GetType());
}
foreach (var modifiedKnownState in updateContext.ModifiedKnownStates)
{
changes.AddOrReplace(modifiedKnownState.Key, modifiedKnownState.Value);
}
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
@ -122,7 +121,7 @@ namespace BTCPayServer.HostedServices
break;
}
if (!changed || cancellation.IsCancellationRequested)
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
break;
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
@ -158,8 +157,6 @@ namespace BTCPayServer.HostedServices
if (coins.TimestampedCoins.Length == 0)
continue;
bool dirtyAddress = false;
if (coins.State != null)
context.ModifiedKnownStates.AddOrReplace(coins.Wallet.Network, coins.State);
var alreadyAccounted = new HashSet<OutPoint>(invoice.GetPayments(coins.Wallet.Network).Select(p => p.Outpoint));
foreach (var coin in coins.TimestampedCoins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
@ -190,7 +187,7 @@ namespace BTCPayServer.HostedServices
{
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();
}
@ -202,13 +199,6 @@ namespace BTCPayServer.HostedServices
}
}
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";
@ -283,7 +273,7 @@ namespace BTCPayServer.HostedServices
Strategy: d.DerivationStrategyBase))
.Where(d => d.Wallet != null)
.Select(d => (Network: d.Network,
Coins: d.Wallet.GetCoins(d.Strategy, context.KnownStates.TryGet(d.Network))))
Coins: d.Wallet.GetCoins(d.Strategy)))
.Select(async d =>
{
var coins = await d.Coins;
@ -442,7 +432,7 @@ namespace BTCPayServer.HostedServices
_Loop = StartLoop(_Cts.Token);
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.TxOutReceivedEvent>(async b => { await NotifyReceived(b); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == "invoice_created")
@ -483,29 +473,31 @@ 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))
{
bool added = false;
var task = executing.GetOrAdd(item, async i =>
var localItem = item;
var toExecute = new Lazy<Task>(async () =>
{
try
{
added = true;
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 {i})");
executing.TryRemove(localItem, out Lazy<Task> unused);
}
});
if (!added && task.Status == TaskStatus.RanToCompletion)
}, false);
var executingTask = executing.GetOrAdd(item, toExecute);
executingTask.Value.GetAwaiter(); // Make sure it run
if (executingTask != toExecute)
{
executing.TryRemove(item, out Task t);
_WatchRequests.Add(item);
// What was planned can't run for now, rebook it when the executingTask finish
var unused = executingTask.Value.ContinueWith(t => _WatchRequests.Add(localItem));
}
}
}
@ -514,7 +506,7 @@ namespace BTCPayServer.HostedServices
}
finally
{
await Task.WhenAll(executing.Values);
await Task.WhenAll(executing.Values.Select(v => v.Value).ToArray());
}
Logs.PayServer.LogInformation("Stop watching invoices");
}

View File

@ -25,11 +25,9 @@ namespace BTCPayServer.HostedServices
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
NBXplorerDashboard _Dashboards;
TransactionCacheProvider _TxCache;
public NBXplorerListener(ExplorerClientProvider explorerClients,
NBXplorerDashboard dashboard,
TransactionCacheProvider cacheProvider,
InvoiceRepository invoiceRepository,
EventAggregator aggregator, IApplicationLifetime lifetime)
{
@ -39,7 +37,6 @@ namespace BTCPayServer.HostedServices
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
_Lifetime = lifetime;
_TxCache = cacheProvider;
}
CompositeDisposable leases = new CompositeDisposable();
@ -137,17 +134,16 @@ namespace BTCPayServer.HostedServices
switch (newEvent)
{
case NBXplorer.Models.NewBlockEvent evt:
_TxCache.GetTransactionCache(network).NewBlock(evt.Hash, evt.PreviousBlockHash);
_Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode });
break;
case NBXplorer.Models.NewTransactionEvent evt:
foreach (var txout in evt.Outputs)
{
_TxCache.GetTransactionCache(network).AddToCache(evt.TransactionData);
_Aggregator.Publish(new Events.TxOutReceivedEvent()
{
Network = network,
ScriptPubKey = txout.ScriptPubKey
ScriptPubKey = txout.ScriptPubKey,
DerivationStrategy = txout.DerivationStrategy
});
}
break;

View File

@ -142,8 +142,6 @@ namespace BTCPayServer.Hosting
BlockTarget = 20
});
services.AddSingleton<TransactionCacheProvider>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, NBXplorerListener>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,6 @@ namespace BTCPayServer
IWebHost host = null;
var processor = new ConsoleLoggerProcessor();
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider(processor);
var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(loggerProvider);
var logger = loggerFactory.CreateLogger("Configuration");

View File

@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using LedgerWallet;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Services
{
public class HardwareWalletException : Exception
{
public HardwareWalletException() { }
public HardwareWalletException(string message) : base(message) { }
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
}
public class HardwareWalletService
{
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();
}
}
private readonly LedgerClient _Ledger;
public LedgerClient Ledger
{
get
{
return _Ledger;
}
}
WebSocketTransport _Transport = null;
public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
{
if (ledgerWallet == null)
throw new ArgumentNullException(nameof(ledgerWallet));
_Transport = new WebSocketTransport(ledgerWallet);
_Ledger = new LedgerClient(_Transport);
}
public async Task<LedgerTestResult> Test()
{
var version = await _Ledger.GetFirmwareVersionAsync();
return new LedgerTestResult() { Success = true };
}
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
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
});
return new GetXPubResult() { ExtPubKey = derivation.ToString() };
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
{
try
{
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;
}
catch (FormatException)
{
throw new HardwareWalletException("Unsupported ledger app");
}
}
public async Task<bool> SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy)
{
if (network == null)
throw new ArgumentNullException(nameof(Network));
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
return await GetKeyPath(_Ledger, network, strategy) != null;
}
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;
}
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy,
Coin[] coins, BTCPayNetwork network,
(IDestination destination, Money amount, bool substractFees)[] send,
FeeRate feeRate,
IDestination changeAddress,
Dictionary<Script, KeyPath> keypaths = null)
{
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
if (network == null)
throw new ArgumentNullException(nameof(network));
if (feeRate == null)
throw new ArgumentNullException(nameof(feeRate));
if (changeAddress == null)
throw new ArgumentNullException(nameof(changeAddress));
if (keypaths == null)
throw new ArgumentNullException(nameof(keypaths));
if (feeRate.FeePerK <= Money.Zero)
{
throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero");
}
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await GetKeyPath(Ledger, network, strategy);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.AddCoins(coins);
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress);
builder.SendEstimatedFees(feeRate);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
_Transport.Timeout = TimeSpan.FromMinutes(5);
var fullySigned = await Ledger.SignTransactionAsync(
usedCoins.Select(c => new SignatureRequest
{
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(),
unsigned,
hasChange ? foundKeyPath.Derive(keypaths[changeAddress.ScriptPubKey]) : null);
return fullySigned;
}
}
public class LedgerTestResult
{
public bool Success { get; set; }
public string Error { get; set; }
}
public class GetXPubResult
{
public string ExtPubKey { get; set; }
}
}

View File

@ -11,6 +11,7 @@ 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
{
@ -30,9 +31,10 @@ namespace BTCPayServer.Services.Rates
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, RateProvider ?? network.DefaultRateProvider, _Cache) { CacheSpan = CacheSpan });
return (longCache ? _LongCacheProviders : _Providers).GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, RateProvider ?? network.DefaultRateProvider, _Cache) { CacheSpan = longCache ? LongCacheSpan : CacheSpan, AdditionalScope = longCache ? "LONG" : "SHORT" });
}
}
}

View File

@ -40,7 +40,7 @@ namespace BTCPayServer.Services.Rates
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);
@ -49,11 +49,13 @@ namespace BTCPayServer.Services.Rates
public Task<ICollection<Rate>> GetRatesAsync()
{
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) =>
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync();
});
}
public string AdditionalScope { get; set; }
}
}

View File

@ -2,6 +2,7 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
@ -72,24 +73,19 @@ namespace BTCPayServer.Services.Rates
rates = (JObject)rates["symbols"];
}
return rates.Properties()
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase))
.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 =>
{
if (Exchange == null)
{
return ToDecimal(p.Value["last"]);
}
else
{
return ToDecimal(p.Value["bid"]);
}
TryToDecimal(p, out decimal v);
return v;
});
}
}
private decimal ToDecimal(JToken token)
private bool TryToDecimal(JProperty p, out decimal v)
{
return decimal.Parse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
JToken token = p.Value[Exchange == null ? "last" : "bid"];
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
}
public async Task<ICollection<Rate>> GetRatesAsync()

View File

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

View File

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

View File

@ -1,96 +0,0 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBXplorer.Models;
namespace BTCPayServer.Services
{
public class TransactionCacheProvider
{
IOptions<MemoryCacheOptions> _Options;
public TransactionCacheProvider(IOptions<MemoryCacheOptions> options)
{
_Options = options;
}
ConcurrentDictionary<string, TransactionCache> _TransactionCaches = new ConcurrentDictionary<string, TransactionCache>();
public TransactionCache GetTransactionCache(BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
return _TransactionCaches.GetOrAdd(network.CryptoCode, c => new TransactionCache(_Options, network));
}
}
public class TransactionCache : IDisposable
{
//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;
}
//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;
}
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(60);
public BTCPayNetwork Network { get; private set; }
public void AddToCache(TransactionResult tx)
{
//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)
{
//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;
return null; // Does not work correctly yet
}
public void Dispose()
{
//_MemoryCache.Dispose();
}
}
}

View File

@ -1,4 +1,5 @@
using NBitcoin;
using Microsoft.Extensions.Logging;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
@ -10,6 +11,8 @@ using BTCPayServer.Data;
using System.Threading;
using NBXplorer.Models;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
using System.Collections.Concurrent;
namespace BTCPayServer.Services.Wallets
{
@ -25,21 +28,22 @@ namespace BTCPayServer.Services.Wallets
public Coin Coin { get; set; }
}
public TimestampedCoin[] TimestampedCoins { get; set; }
public KnownState State { get; set; }
public DerivationStrategyBase Strategy { get; set; }
public BTCPayWallet Wallet { get; set; }
}
public class BTCPayWallet
{
private ExplorerClient _Client;
private TransactionCache _Cache;
public BTCPayWallet(ExplorerClient client, TransactionCache cache, BTCPayNetwork network)
private IMemoryCache _MemoryCache;
public BTCPayWallet(ExplorerClient client, IMemoryCache memoryCache, BTCPayNetwork network)
{
if (client == null)
throw new ArgumentNullException(nameof(client));
if (memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache));
_Client = client;
_Network = network;
_Cache = cache;
_MemoryCache = memoryCache;
}
@ -52,7 +56,7 @@ namespace BTCPayServer.Services.Wallets
}
}
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(60);
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(30);
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
{
@ -68,6 +72,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);
@ -77,47 +95,93 @@ namespace BTCPayServer.Services.Wallets
{
if (txId == null)
throw new ArgumentNullException(nameof(txId));
var tx = _Cache.GetTransaction(txId);
if (tx != null)
return tx;
tx = await _Client.GetTransactionAsync(txId, cancellation);
_Cache.AddToCache(tx);
var tx = await _Client.GetTransactionAsync(txId, cancellation);
return tx;
}
public async Task<NetworkCoins> GetCoins(DerivationStrategyBase strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
public void InvalidateCache(DerivationStrategyBase strategy)
{
var changes = await _Client.GetUTXOsAsync(strategy, state?.PreviousCall, false, cancellation).ConfigureAwait(false);
_MemoryCache.Remove("CACHEDCOINS_" + strategy.ToString());
}
ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>> _FetchingUTXOs = new ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>>();
public async Task<NetworkCoins> GetCoins(DerivationStrategyBase strategy, CancellationToken cancellation = default(CancellationToken))
{
UTXOChanges changes = await GetUTXOChanges(strategy, cancellation);
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() { PreviousCall = changes },
Strategy = strategy,
Wallet = this
};
}
private async Task<UTXOChanges> GetUTXOChanges(DerivationStrategyBase strategy, CancellationToken cancellation)
{
var thisCompletionSource = new TaskCompletionSource<UTXOChanges>();
var completionSource = _FetchingUTXOs.GetOrAdd(strategy.ToString(), (s) => thisCompletionSource);
if (thisCompletionSource != completionSource)
return await completionSource.Task;
try
{
var utxos = await _MemoryCache.GetOrCreateAsync("CACHEDCOINS_" + strategy.ToString(), async entry =>
{
var now = DateTimeOffset.UtcNow;
UTXOChanges result = null;
try
{
result = await _Client.GetUTXOsAsync(strategy, null, false, cancellation).ConfigureAwait(false);
}
catch
{
Logs.PayServer.LogError("Call to NBXplorer GetUTXOsAsync timed out, this should never happen, please report this issue to NBXplorer developers");
throw;
}
var spentTime = DateTimeOffset.UtcNow - now;
if (spentTime.TotalSeconds > 30)
{
Logs.PayServer.LogWarning($"NBXplorer took {(int)spentTime.TotalSeconds} seconds to reply, there is something wrong, please report this issue to NBXplorer developers");
}
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return result;
});
completionSource.TrySetResult(utxos);
}
catch(Exception ex)
{
completionSource.TrySetException(ex);
}
finally
{
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
}
return await completionSource.Task;
}
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<Money> GetBalance(DerivationStrategyBase derivationStrategy)
{
var result = await _Client.GetUTXOsAsync(derivationStrategy, null, true);
Dictionary<OutPoint, UTXO> received = new Dictionary<OutPoint, UTXO>();
foreach(var utxo in result.Confirmed.UTXOs.Concat(result.Unconfirmed.UTXOs))
public async Task<(Coin[], Dictionary<Script, KeyPath>)> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
{
var changes = await GetUTXOChanges(derivationStrategy, cancellation);
var keyPaths = new Dictionary<Script, KeyPath>();
foreach (var coin in changes.GetUnspentUTXOs())
{
received.TryAdd(utxo.Outpoint, utxo);
keyPaths.TryAdd(coin.ScriptPubKey, coin.KeyPath);
}
foreach (var utxo in result.Confirmed.SpentOutpoints.Concat(result.Unconfirmed.SpentOutpoints))
{
received.Remove(utxo);
}
return received.Values.Select(c => c.Value).Sum();
return (changes.GetUnspentCoins(), keyPaths);
}
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
{
UTXOChanges changes = await GetUTXOChanges(derivationStrategy, cancellation);
return changes.GetUnspentUTXOs().Select(c => c.Value).Sum();
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Wallets
{
@ -10,16 +11,16 @@ namespace BTCPayServer.Services.Wallets
{
private ExplorerClientProvider _Client;
BTCPayNetworkProvider _NetworkProvider;
TransactionCacheProvider _TransactionCacheProvider;
IOptions<MemoryCacheOptions> _Options;
public BTCPayWalletProvider(ExplorerClientProvider client,
TransactionCacheProvider transactionCacheProvider,
IOptions<MemoryCacheOptions> memoryCacheOption,
BTCPayNetworkProvider networkProvider)
{
if (client == null)
throw new ArgumentNullException(nameof(client));
_Client = client;
_TransactionCacheProvider = transactionCacheProvider;
_NetworkProvider = networkProvider;
_Options = memoryCacheOption;
}
public BTCPayWallet GetWallet(BTCPayNetwork network)
@ -36,7 +37,7 @@ namespace BTCPayServer.Services.Wallets
var client = _Client.GetExplorerClient(cryptoCode);
if (network == null || client == null)
return null;
return new BTCPayWallet(client, _TransactionCacheProvider.GetTransactionCache(network), network);
return new BTCPayWallet(client, new MemoryCache(_Options), network);
}
public bool IsAvailable(BTCPayNetwork network)

View File

@ -492,7 +492,7 @@
<div>
<div class="expired__body">
<div class="expired__header" i18n="">What happened?</div>
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for 15 minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for @Model.MaxTimeMinutes minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
<div class="expired__text" i18n="">If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.</div>
<div class="expired__text" i18n="">
If the transaction
@ -504,7 +504,7 @@
<span class="expired__text__bullet" i18n="">Invoice ID:</span> {{srvModel.invoiceId}}<br>
<!---->
<span>
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.OrderId}}
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.orderId}}
</span>
</div>
</div>

View File

@ -13,31 +13,35 @@
</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)
{
@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>
<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)
{
<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">
@ -73,34 +77,49 @@
</tr>
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-info">Continue</button>
}
else
{
<table class="table">
<thead class="thead-inverse">
<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" />
<input type="hidden" asp-for="DerivationSchemeFormat" />
<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>
<th>Key path</th>
<th>Address</th>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</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>
}
</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>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script type="text/javascript">
@Model.ServerUrl.ToJSVariableModel("srvModel");
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,145 @@
$(function () {
var ledgerDetected = false;
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
var recommendedFees = "";
var recommendedBalance = "";
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();
if ($("#amount-textbox").val() === "") {
$("#amount-textbox").val(recommendedBalance);
$("#substract-checkbox").prop("checked", true);
}
if ($("#fee-textbox").val() === "") {
$("#fee-textbox").val(recommendedFees);
}
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");
recommendedFees = result.recommendedSatoshiPerByte;
recommendedBalance = result.balance;
$("#crypto-fee").text(result.recommendedSatoshiPerByte);
$("#crypto-balance").text(result.balance);
$("#crypto-code").text(cryptoCode);
}
});
};
bridge.isSupported()
.then(function (supported) {
if (!supported) {
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
}
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason)
{
if (reason.message === "Sign failed")
reason = "Have you forgot to activate browser support in your ledger app?";
Write('hw', 'error', reason);
})
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('hw', 'error', result.error);
} else {
Write('hw', 'success', 'Ledger detected');
$("#sendform").css("display", "block");
ledgerDetected = true;
updateInfo();
}
});
}
});
});

View File

@ -19,7 +19,7 @@ Vue.config.ignoredElements = [
'low-fee-timeline',
// Ignoring custom HTML5 elements, eg: bp-spinner
/^bp-/
]
];
var checkoutCtrl = new Vue({
el: '#checkoutCtrl',
components: {
@ -28,7 +28,7 @@ var checkoutCtrl = new Vue({
data: {
srvModel: srvModel
}
})
});
var display = $(".timer-row__time-left"); // Timer container
@ -88,7 +88,7 @@ function emailForm() {
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
}
})
});
}
/* =============== Even listeners =============== */
@ -136,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");
@ -284,7 +284,7 @@ function progressStart(timerMax) {
var now = new Date();
var timeDiff = end.getTime() - now.getTime();
var perc = 100 - Math.round((timeDiff / timerMax) * 100);
var perc = 100 - Math.round(timeDiff / timerMax * 100);
if (perc === 75 && (status === "paidPartial" || status === "new")) {
$(".timer-row").addClass("expiring-soon");

File diff suppressed because one or more lines are too long

View File

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

View File

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