mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Decouple HardwareWalletService into two classes: LedgerHardwareWalletService and HardwareWalletService
This commit is contained in:
@@ -557,22 +557,18 @@ namespace BTCPayServer.Controllers
|
|||||||
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var strategy = GetDirectDerivationStrategy(derivationSettings.AccountDerivation);
|
|
||||||
|
|
||||||
// Some deployment does not have the AccountKeyPath set, let's fix this...
|
// Some deployment does not have the AccountKeyPath set, let's fix this...
|
||||||
if (derivationSettings.AccountKeyPath == null)
|
if (derivationSettings.AccountKeyPath == null)
|
||||||
{
|
{
|
||||||
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||||
var foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
|
var foundKeyPath = await hw.FindKeyPathFromPubkeys(network,
|
||||||
if (foundKeyPath == null)
|
derivationSettings.AccountDerivation.GetExtPubKeys().Select(p => p.GetPublicKey()).ToArray(),
|
||||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
normalOperationTimeout.Token);
|
||||||
derivationSettings.AccountKeyPath = foundKeyPath;
|
derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||||
await Repository.UpdateStore(storeData);
|
await Repository.UpdateStore(storeData);
|
||||||
}
|
}
|
||||||
// If it has the AccountKeyPath, let's check if we opened the right ledger
|
// If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
|
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
|
||||||
@@ -587,8 +583,8 @@ namespace BTCPayServer.Controllers
|
|||||||
// We have the root fingerprint, we can check the root from it
|
// We have the root fingerprint, we can check the root from it
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var actualPubKey = await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token);
|
var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
|
||||||
if (actualPubKey.GetPublicKey().GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
|
if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
|
||||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,7 +592,7 @@ namespace BTCPayServer.Controllers
|
|||||||
// Some deployment does not have the RootFingerprint set, let's fix this...
|
// Some deployment does not have the RootFingerprint set, let's fix this...
|
||||||
if (derivationSettings.RootFingerprint == null)
|
if (derivationSettings.RootFingerprint == null)
|
||||||
{
|
{
|
||||||
derivationSettings.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetPublicKey().GetHDFingerPrint();
|
derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
|
||||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||||
await Repository.UpdateStore(storeData);
|
await Repository.UpdateStore(storeData);
|
||||||
}
|
}
|
||||||
@@ -648,16 +644,6 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
return new EmptyResult();
|
return new EmptyResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
|
|
||||||
{
|
|
||||||
if (strategy == null)
|
|
||||||
throw new Exception("The derivation scheme is not provided");
|
|
||||||
var directStrategy = strategy as DirectDerivationStrategy;
|
|
||||||
if (directStrategy == null)
|
|
||||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
|
||||||
return directStrategy;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,189 +20,46 @@ namespace BTCPayServer.Services
|
|||||||
public HardwareWalletException(string message) : base(message) { }
|
public HardwareWalletException(string message) : base(message) { }
|
||||||
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
|
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
|
||||||
}
|
}
|
||||||
public class HardwareWalletService : IDisposable
|
public abstract class HardwareWalletService : IDisposable
|
||||||
{
|
{
|
||||||
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable
|
public abstract string Device { get; }
|
||||||
|
public abstract Task<LedgerTestResult> Test(CancellationToken cancellation);
|
||||||
|
|
||||||
|
public abstract Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation);
|
||||||
|
public virtual async Task<PubKey> GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
private readonly WebSocket webSocket;
|
return (await GetExtPubKey(network, keyPath, cancellation)).GetPublicKey();
|
||||||
|
|
||||||
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
|
|
||||||
{
|
|
||||||
if (webSocket == null)
|
|
||||||
throw new ArgumentNullException(nameof(webSocket));
|
|
||||||
this.webSocket = webSocket;
|
|
||||||
}
|
|
||||||
|
|
||||||
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
|
|
||||||
public async Task<byte[][]> Exchange(byte[][] apdus, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await _Semaphore.WaitAsync();
|
|
||||||
List<byte[]> responses = new List<byte[]>();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var apdu in apdus)
|
|
||||||
{
|
|
||||||
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cancellationToken);
|
|
||||||
}
|
|
||||||
foreach (var apdu in apdus)
|
|
||||||
{
|
|
||||||
byte[] response = new byte[300];
|
|
||||||
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cancellationToken);
|
|
||||||
Array.Resize(ref response, result.Count);
|
|
||||||
responses.Add(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_Semaphore.Release();
|
|
||||||
}
|
|
||||||
return responses.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_Semaphore.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly LedgerClient _Ledger;
|
public async Task<KeyPath> FindKeyPathFromPubkeys(BTCPayNetwork network, PubKey[] pubKeys, CancellationToken cancellation)
|
||||||
public LedgerClient Ledger
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _Ledger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Device => "Ledger wallet";
|
|
||||||
|
|
||||||
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);
|
|
||||||
_Ledger.MaxAPDUSize = 90;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<LedgerTestResult> Test(CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
var version = await Ledger.GetFirmwareVersionAsync(cancellation);
|
|
||||||
return new LedgerTestResult() { Success = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
if (network == null)
|
|
||||||
throw new ArgumentNullException(nameof(network));
|
|
||||||
return await GetExtPubKey(Ledger, network, keyPath, false, cancellation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var pubKey = await ledger.GetWalletPubKeyAsync(account, cancellation: cancellation);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
pubKey.GetAddress(network.NBitcoinNetwork);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
|
|
||||||
throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
|
|
||||||
}
|
|
||||||
var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint();
|
|
||||||
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(),
|
|
||||||
pubKey.ChainCode,
|
|
||||||
(byte)account.Indexes.Length,
|
|
||||||
parentFP,
|
|
||||||
account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
|
||||||
return extpubkey;
|
|
||||||
}
|
|
||||||
catch (FormatException)
|
|
||||||
{
|
|
||||||
throw new HardwareWalletException("Unsupported ledger app");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<KeyPath> FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
|
|
||||||
{
|
{
|
||||||
List<KeyPath> derivations = new List<KeyPath>();
|
List<KeyPath> derivations = new List<KeyPath>();
|
||||||
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
|
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||||
derivations.Add(new KeyPath("49'"));
|
derivations.Add(new KeyPath("49'"));
|
||||||
derivations.Add(new KeyPath("44'"));
|
derivations.Add(new KeyPath("44'"));
|
||||||
|
derivations.Add(new KeyPath("84'"));
|
||||||
KeyPath foundKeyPath = null;
|
KeyPath foundKeyPath = null;
|
||||||
foreach (var account in
|
foreach (var account in
|
||||||
derivations
|
derivations
|
||||||
.Select(purpose => purpose.Derive(network.CoinType))
|
.Select(purpose => purpose.Derive(network.CoinType))
|
||||||
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
|
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
|
||||||
{
|
{
|
||||||
try
|
var pubkey = await GetPubKey(network, account, cancellation);
|
||||||
|
if (pubKeys.Contains(pubkey))
|
||||||
{
|
{
|
||||||
var extpubkey = await GetExtPubKey(Ledger, network, account, true, cancellation);
|
foundKeyPath = account;
|
||||||
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
|
break;
|
||||||
{
|
|
||||||
foundKeyPath = account;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FormatException)
|
|
||||||
{
|
|
||||||
throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return foundKeyPath;
|
return foundKeyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint,
|
public abstract Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var unsigned = psbt.GetGlobalTransaction();
|
|
||||||
var changeKeyPath = psbt.Outputs
|
|
||||||
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
|
|
||||||
.Where(o => o.HDKeyPaths.Any())
|
|
||||||
.Select(o => o.HDKeyPaths.First().Value.Item2)
|
|
||||||
.FirstOrDefault();
|
|
||||||
var signatureRequests = psbt
|
|
||||||
.Inputs
|
|
||||||
.Where(o => o.HDKeyPaths.Any())
|
|
||||||
.Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
|
|
||||||
.Select(i => new SignatureRequest()
|
|
||||||
{
|
|
||||||
InputCoin = i.GetSignableCoin(),
|
|
||||||
InputTransaction = i.NonWitnessUtxo,
|
|
||||||
KeyPath = i.HDKeyPaths.First().Value.Item2,
|
|
||||||
PubKey = i.HDKeyPaths.First().Key
|
|
||||||
}).ToArray();
|
|
||||||
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
|
||||||
if (signedTransaction == null)
|
|
||||||
throw new Exception("The ledger failed to sign the transaction");
|
|
||||||
|
|
||||||
psbt = psbt.Clone();
|
public virtual void Dispose()
|
||||||
foreach (var signature in signatureRequests)
|
|
||||||
{
|
|
||||||
var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
|
|
||||||
if (input == null)
|
|
||||||
continue;
|
|
||||||
input.PartialSigs.Add(signature.PubKey, signature.Signature);
|
|
||||||
}
|
|
||||||
return psbt;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception("The ledger failed to sign the transaction", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
{
|
||||||
if (_Transport != null)
|
|
||||||
_Transport.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
158
BTCPayServer/Services/LedgerHardwareWalletService.cs
Normal file
158
BTCPayServer/Services/LedgerHardwareWalletService.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LedgerWallet;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services
|
||||||
|
{
|
||||||
|
public class LedgerHardwareWalletService : HardwareWalletService
|
||||||
|
{
|
||||||
|
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable
|
||||||
|
{
|
||||||
|
private readonly WebSocket webSocket;
|
||||||
|
|
||||||
|
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
|
||||||
|
{
|
||||||
|
if (webSocket == null)
|
||||||
|
throw new ArgumentNullException(nameof(webSocket));
|
||||||
|
this.webSocket = webSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
|
||||||
|
public async Task<byte[][]> Exchange(byte[][] apdus, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _Semaphore.WaitAsync();
|
||||||
|
List<byte[]> responses = new List<byte[]>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var apdu in apdus)
|
||||||
|
{
|
||||||
|
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cancellationToken);
|
||||||
|
}
|
||||||
|
foreach (var apdu in apdus)
|
||||||
|
{
|
||||||
|
byte[] response = new byte[300];
|
||||||
|
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cancellationToken);
|
||||||
|
Array.Resize(ref response, result.Count);
|
||||||
|
responses.Add(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_Semaphore.Release();
|
||||||
|
}
|
||||||
|
return responses.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_Semaphore.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly LedgerClient _Ledger;
|
||||||
|
public LedgerClient Ledger
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _Ledger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Device => "Ledger wallet";
|
||||||
|
|
||||||
|
WebSocketTransport _Transport = null;
|
||||||
|
public LedgerHardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
|
||||||
|
{
|
||||||
|
if (ledgerWallet == null)
|
||||||
|
throw new ArgumentNullException(nameof(ledgerWallet));
|
||||||
|
_Transport = new WebSocketTransport(ledgerWallet);
|
||||||
|
_Ledger = new LedgerClient(_Transport);
|
||||||
|
_Ledger.MaxAPDUSize = 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<LedgerTestResult> Test(CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
var version = await Ledger.GetFirmwareVersionAsync(cancellation);
|
||||||
|
return new LedgerTestResult() { Success = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
if (network == null)
|
||||||
|
throw new ArgumentNullException(nameof(network));
|
||||||
|
return await GetExtPubKey(network, keyPath, false, cancellation);
|
||||||
|
}
|
||||||
|
public override async Task<PubKey> GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
if (network == null)
|
||||||
|
throw new ArgumentNullException(nameof(network));
|
||||||
|
return (await GetExtPubKey(network, keyPath, false, cancellation)).GetPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
var pubKey = await Ledger.GetWalletPubKeyAsync(account, cancellation: cancellation);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pubKey.GetAddress(network.NBitcoinNetwork);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
|
||||||
|
throw new HardwareWalletException($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
|
||||||
|
}
|
||||||
|
var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await Ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint();
|
||||||
|
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(),
|
||||||
|
pubKey.ChainCode,
|
||||||
|
(byte)account.Indexes.Length,
|
||||||
|
parentFP,
|
||||||
|
account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
||||||
|
return extpubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var unsigned = psbt.GetGlobalTransaction();
|
||||||
|
var changeKeyPath = psbt.Outputs
|
||||||
|
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
|
||||||
|
.Where(o => o.HDKeyPaths.Any())
|
||||||
|
.Select(o => o.HDKeyPaths.First().Value.Item2)
|
||||||
|
.FirstOrDefault();
|
||||||
|
var signatureRequests = psbt
|
||||||
|
.Inputs
|
||||||
|
.Where(o => o.HDKeyPaths.Any())
|
||||||
|
.Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
|
||||||
|
.Select(i => new SignatureRequest()
|
||||||
|
{
|
||||||
|
InputCoin = i.GetSignableCoin(),
|
||||||
|
InputTransaction = i.NonWitnessUtxo,
|
||||||
|
KeyPath = i.HDKeyPaths.First().Value.Item2,
|
||||||
|
PubKey = i.HDKeyPaths.First().Key
|
||||||
|
}).ToArray();
|
||||||
|
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||||
|
if (signedTransaction == null)
|
||||||
|
throw new HardwareWalletException("The ledger failed to sign the transaction");
|
||||||
|
|
||||||
|
psbt = psbt.Clone();
|
||||||
|
foreach (var signature in signatureRequests)
|
||||||
|
{
|
||||||
|
var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
|
||||||
|
if (input == null)
|
||||||
|
continue;
|
||||||
|
input.PartialSigs.Add(signature.PubKey, signature.Signature);
|
||||||
|
}
|
||||||
|
return psbt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
if (_Transport != null)
|
||||||
|
_Transport.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user