344 lines
9.3 KiB
Go
344 lines
9.3 KiB
Go
// +build walletrpc
|
|
|
|
package walletrpc
|
|
|
|
import (
|
|
"bytes"
|
|
fmt "fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
signrpc "github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
context "golang.org/x/net/context"
|
|
grpc "google.golang.org/grpc"
|
|
"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",
|
|
}},
|
|
}
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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)
|
|
|
|
// fileExists reports whether the named file or directory exists.
|
|
func fileExists(name string) bool {
|
|
if _, err := os.Stat(name); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// 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
|
|
if !fileExists(macFilePath) && cfg.MacService != nil {
|
|
log.Infof("Baking macaroons for WaleltKit RPC Server at: %v",
|
|
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.
|
|
walletKitMac, err := cfg.MacService.Oven.NewMacaroon(
|
|
context.Background(), bakery.LatestVersion, nil,
|
|
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
|
|
}
|
|
|
|
// 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{
|
|
KeyLoc: &signrpc.KeyLocator{
|
|
KeyFamily: int32(nextKeyDesc.Family),
|
|
KeyIndex: int32(nextKeyDesc.Index),
|
|
},
|
|
RawKeyBytes: nextKeyDesc.PubKey.SerializeCompressed(),
|
|
}, 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{
|
|
KeyLoc: &signrpc.KeyLocator{
|
|
KeyFamily: int32(keyDesc.Family),
|
|
KeyIndex: int32(keyDesc.Index),
|
|
},
|
|
RawKeyBytes: keyDesc.PubKey.SerializeCompressed(),
|
|
}, 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
|
|
}
|
|
|
|
err := w.cfg.Wallet.PublishTransaction(tx)
|
|
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,
|
|
})
|
|
}
|
|
|
|
// Now that we have the outputs mapped, we can request that the wallet
|
|
// attempt to create this transaction.
|
|
tx, err := w.cfg.Wallet.SendOutputs(
|
|
outputsToCreate, lnwallet.SatPerKWeight(req.SatPerKw),
|
|
)
|
|
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
|
|
}
|