2018-10-25 05:31:07 +03:00
|
|
|
// +build walletrpc
|
|
|
|
|
|
|
|
package walletrpc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2019-09-29 02:06:34 +03:00
|
|
|
"context"
|
2019-08-22 03:24:07 +03:00
|
|
|
"errors"
|
2019-02-02 17:46:23 +03:00
|
|
|
"fmt"
|
2018-10-25 05:31:07 +03:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2020-06-10 05:37:17 +03:00
|
|
|
"time"
|
2018-10-25 05:31:07 +03:00
|
|
|
|
2019-05-30 00:00:29 +03:00
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
|
|
"github.com/btcsuite/btcd/txscript"
|
2018-10-25 05:31:07 +03:00
|
|
|
"github.com/btcsuite/btcd/wire"
|
2020-10-01 17:21:45 +03:00
|
|
|
"github.com/btcsuite/btcutil"
|
|
|
|
"github.com/btcsuite/btcutil/psbt"
|
2020-05-21 01:34:16 +03:00
|
|
|
"github.com/btcsuite/btcwallet/wtxmgr"
|
2020-04-02 16:12:28 +03:00
|
|
|
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
2019-05-17 05:53:25 +03:00
|
|
|
"github.com/lightningnetwork/lnd/input"
|
2018-10-25 05:31:07 +03:00
|
|
|
"github.com/lightningnetwork/lnd/keychain"
|
2020-05-18 15:13:23 +03:00
|
|
|
"github.com/lightningnetwork/lnd/labels"
|
2018-10-25 05:31:07 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
2019-02-02 17:46:23 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
2018-10-25 05:31:07 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
2020-05-05 22:34:38 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
2019-10-31 05:43:05 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
2020-07-23 19:36:42 +03:00
|
|
|
"github.com/lightningnetwork/lnd/macaroons"
|
2019-05-30 00:00:29 +03:00
|
|
|
"github.com/lightningnetwork/lnd/sweep"
|
2019-02-02 17:46:23 +03:00
|
|
|
"google.golang.org/grpc"
|
2018-10-25 05:31:07 +03:00
|
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// subServerName is the name of the sub rpc server. We'll use this name
|
|
|
|
// to register ourselves, and we also require that the main
|
|
|
|
// SubServerConfigDispatcher instance recognize as the name of our
|
|
|
|
subServerName = "WalletKitRPC"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// macaroonOps are the set of capabilities that our minted macaroon (if
|
|
|
|
// it doesn't already exist) will have.
|
|
|
|
macaroonOps = []bakery.Op{
|
|
|
|
{
|
|
|
|
Entity: "address",
|
|
|
|
Action: "write",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Entity: "address",
|
|
|
|
Action: "read",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "read",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// macPermissions maps RPC calls to the permissions they require.
|
|
|
|
macPermissions = map[string][]bakery.Op{
|
|
|
|
"/walletrpc.WalletKit/DeriveNextKey": {{
|
|
|
|
Entity: "address",
|
|
|
|
Action: "read",
|
|
|
|
}},
|
|
|
|
"/walletrpc.WalletKit/DeriveKey": {{
|
|
|
|
Entity: "address",
|
|
|
|
Action: "read",
|
|
|
|
}},
|
|
|
|
"/walletrpc.WalletKit/NextAddr": {{
|
|
|
|
Entity: "address",
|
|
|
|
Action: "read",
|
|
|
|
}},
|
|
|
|
"/walletrpc.WalletKit/PublishTransaction": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
}},
|
|
|
|
"/walletrpc.WalletKit/SendOutputs": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
}},
|
|
|
|
"/walletrpc.WalletKit/EstimateFee": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "read",
|
|
|
|
}},
|
2019-05-17 05:53:25 +03:00
|
|
|
"/walletrpc.WalletKit/PendingSweeps": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "read",
|
|
|
|
}},
|
2019-05-30 00:00:29 +03:00
|
|
|
"/walletrpc.WalletKit/BumpFee": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
}},
|
2020-05-05 22:34:38 +03:00
|
|
|
"/walletrpc.WalletKit/ListSweeps": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "read",
|
|
|
|
}},
|
2020-05-25 09:38:25 +03:00
|
|
|
"/walletrpc.WalletKit/LabelTransaction": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
}},
|
2020-05-21 01:34:16 +03:00
|
|
|
"/walletrpc.WalletKit/LeaseOutput": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
}},
|
|
|
|
"/walletrpc.WalletKit/ReleaseOutput": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
}},
|
2020-05-21 02:12:51 +03:00
|
|
|
"/walletrpc.WalletKit/ListUnspent": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "read",
|
|
|
|
}},
|
2020-10-01 17:21:45 +03:00
|
|
|
"/walletrpc.WalletKit/FundPsbt": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
}},
|
|
|
|
"/walletrpc.WalletKit/FinalizePsbt": {{
|
|
|
|
Entity: "onchain",
|
|
|
|
Action: "write",
|
|
|
|
}},
|
2018-10-25 05:31:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// DefaultWalletKitMacFilename is the default name of the wallet kit
|
|
|
|
// macaroon that we expect to find via a file handle within the main
|
|
|
|
// configuration file in this package.
|
|
|
|
DefaultWalletKitMacFilename = "walletkit.macaroon"
|
2020-10-01 17:21:45 +03:00
|
|
|
|
|
|
|
// LndInternalLockID is the binary representation of the SHA256 hash of
|
|
|
|
// the string "lnd-internal-lock-id" and is used for UTXO lock leases to
|
|
|
|
// identify that we ourselves are locking an UTXO, for example when
|
|
|
|
// giving out a funded PSBT. The ID corresponds to the hex value of
|
|
|
|
// ede19a92ed321a4705f8a1cccc1d4f6182545d4bb4fae08bd5937831b7e38f98.
|
|
|
|
LndInternalLockID = wtxmgr.LockID{
|
|
|
|
0xed, 0xe1, 0x9a, 0x92, 0xed, 0x32, 0x1a, 0x47,
|
|
|
|
0x05, 0xf8, 0xa1, 0xcc, 0xcc, 0x1d, 0x4f, 0x61,
|
|
|
|
0x82, 0x54, 0x5d, 0x4b, 0xb4, 0xfa, 0xe0, 0x8b,
|
|
|
|
0xd5, 0x93, 0x78, 0x31, 0xb7, 0xe3, 0x8f, 0x98,
|
|
|
|
}
|
2018-10-25 05:31:07 +03:00
|
|
|
)
|
|
|
|
|
2020-05-25 09:38:25 +03:00
|
|
|
// ErrZeroLabel is returned when an attempt is made to label a transaction with
|
|
|
|
// an empty label.
|
|
|
|
var ErrZeroLabel = errors.New("cannot label transaction with empty label")
|
|
|
|
|
2018-10-25 05:31:07 +03:00
|
|
|
// WalletKit is a sub-RPC server that exposes a tool kit which allows clients
|
|
|
|
// to execute common wallet operations. This includes requesting new addresses,
|
|
|
|
// keys (for contracts!), and publishing transactions.
|
|
|
|
type WalletKit struct {
|
|
|
|
cfg *Config
|
|
|
|
}
|
|
|
|
|
|
|
|
// A compile time check to ensure that WalletKit fully implements the
|
|
|
|
// WalletKitServer gRPC service.
|
|
|
|
var _ WalletKitServer = (*WalletKit)(nil)
|
|
|
|
|
|
|
|
// New creates a new instance of the WalletKit sub-RPC server.
|
|
|
|
func New(cfg *Config) (*WalletKit, lnrpc.MacaroonPerms, error) {
|
|
|
|
// If the path of the wallet kit macaroon wasn't specified, then we'll
|
|
|
|
// assume that it's found at the default network directory.
|
|
|
|
if cfg.WalletKitMacPath == "" {
|
|
|
|
cfg.WalletKitMacPath = filepath.Join(
|
|
|
|
cfg.NetworkDir, DefaultWalletKitMacFilename,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now that we know the full path of the wallet kit macaroon, we can
|
|
|
|
// check to see if we need to create it or not.
|
|
|
|
macFilePath := cfg.WalletKitMacPath
|
2019-01-11 12:50:12 +03:00
|
|
|
if !lnrpc.FileExists(macFilePath) && cfg.MacService != nil {
|
2019-01-27 03:57:09 +03:00
|
|
|
log.Infof("Baking macaroons for WalletKit RPC Server at: %v",
|
2018-10-25 05:31:07 +03:00
|
|
|
macFilePath)
|
|
|
|
|
|
|
|
// At this point, we know that the wallet kit macaroon doesn't
|
|
|
|
// yet, exist, so we need to create it with the help of the
|
|
|
|
// main macaroon service.
|
2020-07-23 19:36:42 +03:00
|
|
|
walletKitMac, err := cfg.MacService.NewMacaroon(
|
|
|
|
context.Background(),
|
|
|
|
macaroons.DefaultRootKeyID,
|
2018-10-25 05:31:07 +03:00
|
|
|
macaroonOps...,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
walletKitMacBytes, err := walletKitMac.M().MarshalBinary()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
err = ioutil.WriteFile(macFilePath, walletKitMacBytes, 0644)
|
|
|
|
if err != nil {
|
|
|
|
os.Remove(macFilePath)
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
walletKit := &WalletKit{
|
|
|
|
cfg: cfg,
|
|
|
|
}
|
|
|
|
|
|
|
|
return walletKit, macPermissions, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start launches any helper goroutines required for the sub-server to function.
|
|
|
|
//
|
|
|
|
// NOTE: This is part of the lnrpc.SubServer interface.
|
|
|
|
func (w *WalletKit) Start() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop signals any active goroutines for a graceful closure.
|
|
|
|
//
|
|
|
|
// NOTE: This is part of the lnrpc.SubServer interface.
|
|
|
|
func (w *WalletKit) Stop() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns a unique string representation of the sub-server. This can be
|
|
|
|
// used to identify the sub-server and also de-duplicate them.
|
|
|
|
//
|
|
|
|
// NOTE: This is part of the lnrpc.SubServer interface.
|
|
|
|
func (w *WalletKit) Name() string {
|
|
|
|
return subServerName
|
|
|
|
}
|
|
|
|
|
|
|
|
// RegisterWithRootServer will be called by the root gRPC server to direct a
|
|
|
|
// sub RPC server to register itself with the main gRPC root server. Until this
|
|
|
|
// is called, each sub-server won't be able to have requests routed towards it.
|
|
|
|
//
|
|
|
|
// NOTE: This is part of the lnrpc.SubServer interface.
|
|
|
|
func (w *WalletKit) RegisterWithRootServer(grpcServer *grpc.Server) error {
|
|
|
|
// We make sure that we register it with the main gRPC server to ensure
|
|
|
|
// all our methods are routed properly.
|
|
|
|
RegisterWalletKitServer(grpcServer, w)
|
|
|
|
|
|
|
|
log.Debugf("WalletKit RPC server successfully registered with " +
|
|
|
|
"root gRPC server")
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-02 16:12:28 +03:00
|
|
|
// RegisterWithRestServer will be called by the root REST mux to direct a sub
|
|
|
|
// RPC server to register itself with the main REST mux server. Until this is
|
|
|
|
// called, each sub-server won't be able to have requests routed towards it.
|
|
|
|
//
|
|
|
|
// NOTE: This is part of the lnrpc.SubServer interface.
|
|
|
|
func (w *WalletKit) RegisterWithRestServer(ctx context.Context,
|
|
|
|
mux *runtime.ServeMux, dest string, opts []grpc.DialOption) error {
|
|
|
|
|
|
|
|
// We make sure that we register it with the main REST server to ensure
|
|
|
|
// all our methods are routed properly.
|
|
|
|
err := RegisterWalletKitHandlerFromEndpoint(ctx, mux, dest, opts)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Could not register WalletKit REST server "+
|
|
|
|
"with root REST server: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("WalletKit REST server successfully registered with " +
|
|
|
|
"root REST server")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-05-21 02:12:51 +03:00
|
|
|
// ListUnspent returns useful information about each unspent output owned by the
|
|
|
|
// wallet, as reported by the underlying `ListUnspentWitness`; the information
|
|
|
|
// returned is: outpoint, amount in satoshis, address, address type,
|
|
|
|
// scriptPubKey in hex and number of confirmations. The result is filtered to
|
|
|
|
// contain outputs whose number of confirmations is between a
|
|
|
|
// minimum and maximum number of confirmations specified by the user, with 0
|
|
|
|
// meaning unconfirmed.
|
|
|
|
func (w *WalletKit) ListUnspent(ctx context.Context,
|
|
|
|
req *ListUnspentRequest) (*ListUnspentResponse, error) {
|
|
|
|
|
|
|
|
// Validate the confirmation arguments.
|
|
|
|
minConfs, maxConfs, err := lnrpc.ParseConfs(req.MinConfs, req.MaxConfs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// With our arguments validated, we'll query the internal wallet for
|
|
|
|
// the set of UTXOs that match our query.
|
2020-06-10 05:37:17 +03:00
|
|
|
//
|
|
|
|
// We'll acquire the global coin selection lock to ensure there aren't
|
|
|
|
// any other concurrent processes attempting to lock any UTXOs which may
|
|
|
|
// be shown available to us.
|
|
|
|
var utxos []*lnwallet.Utxo
|
|
|
|
err = w.cfg.CoinSelectionLocker.WithCoinSelectLock(func() error {
|
|
|
|
utxos, err = w.cfg.Wallet.ListUnspentWitness(minConfs, maxConfs)
|
|
|
|
return err
|
|
|
|
})
|
2020-05-21 02:12:51 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
rpcUtxos, err := lnrpc.MarshalUtxos(utxos, w.cfg.ChainParams)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &ListUnspentResponse{
|
|
|
|
Utxos: rpcUtxos,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-05-21 01:34:16 +03:00
|
|
|
// LeaseOutput locks an output to the given ID, preventing it from being
|
|
|
|
// available for any future coin selection attempts. The absolute time of the
|
|
|
|
// lock's expiration is returned. The expiration of the lock can be extended by
|
|
|
|
// successive invocations of this call. Outputs can be unlocked before their
|
|
|
|
// expiration through `ReleaseOutput`.
|
|
|
|
//
|
|
|
|
// If the output is not known, wtxmgr.ErrUnknownOutput is returned. If the
|
|
|
|
// output has already been locked to a different ID, then
|
|
|
|
// wtxmgr.ErrOutputAlreadyLocked is returned.
|
|
|
|
func (w *WalletKit) LeaseOutput(ctx context.Context,
|
|
|
|
req *LeaseOutputRequest) (*LeaseOutputResponse, error) {
|
|
|
|
|
|
|
|
if len(req.Id) != 32 {
|
|
|
|
return nil, errors.New("id must be 32 random bytes")
|
|
|
|
}
|
|
|
|
var lockID wtxmgr.LockID
|
|
|
|
copy(lockID[:], req.Id)
|
|
|
|
|
|
|
|
// Don't allow ID's of 32 bytes, but all zeros.
|
|
|
|
if lockID == (wtxmgr.LockID{}) {
|
|
|
|
return nil, errors.New("id must be 32 random bytes")
|
|
|
|
}
|
|
|
|
|
2020-10-01 17:21:47 +03:00
|
|
|
// Don't allow our internal ID to be used externally for locking. Only
|
|
|
|
// unlocking is allowed.
|
|
|
|
if lockID == LndInternalLockID {
|
|
|
|
return nil, errors.New("reserved id cannot be used")
|
|
|
|
}
|
|
|
|
|
2020-05-21 01:34:16 +03:00
|
|
|
op, err := unmarshallOutPoint(req.Outpoint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-06-10 05:37:17 +03:00
|
|
|
// Acquire the global coin selection lock to ensure there aren't any
|
|
|
|
// other concurrent processes attempting to lease the same UTXO.
|
|
|
|
var expiration time.Time
|
|
|
|
err = w.cfg.CoinSelectionLocker.WithCoinSelectLock(func() error {
|
|
|
|
expiration, err = w.cfg.Wallet.LeaseOutput(lockID, *op)
|
|
|
|
return err
|
|
|
|
})
|
2020-05-21 01:34:16 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &LeaseOutputResponse{
|
|
|
|
Expiration: uint64(expiration.Unix()),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReleaseOutput unlocks an output, allowing it to be available for coin
|
|
|
|
// selection if it remains unspent. The ID should match the one used to
|
|
|
|
// originally lock the output.
|
|
|
|
func (w *WalletKit) ReleaseOutput(ctx context.Context,
|
|
|
|
req *ReleaseOutputRequest) (*ReleaseOutputResponse, error) {
|
|
|
|
|
|
|
|
if len(req.Id) != 32 {
|
|
|
|
return nil, errors.New("id must be 32 random bytes")
|
|
|
|
}
|
|
|
|
var lockID wtxmgr.LockID
|
|
|
|
copy(lockID[:], req.Id)
|
|
|
|
|
|
|
|
op, err := unmarshallOutPoint(req.Outpoint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-06-10 05:37:17 +03:00
|
|
|
// Acquire the global coin selection lock to maintain consistency as
|
|
|
|
// it's acquired when we initially leased the output.
|
|
|
|
err = w.cfg.CoinSelectionLocker.WithCoinSelectLock(func() error {
|
|
|
|
return w.cfg.Wallet.ReleaseOutput(lockID, *op)
|
|
|
|
})
|
|
|
|
if err != nil {
|
2020-05-21 01:34:16 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &ReleaseOutputResponse{}, nil
|
|
|
|
}
|
|
|
|
|
2018-10-25 05:31:07 +03:00
|
|
|
// DeriveNextKey attempts to derive the *next* key within the key family
|
|
|
|
// (account in BIP43) specified. This method should return the next external
|
|
|
|
// child within this branch.
|
|
|
|
func (w *WalletKit) DeriveNextKey(ctx context.Context,
|
|
|
|
req *KeyReq) (*signrpc.KeyDescriptor, error) {
|
|
|
|
|
|
|
|
nextKeyDesc, err := w.cfg.KeyRing.DeriveNextKey(
|
|
|
|
keychain.KeyFamily(req.KeyFamily),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &signrpc.KeyDescriptor{
|
2018-12-04 08:26:35 +03:00
|
|
|
KeyLoc: &signrpc.KeyLocator{
|
|
|
|
KeyFamily: int32(nextKeyDesc.Family),
|
|
|
|
KeyIndex: int32(nextKeyDesc.Index),
|
2018-10-25 05:31:07 +03:00
|
|
|
},
|
2018-12-04 08:26:35 +03:00
|
|
|
RawKeyBytes: nextKeyDesc.PubKey.SerializeCompressed(),
|
2018-10-25 05:31:07 +03:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeriveKey attempts to derive an arbitrary key specified by the passed
|
|
|
|
// KeyLocator.
|
|
|
|
func (w *WalletKit) DeriveKey(ctx context.Context,
|
|
|
|
req *signrpc.KeyLocator) (*signrpc.KeyDescriptor, error) {
|
|
|
|
|
|
|
|
keyDesc, err := w.cfg.KeyRing.DeriveKey(keychain.KeyLocator{
|
|
|
|
Family: keychain.KeyFamily(req.KeyFamily),
|
|
|
|
Index: uint32(req.KeyIndex),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &signrpc.KeyDescriptor{
|
2018-12-04 08:26:35 +03:00
|
|
|
KeyLoc: &signrpc.KeyLocator{
|
|
|
|
KeyFamily: int32(keyDesc.Family),
|
|
|
|
KeyIndex: int32(keyDesc.Index),
|
2018-10-25 05:31:07 +03:00
|
|
|
},
|
2018-12-04 08:26:35 +03:00
|
|
|
RawKeyBytes: keyDesc.PubKey.SerializeCompressed(),
|
2018-10-25 05:31:07 +03:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NextAddr returns the next unused address within the wallet.
|
|
|
|
func (w *WalletKit) NextAddr(ctx context.Context,
|
|
|
|
req *AddrRequest) (*AddrResponse, error) {
|
|
|
|
|
|
|
|
addr, err := w.cfg.Wallet.NewAddress(lnwallet.WitnessPubKey, false)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &AddrResponse{
|
|
|
|
Addr: addr.String(),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attempts to publish the passed transaction to the network. Once this returns
|
|
|
|
// without an error, the wallet will continually attempt to re-broadcast the
|
|
|
|
// transaction on start up, until it enters the chain.
|
|
|
|
func (w *WalletKit) PublishTransaction(ctx context.Context,
|
|
|
|
req *Transaction) (*PublishResponse, error) {
|
|
|
|
|
|
|
|
switch {
|
|
|
|
// If the client doesn't specify a transaction, then there's nothing to
|
|
|
|
// publish.
|
|
|
|
case len(req.TxHex) == 0:
|
|
|
|
return nil, fmt.Errorf("must provide a transaction to " +
|
|
|
|
"publish")
|
|
|
|
}
|
|
|
|
|
|
|
|
tx := &wire.MsgTx{}
|
|
|
|
txReader := bytes.NewReader(req.TxHex)
|
|
|
|
if err := tx.Deserialize(txReader); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-05-18 15:13:24 +03:00
|
|
|
label, err := labels.ValidateAPI(req.Label)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = w.cfg.Wallet.PublishTransaction(tx, label)
|
2018-10-25 05:31:07 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &PublishResponse{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendOutputs is similar to the existing sendmany call in Bitcoind, and allows
|
|
|
|
// the caller to create a transaction that sends to several outputs at once.
|
|
|
|
// This is ideal when wanting to batch create a set of transactions.
|
|
|
|
func (w *WalletKit) SendOutputs(ctx context.Context,
|
|
|
|
req *SendOutputsRequest) (*SendOutputsResponse, error) {
|
|
|
|
|
|
|
|
switch {
|
|
|
|
// If the client didn't specify any outputs to create, then we can't
|
|
|
|
// proceed .
|
|
|
|
case len(req.Outputs) == 0:
|
|
|
|
return nil, fmt.Errorf("must specify at least one output " +
|
|
|
|
"to create")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Before we can request this transaction to be created, we'll need to
|
|
|
|
// amp the protos back into the format that the internal wallet will
|
|
|
|
// recognize.
|
|
|
|
outputsToCreate := make([]*wire.TxOut, 0, len(req.Outputs))
|
|
|
|
for _, output := range req.Outputs {
|
|
|
|
outputsToCreate = append(outputsToCreate, &wire.TxOut{
|
|
|
|
Value: output.Value,
|
|
|
|
PkScript: output.PkScript,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-09-27 18:48:15 +03:00
|
|
|
// 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.MinConfs, req.SpendUnconfirmed)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-05-18 15:13:24 +03:00
|
|
|
label, err := labels.ValidateAPI(req.Label)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-10-25 05:31:07 +03:00
|
|
|
// Now that we have the outputs mapped, we can request that the wallet
|
|
|
|
// attempt to create this transaction.
|
|
|
|
tx, err := w.cfg.Wallet.SendOutputs(
|
2020-09-27 18:48:15 +03:00
|
|
|
outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw), minConfs, label,
|
2018-10-25 05:31:07 +03:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
if err := tx.Serialize(&b); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &SendOutputsResponse{
|
|
|
|
RawTx: b.Bytes(),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// EstimateFee attempts to query the internal fee estimator of the wallet to
|
|
|
|
// determine the fee (in sat/kw) to attach to a transaction in order to achieve
|
|
|
|
// the confirmation target.
|
|
|
|
func (w *WalletKit) EstimateFee(ctx context.Context,
|
|
|
|
req *EstimateFeeRequest) (*EstimateFeeResponse, error) {
|
|
|
|
|
|
|
|
switch {
|
|
|
|
// A confirmation target of zero doesn't make any sense. Similarly, we
|
|
|
|
// reject confirmation targets of 1 as they're unreasonable.
|
|
|
|
case req.ConfTarget == 0 || req.ConfTarget == 1:
|
|
|
|
return nil, fmt.Errorf("confirmation target must be greater " +
|
|
|
|
"than 1")
|
|
|
|
}
|
|
|
|
|
|
|
|
satPerKw, err := w.cfg.FeeEstimator.EstimateFeePerKW(
|
|
|
|
uint32(req.ConfTarget),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &EstimateFeeResponse{
|
|
|
|
SatPerKw: int64(satPerKw),
|
|
|
|
}, nil
|
|
|
|
}
|
2019-05-17 05:53:25 +03:00
|
|
|
|
|
|
|
// PendingSweeps returns lists of on-chain outputs that lnd is currently
|
|
|
|
// attempting to sweep within its central batching engine. Outputs with similar
|
|
|
|
// fee rates are batched together in order to sweep them within a single
|
|
|
|
// transaction. The fee rate of each sweeping transaction is determined by
|
|
|
|
// taking the average fee rate of all the outputs it's trying to sweep.
|
|
|
|
func (w *WalletKit) PendingSweeps(ctx context.Context,
|
|
|
|
in *PendingSweepsRequest) (*PendingSweepsResponse, error) {
|
|
|
|
|
|
|
|
// Retrieve all of the outputs the UtxoSweeper is currently trying to
|
|
|
|
// sweep.
|
|
|
|
pendingInputs, err := w.cfg.Sweeper.PendingInputs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert them into their respective RPC format.
|
|
|
|
rpcPendingSweeps := make([]*PendingSweep, 0, len(pendingInputs))
|
|
|
|
for _, pendingInput := range pendingInputs {
|
|
|
|
var witnessType WitnessType
|
|
|
|
switch pendingInput.WitnessType {
|
|
|
|
case input.CommitmentTimeLock:
|
|
|
|
witnessType = WitnessType_COMMITMENT_TIME_LOCK
|
|
|
|
case input.CommitmentNoDelay:
|
|
|
|
witnessType = WitnessType_COMMITMENT_NO_DELAY
|
|
|
|
case input.CommitmentRevoke:
|
|
|
|
witnessType = WitnessType_COMMITMENT_REVOKE
|
|
|
|
case input.HtlcOfferedRevoke:
|
|
|
|
witnessType = WitnessType_HTLC_OFFERED_REVOKE
|
|
|
|
case input.HtlcAcceptedRevoke:
|
|
|
|
witnessType = WitnessType_HTLC_ACCEPTED_REVOKE
|
|
|
|
case input.HtlcOfferedTimeoutSecondLevel:
|
|
|
|
witnessType = WitnessType_HTLC_OFFERED_TIMEOUT_SECOND_LEVEL
|
|
|
|
case input.HtlcAcceptedSuccessSecondLevel:
|
|
|
|
witnessType = WitnessType_HTLC_ACCEPTED_SUCCESS_SECOND_LEVEL
|
|
|
|
case input.HtlcOfferedRemoteTimeout:
|
|
|
|
witnessType = WitnessType_HTLC_OFFERED_REMOTE_TIMEOUT
|
|
|
|
case input.HtlcAcceptedRemoteSuccess:
|
|
|
|
witnessType = WitnessType_HTLC_ACCEPTED_REMOTE_SUCCESS
|
|
|
|
case input.HtlcSecondLevelRevoke:
|
|
|
|
witnessType = WitnessType_HTLC_SECOND_LEVEL_REVOKE
|
|
|
|
case input.WitnessKeyHash:
|
|
|
|
witnessType = WitnessType_WITNESS_KEY_HASH
|
|
|
|
case input.NestedWitnessKeyHash:
|
|
|
|
witnessType = WitnessType_NESTED_WITNESS_KEY_HASH
|
2020-03-10 15:23:17 +03:00
|
|
|
case input.CommitmentAnchor:
|
|
|
|
witnessType = WitnessType_COMMITMENT_ANCHOR
|
2019-05-17 05:53:25 +03:00
|
|
|
default:
|
|
|
|
log.Warnf("Unhandled witness type %v for input %v",
|
|
|
|
pendingInput.WitnessType, pendingInput.OutPoint)
|
|
|
|
}
|
|
|
|
|
|
|
|
op := &lnrpc.OutPoint{
|
|
|
|
TxidBytes: pendingInput.OutPoint.Hash[:],
|
|
|
|
OutputIndex: pendingInput.OutPoint.Index,
|
|
|
|
}
|
|
|
|
amountSat := uint32(pendingInput.Amount)
|
|
|
|
satPerByte := uint32(pendingInput.LastFeeRate.FeePerKVByte() / 1000)
|
|
|
|
broadcastAttempts := uint32(pendingInput.BroadcastAttempts)
|
|
|
|
nextBroadcastHeight := uint32(pendingInput.NextBroadcastHeight)
|
|
|
|
|
2020-01-09 15:53:25 +03:00
|
|
|
requestedFee := pendingInput.Params.Fee
|
|
|
|
requestedFeeRate := uint32(requestedFee.FeeRate.FeePerKVByte() / 1000)
|
|
|
|
|
2019-05-17 05:53:25 +03:00
|
|
|
rpcPendingSweeps = append(rpcPendingSweeps, &PendingSweep{
|
|
|
|
Outpoint: op,
|
|
|
|
WitnessType: witnessType,
|
|
|
|
AmountSat: amountSat,
|
|
|
|
SatPerByte: satPerByte,
|
|
|
|
BroadcastAttempts: broadcastAttempts,
|
|
|
|
NextBroadcastHeight: nextBroadcastHeight,
|
2020-01-09 15:53:25 +03:00
|
|
|
RequestedSatPerByte: requestedFeeRate,
|
|
|
|
RequestedConfTarget: requestedFee.ConfTarget,
|
2019-12-09 17:40:05 +03:00
|
|
|
Force: pendingInput.Params.Force,
|
2019-05-17 05:53:25 +03:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return &PendingSweepsResponse{
|
|
|
|
PendingSweeps: rpcPendingSweeps,
|
|
|
|
}, nil
|
|
|
|
}
|
2019-05-30 00:00:29 +03:00
|
|
|
|
|
|
|
// unmarshallOutPoint converts an outpoint from its lnrpc type to its canonical
|
|
|
|
// type.
|
|
|
|
func unmarshallOutPoint(op *lnrpc.OutPoint) (*wire.OutPoint, error) {
|
2019-06-18 02:56:16 +03:00
|
|
|
if op == nil {
|
|
|
|
return nil, fmt.Errorf("empty outpoint provided")
|
|
|
|
}
|
|
|
|
|
2019-05-30 00:00:29 +03:00
|
|
|
var hash chainhash.Hash
|
|
|
|
switch {
|
|
|
|
case len(op.TxidBytes) == 0 && len(op.TxidStr) == 0:
|
|
|
|
fallthrough
|
|
|
|
|
|
|
|
case len(op.TxidBytes) != 0 && len(op.TxidStr) != 0:
|
|
|
|
return nil, fmt.Errorf("either TxidBytes or TxidStr must be " +
|
|
|
|
"specified, but not both")
|
|
|
|
|
|
|
|
// The hash was provided as raw bytes.
|
|
|
|
case len(op.TxidBytes) != 0:
|
|
|
|
copy(hash[:], op.TxidBytes)
|
|
|
|
|
|
|
|
// The hash was provided as a hex-encoded string.
|
|
|
|
case len(op.TxidStr) != 0:
|
|
|
|
h, err := chainhash.NewHashFromStr(op.TxidStr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
hash = *h
|
|
|
|
}
|
|
|
|
|
|
|
|
return &wire.OutPoint{
|
|
|
|
Hash: hash,
|
|
|
|
Index: op.OutputIndex,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// BumpFee allows bumping the fee rate of an arbitrary input. A fee preference
|
|
|
|
// can be expressed either as a specific fee rate or a delta of blocks in which
|
|
|
|
// the output should be swept on-chain within. If a fee preference is not
|
|
|
|
// explicitly specified, then an error is returned. The status of the input
|
|
|
|
// sweep can be checked through the PendingSweeps RPC.
|
|
|
|
func (w *WalletKit) BumpFee(ctx context.Context,
|
|
|
|
in *BumpFeeRequest) (*BumpFeeResponse, error) {
|
|
|
|
|
|
|
|
// Parse the outpoint from the request.
|
|
|
|
op, err := unmarshallOutPoint(in.Outpoint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct the request's fee preference.
|
2019-10-31 05:43:05 +03:00
|
|
|
satPerKw := chainfee.SatPerKVByte(in.SatPerByte * 1000).FeePerKWeight()
|
2019-05-30 00:00:29 +03:00
|
|
|
feePreference := sweep.FeePreference{
|
|
|
|
ConfTarget: uint32(in.TargetConf),
|
|
|
|
FeeRate: satPerKw,
|
|
|
|
}
|
|
|
|
|
|
|
|
// We'll attempt to bump the fee of the input through the UtxoSweeper.
|
|
|
|
// If it is currently attempting to sweep the input, then it'll simply
|
|
|
|
// bump its fee, which will result in a replacement transaction (RBF)
|
|
|
|
// being broadcast. If it is not aware of the input however,
|
|
|
|
// lnwallet.ErrNotMine is returned.
|
2020-03-11 20:14:26 +03:00
|
|
|
params := sweep.ParamsUpdate{
|
2019-12-09 17:40:05 +03:00
|
|
|
Fee: feePreference,
|
|
|
|
Force: in.Force,
|
2020-01-09 16:41:28 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = w.cfg.Sweeper.UpdateParams(*op, params)
|
2019-05-30 00:00:29 +03:00
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
return &BumpFeeResponse{}, nil
|
|
|
|
case lnwallet.ErrNotMine:
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-04-08 15:44:31 +03:00
|
|
|
log.Debugf("Attempting to CPFP outpoint %s", op)
|
|
|
|
|
2019-05-30 00:00:29 +03:00
|
|
|
// Since we're unable to perform a bump through RBF, we'll assume the
|
|
|
|
// user is attempting to bump an unconfirmed transaction's fee rate by
|
|
|
|
// sweeping an output within it under control of the wallet with a
|
|
|
|
// higher fee rate, essentially performing a Child-Pays-For-Parent
|
|
|
|
// (CPFP).
|
|
|
|
//
|
|
|
|
// We'll gather all of the information required by the UtxoSweeper in
|
|
|
|
// order to sweep the output.
|
2019-08-20 00:26:36 +03:00
|
|
|
utxo, err := w.cfg.Wallet.FetchInputInfo(op)
|
2019-05-30 00:00:29 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-08-22 03:24:07 +03:00
|
|
|
// We're only able to bump the fee of unconfirmed transactions.
|
|
|
|
if utxo.Confirmations > 0 {
|
|
|
|
return nil, errors.New("unable to bump fee of a confirmed " +
|
|
|
|
"transaction")
|
|
|
|
}
|
|
|
|
|
2019-05-30 00:00:29 +03:00
|
|
|
var witnessType input.WitnessType
|
2019-08-20 00:26:36 +03:00
|
|
|
switch utxo.AddressType {
|
|
|
|
case lnwallet.WitnessPubKey:
|
2019-05-30 00:00:29 +03:00
|
|
|
witnessType = input.WitnessKeyHash
|
2019-08-20 00:26:36 +03:00
|
|
|
case lnwallet.NestedWitnessPubKey:
|
2019-05-30 00:00:29 +03:00
|
|
|
witnessType = input.NestedWitnessKeyHash
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unknown input witness %v", op)
|
|
|
|
}
|
|
|
|
|
|
|
|
signDesc := &input.SignDescriptor{
|
2019-08-20 00:26:36 +03:00
|
|
|
Output: &wire.TxOut{
|
|
|
|
PkScript: utxo.PkScript,
|
|
|
|
Value: int64(utxo.Value),
|
|
|
|
},
|
2019-05-30 00:00:29 +03:00
|
|
|
HashType: txscript.SigHashAll,
|
|
|
|
}
|
|
|
|
|
2019-08-22 03:24:07 +03:00
|
|
|
// We'll use the current height as the height hint since we're dealing
|
|
|
|
// with an unconfirmed transaction.
|
|
|
|
_, currentHeight, err := w.cfg.Chain.GetBestBlock()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to retrieve current height: %v",
|
|
|
|
err)
|
|
|
|
}
|
|
|
|
|
|
|
|
input := input.NewBaseInput(op, witnessType, signDesc, uint32(currentHeight))
|
2019-12-09 13:42:58 +03:00
|
|
|
if _, err = w.cfg.Sweeper.SweepInput(input, sweep.Params{Fee: feePreference}); err != nil {
|
2019-05-30 00:00:29 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &BumpFeeResponse{}, nil
|
|
|
|
}
|
2020-05-05 22:34:38 +03:00
|
|
|
|
|
|
|
// ListSweeps returns a list of the sweeps that our node has published.
|
|
|
|
func (w *WalletKit) ListSweeps(ctx context.Context,
|
|
|
|
in *ListSweepsRequest) (*ListSweepsResponse, error) {
|
|
|
|
|
|
|
|
sweeps, err := w.cfg.Sweeper.ListSweeps()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
sweepTxns := make(map[string]bool)
|
|
|
|
|
|
|
|
txids := make([]string, len(sweeps))
|
|
|
|
for i, sweep := range sweeps {
|
|
|
|
sweepTxns[sweep.String()] = true
|
|
|
|
txids[i] = sweep.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the caller does not want verbose output, just return the set of
|
|
|
|
// sweep txids.
|
|
|
|
if !in.Verbose {
|
|
|
|
txidResp := &ListSweepsResponse_TransactionIDs{
|
|
|
|
TransactionIds: txids,
|
|
|
|
}
|
|
|
|
|
|
|
|
return &ListSweepsResponse{
|
|
|
|
Sweeps: &ListSweepsResponse_TransactionIds{
|
|
|
|
TransactionIds: txidResp,
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the caller does want full transaction lookups, query our wallet
|
|
|
|
// for all transactions, including unconfirmed transactions.
|
|
|
|
transactions, err := w.cfg.Wallet.ListTransactionDetails(
|
|
|
|
0, btcwallet.UnconfirmedHeight,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var sweepTxDetails []*lnwallet.TransactionDetail
|
|
|
|
for _, tx := range transactions {
|
|
|
|
_, ok := sweepTxns[tx.Hash.String()]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
sweepTxDetails = append(sweepTxDetails, tx)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fail if we have not retrieved all of our sweep transactions from the
|
|
|
|
// wallet.
|
|
|
|
if len(sweepTxDetails) != len(txids) {
|
|
|
|
return nil, fmt.Errorf("not all sweeps found by list "+
|
|
|
|
"transactions: %v, %v", len(sweepTxDetails), len(txids))
|
|
|
|
}
|
|
|
|
|
|
|
|
return &ListSweepsResponse{
|
|
|
|
Sweeps: &ListSweepsResponse_TransactionDetails{
|
|
|
|
TransactionDetails: lnrpc.RPCTransactionDetails(transactions),
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
2020-05-25 09:38:25 +03:00
|
|
|
|
|
|
|
// LabelTransaction adds a label to a transaction.
|
|
|
|
func (w *WalletKit) LabelTransaction(ctx context.Context,
|
|
|
|
req *LabelTransactionRequest) (*LabelTransactionResponse, error) {
|
|
|
|
|
|
|
|
// Check that the label provided in non-zero.
|
|
|
|
if len(req.Label) == 0 {
|
|
|
|
return nil, ErrZeroLabel
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the length of the non-zero label. We do not need to use the
|
|
|
|
// label returned here, because the original is non-zero so will not
|
|
|
|
// be replaced.
|
|
|
|
if _, err := labels.ValidateAPI(req.Label); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
hash, err := chainhash.NewHash(req.Txid)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = w.cfg.Wallet.LabelTransaction(*hash, req.Label, req.Overwrite)
|
|
|
|
return &LabelTransactionResponse{}, err
|
|
|
|
}
|
2020-10-01 17:21:45 +03:00
|
|
|
|
|
|
|
// FundPsbt creates a fully populated PSBT that contains enough inputs to fund
|
|
|
|
// the outputs specified in the template. There are two ways of specifying a
|
|
|
|
// template: Either by passing in a PSBT with at least one output declared or
|
|
|
|
// by passing in a raw TxTemplate message. If there are no inputs specified in
|
|
|
|
// the template, coin selection is performed automatically. If the template does
|
|
|
|
// contain any inputs, it is assumed that full coin selection happened
|
|
|
|
// externally and no additional inputs are added. If the specified inputs aren't
|
|
|
|
// enough to fund the outputs with the given fee rate, an error is returned.
|
|
|
|
// After either selecting or verifying the inputs, all input UTXOs are locked
|
|
|
|
// with an internal app ID.
|
|
|
|
//
|
|
|
|
// NOTE: If this method returns without an error, it is the caller's
|
|
|
|
// responsibility to either spend the locked UTXOs (by finalizing and then
|
|
|
|
// publishing the transaction) or to unlock/release the locked UTXOs in case of
|
|
|
|
// an error on the caller's side.
|
|
|
|
func (w *WalletKit) FundPsbt(_ context.Context,
|
|
|
|
req *FundPsbtRequest) (*FundPsbtResponse, error) {
|
|
|
|
|
|
|
|
var (
|
|
|
|
err error
|
|
|
|
packet *psbt.Packet
|
|
|
|
feeSatPerKW chainfee.SatPerKWeight
|
|
|
|
locks []*utxoLock
|
|
|
|
rawPsbt bytes.Buffer
|
|
|
|
)
|
|
|
|
|
|
|
|
// 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
|
|
|
|
// user wants to use, they are mutually exclusive.
|
|
|
|
switch {
|
|
|
|
// 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)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not parse PSBT: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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:
|
|
|
|
tpl := req.GetRaw()
|
|
|
|
if len(tpl.Outputs) == 0 {
|
|
|
|
return nil, fmt.Errorf("no outputs specified")
|
|
|
|
}
|
|
|
|
|
|
|
|
txOut := make([]*wire.TxOut, 0, len(tpl.Outputs))
|
|
|
|
for addrStr, amt := range tpl.Outputs {
|
|
|
|
addr, err := btcutil.DecodeAddress(
|
|
|
|
addrStr, w.cfg.ChainParams,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error parsing address "+
|
|
|
|
"%s for network %s: %v", addrStr,
|
|
|
|
w.cfg.ChainParams.Name, err)
|
|
|
|
}
|
|
|
|
pkScript, err := txscript.PayToAddrScript(addr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error getting pk "+
|
|
|
|
"script for address %s: %v", addrStr,
|
|
|
|
err)
|
|
|
|
}
|
|
|
|
|
|
|
|
txOut = append(txOut, &wire.TxOut{
|
|
|
|
Value: int64(amt),
|
|
|
|
PkScript: pkScript,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
txIn := make([]*wire.OutPoint, len(tpl.Inputs))
|
|
|
|
for idx, in := range tpl.Inputs {
|
|
|
|
op, err := unmarshallOutPoint(in)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error parsing "+
|
|
|
|
"outpoint: %v", err)
|
|
|
|
}
|
|
|
|
txIn[idx] = op
|
|
|
|
}
|
|
|
|
|
|
|
|
sequences := make([]uint32, len(txIn))
|
|
|
|
packet, err = psbt.New(txIn, txOut, 2, 0, sequences)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not create PSBT: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
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 set_per_vbyte")
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
// 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 {
|
|
|
|
// 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 {
|
|
|
|
// Get a list of all unspent witness outputs.
|
|
|
|
utxos, err := w.cfg.Wallet.ListUnspentWitness(
|
|
|
|
defaultMinConf, defaultMaxConf,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate all inputs against our known list of UTXOs
|
|
|
|
// now.
|
|
|
|
err = verifyInputsUnspent(packet.UnsignedTx.TxIn, utxos)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We made sure the input from the user is as sane as possible.
|
|
|
|
// 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, feeSatPerKW)
|
|
|
|
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
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert the lock leases to the RPC format.
|
|
|
|
rpcLocks := make([]*UtxoLease, len(locks))
|
|
|
|
for idx, lock := range locks {
|
|
|
|
rpcLocks[idx] = &UtxoLease{
|
|
|
|
Id: lock.lockID[:],
|
|
|
|
Outpoint: &lnrpc.OutPoint{
|
|
|
|
TxidBytes: lock.outpoint.Hash[:],
|
|
|
|
TxidStr: lock.outpoint.String(),
|
|
|
|
OutputIndex: lock.outpoint.Index,
|
|
|
|
},
|
|
|
|
Expiration: uint64(lock.expiration.Unix()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &FundPsbtResponse{
|
|
|
|
FundedPsbt: rawPsbt.Bytes(),
|
|
|
|
ChangeOutputIndex: changeIndex,
|
|
|
|
LockedUtxos: rpcLocks,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// FinalizePsbt expects a partial transaction with all inputs and outputs fully
|
|
|
|
// declared and tries to sign all inputs that belong to the wallet. Lnd must be
|
|
|
|
// the last signer of the transaction. That means, if there are any unsigned
|
|
|
|
// non-witness inputs or inputs without UTXO information attached or inputs
|
|
|
|
// without witness data that do not belong to lnd's wallet, this method will
|
|
|
|
// fail. If no error is returned, the PSBT is ready to be extracted and the
|
|
|
|
// final TX within to be broadcast.
|
|
|
|
//
|
|
|
|
// NOTE: This method does NOT publish the transaction once finalized. It is the
|
|
|
|
// caller's responsibility to either publish the transaction on success or
|
|
|
|
// unlock/release any locked UTXOs in case of an error in this method.
|
|
|
|
func (w *WalletKit) FinalizePsbt(_ context.Context,
|
|
|
|
req *FinalizePsbtRequest) (*FinalizePsbtResponse, error) {
|
|
|
|
|
|
|
|
// Parse the funded PSBT. No additional checks are required at this
|
|
|
|
// level as the wallet will perform all of them.
|
|
|
|
packet, err := psbt.NewFromRawBytes(
|
|
|
|
bytes.NewReader(req.FundedPsbt), false,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error parsing PSBT: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Let the wallet do the heavy lifting. This will sign all inputs that
|
|
|
|
// we have the UTXO for. If some inputs can't be signed and don't have
|
|
|
|
// witness data attached, this will fail.
|
|
|
|
err = w.cfg.Wallet.FinalizePsbt(packet)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error finalizing PSBT: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
finalPsbtBytes bytes.Buffer
|
|
|
|
finalTxBytes bytes.Buffer
|
|
|
|
)
|
|
|
|
|
|
|
|
// Serialize the finalized PSBT in both the packet and wire format.
|
|
|
|
err = packet.Serialize(&finalPsbtBytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error serializing PSBT: %v", err)
|
|
|
|
}
|
|
|
|
finalTx, err := psbt.Extract(packet)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to extract final TX: %v", err)
|
|
|
|
}
|
|
|
|
err = finalTx.Serialize(&finalTxBytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error serializing final TX: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &FinalizePsbtResponse{
|
|
|
|
SignedPsbt: finalPsbtBytes.Bytes(),
|
|
|
|
RawFinalTx: finalTxBytes.Bytes(),
|
|
|
|
}, nil
|
|
|
|
}
|