diff --git a/lndclient/lightning_client.go b/lndclient/lightning_client.go index 77f3b43..2506769 100644 --- a/lndclient/lightning_client.go +++ b/lndclient/lightning_client.go @@ -50,6 +50,9 @@ type LightningClient interface { // ListChannels retrieves all channels of the backing lnd node. ListChannels(ctx context.Context) ([]ChannelInfo, error) + // ClosedChannels returns all closed channels of the backing lnd node. + ClosedChannels(ctx context.Context) ([]ClosedChannel, error) + // ChannelBackup retrieves the backup for a particular channel. The // backup is returned as an encrypted chanbackup.Single payload. ChannelBackup(context.Context, wire.OutPoint) ([]byte, error) @@ -109,6 +112,138 @@ type ChannelInfo struct { Uptime time.Duration } +// ClosedChannel represents a channel that has been closed. +type ClosedChannel struct { + // ChannelPoint is the funding outpoint of the channel. + ChannelPoint string + + // ChannelID holds the unique channel ID for the channel. The first 3 + // bytes are the block height, the next 3 the index within the block, + // and the last 2 bytes are the output index for the channel. + ChannelID uint64 + + // ClosingTxHash is the tx hash of the close transaction for the channel. + ClosingTxHash string + + // CloseType is the type of channel closure. + CloseType CloseType + + // OpenInitiator is true if we opened the channel. This value is not + // always available (older channels do not have it). + OpenInitiator Initiator + + // Initiator indicates which party initiated the channel close. Since + // this value is not always set in the rpc response, we also make a best + // effort attempt to set it based on CloseType. + CloseInitiator Initiator + + // PubKeyBytes is the raw bytes of the public key of the remote node. + PubKeyBytes route.Vertex + + // Capacity is the total amount of funds held in this channel. + Capacity btcutil.Amount + + // SettledBalance is the amount we were paid out directly in this + // channel close. Note that this does not include cases where we need to + // sweep our commitment or htlcs. + SettledBalance btcutil.Amount +} + +// CloseType is an enum which represents the types of closes our channels may +// have. This type maps to the rpc value. +type CloseType uint8 + +const ( + // CloseTypeCooperative represents cooperative closes. + CloseTypeCooperative CloseType = iota + + // CloseTypeLocalForce represents force closes that we initiated. + CloseTypeLocalForce + + // CloseTypeRemoteForce represents force closes that our peer initiated. + CloseTypeRemoteForce + + // CloseTypeBreach represents breach closes from our peer. + CloseTypeBreach + + // CloseTypeFundingCancelled represents channels which were never + // created because their funding transaction was cancelled. + CloseTypeFundingCancelled + + // CloseTypeAbandoned represents a channel that was abandoned. + CloseTypeAbandoned +) + +// String returns the string representation of a close type. +func (c CloseType) String() string { + switch c { + case CloseTypeCooperative: + return "Cooperative" + + case CloseTypeLocalForce: + return "Local Force" + + case CloseTypeRemoteForce: + return "Remote Force" + + case CloseTypeBreach: + return "Breach" + + case CloseTypeFundingCancelled: + return "Funding Cancelled" + + case CloseTypeAbandoned: + return "Abandoned" + + default: + return "Unknown" + } +} + +// Initiator indicates the party that opened or closed a channel. This enum is +// used for cases where we may not have a full set of initiator information +// available over rpc (this is the case for older channels). +type Initiator uint8 + +const ( + // InitiatorUnrecorded is set when we do not know the open/close + // initiator for a channel, this is the case when the channel was + // closed before lnd started tracking initiators. + InitiatorUnrecorded Initiator = iota + + // InitiatorLocal is set when we initiated a channel open or close. + InitiatorLocal + + // InitiatorRemote is set when the remote party initiated a chanel open + // or close. + InitiatorRemote + + // InitiatorBoth is set in the case where both parties initiated a + // cooperative close (this is possible with multiple rounds of + // negotiation). + InitiatorBoth +) + +// String provides the string represenetation of a close initiator. +func (c Initiator) String() string { + switch c { + case InitiatorUnrecorded: + return "Unrecorded" + + case InitiatorLocal: + return "Local" + + case InitiatorRemote: + return "Remote" + + case InitiatorBoth: + return "Both" + + default: + return fmt.Sprintf("unknown initiator: %d", c) + } +} + var ( // ErrMalformedServerResponse is returned when the swap and/or prepay // invoice is malformed. @@ -583,6 +718,130 @@ func (s *lightningClient) ListChannels(ctx context.Context) ( return result, nil } +// ClosedChannels returns a list of our closed channels. +func (s *lightningClient) ClosedChannels(ctx context.Context) ([]ClosedChannel, + error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + response, err := s.client.ClosedChannels( + s.adminMac.WithMacaroonAuth(rpcCtx), + &lnrpc.ClosedChannelsRequest{}, + ) + if err != nil { + return nil, err + } + + channels := make([]ClosedChannel, len(response.Channels)) + for i, channel := range response.Channels { + remote, err := route.NewVertexFromStr(channel.RemotePubkey) + if err != nil { + return nil, err + } + + closeType, err := rpcCloseType(channel.CloseType) + if err != nil { + return nil, err + } + + openInitiator, err := getInitiator(channel.OpenInitiator) + if err != nil { + return nil, err + } + + closeInitiator, err := rpcCloseInitiator( + channel.CloseInitiator, closeType, + ) + if err != nil { + return nil, err + } + + channels[i] = ClosedChannel{ + ChannelPoint: channel.ChannelPoint, + ChannelID: channel.ChanId, + ClosingTxHash: channel.ClosingTxHash, + CloseType: closeType, + OpenInitiator: openInitiator, + CloseInitiator: closeInitiator, + PubKeyBytes: remote, + Capacity: btcutil.Amount(channel.Capacity), + SettledBalance: btcutil.Amount(channel.SettledBalance), + } + } + + return channels, nil +} + +// rpcCloseType maps a rpc close type to our local enum. +func rpcCloseType(t lnrpc.ChannelCloseSummary_ClosureType) (CloseType, error) { + switch t { + case lnrpc.ChannelCloseSummary_COOPERATIVE_CLOSE: + return CloseTypeCooperative, nil + + case lnrpc.ChannelCloseSummary_LOCAL_FORCE_CLOSE: + return CloseTypeLocalForce, nil + + case lnrpc.ChannelCloseSummary_REMOTE_FORCE_CLOSE: + return CloseTypeRemoteForce, nil + + case lnrpc.ChannelCloseSummary_BREACH_CLOSE: + return CloseTypeBreach, nil + + case lnrpc.ChannelCloseSummary_FUNDING_CANCELED: + return CloseTypeFundingCancelled, nil + + case lnrpc.ChannelCloseSummary_ABANDONED: + return CloseTypeAbandoned, nil + + default: + return 0, fmt.Errorf("unknown close type: %v", t) + } +} + +// rpcCloseInitiator maps a close initiator to our local type. Since this field +// is not always set in lnd for older channels, also use our close type to infer +// who initiated the close when we have force closes. +func rpcCloseInitiator(initiator lnrpc.Initiator, + closeType CloseType) (Initiator, error) { + + // Since our close type is always set on the rpc, we first check whether + // we can figure out the close initiator from this value. This is only + // possible for force closes/breaches. + switch closeType { + case CloseTypeLocalForce: + return InitiatorLocal, nil + + case CloseTypeRemoteForce, CloseTypeBreach: + return InitiatorRemote, nil + } + + // Otherwise, we check whether our initiator field is set, and fail only + // if we have an unknown type. + return getInitiator(initiator) +} + +// getInitiator maps a rpc initiator value to our initiator enum. +func getInitiator(initiator lnrpc.Initiator) (Initiator, error) { + switch initiator { + case lnrpc.Initiator_INITIATOR_LOCAL: + return InitiatorLocal, nil + + case lnrpc.Initiator_INITIATOR_REMOTE: + return InitiatorRemote, nil + + case lnrpc.Initiator_INITIATOR_BOTH: + return InitiatorBoth, nil + + case lnrpc.Initiator_INITIATOR_UNKNOWN: + return InitiatorUnrecorded, nil + + default: + return InitiatorUnrecorded, fmt.Errorf("unknown "+ + "initiator: %v", initiator) + } +} + // ChannelBackup retrieves the backup for a particular channel. The backup is // returned as an encrypted chanbackup.Single payload. func (s *lightningClient) ChannelBackup(ctx context.Context, diff --git a/test/lightning_client_mock.go b/test/lightning_client_mock.go index 2bac092..e030afe 100644 --- a/test/lightning_client_mock.go +++ b/test/lightning_client_mock.go @@ -175,6 +175,13 @@ func (h *mockLightningClient) ListChannels(ctx context.Context) ( return h.lnd.Channels, nil } +// ClosedChannels returns a list of our closed channels. +func (h *mockLightningClient) ClosedChannels(_ context.Context) ([]lndclient.ClosedChannel, + error) { + + return h.lnd.ClosedChannels, nil +} + // ChannelBackup retrieves the backup for a particular channel. The // backup is returned as an encrypted chanbackup.Single payload. func (h *mockLightningClient) ChannelBackup(context.Context, wire.OutPoint) ([]byte, error) { diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index b02769c..7c48baf 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -163,7 +163,8 @@ type LndMockServices struct { // keyed by hash string. Invoices map[lntypes.Hash]*lndclient.Invoice - Channels []lndclient.ChannelInfo + Channels []lndclient.ChannelInfo + ClosedChannels []lndclient.ClosedChannel WaitForFinished func()