You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/lndclient/walletkit_client.go

350 lines
9.5 KiB
Go

package lndclient
import (
"context"
"encoding/hex"
"fmt"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"google.golang.org/grpc"
)
// WalletKitClient exposes wallet functionality.
type WalletKitClient interface {
// ListUnspent returns a list of all utxos spendable by the wallet with
// a number of confirmations between the specified minimum and maximum.
ListUnspent(ctx context.Context, minConfs, maxConfs int32) (
[]*lnwallet.Utxo, error)
// 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`.
LeaseOutput(ctx context.Context, lockID wtxmgr.LockID,
op wire.OutPoint) (time.Time, error)
// 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.
ReleaseOutput(ctx context.Context, lockID wtxmgr.LockID,
op wire.OutPoint) error
DeriveNextKey(ctx context.Context, family int32) (
*keychain.KeyDescriptor, error)
DeriveKey(ctx context.Context, locator *keychain.KeyLocator) (
*keychain.KeyDescriptor, error)
NextAddr(ctx context.Context) (btcutil.Address, error)
PublishTransaction(ctx context.Context, tx *wire.MsgTx) error
SendOutputs(ctx context.Context, outputs []*wire.TxOut,
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, error)
EstimateFee(ctx context.Context, confTarget int32) (chainfee.SatPerKWeight,
error)
// ListSweeps returns a list of sweep transaction ids known to our node.
// Note that this function only looks up transaction ids, and does not
// query our wallet for the full set of transactions.
ListSweeps(ctx context.Context) ([]string, error)
}
type walletKitClient struct {
client walletrpc.WalletKitClient
walletKitMac serializedMacaroon
}
// A compile-time constraint to ensure walletKitclient satisfies the
// WalletKitClient interface.
var _ WalletKitClient = (*walletKitClient)(nil)
func newWalletKitClient(conn *grpc.ClientConn,
walletKitMac serializedMacaroon) *walletKitClient {
return &walletKitClient{
client: walletrpc.NewWalletKitClient(conn),
walletKitMac: walletKitMac,
}
}
// ListUnspent returns a list of all utxos spendable by the wallet with a number
// of confirmations between the specified minimum and maximum.
func (m *walletKitClient) ListUnspent(ctx context.Context, minConfs,
maxConfs int32) ([]*lnwallet.Utxo, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
resp, err := m.client.ListUnspent(rpcCtx, &walletrpc.ListUnspentRequest{
MinConfs: minConfs,
MaxConfs: maxConfs,
})
if err != nil {
return nil, err
}
utxos := make([]*lnwallet.Utxo, 0, len(resp.Utxos))
for _, utxo := range resp.Utxos {
var addrType lnwallet.AddressType
switch utxo.AddressType {
case lnrpc.AddressType_WITNESS_PUBKEY_HASH:
addrType = lnwallet.WitnessPubKey
case lnrpc.AddressType_NESTED_PUBKEY_HASH:
addrType = lnwallet.NestedWitnessPubKey
default:
return nil, fmt.Errorf("invalid utxo address type %v",
utxo.AddressType)
}
pkScript, err := hex.DecodeString(utxo.PkScript)
if err != nil {
return nil, err
}
opHash, err := chainhash.NewHash(utxo.Outpoint.TxidBytes)
if err != nil {
return nil, err
}
utxos = append(utxos, &lnwallet.Utxo{
AddressType: addrType,
Value: btcutil.Amount(utxo.AmountSat),
Confirmations: utxo.Confirmations,
PkScript: pkScript,
OutPoint: wire.OutPoint{
Hash: *opHash,
Index: utxo.Outpoint.OutputIndex,
},
})
}
return utxos, nil
}
// 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`.
func (m *walletKitClient) LeaseOutput(ctx context.Context, lockID wtxmgr.LockID,
op wire.OutPoint) (time.Time, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
resp, err := m.client.LeaseOutput(rpcCtx, &walletrpc.LeaseOutputRequest{
Id: lockID[:],
Outpoint: &lnrpc.OutPoint{
TxidBytes: op.Hash[:],
OutputIndex: op.Index,
},
})
if err != nil {
return time.Time{}, err
}
return time.Unix(int64(resp.Expiration), 0), 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 (m *walletKitClient) ReleaseOutput(ctx context.Context,
lockID wtxmgr.LockID, op wire.OutPoint) error {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
_, err := m.client.ReleaseOutput(rpcCtx, &walletrpc.ReleaseOutputRequest{
Id: lockID[:],
Outpoint: &lnrpc.OutPoint{
TxidBytes: op.Hash[:],
OutputIndex: op.Index,
},
})
return err
}
func (m *walletKitClient) DeriveNextKey(ctx context.Context, family int32) (
*keychain.KeyDescriptor, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
resp, err := m.client.DeriveNextKey(rpcCtx, &walletrpc.KeyReq{
KeyFamily: family,
})
if err != nil {
return nil, err
}
key, err := btcec.ParsePubKey(resp.RawKeyBytes, btcec.S256())
if err != nil {
return nil, err
}
return &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(resp.KeyLoc.KeyFamily),
Index: uint32(resp.KeyLoc.KeyIndex),
},
PubKey: key,
}, nil
}
func (m *walletKitClient) DeriveKey(ctx context.Context, in *keychain.KeyLocator) (
*keychain.KeyDescriptor, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
resp, err := m.client.DeriveKey(rpcCtx, &signrpc.KeyLocator{
KeyFamily: int32(in.Family),
KeyIndex: int32(in.Index),
})
if err != nil {
return nil, err
}
key, err := btcec.ParsePubKey(resp.RawKeyBytes, btcec.S256())
if err != nil {
return nil, err
}
return &keychain.KeyDescriptor{
KeyLocator: *in,
PubKey: key,
}, nil
}
func (m *walletKitClient) NextAddr(ctx context.Context) (
btcutil.Address, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
resp, err := m.client.NextAddr(rpcCtx, &walletrpc.AddrRequest{})
if err != nil {
return nil, err
}
addr, err := btcutil.DecodeAddress(resp.Addr, nil)
if err != nil {
return nil, err
}
return addr, nil
}
func (m *walletKitClient) PublishTransaction(ctx context.Context,
tx *wire.MsgTx) error {
txHex, err := swap.EncodeTx(tx)
if err != nil {
return err
}
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
_, err = m.client.PublishTransaction(rpcCtx, &walletrpc.Transaction{
TxHex: txHex,
})
return err
}
func (m *walletKitClient) SendOutputs(ctx context.Context,
outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight) (
*wire.MsgTx, error) {
rpcOutputs := make([]*signrpc.TxOut, len(outputs))
for i, output := range outputs {
rpcOutputs[i] = &signrpc.TxOut{
PkScript: output.PkScript,
Value: output.Value,
}
}
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
resp, err := m.client.SendOutputs(rpcCtx, &walletrpc.SendOutputsRequest{
Outputs: rpcOutputs,
SatPerKw: int64(feeRate),
})
if err != nil {
return nil, err
}
tx, err := swap.DecodeTx(resp.RawTx)
if err != nil {
return nil, err
}
return tx, nil
}
func (m *walletKitClient) EstimateFee(ctx context.Context, confTarget int32) (
chainfee.SatPerKWeight, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
resp, err := m.client.EstimateFee(rpcCtx, &walletrpc.EstimateFeeRequest{
ConfTarget: int32(confTarget),
})
if err != nil {
return 0, err
}
return chainfee.SatPerKWeight(resp.SatPerKw), nil
}
// ListSweeps returns a list of sweep transaction ids known to our node.
// Note that this function only looks up transaction ids (Verbose=false), and
// does not query our wallet for the full set of transactions.
func (m *walletKitClient) ListSweeps(ctx context.Context) ([]string, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := m.client.ListSweeps(
m.walletKitMac.WithMacaroonAuth(rpcCtx),
&walletrpc.ListSweepsRequest{
Verbose: false,
},
)
if err != nil {
return nil, err
}
// Since we have requested the abbreviated response from lnd, we can
// just get our response to a list of sweeps and return it.
sweeps := resp.GetTransactionIds()
return sweeps.TransactionIds, nil
}