8000 PSBT support - separate the add inputs and sign and use psbt by dangershony · Pull Request #361 · block-core/angor · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

PSBT support - separate the add inputs and sign and use psbt #361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/Angor.Test/WalletOperationsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,12 @@ public void AddInputsAndSignTransaction()
var investorPrivateKey = _derivationOperations.DeriveInvestorPrivateKey(words, projectInfo.FounderKey);

var investmentTransaction = _investorTransactionActions.CreateInvestmentTransaction(projectInfo, investorKey, Money.Coins(investmentAmount).Satoshi);
var signedInvestmentTransaction = _sut.AddInputsAndSignTransaction(accountInfo.GetNextReceiveAddress(), investmentTransaction, words, accountInfo, 3000);

var signedInvestmentTransaction1 = _sut.AddInputsAndSignTransaction(accountInfo.GetNextReceiveAddress(), investmentTransaction, words, accountInfo, 3000);

var psbt = _sut.CreatePsbtForTransaction(investmentTransaction, accountInfo, 3000);
var signedInvestmentTransaction = _sut.SignPsbt(psbt, words);

var strippedInvestmentTransaction = network.CreateTransaction(signedInvestmentTransaction.Transaction.ToHex());
strippedInvestmentTransaction.Inputs.ForEach(f => f.WitScript = Blockcore.Consensus.TransactionInfo.WitScript.Empty);
Assert.Equal(signedInvestmentTransaction.Transaction.GetHash(), strippedInvestmentTransaction.GetHash());
Expand Down
36 changes: 28 additions & 8 deletions src/Angor/Client/Pages/Invest.razor
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
@inject IRelayService _RelayService;
@inject IWalletUIService _walletUIService
@inject IIndexerService _IndexerService

@inject IFeatureFlagService _featureFlagService
@inject INetworkService _networkService
@inject NostrConversionHelper NostrHelper
@inject IInvestorTransactionActions _InvestorTransactionActions;
Expand Down Expand Up @@ -814,7 +814,17 @@ else

unSignedTransaction = _InvestorTransactionActions.CreateInvestmentTransaction(project.ProjectInfo, InvestorPubKey, Money.Coins(Investment.InvestmentAmountBtc).Satoshi);

signedTransaction = _WalletOperations.AddInputsAndSignTransaction(accountBalanceInfo.AccountInfo.GetNextChangeReceiveAddress(), unSignedTransaction, words, accountBalanceInfo.AccountInfo, feeData.SelectedFeeEstimation.FeeRate);
if (_featureFlagService.IsFeatureHWSupportEnabled())
{
var psbt = _WalletOperations.CreatePsbtForTransaction(unSignedTransaction, accountBalanceInfo.AccountInfo, feeData.SelectedFeeEstimation.FeeRate);

signedTransaction = _WalletOperations.SignPsbt(psbt, words);
}
else
{
signedTransaction = _WalletOperations.AddInputsAndSignTransaction(accountBalanceInfo.AccountInfo.GetNextChangeReceiveAddress(),
unSignedTransaction, words, accountBalanceInfo.AccountInfo, feeData.SelectedFeeEstimation.FeeRate);
}

showCreateModal = true;
}
Expand Down Expand Up @@ -851,12 +861,22 @@ else
}

var accountInfo = storage.GetAccountInfo(network.Name);
signedTransaction = _WalletOperations.AddInputsAndSignTransaction(
accountInfo.GetNextChangeReceiveAddress(),
unSignedTransaction,
await passwordComponent.GetWalletAsync(),
accountInfo,
feeData.SelectedFeeEstimation.FeeRate);

if (_featureFlagService.IsFeatureHWSupportEnabled())
{
var psbt = _WalletOperations.CreatePsbtForTransaction(unSignedTransaction, accountInfo, feeData.SelectedFeeEstimation.FeeRate);

signedTransaction = _WalletOperations.SignPsbt(psbt, await passwordComponent.GetWalletAsync());
}
else
{
signedTransaction = _WalletOperations.AddInputsAndSignTransaction(
accountInfo.GetNextChangeReceiveAddress(),
unSignedTransaction,
await passwordComponent.GetWalletAsync(),
accountInfo,
feeData.SelectedFeeEstimation.FeeRate);
}

StateHasChanged();
}
Expand Down
4 changes: 4 additions & 0 deletions src/Angor/Shared/IWalletOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ namespace Angor.Shared;
public interface IWalletOperations
{
string GenerateWalletWords();

PsbtData CreatePsbtForTransaction(Transaction transaction, AccountInfo accountInfo, long feeRate, string? changeAddress = null);
TransactionInfo SignPsbt(PsbtData psbtData, WalletWords walletWords);

Task<OperationResult<Transaction>> SendAmountToAddress(WalletWords walletWords, SendInfo sendInfo);
AccountInfo BuildAccountInfoForWalletWords(WalletWords walletWords);
Task UpdateDataForExistingAddressesAsync(AccountInfo accountInfo);
Expand Down
1 change: 1 addition & 0 deletions src/Angor/Shared/Models/AccountInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Angor.Shared.Models;
public class AccountInfo
{
public string ExtPubKey { get; set; }
public string RootExtPubKey { get; set; }
public string Path { get; set; }
public int LastFetchIndex { get; set; }
public int LastFetchChangeIndex { get; set; }
Expand Down
8 changes: 8 additions & 0 deletions src/Angor/Shared/Models/PsbtData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Blockcore.Consensus.TransactionInfo;

namespace Angor.Shared.Models;

public class PsbtData
{
public string PsbtHex { get; set; }
}
84 changes: 83 additions & 1 deletion src/Angor/Shared/WalletOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,88 @@ public string GenerateWalletWords()
string walletWords = mnemonic.ToString();
return walletWords;
}


public PsbtData CreatePsbtForTransaction(Transaction transaction, AccountInfo accountInfo, long feeRate, string? changeAddress = null)
{
if (string.IsNullOrEmpty(accountInfo.RootExtPubKey))
{
throw new ApplicationException("The Root ExtPubKey is missing");
}

Network network = _networkConfiguration.GetNetwork();
var nbitcoinNetwork = NetworkMapper.Map(network);
6D40
changeAddress = changeAddress ?? accountInfo.GetNextChangeReceiveAddress();

var utxoDataWithPaths = FindOutputsForTransaction((long)transaction.Outputs.Sum(_ => _.Value), accountInfo);
var coins = utxoDataWithPaths.Select(u => new Coin(uint256.Parse(u.UtxoData.outpoint.transactionId), (uint)u.UtxoData.outpoint.outputIndex, Money.Satoshis(u.UtxoData.value), Script.FromHex(u.UtxoData.scriptHex))).ToList();

if (!coins.Any())
throw new ApplicationException("No coins found to fund the transaction.");

var builder = new TransactionBuilder(network)
.AddCoins(coins)
.SetChange(BitcoinAddress.Create(changeAddress, network))
.ContinueToBuild(transaction) // Add the predefined outputs
.SendEstimatedFees(new FeeRate(Money.Satoshis(feeRate)))
.CoverTheRest(); // Ensure enough input value covers outputs + fee, adjusting change if needed

var unsignedTx = builder.BuildTransaction(false);

var psbt = NBitcoin.PSBT.FromTransaction(NBitcoin.Transaction.Parse(unsignedTx.ToHex(), nbitcoinNetwork), nbitcoinNetwork);

NBitcoin.ExtPubKey accountExtPubKey = NBitcoin.ExtPubKey.Parse(accountInfo.RootExtPubKey, nbitcoinNetwork);

for (int i = 0; i < unsignedTx.Inputs.Count; i++)
{
var input = unsignedTx.Inputs[i];
var utxoInfo = utxoDataWithPaths.FirstOrDefault(u => u.UtxoData.outpoint.ToString() == input.PrevOut.ToString());

if (utxoInfo == null)
throw new InvalidOperationException($"Could not find UTXO information for input {input.PrevOut}");

psbt.Inputs[i].WitnessUtxo = new NBitcoin.TxOut(NBitcoin.Money.Satoshis(utxoInfo.UtxoData.value), NBitcoin.Script.FromHex(utxoInfo.UtxoData.scriptHex));

var keyPath = new NBitcoin.KeyPath(utxoInfo.HdPath);
var rootedKeyPath = new NBitcoin.RootedKeyPath(accountExtPubKey, keyPath);

var pubKey = _hdOperations.GeneratePublicKey(ExtPubKey.Parse(accountInfo.ExtPubKey, network), (int)keyPath.Indexes[4], keyPath.Indexes[3] == 1);
var path = _hdOperations.CreateHdPath(Purpose, network.Consensus.CoinType, AccountIndex, keyPath.Indexes[3] == 1, (int)keyPath.Indexes[4]);

if (path != utxoInfo.HdPath)
throw new InvalidOperationException($"Path does not match {path} {utxoInfo.HdPath}");

psbt.Inputs[i].HDKeyPaths.Add(new NBitcoin.PubKey(pubKey.ToBytes()), rootedKeyPath);
}

return new PsbtData { PsbtHex = psbt.ToHex() };
}

public TransactionInfo SignPsbt(PsbtData psbtData, WalletWords walletWords)
{
Network network = _networkConfiguration.GetNetwork();
var nbitcoinNetwork = NetworkMapper.Map(network);

var psbt = NBitcoin.PSBT.Parse(psbtData.PsbtHex, nbitcoinNetwork);

ExtKey extendedKey = _hdOperations.GetExtendedKey(walletWords.Words, walletWords.Passphrase);

var nbitcoinExtendedKey = NBitcoin.ExtKey.CreateFromBytes(extendedKey.ToBytes(network.Consensus.ConsensusFactory));

psbt.SignAll(NBitcoin.ScriptPubKeyType.Segwit, nbitcoinExtendedKey);

if (!psbt.TryFinalize(out IList<NBitcoin.PSBTError>? errors))
{
throw new NBitcoin.PSBTException(errors);
}

NBitcoin.Transaction signedTransaction = psbt.ExtractTransaction();

NBitcoin.Money fee = psbt.GetFee();

return new TransactionInfo { Transaction = network.CreateTransaction(signedTransaction.ToHex()), TransactionFee = fee.Satoshi };
}

public TransactionInfo AddInputsAndSignTransaction(string changeAddress, Transaction transaction,
WalletWords walletWords, AccountInfo accountInfo, long feeRate)
{
Expand Down Expand Up @@ -329,6 +410,7 @@ public AccountInfo BuildAccountInfoForWalletWords(WalletWords walletWords)
ExtPubKey accountExtPubKeyTostore =
_hdOperations.GetExtendedPublicKey(privateKey, extendedKey.ChainCode, accountHdPath);

accountInfo.RootExtPubKey = extendedKey.Neuter().ToString(network);
accountInfo.ExtPubKey = accountExtPubKeyTostore.ToString(network);
accountInfo.Path = accountHdPath;

Expand Down
0