Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
c17793aca9 | |||
01d898b618 | |||
17069c311b | |||
921d072942 | |||
6181e8b3e4 | |||
93fc12bb2e | |||
8e73c1a2f0 | |||
e97c15578d | |||
fd4f4e6aff | |||
cedf8f75e8 |
@ -37,7 +37,7 @@ services:
|
||||
- postgres
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.1.12
|
||||
image: nicolasdorier/nbxplorer:1.0.1.13
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.22</Version>
|
||||
<Version>1.0.1.28</Version>
|
||||
<NoWarn>NU1701</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
@ -120,61 +120,17 @@ namespace BTCPayServer.Controllers
|
||||
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 class GetInfoResult
|
||||
{
|
||||
public int RecommendedSatoshiPerByte { get; set; }
|
||||
public double Balance { get; set; }
|
||||
}
|
||||
|
||||
class SendToAddressResult
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
}
|
||||
|
||||
class GetXPubResult
|
||||
{
|
||||
public string ExtPubKey { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/ws/ledger")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
@ -193,185 +149,131 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var ledgerTransport = new WebSocketTransport(webSocket);
|
||||
var ledger = new LedgerWallet.LedgerClient(ledgerTransport);
|
||||
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
object result = null;
|
||||
try
|
||||
{
|
||||
if (command == "test")
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
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 });
|
||||
network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
BitcoinAddress destinationAddress = null;
|
||||
if (destination != null)
|
||||
{
|
||||
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();
|
||||
destinationAddress = BitcoinAddress.Create(destination);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
throw new FormatException("Invalid value for destination");
|
||||
}
|
||||
|
||||
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;
|
||||
FeeRate feeRateValue = null;
|
||||
if (feeRate != 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();
|
||||
}
|
||||
catch { }
|
||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
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);
|
||||
Money amountBTC = null;
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { fullySigned });
|
||||
if (!result[0].Success)
|
||||
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)
|
||||
{
|
||||
await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"RPC Error while broadcasting: {result[0].RPCCode} {result[0].RPCCodeMessage} {result[0].RPCMessage}" });
|
||||
return new EmptyResult();
|
||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Error while broadcasting: " + ex.Message });
|
||||
return new EmptyResult();
|
||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
await Send(webSocket, new SendToAddressResult() { TransactionId = fullySigned.GetHash().ToString() });
|
||||
result = new SendToAddressResult() { TransactionId = transaction.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 { } }
|
||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||
catch (Exception ex)
|
||||
{ try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = ex.Message }); } catch { } }
|
||||
{ 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();
|
||||
@ -380,58 +282,26 @@ namespace BTCPayServer.Controllers
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
||||
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
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;
|
||||
var strategy = GetDerivationStrategy(store, network);
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy.DerivationStrategyBase as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
if (!directStrategy.Segwit)
|
||||
return null;
|
||||
return directStrategy;
|
||||
}
|
||||
|
||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||
private async Task Send(WebSocket webSocket, object result)
|
||||
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
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]
|
||||
@ -442,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(_ => _));
|
||||
@ -463,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)
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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>();
|
||||
|
@ -95,7 +95,7 @@ namespace BTCPayServer.Hosting
|
||||
// Needed to debug U2F for ledger support
|
||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
||||
//{
|
||||
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
|
||||
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
|
||||
// {
|
||||
// l.UseHttps("devtest.pfx", "toto");
|
||||
// });
|
||||
|
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,7 @@ using BTCPayServer.Data;
|
||||
using System.Threading;
|
||||
using NBXplorer.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.Services.Wallets
|
||||
{
|
||||
@ -25,21 +27,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 +55,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)
|
||||
{
|
||||
@ -91,47 +94,75 @@ 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());
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return 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;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
|
||||
{
|
||||
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async Task<(Coin[], Dictionary<OutPoint, KeyPath>)> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
|
||||
|
||||
|
||||
public async Task<(Coin[], Dictionary<Script, 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>();
|
||||
var changes = await GetUTXOChanges(derivationStrategy, cancellation);
|
||||
var keyPaths = new Dictionary<Script, KeyPath>();
|
||||
foreach (var coin in changes.GetUnspentUTXOs())
|
||||
{
|
||||
keyPaths.TryAdd(coin.Outpoint, coin.KeyPath);
|
||||
keyPaths.TryAdd(coin.ScriptPubKey, coin.KeyPath);
|
||||
}
|
||||
return (changes.GetUnspentCoins(), keyPaths);
|
||||
}
|
||||
|
||||
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
|
||||
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var result = await _Client.GetUTXOsAsync(derivationStrategy, null, true);
|
||||
return result.GetUnspentUTXOs().Select(c => c.Value).Sum();
|
||||
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)
|
||||
|
@ -16,100 +16,101 @@
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
@if(!Model.Confirmation)
|
||||
{
|
||||
<div class="form-group">
|
||||
<h5>Derivation Scheme</h5>
|
||||
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CryptoCurrency"></label>
|
||||
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
</div>
|
||||
{
|
||||
<div class="form-group">
|
||||
<h5>Derivation Scheme</h5>
|
||||
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CryptoCurrency"></label>
|
||||
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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">
|
||||
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 class="form-group">
|
||||
<label asp-for="DerivationSchemeFormat"></label>
|
||||
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span>BTCPay format memo</span>
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Address type</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P2WPKH</td>
|
||||
<td>xpub</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2SH-P2WPKH</td>
|
||||
<td>xpub-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2PKH</td>
|
||||
<td>xpub-[legacy]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2WSH</td>
|
||||
<td>2-of-xpub1-xpub2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2SH-P2WSH</td>
|
||||
<td>2-of-xpub1-xpub2-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2SH</td>
|
||||
<td>2-of-xpub1-xpub2-[legacy]</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-info">Continue</button>
|
||||
}
|
||||
else
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationScheme"></label>
|
||||
<input asp-for="DerivationScheme" class="form-control" />
|
||||
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
||||
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
|
||||
No ledger wallet detected. If you own one, use chrome, open the app, activate the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a>, and refresh this page.
|
||||
</p>
|
||||
<p id="ledger-info" class="form-text text-muted" style="display: none;">
|
||||
<span>A ledger wallet is detected, please use our <a id="ledger-info-recommended" href="#">recommended choice</a></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationSchemeFormat"></label>
|
||||
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span>BTCPay format memo</span>
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Address type</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P2WPKH</td>
|
||||
<td>xpub</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2SH-P2WPKH</td>
|
||||
<td>xpub-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2PKH</td>
|
||||
<td>xpub-[legacy]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2WSH</td>
|
||||
<td>2-of-xpub1-xpub2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2SH-P2WSH</td>
|
||||
<td>2-of-xpub1-xpub2-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2SH</td>
|
||||
<td>2-of-xpub1-xpub2-[legacy]</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-info">Continue</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-group">
|
||||
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
|
||||
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
|
||||
</div>
|
||||
<input type="hidden" asp-for="CryptoCurrency" />
|
||||
<input type="hidden" asp-for="Confirmation" />
|
||||
<input type="hidden" asp-for="DerivationScheme" />
|
||||
<input type="hidden" asp-for="DerivationSchemeFormat" />
|
||||
<div class="form-group">
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Key path</th>
|
||||
<th>Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var sample in Model.AddressSamples)
|
||||
{
|
||||
<div class="form-group">
|
||||
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
|
||||
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
|
||||
</div>
|
||||
<input type="hidden" asp-for="CryptoCurrency" />
|
||||
<input type="hidden" asp-for="Confirmation" />
|
||||
<input type="hidden" asp-for="DerivationScheme" />
|
||||
<div class="form-group">
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Key path</th>
|
||||
<th>Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var sample in Model.AddressSamples)
|
||||
{
|
||||
<tr>
|
||||
<td>@sample.KeyPath</td>
|
||||
<td>@sample.Address</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||
}
|
||||
<tr>
|
||||
<td>@sample.KeyPath</td>
|
||||
<td>@sample.Address</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,7 +54,7 @@
|
||||
<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.
|
||||
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">
|
||||
|
@ -8,8 +8,10 @@
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -20,7 +22,7 @@
|
||||
});
|
||||
|
||||
$("#CryptoCurrency").on("change", function (elem) {
|
||||
$("#no-ledger-info").css("display", "block");
|
||||
$("#no-ledger-info").css("display", "none");
|
||||
$("#ledger-info").css("display", "none");
|
||||
updateInfo();
|
||||
});
|
||||
@ -32,6 +34,8 @@
|
||||
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;
|
||||
@ -51,9 +55,15 @@
|
||||
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
|
||||
}
|
||||
else {
|
||||
bridge.sendCommand('test')
|
||||
.catch(function (reason) { Write('hw', 'error', reason); })
|
||||
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 {
|
||||
|
@ -1,6 +1,8 @@
|
||||
$(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");
|
||||
@ -25,6 +27,15 @@
|
||||
$("#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();
|
||||
@ -45,6 +56,8 @@
|
||||
confirmButton.removeClass("disabled");
|
||||
})
|
||||
.then(function (result) {
|
||||
if (!result)
|
||||
return;
|
||||
confirmButton.prop("disabled", false);
|
||||
confirmButton.removeClass("disabled");
|
||||
if (result.error) {
|
||||
@ -84,6 +97,8 @@
|
||||
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;
|
||||
@ -91,6 +106,8 @@
|
||||
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);
|
||||
@ -104,9 +121,16 @@
|
||||
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
|
||||
}
|
||||
else {
|
||||
bridge.sendCommand('test')
|
||||
.catch(function (reason) { Write('hw', 'error', reason); })
|
||||
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 {
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user