mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-19 06:54:19 +01:00
Ledger wallet support
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
<NoWarn>NU1701</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ services:
|
|||||||
- postgres
|
- postgres
|
||||||
|
|
||||||
nbxplorer:
|
nbxplorer:
|
||||||
image: nicolasdorier/nbxplorer:1.0.1.11
|
image: nicolasdorier/nbxplorer:1.0.1.12
|
||||||
ports:
|
ports:
|
||||||
- "32838:32838"
|
- "32838:32838"
|
||||||
expose:
|
expose:
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ namespace BTCPayServer
|
|||||||
|
|
||||||
|
|
||||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||||
|
public KeyPath CoinType { get; internal set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return CryptoCode;
|
return CryptoCode;
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ namespace BTCPayServer
|
|||||||
UriScheme = "bitcoin",
|
UriScheme = "bitcoin",
|
||||||
DefaultRateProvider = btcRate,
|
DefaultRateProvider = btcRate,
|
||||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
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.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
|
using NBitcoin;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
|
|
||||||
namespace BTCPayServer
|
namespace BTCPayServer
|
||||||
@@ -24,7 +25,8 @@ namespace BTCPayServer
|
|||||||
UriScheme = "litecoin",
|
UriScheme = "litecoin",
|
||||||
DefaultRateProvider = ltcRate,
|
DefaultRateProvider = ltcRate,
|
||||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
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>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||||
<Version>1.0.1.21</Version>
|
<Version>1.0.1.22</Version>
|
||||||
|
<NoWarn>NU1701</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Build\dockerfiles\**" />
|
<Compile Remove="Build\dockerfiles\**" />
|
||||||
@@ -20,11 +21,12 @@
|
|||||||
<PackageReference Include="Hangfire" Version="1.6.17" />
|
<PackageReference Include="Hangfire" Version="1.6.17" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
<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="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||||
<PackageReference Include="NBitcoin" Version="4.0.0.54" />
|
<PackageReference Include="NBitcoin" Version="4.0.0.54" />
|
||||||
<PackageReference Include="NBitpayClient" Version="1.0.0.16" />
|
<PackageReference Include="NBitpayClient" Version="1.0.0.16" />
|
||||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
<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" Version="1.0.0.1" />
|
||||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||||
@@ -99,4 +101,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Build\" />
|
<Folder Include="Build\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="devtest.pfx">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ namespace BTCPayServer.Controllers
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
leases.Dispose();
|
leases.Dispose();
|
||||||
await CloseSocket(webSocket);
|
await webSocket.CloseSocket();
|
||||||
}
|
}
|
||||||
return new EmptyResult();
|
return new EmptyResult();
|
||||||
}
|
}
|
||||||
@@ -269,21 +269,6 @@ namespace BTCPayServer.Controllers
|
|||||||
catch { try { webSocket.Dispose(); } catch { } }
|
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]
|
[HttpPost]
|
||||||
[Route("i/{invoiceId}/UpdateCustomer")]
|
[Route("i/{invoiceId}/UpdateCustomer")]
|
||||||
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
||||||
|
|||||||
@@ -2,22 +2,30 @@
|
|||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Fees;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
|
using LedgerWallet;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
@@ -29,6 +37,7 @@ namespace BTCPayServer.Controllers
|
|||||||
public class StoresController : Controller
|
public class StoresController : Controller
|
||||||
{
|
{
|
||||||
public StoresController(
|
public StoresController(
|
||||||
|
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||||
StoreRepository repo,
|
StoreRepository repo,
|
||||||
TokenRepository tokenRepo,
|
TokenRepository tokenRepo,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
@@ -36,6 +45,7 @@ namespace BTCPayServer.Controllers
|
|||||||
BTCPayWalletProvider walletProvider,
|
BTCPayWalletProvider walletProvider,
|
||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
ExplorerClientProvider explorerProvider,
|
ExplorerClientProvider explorerProvider,
|
||||||
|
IFeeProviderFactory feeRateProvider,
|
||||||
IHostingEnvironment env)
|
IHostingEnvironment env)
|
||||||
{
|
{
|
||||||
_Repo = repo;
|
_Repo = repo;
|
||||||
@@ -46,9 +56,13 @@ namespace BTCPayServer.Controllers
|
|||||||
_Env = env;
|
_Env = env;
|
||||||
_NetworkProvider = networkProvider;
|
_NetworkProvider = networkProvider;
|
||||||
_ExplorerProvider = explorerProvider;
|
_ExplorerProvider = explorerProvider;
|
||||||
|
_MvcJsonOptions = mvcJsonOptions.Value;
|
||||||
|
_FeeRateProvider = feeRateProvider;
|
||||||
}
|
}
|
||||||
BTCPayNetworkProvider _NetworkProvider;
|
BTCPayNetworkProvider _NetworkProvider;
|
||||||
private ExplorerClientProvider _ExplorerProvider;
|
private ExplorerClientProvider _ExplorerProvider;
|
||||||
|
private MvcJsonOptions _MvcJsonOptions;
|
||||||
|
private IFeeProviderFactory _FeeRateProvider;
|
||||||
BTCPayWalletProvider _WalletProvider;
|
BTCPayWalletProvider _WalletProvider;
|
||||||
AccessTokenController _TokenController;
|
AccessTokenController _TokenController;
|
||||||
StoreRepository _Repo;
|
StoreRepository _Repo;
|
||||||
@@ -88,6 +102,338 @@ namespace BTCPayServer.Controllers
|
|||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/wallet")]
|
||||||
|
public async Task<IActionResult> Wallet(string storeId)
|
||||||
|
{
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
WalletModel model = new WalletModel();
|
||||||
|
model.ServerUrl = GetStoreUrl(storeId);
|
||||||
|
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetStoreUrl(string storeId)
|
||||||
|
{
|
||||||
|
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport
|
||||||
|
{
|
||||||
|
private readonly WebSocket webSocket;
|
||||||
|
|
||||||
|
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
|
||||||
|
{
|
||||||
|
if (webSocket == null)
|
||||||
|
throw new ArgumentNullException(nameof(webSocket));
|
||||||
|
this.webSocket = webSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
public async Task<byte[][]> Exchange(byte[][] apdus)
|
||||||
|
{
|
||||||
|
List<byte[]> responses = new List<byte[]>();
|
||||||
|
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
|
||||||
|
{
|
||||||
|
foreach (var apdu in apdus)
|
||||||
|
{
|
||||||
|
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
|
||||||
|
}
|
||||||
|
foreach (var apdu in apdus)
|
||||||
|
{
|
||||||
|
byte[] response = new byte[300];
|
||||||
|
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
|
||||||
|
Array.Resize(ref response, result.Count);
|
||||||
|
responses.Add(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LedgerTestResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetInfoResult
|
||||||
|
{
|
||||||
|
public int RecommendedSatoshiPerByte { get; set; }
|
||||||
|
public double Balance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class SendToAddressResult
|
||||||
|
{
|
||||||
|
public string TransactionId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetXPubResult
|
||||||
|
{
|
||||||
|
public string ExtPubKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/ws/ledger")]
|
||||||
|
public async Task<IActionResult> LedgerConnection(
|
||||||
|
string storeId,
|
||||||
|
string command,
|
||||||
|
// getinfo
|
||||||
|
string cryptoCode = null,
|
||||||
|
// sendtoaddress
|
||||||
|
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||||
|
return NotFound();
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||||
|
var ledgerTransport = new WebSocketTransport(webSocket);
|
||||||
|
var ledger = new LedgerWallet.LedgerClient(ledgerTransport);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (command == "test")
|
||||||
|
{
|
||||||
|
var version = await ledger.GetFirmwareVersionAsync();
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = true });
|
||||||
|
}
|
||||||
|
if (command == "getxpub")
|
||||||
|
{
|
||||||
|
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pubkey = await GetExtPubKey(ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true));
|
||||||
|
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
|
||||||
|
{
|
||||||
|
P2SH = true,
|
||||||
|
Legacy = false
|
||||||
|
});
|
||||||
|
await Send(webSocket, new GetXPubResult() { ExtPubKey = derivation.ToString() });
|
||||||
|
}
|
||||||
|
catch(FormatException)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Unsupported ledger app" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (command == "getinfo")
|
||||||
|
{
|
||||||
|
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||||
|
var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
||||||
|
if (strategy == null)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Derivation strategy for {cryptoCode} is not set" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
DirectDerivationStrategy directStrategy = GetDirectStrategy(strategy);
|
||||||
|
if (directStrategy == null)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"The feature does not work for multi-sig wallets" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundKeyPath = await GetKeyPath(ledger, network, directStrategy);
|
||||||
|
|
||||||
|
if (foundKeyPath == null)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"This store is not configured to use this ledger" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
|
||||||
|
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||||
|
var balance = _WalletProvider.GetWallet(network).GetBalance(strategy.DerivationStrategyBase);
|
||||||
|
|
||||||
|
await Send(webSocket, new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command == "sendtoaddress")
|
||||||
|
{
|
||||||
|
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||||
|
var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
||||||
|
if (strategy == null)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Derivation strategy for {cryptoCode} is not set" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectDerivationStrategy directStrategy = GetDirectStrategy(strategy);
|
||||||
|
if (directStrategy == null)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"The feature does not work for multi-sig wallets" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundKeyPath = await GetKeyPath(ledger, network, directStrategy);
|
||||||
|
|
||||||
|
if (foundKeyPath == null)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"This store is not configured to use this ledger" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
BitcoinAddress destinationAddress = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
destinationAddress = BitcoinAddress.Create(destination.Trim());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Invalid destination address" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
Money amountBTC = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
amountBTC = Money.Parse(amount);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Invalid amount" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
if (amount <= Money.Zero)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "The amount should be above zero" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
FeeRate feeRateValue = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate)), 1);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Invalid fee rate" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feeRateValue.FeePerK <= Money.Zero)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "The fee rate should be above zero" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool substractFeeBool = bool.Parse(substractFees);
|
||||||
|
|
||||||
|
var wallet = _WalletProvider.GetWallet(network);
|
||||||
|
var unspentCoins = await wallet.GetUnspentCoins(strategy.DerivationStrategyBase);
|
||||||
|
|
||||||
|
TransactionBuilder builder = new TransactionBuilder();
|
||||||
|
builder.AddCoins(unspentCoins.Item1);
|
||||||
|
builder.Send(destinationAddress, amountBTC);
|
||||||
|
if (substractFeeBool)
|
||||||
|
builder.SubtractFees();
|
||||||
|
var change = await wallet.GetChangeAddressAsync(strategy.DerivationStrategyBase);
|
||||||
|
builder.SetChange(change.Item1);
|
||||||
|
builder.SendEstimatedFees(feeRateValue);
|
||||||
|
builder.Shuffle();
|
||||||
|
var unsigned = builder.BuildTransaction(false);
|
||||||
|
|
||||||
|
Dictionary<OutPoint, KeyPath> keyPaths = unspentCoins.Item2;
|
||||||
|
var hasChange = unsigned.Outputs.Count == 2;
|
||||||
|
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||||
|
ledgerTransport.Timeout = TimeSpan.FromMinutes(5);
|
||||||
|
var fullySigned = await ledger.SignTransactionAsync(
|
||||||
|
usedCoins.Select(c => new SignatureRequest
|
||||||
|
{
|
||||||
|
InputCoin = c,
|
||||||
|
KeyPath = foundKeyPath.Derive(keyPaths[c.Outpoint]),
|
||||||
|
PubKey = directStrategy.Root.Derive(keyPaths[c.Outpoint]).PubKey
|
||||||
|
}).ToArray(),
|
||||||
|
unsigned,
|
||||||
|
hasChange ? foundKeyPath.Derive(change.Item2) : null);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { fullySigned });
|
||||||
|
if (!result[0].Success)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"RPC Error while broadcasting: {result[0].RPCCode} {result[0].RPCCodeMessage} {result[0].RPCMessage}" });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Error while broadcasting: " + ex.Message });
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
await Send(webSocket, new SendToAddressResult() { TransactionId = fullySigned.GetHash().ToString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (LedgerWallet.LedgerWalletException ex)
|
||||||
|
{ try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = ex.Message }); } catch { } }
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{ try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = "timeout" }); } catch { } }
|
||||||
|
catch (Exception ex)
|
||||||
|
{ try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = ex.Message }); } catch { } }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await webSocket.CloseSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
||||||
|
{
|
||||||
|
KeyPath foundKeyPath = null;
|
||||||
|
foreach (var account in
|
||||||
|
new[] { new KeyPath("49'"), new KeyPath("44'") }
|
||||||
|
.Select(purpose => purpose.Derive(network.CoinType))
|
||||||
|
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var extpubkey = await GetExtPubKey(ledger, network, account);
|
||||||
|
if (directStrategy.ToString().Contains(extpubkey.ToString()))
|
||||||
|
{
|
||||||
|
foundKeyPath = account;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundKeyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account)
|
||||||
|
{
|
||||||
|
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 parent = (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress();
|
||||||
|
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, parent.Hash.ToBytes().Take(4).ToArray(), account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
||||||
|
return extpubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DirectDerivationStrategy GetDirectStrategy(DerivationStrategy strategy)
|
||||||
|
{
|
||||||
|
var directStrategy = strategy.DerivationStrategyBase as DirectDerivationStrategy;
|
||||||
|
if (directStrategy == null)
|
||||||
|
directStrategy = (strategy.DerivationStrategyBase as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||||
|
return directStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||||
|
private async Task Send(WebSocket webSocket, object result)
|
||||||
|
{
|
||||||
|
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
|
||||||
|
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> ListStores()
|
public async Task<IActionResult> ListStores()
|
||||||
{
|
{
|
||||||
@@ -197,6 +543,7 @@ namespace BTCPayServer.Controllers
|
|||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||||
|
vm.ServerUrl = GetStoreUrl(storeId);
|
||||||
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
@@ -206,6 +553,7 @@ namespace BTCPayServer.Controllers
|
|||||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string selectedScheme = null)
|
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string selectedScheme = null)
|
||||||
{
|
{
|
||||||
selectedScheme = selectedScheme ?? "BTC";
|
selectedScheme = selectedScheme ?? "BTC";
|
||||||
|
vm.ServerUrl = GetStoreUrl(storeId);
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|||||||
@@ -21,11 +21,26 @@ using BTCPayServer.Services.Wallets;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
namespace BTCPayServer
|
namespace BTCPayServer
|
||||||
{
|
{
|
||||||
public static class Extensions
|
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)
|
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||||
{
|
{
|
||||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
@@ -36,6 +37,8 @@ using System.Threading;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
||||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
namespace BTCPayServer.Hosting
|
namespace BTCPayServer.Hosting
|
||||||
{
|
{
|
||||||
@@ -88,6 +91,15 @@ namespace BTCPayServer.Hosting
|
|||||||
options.Password.RequireNonAlphanumeric = false;
|
options.Password.RequireNonAlphanumeric = false;
|
||||||
options.Password.RequireUppercase = 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
|
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
public SelectList CryptoCurrencies { get; set; }
|
public SelectList CryptoCurrencies { get; set; }
|
||||||
public SelectList DerivationSchemeFormats { get; set; }
|
public SelectList DerivationSchemeFormats { get; set; }
|
||||||
|
|
||||||
|
public string ServerUrl { get; set; }
|
||||||
|
|
||||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
||||||
{
|
{
|
||||||
|
|||||||
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;
|
IWebHost host = null;
|
||||||
var processor = new ConsoleLoggerProcessor();
|
var processor = new ConsoleLoggerProcessor();
|
||||||
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider(processor);
|
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider(processor);
|
||||||
|
|
||||||
var loggerFactory = new LoggerFactory();
|
var loggerFactory = new LoggerFactory();
|
||||||
loggerFactory.AddProvider(loggerProvider);
|
loggerFactory.AddProvider(loggerProvider);
|
||||||
var logger = loggerFactory.CreateLogger("Configuration");
|
var logger = loggerFactory.CreateLogger("Configuration");
|
||||||
|
|||||||
@@ -68,6 +68,20 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
return pathInfo.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork);
|
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)
|
public async Task TrackAsync(DerivationStrategyBase derivationStrategy)
|
||||||
{
|
{
|
||||||
await _Client.TrackAsync(derivationStrategy);
|
await _Client.TrackAsync(derivationStrategy);
|
||||||
@@ -97,27 +111,27 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task BroadcastTransactionsAsync(List<Transaction> transactions)
|
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
|
||||||
{
|
{
|
||||||
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
|
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
|
||||||
return Task.WhenAll(tasks);
|
return Task.WhenAll(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(Coin[], Dictionary<OutPoint, KeyPath>)> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var changes = await _Client.GetUTXOsAsync(derivationStrategy, null, false, cancellation).ConfigureAwait(false);
|
||||||
|
var keyPaths = new Dictionary<OutPoint, KeyPath>();
|
||||||
|
foreach (var coin in changes.GetUnspentUTXOs())
|
||||||
|
{
|
||||||
|
keyPaths.TryAdd(coin.Outpoint, coin.KeyPath);
|
||||||
|
}
|
||||||
|
return (changes.GetUnspentCoins(), keyPaths);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
|
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
|
||||||
{
|
{
|
||||||
var result = await _Client.GetUTXOsAsync(derivationStrategy, null, true);
|
var result = await _Client.GetUTXOsAsync(derivationStrategy, null, true);
|
||||||
|
return result.GetUnspentUTXOs().Select(c => c.Value).Sum();
|
||||||
Dictionary<OutPoint, UTXO> received = new Dictionary<OutPoint, UTXO>();
|
|
||||||
foreach(var utxo in result.Confirmed.UTXOs.Concat(result.Unconfirmed.UTXOs))
|
|
||||||
{
|
|
||||||
received.TryAdd(utxo.Outpoint, utxo);
|
|
||||||
}
|
|
||||||
foreach (var utxo in result.Confirmed.SpentOutpoints.Concat(result.Unconfirmed.SpentOutpoints))
|
|
||||||
{
|
|
||||||
received.Remove(utxo);
|
|
||||||
}
|
|
||||||
return received.Values.Select(c => c.Value).Sum();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-8">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@if(!Model.Confirmation)
|
@if(!Model.Confirmation)
|
||||||
{
|
{
|
||||||
@@ -27,8 +27,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<label asp-for="DerivationScheme"></label>
|
||||||
<input asp-for="DerivationScheme" class="form-control" />
|
<input asp-for="DerivationScheme" class="form-control" />
|
||||||
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
||||||
|
<p id="no-ledger-info" class="form-text text-muted">
|
||||||
|
If you own a ledger wallet use chrome, open the app activates 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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="DerivationSchemeFormat"></label>
|
<label asp-for="DerivationSchemeFormat"></label>
|
||||||
@@ -72,9 +79,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<button name="command" type="submit" class="btn btn-info">Continue</button>
|
<button name="command" type="submit" class="btn btn-info">Continue</button>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
|
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
|
||||||
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
|
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
|
||||||
@@ -102,11 +109,16 @@ else
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<button name="command" type="submit" class="btn btn-success">Confirm</button>
|
<button name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
@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 Tokens => "Tokens";
|
||||||
|
public static string Wallet => "Wallet";
|
||||||
|
|
||||||
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
|
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);
|
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 bytes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Substract fees from amount</label>
|
||||||
|
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@section Scripts
|
||||||
|
{
|
||||||
|
<script type="text/javascript">
|
||||||
|
@Model.ToJSVariableModel("srvModel")
|
||||||
|
</script>
|
||||||
|
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||||
|
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
|
||||||
|
}
|
||||||
@@ -4,5 +4,6 @@
|
|||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
|
<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.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
|
||||||
|
<li class="@StoreNavPages.WalletNavClass(ViewContext)"><a asp-action="Wallet">Wallet</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
67
BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js
Normal file
67
BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
$(function () {
|
||||||
|
var ledgerDetected = false;
|
||||||
|
var recommendedPubKey = "";
|
||||||
|
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
|
||||||
|
|
||||||
|
function WriteAlert(type, message) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write(prefix, type, message) {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#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", "block");
|
||||||
|
$("#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.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')
|
||||||
|
.catch(function (reason) { Write('hw', 'error', reason); })
|
||||||
|
.then(function (result) {
|
||||||
|
if (result.error) {
|
||||||
|
Write('hw', 'error', result.error);
|
||||||
|
} else {
|
||||||
|
Write('hw', 'success', 'Ledger detected');
|
||||||
|
ledgerDetected = true;
|
||||||
|
updateInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
121
BTCPayServer/wwwroot/js/StoreWallet.js
Normal file
121
BTCPayServer/wwwroot/js/StoreWallet.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
$(function () {
|
||||||
|
var ledgerDetected = false;
|
||||||
|
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
|
||||||
|
|
||||||
|
function WriteAlert(type, message) {
|
||||||
|
$(".alert").removeClass("alert-danger");
|
||||||
|
$(".alert").removeClass("alert-warning");
|
||||||
|
$(".alert").removeClass("alert-success");
|
||||||
|
$(".alert").addClass("alert-" + type);
|
||||||
|
$(".alert").css("display", "block");
|
||||||
|
$("#alertMessage").text(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write(prefix, type, message) {
|
||||||
|
|
||||||
|
$("#" + prefix + "-loading").css("display", "none");
|
||||||
|
$("#" + prefix + "-error").css("display", "none");
|
||||||
|
$("#" + prefix + "-success").css("display", "none");
|
||||||
|
|
||||||
|
$("#" + prefix+"-" + type).css("display", "block");
|
||||||
|
|
||||||
|
$("." + prefix +"-label").text(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#sendform").on("submit", function (elem) {
|
||||||
|
elem.preventDefault();
|
||||||
|
|
||||||
|
var args = "";
|
||||||
|
args += "cryptoCode=" + $("#cryptoCurrencies").val();
|
||||||
|
args += "&destination=" + $("#destination-textbox").val();
|
||||||
|
args += "&amount=" + $("#amount-textbox").val();
|
||||||
|
args += "&feeRate=" + $("#fee-textbox").val();
|
||||||
|
args += "&substractFees=" + $("#substract-checkbox").prop("checked");
|
||||||
|
|
||||||
|
WriteAlert("warning", 'Please validate the transaction on your ledger');
|
||||||
|
|
||||||
|
var confirmButton = $("#confirm-button");
|
||||||
|
confirmButton.prop("disabled", true);
|
||||||
|
confirmButton.addClass("disabled");
|
||||||
|
|
||||||
|
bridge.sendCommand('sendtoaddress', args, 60 * 5 /* timeout */)
|
||||||
|
.catch(function (reason) {
|
||||||
|
WriteAlert("danger", reason);
|
||||||
|
confirmButton.prop("disabled", false);
|
||||||
|
confirmButton.removeClass("disabled");
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
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.error) {
|
||||||
|
Write('check', 'error', result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write('check', 'success', 'This store is configured to use your ledger');
|
||||||
|
$(".crypto-info").css("display", "block");
|
||||||
|
$("#crypto-fee").text(result.recommendedSatoshiPerByte);
|
||||||
|
$("#crypto-balance").text(result.balance);
|
||||||
|
$("#crypto-code").text(cryptoCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
bridge.isSupported()
|
||||||
|
.then(function (supported) {
|
||||||
|
if (!supported) {
|
||||||
|
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bridge.sendCommand('test')
|
||||||
|
.catch(function (reason) { Write('hw', 'error', reason); })
|
||||||
|
.then(function (result) {
|
||||||
|
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