From 903b5dde8934318ccab89192f3ec819b544d7fd5 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Sun, 23 Apr 2023 19:57:51 +0200 Subject: [PATCH] rpc: SendOutputs considers anchor reserve --- lnrpc/walletrpc/errors.go | 17 +++++++++++++++ lnrpc/walletrpc/walletkit_server.go | 33 +++++++++++++++++++++++------ lntest/rpc/wallet_kit.go | 15 +++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 lnrpc/walletrpc/errors.go diff --git a/lnrpc/walletrpc/errors.go b/lnrpc/walletrpc/errors.go new file mode 100644 index 000000000..367fbce42 --- /dev/null +++ b/lnrpc/walletrpc/errors.go @@ -0,0 +1,17 @@ +package walletrpc + +import "errors" + +var ( + + // ErrZeroLabel is returned when an attempt is made to label a + // transaction with an empty label. + ErrZeroLabel = errors.New("cannot label transaction with empty " + + "label") + + // ErrInsufficientReserve is returned when SendOutputs wouldn't leave + // enough funds in the wallet to cover for the anchor reserve. + ErrInsufficientReserve = errors.New("the outputs to be sent " + + "would leave insufficient reserves for anchor channels in " + + "the wallet") +) diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index 287572dd0..a3fabb381 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -222,10 +222,6 @@ var ( } ) -// 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") - // ServerShell is a shell struct holding a reference to the actual sub-server. // It is used to register the gRPC sub-server with the root server before we // have the necessary dependencies to populate the actual sub-server. @@ -666,12 +662,14 @@ func (w *WalletKit) SendOutputs(ctx context.Context, // 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. + var totalOutputValue int64 outputsToCreate := make([]*wire.TxOut, 0, len(req.Outputs)) for _, output := range req.Outputs { outputsToCreate = append(outputsToCreate, &wire.TxOut{ Value: output.Value, PkScript: output.PkScript, }) + totalOutputValue += output.Value } // Then, we'll extract the minimum number of confirmations that each @@ -681,13 +679,36 @@ func (w *WalletKit) SendOutputs(ctx context.Context, return nil, err } + // Before sending out funds we need to ensure that the remainder of our + // wallet funds would cover for the anchor reserve requirement. We'll + // also take unconfirmed funds into account. + walletBalance, err := w.cfg.Wallet.ConfirmedBalance( + 0, lnwallet.DefaultAccountName, + ) + if err != nil { + return nil, err + } + + // We'll get the currently required reserve amount. + reserve, err := w.RequiredReserve(ctx, &RequiredReserveRequest{}) + if err != nil { + return nil, err + } + + // Then we check if our current wallet balance undershoots the required + // reserve if we'd send out the outputs specified in the request. + if int64(walletBalance)-totalOutputValue < reserve.RequiredReserve { + return nil, ErrInsufficientReserve + } + label, err := labels.ValidateAPI(req.Label) if err != nil { return nil, err } - // Now that we have the outputs mapped, we can request that the wallet - // attempt to create this transaction. + // Now that we have the outputs mapped and checked for the reserve + // requirement, we can request that the wallet attempts to create this + // transaction. tx, err := w.cfg.Wallet.SendOutputs( outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw), minConfs, label, ) diff --git a/lntest/rpc/wallet_kit.go b/lntest/rpc/wallet_kit.go index 9a88fe6e7..95af5f92d 100644 --- a/lntest/rpc/wallet_kit.go +++ b/lntest/rpc/wallet_kit.go @@ -290,3 +290,18 @@ func (h *HarnessRPC) ImportTapscript( return resp } + +// RequiredReserve makes a RPC call to the node's WalletKitClient and asserts. +// +//nolint:lll +func (h *HarnessRPC) RequiredReserve( + req *walletrpc.RequiredReserveRequest) *walletrpc.RequiredReserveResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.WalletKit.RequiredReserve(ctxt, req) + h.NoError(err, "RequiredReserve") + + return resp +}