Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
335dd9e66d | |||
cd1611dbcd | |||
c17793aca9 | |||
01d898b618 | |||
17069c311b | |||
921d072942 | |||
6181e8b3e4 | |||
93fc12bb2e | |||
8e73c1a2f0 | |||
e97c15578d | |||
fd4f4e6aff | |||
cedf8f75e8 | |||
cd0a650df4 |
@ -4,6 +4,7 @@
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -37,7 +37,7 @@ services:
|
||||
- postgres
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.1.11
|
||||
image: nicolasdorier/nbxplorer:1.0.1.13
|
||||
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;
|
||||
|
@ -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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,8 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.21</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.54" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.16" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.8" />
|
||||
<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>
|
||||
|
@ -250,7 +250,7 @@ namespace BTCPayServer.Controllers
|
||||
finally
|
||||
{
|
||||
leases.Dispose();
|
||||
await CloseSocket(webSocket);
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
@ -269,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)
|
||||
|
@ -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,6 +428,7 @@ namespace BTCPayServer.Controllers
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
||||
return View(vm);
|
||||
}
|
||||
@ -206,6 +438,7 @@ namespace BTCPayServer.Controllers
|
||||
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();
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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";
|
||||
|
@ -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,14 +61,15 @@ 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)
|
||||
var invoiceId = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(evt.ScriptPubKey, evt.Network.CryptoCode);
|
||||
if (invoiceId != null)
|
||||
{
|
||||
String address = scriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? scriptPubKey.ToString();
|
||||
Logs.PayServer.LogInformation($"{address} is mapping to invoice {invoice}");
|
||||
_WatchRequests.Add(invoice);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,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)
|
||||
@ -116,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))
|
||||
{
|
||||
@ -165,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)))
|
||||
@ -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")
|
||||
|
@ -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;
|
||||
|
@ -142,8 +142,6 @@ namespace BTCPayServer.Hosting
|
||||
BlockTarget = 20
|
||||
});
|
||||
|
||||
services.AddSingleton<TransactionCacheProvider>();
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, NBXplorerListener>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
|
@ -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
|
||||
|
@ -53,7 +53,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
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();
|
||||
|
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
226
BTCPayServer/Services/HardwareWalletService.cs
Normal file
226
BTCPayServer/Services/HardwareWalletService.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
@if(!Model.Confirmation)
|
||||
{
|
||||
@ -27,8 +27,15 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@ -82,6 +89,7 @@ else
|
||||
<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">
|
||||
@ -109,4 +117,9 @@ else
|
||||
|
||||
@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);
|
||||
|
||||
|
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 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>
|
||||
}
|
@ -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>
|
||||
|
||||
|
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
145
BTCPayServer/wwwroot/js/StoreWallet.js
Normal file
145
BTCPayServer/wwwroot/js/StoreWallet.js
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
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
Reference in New Issue
Block a user