mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-22 14:22:37 +01:00
walletrpc: refactor to prepare for third funding option
In the following commits we'll add a new, third, funding option for funding PSBTs: It will allow users to specify pre-selected inputs but still request the wallet to perform coin selection up to the total output sum amount.
This commit is contained in:
parent
17645cd196
commit
446152e1a5
2 changed files with 113 additions and 79 deletions
|
@ -8,7 +8,6 @@ import (
|
|||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
base "github.com/btcsuite/btcwallet/wallet"
|
||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
||||
|
@ -49,16 +48,16 @@ func verifyInputsUnspent(inputs []*wire.TxIn, utxos []*lnwallet.Utxo) error {
|
|||
// lockInputs requests a lock lease for all inputs specified in a PSBT packet
|
||||
// by using the internal, static lock ID of lnd's wallet.
|
||||
func lockInputs(w lnwallet.WalletController,
|
||||
packet *psbt.Packet) ([]*base.ListLeasedOutputResult, error) {
|
||||
outpoints []wire.OutPoint) ([]*base.ListLeasedOutputResult, error) {
|
||||
|
||||
locks := make(
|
||||
[]*base.ListLeasedOutputResult, len(packet.UnsignedTx.TxIn),
|
||||
[]*base.ListLeasedOutputResult, len(outpoints),
|
||||
)
|
||||
for idx, rawInput := range packet.UnsignedTx.TxIn {
|
||||
for idx := range outpoints {
|
||||
lock := &base.ListLeasedOutputResult{
|
||||
LockedOutput: &wtxmgr.LockedOutput{
|
||||
LockID: LndInternalLockID,
|
||||
Outpoint: rawInput.PreviousOutPoint,
|
||||
Outpoint: outpoints[idx],
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1175,12 +1175,54 @@ func (w *WalletKit) FundPsbt(_ context.Context,
|
|||
|
||||
var (
|
||||
err error
|
||||
packet *psbt.Packet
|
||||
feeSatPerKW chainfee.SatPerKWeight
|
||||
locks []*base.ListLeasedOutputResult
|
||||
rawPsbt bytes.Buffer
|
||||
)
|
||||
|
||||
// Determine the desired transaction fee.
|
||||
switch {
|
||||
// Estimate the fee by the target number of blocks to confirmation.
|
||||
case req.GetTargetConf() != 0:
|
||||
targetConf := req.GetTargetConf()
|
||||
if targetConf < 2 {
|
||||
return nil, fmt.Errorf("confirmation target must be " +
|
||||
"greater than 1")
|
||||
}
|
||||
|
||||
feeSatPerKW, err = w.cfg.FeeEstimator.EstimateFeePerKW(
|
||||
targetConf,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not estimate fee: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
// Convert the fee to sat/kW from the specified sat/vByte.
|
||||
case req.GetSatPerVbyte() != 0:
|
||||
feeSatPerKW = chainfee.SatPerKVByte(
|
||||
req.GetSatPerVbyte() * 1000,
|
||||
).FeePerKWeight()
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("fee definition missing, need to " +
|
||||
"specify either target_conf or sat_per_vbyte")
|
||||
}
|
||||
|
||||
// Then, we'll extract the minimum number of confirmations that each
|
||||
// output we use to fund the transaction should satisfy.
|
||||
minConfs, err := lnrpc.ExtractMinConfs(
|
||||
req.GetMinConfs(), req.GetSpendUnconfirmed(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We'll assume the PSBT will be funded by the default account unless
|
||||
// otherwise specified.
|
||||
account := lnwallet.DefaultAccountName
|
||||
if req.Account != "" {
|
||||
account = req.Account
|
||||
}
|
||||
|
||||
// There are two ways a user can specify what we call the template (a
|
||||
// list of inputs and outputs to use in the PSBT): Either as a PSBT
|
||||
// packet directly or as a special RPC message. Find out which one the
|
||||
|
@ -1189,11 +1231,18 @@ func (w *WalletKit) FundPsbt(_ context.Context,
|
|||
// The template is specified as a PSBT. All we have to do is parse it.
|
||||
case req.GetPsbt() != nil:
|
||||
r := bytes.NewReader(req.GetPsbt())
|
||||
packet, err = psbt.NewFromRawBytes(r, false)
|
||||
packet, err := psbt.NewFromRawBytes(r, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse PSBT: %v", err)
|
||||
return nil, fmt.Errorf("could not parse PSBT: %w", err)
|
||||
}
|
||||
|
||||
// Run the actual funding process now, using the internal
|
||||
// wallet.
|
||||
return w.fundPsbtInternalWallet(
|
||||
account, keyScopeFromChangeAddressType(req.ChangeType),
|
||||
packet, minConfs, feeSatPerKW,
|
||||
)
|
||||
|
||||
// The template is specified as a RPC message. We need to create a new
|
||||
// PSBT and copy the RPC information over.
|
||||
case req.GetRaw() != nil:
|
||||
|
@ -1218,7 +1267,7 @@ func (w *WalletKit) FundPsbt(_ context.Context,
|
|||
pkScript, err := txscript.PayToAddrScript(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting pk "+
|
||||
"script for address %s: %v", addrStr,
|
||||
"script for address %s: %w", addrStr,
|
||||
err)
|
||||
}
|
||||
|
||||
|
@ -1233,73 +1282,42 @@ func (w *WalletKit) FundPsbt(_ context.Context,
|
|||
op, err := UnmarshallOutPoint(in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing "+
|
||||
"outpoint: %v", err)
|
||||
"outpoint: %w", err)
|
||||
}
|
||||
txIn[idx] = op
|
||||
}
|
||||
|
||||
sequences := make([]uint32, len(txIn))
|
||||
packet, err = psbt.New(txIn, txOut, 2, 0, sequences)
|
||||
packet, err := psbt.New(txIn, txOut, 2, 0, sequences)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create PSBT: %v", err)
|
||||
return nil, fmt.Errorf("could not create PSBT: %w", err)
|
||||
}
|
||||
|
||||
// Run the actual funding process now, using the internal
|
||||
// wallet.
|
||||
return w.fundPsbtInternalWallet(
|
||||
account, keyScopeFromChangeAddressType(req.ChangeType),
|
||||
packet, minConfs, feeSatPerKW,
|
||||
)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("transaction template missing, need " +
|
||||
"to specify either PSBT or raw TX template")
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the desired transaction fee.
|
||||
switch {
|
||||
// Estimate the fee by the target number of blocks to confirmation.
|
||||
case req.GetTargetConf() != 0:
|
||||
targetConf := req.GetTargetConf()
|
||||
if targetConf < 2 {
|
||||
return nil, fmt.Errorf("confirmation target must be " +
|
||||
"greater than 1")
|
||||
}
|
||||
|
||||
feeSatPerKW, err = w.cfg.FeeEstimator.EstimateFeePerKW(
|
||||
targetConf,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not estimate fee: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
// Convert the fee to sat/kW from the specified sat/vByte.
|
||||
case req.GetSatPerVbyte() != 0:
|
||||
feeSatPerKW = chainfee.SatPerKVByte(
|
||||
req.GetSatPerVbyte() * 1000,
|
||||
).FeePerKWeight()
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("fee definition missing, need to " +
|
||||
"specify either target_conf or sat_per_vbyte")
|
||||
}
|
||||
|
||||
// Then, we'll extract the minimum number of confirmations that each
|
||||
// output we use to fund the transaction should satisfy.
|
||||
minConfs, err := lnrpc.ExtractMinConfs(
|
||||
req.GetMinConfs(), req.GetSpendUnconfirmed(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// fundPsbtInternalWallet uses the "old" PSBT funding method of the internal
|
||||
// wallet that does not allow specifying custom inputs while selecting coins.
|
||||
func (w *WalletKit) fundPsbtInternalWallet(account string,
|
||||
keyScope *waddrmgr.KeyScope, packet *psbt.Packet, minConfs int32,
|
||||
feeSatPerKW chainfee.SatPerKWeight) (*FundPsbtResponse, error) {
|
||||
|
||||
// The RPC parsing part is now over. Several of the following operations
|
||||
// require us to hold the global coin selection lock so we do the rest
|
||||
// require us to hold the global coin selection lock, so we do the rest
|
||||
// of the tasks while holding the lock. The result is a list of locked
|
||||
// UTXOs.
|
||||
changeIndex := int32(-1)
|
||||
err = w.cfg.CoinSelectionLocker.WithCoinSelectLock(func() error {
|
||||
// We'll assume the PSBT will be funded by the default account
|
||||
// unless otherwise specified.
|
||||
account := lnwallet.DefaultAccountName
|
||||
if req.Account != "" {
|
||||
account = req.Account
|
||||
}
|
||||
|
||||
var response *FundPsbtResponse
|
||||
err := w.cfg.CoinSelectionLocker.WithCoinSelectLock(func() error {
|
||||
// In case the user did specify inputs, we need to make sure
|
||||
// they are known to us, still unspent and not yet locked.
|
||||
if len(packet.UnsignedTx.TxIn) > 0 {
|
||||
|
@ -1323,47 +1341,64 @@ func (w *WalletKit) FundPsbt(_ context.Context,
|
|||
// We can now ask the wallet to fund the TX. This will not yet
|
||||
// lock any coins but might still change the wallet DB by
|
||||
// generating a new change address.
|
||||
changeIndex, err = w.cfg.Wallet.FundPsbt(
|
||||
packet, minConfs, feeSatPerKW, account,
|
||||
keyScopeFromChangeAddressType(req.ChangeType),
|
||||
changeIndex, err := w.cfg.Wallet.FundPsbt(
|
||||
packet, minConfs, feeSatPerKW, account, keyScope,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wallet couldn't fund PSBT: %v", err)
|
||||
}
|
||||
|
||||
// Make sure we can properly serialize the packet. If this goes
|
||||
// wrong then something isn't right with the inputs and we
|
||||
// probably shouldn't try to lock any of them.
|
||||
err = packet.Serialize(&rawPsbt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing funded PSBT: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
// Now we have obtained a set of coins that can be used to fund
|
||||
// the TX. Let's lock them to be sure they aren't spent by the
|
||||
// time the PSBT is published. This is the action we do here
|
||||
// that could cause an error. Therefore if some of the UTXOs
|
||||
// that could cause an error. Therefore, if some of the UTXOs
|
||||
// cannot be locked, the rollback of the other's locks also
|
||||
// happens in this function. If we ever need to do more after
|
||||
// this function, we need to extract the rollback needs to be
|
||||
// extracted into a defer.
|
||||
locks, err = lockInputs(w.cfg.Wallet, packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not lock inputs: %v", err)
|
||||
outpoints := make([]wire.OutPoint, len(packet.UnsignedTx.TxIn))
|
||||
for i, txIn := range packet.UnsignedTx.TxIn {
|
||||
outpoints[i] = txIn.PreviousOutPoint
|
||||
}
|
||||
|
||||
return nil
|
||||
response, err = w.lockAndCreateFundingResponse(
|
||||
packet, outpoints, changeIndex,
|
||||
)
|
||||
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// lockAndCreateFundingResponse locks the given outpoints and creates a funding
|
||||
// response with the serialized PSBT, the change index and the locked UTXOs.
|
||||
func (w *WalletKit) lockAndCreateFundingResponse(packet *psbt.Packet,
|
||||
newOutpoints []wire.OutPoint, changeIndex int32) (*FundPsbtResponse,
|
||||
error) {
|
||||
|
||||
// Make sure we can properly serialize the packet. If this goes wrong
|
||||
// then something isn't right with the inputs, and we probably shouldn't
|
||||
// try to lock any of them.
|
||||
var buf bytes.Buffer
|
||||
err := packet.Serialize(&buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error serializing funded PSBT: %w", err)
|
||||
}
|
||||
|
||||
locks, err := lockInputs(w.cfg.Wallet, newOutpoints)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not lock inputs: %w", err)
|
||||
}
|
||||
|
||||
// Convert the lock leases to the RPC format.
|
||||
rpcLocks := marshallLeases(locks)
|
||||
|
||||
return &FundPsbtResponse{
|
||||
FundedPsbt: rawPsbt.Bytes(),
|
||||
FundedPsbt: buf.Bytes(),
|
||||
ChangeOutputIndex: changeIndex,
|
||||
LockedUtxos: rpcLocks,
|
||||
}, nil
|
||||
|
|
Loading…
Add table
Reference in a new issue