mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Allow signing on non segwit transactions via the ledger
This commit is contained in:
@@ -10,6 +10,7 @@ using BTCPayServer.Data;
|
|||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using LedgerWallet;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
@@ -264,7 +265,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
var strategy = GetDirectDerivationStrategy(store, network);
|
var strategy = GetDirectDerivationStrategy(store, network);
|
||||||
var strategyBase = GetDerivationStrategy(store, network);
|
var strategyBase = GetDerivationStrategy(store, network);
|
||||||
if (strategy == null || !await hw.SupportDerivation(network, strategy))
|
if (strategy == null || await hw.GetKeyPath(network, strategy) == null)
|
||||||
{
|
{
|
||||||
throw new Exception($"This store is not configured to use this ledger");
|
throw new Exception($"This store is not configured to use this ledger");
|
||||||
}
|
}
|
||||||
@@ -286,11 +287,76 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||||
var changeAddress = await change;
|
var changeAddress = await change;
|
||||||
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
|
var send = new[] { (
|
||||||
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
|
destination: destinationAddress as IDestination,
|
||||||
feeRateValue,
|
amount: amountBTC,
|
||||||
changeAddress.Item1,
|
substractFees: subsctractFeesValue) };
|
||||||
changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee);
|
|
||||||
|
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 hw.GetKeyPath(network, strategy);
|
||||||
|
if (foundKeyPath == null)
|
||||||
|
{
|
||||||
|
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionBuilder builder = new TransactionBuilder();
|
||||||
|
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||||
|
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||||
|
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||||
|
|
||||||
|
foreach (var element in send)
|
||||||
|
{
|
||||||
|
builder.Send(element.destination, element.amount);
|
||||||
|
if (element.substractFees)
|
||||||
|
builder.SubtractFees();
|
||||||
|
}
|
||||||
|
builder.SetChange(changeAddress.Item1);
|
||||||
|
builder.SendEstimatedFees(feeRateValue);
|
||||||
|
builder.Shuffle();
|
||||||
|
var unsigned = builder.BuildTransaction(false);
|
||||||
|
|
||||||
|
var keypaths = new Dictionary<Script, KeyPath>();
|
||||||
|
foreach (var c in unspentCoins)
|
||||||
|
{
|
||||||
|
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasChange = unsigned.Outputs.Count == 2;
|
||||||
|
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||||
|
|
||||||
|
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||||
|
|
||||||
|
if(!strategy.Segwit)
|
||||||
|
{
|
||||||
|
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||||
|
var explorer = _ExplorerProvider.GetExplorerClient(network);
|
||||||
|
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||||
|
foreach(var getTransactionAsync in getTransactionAsyncs)
|
||||||
|
{
|
||||||
|
var tx = (await getTransactionAsync.Op);
|
||||||
|
if(tx == null)
|
||||||
|
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||||
|
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
|
||||||
|
{
|
||||||
|
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
|
||||||
|
InputCoin = c,
|
||||||
|
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||||
|
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||||
|
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||||
@@ -336,8 +402,6 @@ namespace BTCPayServer.Controllers
|
|||||||
var directStrategy = strategy as DirectDerivationStrategy;
|
var directStrategy = strategy as DirectDerivationStrategy;
|
||||||
if (directStrategy == null)
|
if (directStrategy == null)
|
||||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||||
if (!directStrategy.Segwit)
|
|
||||||
return null;
|
|
||||||
return directStrategy;
|
return directStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,18 +118,7 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy)
|
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
||||||
{
|
|
||||||
if (network == null)
|
|
||||||
throw new ArgumentNullException(nameof(network));
|
|
||||||
if (strategy == null)
|
|
||||||
throw new ArgumentNullException(nameof(strategy));
|
|
||||||
if (!strategy.Segwit)
|
|
||||||
return false;
|
|
||||||
return await GetKeyPath(_Ledger, network, strategy) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
|
||||||
{
|
{
|
||||||
List<KeyPath> derivations = new List<KeyPath>();
|
List<KeyPath> derivations = new List<KeyPath>();
|
||||||
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
|
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||||
@@ -143,7 +132,7 @@ namespace BTCPayServer.Services
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var extpubkey = await GetExtPubKey(ledger, network, account, true);
|
var extpubkey = await GetExtPubKey(_Ledger, network, account, true);
|
||||||
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
|
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
|
||||||
{
|
{
|
||||||
foundKeyPath = account;
|
foundKeyPath = account;
|
||||||
@@ -159,79 +148,12 @@ namespace BTCPayServer.Services
|
|||||||
return foundKeyPath;
|
return foundKeyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy,
|
public async Task<Transaction> SignTransactionAsync(SignatureRequest[] signatureRequests,
|
||||||
ReceivedCoin[] coins, BTCPayNetwork network,
|
Transaction unsigned,
|
||||||
(IDestination destination, Money amount, bool substractFees)[] send,
|
KeyPath changeKeyPath)
|
||||||
FeeRate feeRate,
|
|
||||||
IDestination changeAddress,
|
|
||||||
KeyPath changeKeyPath,
|
|
||||||
FeeRate minTxRelayFee)
|
|
||||||
{
|
{
|
||||||
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 (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.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
|
|
||||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
|
||||||
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
|
|
||||||
|
|
||||||
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 keypaths = new Dictionary<Script, KeyPath>();
|
|
||||||
foreach(var c in coins)
|
|
||||||
{
|
|
||||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasChange = unsigned.Outputs.Count == 2;
|
|
||||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
|
||||||
_Transport.Timeout = TimeSpan.FromMinutes(5);
|
_Transport.Timeout = TimeSpan.FromMinutes(5);
|
||||||
var fullySigned = await Ledger.SignTransactionAsync(
|
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath);
|
||||||
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(changeKeyPath) : null);
|
|
||||||
return fullySigned;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user