Files
lspd/cln/cln_client.go
2024-02-23 08:54:00 +01:00

381 lines
9.3 KiB
Go

package cln
import (
"encoding/hex"
"fmt"
"log"
"path/filepath"
"strings"
"sync"
"time"
"github.com/breez/lspd/lightning"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/elementsproject/glightning/glightning"
"github.com/elementsproject/glightning/jrpc2"
"golang.org/x/exp/slices"
)
type ClnClient struct {
socketPath string
client *glightning.Lightning
mtx sync.Mutex
}
var (
OPEN_STATUSES = []string{"CHANNELD_NORMAL"}
PENDING_STATUSES = []string{"OPENINGD", "CHANNELD_AWAITING_LOCKIN"}
CLOSING_STATUSES = []string{"CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "AWAITING_UNILATERAL", "FUNDING_SPEND_SEEN", "ONCHAIN"}
CLOSED_STATUSES = []string{"CLOSED"}
)
func NewClnClient(socketPath string) (*ClnClient, error) {
client, err := newGlightningClient(socketPath)
if err != nil {
return nil, err
}
return &ClnClient{
socketPath: socketPath,
client: client,
}, nil
}
func newGlightningClient(socketPath string) (*glightning.Lightning, error) {
rpcFile := filepath.Base(socketPath)
if rpcFile == "" || rpcFile == "." {
return nil, fmt.Errorf("invalid socketPath '%s'", socketPath)
}
lightningDir := filepath.Dir(socketPath)
if lightningDir == "" || lightningDir == "." {
return nil, fmt.Errorf("invalid socketPath '%s'", socketPath)
}
client := glightning.NewLightning()
client.SetTimeout(60)
err := client.StartUp(rpcFile, lightningDir)
return client, err
}
func (c *ClnClient) getClient() (*glightning.Lightning, error) {
c.mtx.Lock()
defer c.mtx.Unlock()
if c.client.IsUp() {
return c.client, nil
}
var err error
c.client, err = newGlightningClient(c.socketPath)
if err != nil {
return nil, err
}
if c.client.IsUp() {
return c.client, nil
}
return nil, fmt.Errorf("cln is not accessible")
}
func (c *ClnClient) GetInfo() (*lightning.GetInfoResult, error) {
client, err := c.getClient()
if err != nil {
return nil, err
}
info, err := client.GetInfo()
if err != nil {
log.Printf("CLN: client.GetInfo() error: %v", err)
return nil, err
}
return &lightning.GetInfoResult{
Alias: info.Alias,
Pubkey: info.Id,
}, nil
}
func (c *ClnClient) IsConnected(destination []byte) (bool, error) {
client, err := c.getClient()
if err != nil {
return false, err
}
pubKey := hex.EncodeToString(destination)
peer, err := client.GetPeer(pubKey)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return false, nil
}
log.Printf("CLN: client.GetPeer(%v) error: %v", pubKey, err)
return false, fmt.Errorf("CLN: client.GetPeer(%v) error: %w", pubKey, err)
}
if peer.Connected {
log.Printf("CLN: destination online: %x", destination)
return true, nil
}
log.Printf("CLN: destination offline: %x", destination)
return false, nil
}
func (c *ClnClient) OpenChannel(req *lightning.OpenChannelRequest) (*wire.OutPoint, error) {
client, err := c.getClient()
if err != nil {
return nil, err
}
pubkey := hex.EncodeToString(req.Destination)
var minConfs *uint16
if req.MinConfs != nil {
m := uint16(*req.MinConfs)
minConfs = &m
}
var minDepth uint16 = 0
var rate *glightning.FeeRate
if req.FeeSatPerVByte != nil {
rate = &glightning.FeeRate{
Rate: uint(*req.FeeSatPerVByte * 1000),
Style: glightning.PerKb,
}
} else if req.TargetConf != nil {
if *req.TargetConf < 3 {
rate = &glightning.FeeRate{
Directive: glightning.Urgent,
}
} else if *req.TargetConf < 30 {
rate = &glightning.FeeRate{
Directive: glightning.Normal,
}
} else {
rate = &glightning.FeeRate{
Directive: glightning.Slow,
}
}
}
fundResult, err := client.FundChannelExt(
pubkey,
glightning.NewSat(int(req.CapacitySat)),
rate,
false,
minConfs,
glightning.NewMsat(0),
&minDepth,
glightning.NewMsat(0),
)
if err != nil {
log.Printf("CLN: client.FundChannelExt(%v, %v) error: %v", pubkey, req.CapacitySat, err)
rpcError, ok := err.(*jrpc2.RpcError)
if ok && rpcError.Code == 301 {
return nil, fmt.Errorf("not enough funds: %w", err)
}
return nil, err
}
fundingTxId, err := chainhash.NewHashFromStr(fundResult.FundingTxId)
if err != nil {
log.Printf("CLN: chainhash.NewHashFromStr(%s) error: %v", fundResult.FundingTxId, err)
return nil, err
}
channelPoint, err := lightning.NewOutPoint(fundingTxId[:], uint32(fundResult.FundingTxOutputNum))
if err != nil {
log.Printf("CLN: NewOutPoint(%s, %d) error: %v", fundingTxId.String(), fundResult.FundingTxOutputNum, err)
return nil, err
}
return channelPoint, nil
}
func (c *ClnClient) GetChannel(peerID []byte, channelPoint wire.OutPoint) (*lightning.GetChannelResult, error) {
client, err := c.getClient()
if err != nil {
return nil, err
}
pubkey := hex.EncodeToString(peerID)
channels, err := client.GetPeerChannels(pubkey)
if err != nil {
log.Printf("CLN: client.GetPeerChannels(%s) error: %v", pubkey, err)
return nil, err
}
fundingTxID := channelPoint.Hash.String()
for _, c := range channels {
log.Printf("getChannel destination: %s, Short channel id: %v, local alias: %v , FundingTxID:%v, State:%v ", pubkey, c.ShortChannelId, c.Alias.Local, c.FundingTxId, c.State)
if slices.Contains(OPEN_STATUSES, c.State) && c.FundingTxId == fundingTxID {
aliasScid, confirmedScid, err := mapScidsFromChannel(c)
if err != nil {
return nil, err
}
return &lightning.GetChannelResult{
AliasScid: aliasScid,
ConfirmedScid: confirmedScid,
HtlcMinimumMsat: c.MinimumHtlcOutMsat.MSat(),
}, nil
}
}
log.Printf("No channel found: getChannel(%v, %v)", pubkey, fundingTxID)
return nil, fmt.Errorf("no channel found")
}
func (c *ClnClient) GetClosedChannels(nodeID string, channelPoints map[string]uint64) (map[string]uint64, error) {
client, err := c.getClient()
if err != nil {
return nil, err
}
r := make(map[string]uint64)
if len(channelPoints) == 0 {
return r, nil
}
channels, err := client.GetPeerChannels(nodeID)
if err != nil {
log.Printf("CLN: client.GetPeer(%s) error: %v", nodeID, err)
return nil, err
}
lookup := make(map[string]uint64)
for _, c := range channels {
if slices.Contains(CLOSING_STATUSES, c.State) {
cid, err := lightning.NewShortChannelIDFromString(c.ShortChannelId)
if err != nil {
log.Printf("CLN: GetClosedChannels NewShortChannelIDFromString(%v) error: %v", c.ShortChannelId, err)
continue
}
outnum := uint64(*cid) & 0xFFFFFF
cp := fmt.Sprintf("%s:%d", c.FundingTxId, outnum)
lookup[cp] = uint64(*cid)
}
}
for c, h := range channelPoints {
if _, ok := lookup[c]; !ok {
r[c] = h
}
}
return r, nil
}
func (c *ClnClient) GetPeerId(scid *lightning.ShortChannelID) ([]byte, error) {
client, err := c.getClient()
if err != nil {
return nil, err
}
scidStr := scid.ToString()
channels, err := client.ListPeerChannels()
if err != nil {
return nil, err
}
var dest *string
for _, ch := range channels {
if (ch.Alias != nil && (ch.Alias.Local == scidStr ||
ch.Alias.Remote == scidStr)) ||
ch.ShortChannelId == scidStr {
dest = &ch.PeerId
break
}
}
if dest == nil {
return nil, nil
}
return hex.DecodeString(*dest)
}
var pollingInterval = 400 * time.Millisecond
func (c *ClnClient) WaitOnline(peerID []byte, deadline time.Time) error {
client, err := c.getClient()
if err != nil {
return err
}
peerIDStr := hex.EncodeToString(peerID)
for {
peer, err := client.GetPeer(peerIDStr)
if err == nil && peer.Connected {
return nil
}
select {
case <-time.After(time.Until(deadline)):
return fmt.Errorf("timeout")
case <-time.After(pollingInterval):
}
}
}
func (c *ClnClient) WaitChannelActive(peerID []byte, deadline time.Time) error {
return nil
}
func (c *ClnClient) ListChannels() ([]*lightning.Channel, error) {
channels, err := c.client.ListPeerChannels()
if err != nil {
return nil, err
}
result := make([]*lightning.Channel, len(channels))
for i, channel := range channels {
peerId, err := hex.DecodeString(channel.PeerId)
if err != nil {
log.Printf("cln.ListChannels returned channel without peer id: %+v", channel)
continue
}
aliasScid, confirmedScid, err := mapScidsFromChannel(channel)
if err != nil {
return nil, err
}
var outpoint *wire.OutPoint
fundingTxId, err := hex.DecodeString(channel.FundingTxId)
if err == nil && fundingTxId != nil && len(fundingTxId) > 0 {
outpoint, _ = lightning.NewOutPoint(fundingTxId, channel.FundingOutnum)
}
if outpoint == nil {
log.Printf("cln.ListChannels returned channel without outpoint: %+v", channel)
continue
}
result[i] = &lightning.Channel{
AliasScid: aliasScid,
ConfirmedScid: confirmedScid,
ChannelPoint: outpoint,
PeerId: peerId,
}
}
return result, nil
}
func mapScidsFromChannel(c *glightning.PeerChannel) (*lightning.ShortChannelID, *lightning.ShortChannelID, error) {
var confirmedScid *lightning.ShortChannelID
var aliasScid *lightning.ShortChannelID
var err error
if c.ShortChannelId != "" {
confirmedScid, err = lightning.NewShortChannelIDFromString(c.ShortChannelId)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse scid '%s': %w", c.ShortChannelId, err)
}
}
if c.Alias != nil && c.Alias.Local != "" {
aliasScid, err = lightning.NewShortChannelIDFromString(c.Alias.Local)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse scid '%s': %w", c.Alias.Local, err)
}
}
return aliasScid, confirmedScid, nil
}