mirror of
https://github.com/aljazceru/lspd.git
synced 2025-12-18 14:24:21 +01:00
381 lines
9.3 KiB
Go
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
|
|
}
|