From 0300f4cf9fcf0fd7f2ad3d414ae98f1e78e49eb9 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 11 Apr 2023 10:43:02 -0700 Subject: [PATCH] Refactor raiju interface for importing --- cmd/raiju/raiju.go | 64 ++++++++++++--- fees.go | 57 ++++--------- fees_test.go | 54 ++++-------- print.go | 31 ------- raiju.go | 168 ++++++++++++++++++------------------- raiju_test.go | 201 ++++++++++++++++++++++++--------------------- view/view.go | 47 +++++++++++ 7 files changed, 321 insertions(+), 301 deletions(-) delete mode 100644 print.go create mode 100644 view/view.go diff --git a/cmd/raiju/raiju.go b/cmd/raiju/raiju.go index 6c6a80f..98e43d4 100644 --- a/cmd/raiju/raiju.go +++ b/cmd/raiju/raiju.go @@ -19,6 +19,7 @@ import ( "github.com/nyonson/raiju" "github.com/nyonson/raiju/lightning" "github.com/nyonson/raiju/lnd" + "github.com/nyonson/raiju/view" ) const ( @@ -143,9 +144,16 @@ func main() { Clearnet: *clearnet, } - _, err = r.Candidates(ctx, request) + cmdLog.Printf("filtering candidates by capacity: %d, channels: %d, distance: %d, distant neighbors: %d\n", request.MinCapacity, request.MinChannels, request.MinDistance, request.MinDistantNeighbors) - return err + candidates, err := r.Candidates(ctx, request) + if err != nil { + return err + } + + view.PrintNodes(candidates) + + return nil }, } @@ -182,13 +190,30 @@ func main() { return err } - f.PrintSettings() + view.PrintFees(f) r := raiju.New(c, f) - _, err = r.Fees(ctx, *daemon) + uc, ec, err := r.Fees(ctx) + if err != nil { + return err + } + + // listen for updates + if *daemon { + for { + select { + case u := <-uc: + for id, fee := range u { + cmdLog.Printf("channel %d updated to %f fee PPM", id, fee) + } + case err := <-ec: + return err + } + } + } - return err + return nil }, } @@ -241,7 +266,7 @@ func main() { return err } - f.PrintSettings() + view.PrintFees(f) r := raiju.New(c, f) @@ -252,12 +277,24 @@ func main() { } if *lastHopPubkey != "" { - _, _, err = r.Rebalance(ctx, lightning.ChannelID(*outChannelID), lightning.PubKey(*lastHopPubkey), stepPercent, maxPercent, maxFee) + cmdLog.Println("Rebalancing channel...") + percent, fee, err := r.Rebalance(ctx, lightning.ChannelID(*outChannelID), lightning.PubKey(*lastHopPubkey), stepPercent, maxPercent, maxFee) + if err != nil { + return err + } + cmdLog.Printf("rebalanced %f percent with a %d sat fee\n", percent, fee) } else { - err = r.RebalanceAll(ctx, stepPercent, maxPercent, maxFee) + cmdLog.Println("Rebalancing all channels...") + rebalanced, err := r.RebalanceAll(ctx, stepPercent, maxPercent, maxFee) + if err != nil { + return err + } + for id, percent := range rebalanced { + cmdLog.Printf("rebalanced %f percent of channel %d\n", percent, id) + } } - return err + return nil }, } @@ -295,9 +332,14 @@ func main() { r := raiju.New(c, f) - _, err = r.Reaper(ctx) + channels, err := r.Reaper(ctx) + if err != nil { + return err + } + + view.PrintChannels(channels) - return err + return nil }, } diff --git a/fees.go b/fees.go index 0fdc78d..2aff87f 100644 --- a/fees.go +++ b/fees.go @@ -2,8 +2,6 @@ package raiju import ( "errors" - "fmt" - "os" "github.com/nyonson/raiju/lightning" ) @@ -14,9 +12,9 @@ import ( // When liquidity is low, there is too much inbound. // When liquidity is high, there is too much outbound. type LiquidityFees struct { - thresholds []float64 - fees []lightning.FeePPM - stickiness float64 + Thresholds []float64 + Fees []lightning.FeePPM + Stickiness float64 } // Fee for channel based on its current liquidity. @@ -35,8 +33,8 @@ func (lf LiquidityFees) PotentialFee(channel lightning.Channel, additionalLocal func (lf LiquidityFees) findFee(liquidity float64, currentFee lightning.FeePPM) lightning.FeePPM { bucket := 0 - for bucket < len(lf.thresholds) { - if liquidity > lf.thresholds[bucket] { + for bucket < len(lf.Thresholds) { + if liquidity > lf.Thresholds[bucket] { break } else { bucket += 1 @@ -44,13 +42,13 @@ func (lf LiquidityFees) findFee(liquidity float64, currentFee lightning.FeePPM) } - newFee := lf.fees[bucket] + newFee := lf.Fees[bucket] // apply stickiness if fee is heading in the right direction, but wanna hold on for a bit to limit gossip if liquidity < 50 && newFee < currentFee { lowBucket := 0 - for lowBucket < len(lf.thresholds) { - if liquidity > lf.thresholds[lowBucket]+lf.stickiness { + for lowBucket < len(lf.Thresholds) { + if liquidity > lf.Thresholds[lowBucket]+lf.Stickiness { break } else { lowBucket += 1 @@ -58,14 +56,11 @@ func (lf LiquidityFees) findFee(liquidity float64, currentFee lightning.FeePPM) } - if lowBucket != bucket { - fmt.Fprintf(os.Stderr, "keeping fee due to stickiness\n") - } - newFee = lf.fees[lowBucket] + newFee = lf.Fees[lowBucket] } else if liquidity >= 50 && newFee > currentFee { highBucket := 0 - for highBucket < len(lf.thresholds) { - if liquidity > lf.thresholds[highBucket]-lf.stickiness { + for highBucket < len(lf.Thresholds) { + if liquidity > lf.Thresholds[highBucket]-lf.Stickiness { break } else { highBucket += 1 @@ -73,10 +68,7 @@ func (lf LiquidityFees) findFee(liquidity float64, currentFee lightning.FeePPM) } - if highBucket != bucket { - fmt.Fprintf(os.Stderr, "keeping fee due to stickiness\n") - } - newFee = lf.fees[highBucket] + newFee = lf.Fees[highBucket] } return newFee @@ -86,11 +78,11 @@ func (lf LiquidityFees) findFee(liquidity float64, currentFee lightning.FeePPM) func (lf LiquidityFees) RebalanceChannels(channels lightning.Channels) (high lightning.Channels, low lightning.Channels) { for _, c := range channels { l := c.Liquidity() - if l > lf.thresholds[0] { + if l > lf.Thresholds[0] { high = append(high, c) } - if l <= lf.thresholds[len(lf.thresholds)-1] { + if l <= lf.Thresholds[len(lf.Thresholds)-1] { low = append(low, c) } } @@ -100,20 +92,7 @@ func (lf LiquidityFees) RebalanceChannels(channels lightning.Channels) (high lig // RebalanceFee is the max fee to use in a circular rebalance to ensure its not wasted. func (lf LiquidityFees) RebalanceFee() lightning.FeePPM { - return lf.fees[len(lf.fees)-1] -} - -// PrintSettings to output. -func (lf LiquidityFees) PrintSettings() { - for i := 0; i < len(lf.fees); i++ { - if i == len(lf.fees)-1 { - fmt.Fprintf(os.Stderr, "channels under %g%% local liquidity to %g ppm\n", lf.thresholds[i-1], lf.fees[i]) - } else if i == 0 { - fmt.Fprintf(os.Stderr, "channels over %g%% local liquidity to %g ppm, ", lf.thresholds[i], lf.fees[i]) - } else { - fmt.Fprintf(os.Stderr, "channels between %g%% and %g%% local liquidity to %g ppm, ", lf.thresholds[i-1], lf.thresholds[i], lf.fees[i]) - } - } + return lf.Fees[len(lf.Fees)-1] } // NewLiquidityFees with threshold and fee validation. @@ -144,8 +123,8 @@ func NewLiquidityFees(thresholds []float64, fees []lightning.FeePPM, stickiness } return LiquidityFees{ - thresholds: thresholds, - fees: fees, - stickiness: stickiness, + Thresholds: thresholds, + Fees: fees, + Stickiness: stickiness, }, nil } diff --git a/fees_test.go b/fees_test.go index 9535734..b2ef722 100644 --- a/fees_test.go +++ b/fees_test.go @@ -138,9 +138,9 @@ func TestLiquidityFees_Fee(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { lf := LiquidityFees{ - thresholds: tt.fields.thresholds, - fees: tt.fields.fees, - stickiness: tt.fields.stickiness, + Thresholds: tt.fields.thresholds, + Fees: tt.fields.fees, + Stickiness: tt.fields.stickiness, } if got := lf.Fee(tt.args.channel); !reflect.DeepEqual(got, tt.want) { t.Errorf("LiquidityFees.Fee() = %v, want %v", got, tt.want) @@ -198,9 +198,9 @@ func TestLiquidityFees_PotentialFee(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { lf := LiquidityFees{ - thresholds: tt.fields.thresholds, - fees: tt.fields.fees, - stickiness: tt.fields.stickiness, + Thresholds: tt.fields.thresholds, + Fees: tt.fields.fees, + Stickiness: tt.fields.stickiness, } if got := lf.PotentialFee(tt.args.channel, tt.args.additionalLocal); !reflect.DeepEqual(got, tt.want) { t.Errorf("LiquidityFees.PotentialFee() = %v, want %v", got, tt.want) @@ -330,9 +330,9 @@ func TestLiquidityFees_RebalanceChannels(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { lf := LiquidityFees{ - thresholds: tt.fields.thresholds, - fees: tt.fields.fees, - stickiness: tt.fields.stickiness, + Thresholds: tt.fields.thresholds, + Fees: tt.fields.fees, + Stickiness: tt.fields.stickiness, } gotHigh, gotLow := lf.RebalanceChannels(tt.args.channels) if !reflect.DeepEqual(gotHigh, tt.wantHigh) { @@ -369,9 +369,9 @@ func TestLiquidityFees_RebalanceFee(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { lf := LiquidityFees{ - thresholds: tt.fields.thresholds, - fees: tt.fields.fees, - stickiness: tt.fields.stickiness, + Thresholds: tt.fields.thresholds, + Fees: tt.fields.fees, + Stickiness: tt.fields.stickiness, } if got := lf.RebalanceFee(); !reflect.DeepEqual(got, tt.want) { t.Errorf("LiquidityFees.RebalanceFee() = %v, want %v", got, tt.want) @@ -400,9 +400,9 @@ func TestNewLiquidityFees(t *testing.T) { stickiness: 0, }, want: LiquidityFees{ - thresholds: []float64{80, 20}, - fees: []lightning.FeePPM{5, 50, 500}, - stickiness: 0, + Thresholds: []float64{80, 20}, + Fees: []lightning.FeePPM{5, 50, 500}, + Stickiness: 0, }, wantErr: false, }, @@ -447,27 +447,3 @@ func TestNewLiquidityFees(t *testing.T) { }) } } - -func TestLiquidityFees_PrintSettings(t *testing.T) { - type fields struct { - thresholds []float64 - fees []lightning.FeePPM - stickiness float64 - } - tests := []struct { - name string - fields fields - }{ - // No tests - } - for _, tt := range tests { - t.Run(tt.name, func(_ *testing.T) { - lf := LiquidityFees{ - thresholds: tt.fields.thresholds, - fees: tt.fields.fees, - stickiness: tt.fields.stickiness, - } - lf.PrintSettings() - }) - } -} diff --git a/print.go b/print.go deleted file mode 100644 index 60ec275..0000000 --- a/print.go +++ /dev/null @@ -1,31 +0,0 @@ -package raiju - -import ( - "github.com/nyonson/raiju/lightning" - "github.com/rodaine/table" -) - -// printNodes in table formatted list. -func printNodes(nodes []RelativeNode) error { - tbl := table.New("Pubkey", "Alias", "Distance", "Distant Neighbors", "Capacity (BTC)", "Channels", "Updated", "Addresses") - - for _, v := range nodes { - tbl.AddRow(v.PubKey, v.Alias, v.distance, v.distantNeigbors, lightning.Satoshi(v.capacity).BTC(), v.channels, v.Updated, v.Addresses) - } - tbl.Print() - - return nil -} - -// printChannels in table formatted list. -func printChannels(channels lightning.Channels) error { - tbl := table.New("Channel ID", "Alias", "Capacity (BTC)") - - for _, c := range channels { - tbl.AddRow(c.ChannelID, c.RemoteNode.Alias, lightning.Satoshi(c.Capacity).BTC()) - } - - tbl.Print() - - return nil -} diff --git a/raiju.go b/raiju.go index 9ca8114..84c177a 100644 --- a/raiju.go +++ b/raiju.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "math/rand" - "os" "sort" "time" @@ -44,11 +43,11 @@ func New(l lightninger, r LiquidityFees) Raiju { // RelativeNode has information on a node's graph characteristics relative to other nodes. type RelativeNode struct { lightning.Node - distance int64 - distantNeigbors int64 - channels int64 - capacity lightning.Satoshi - neighbors []lightning.PubKey + Distance int64 + DistantNeigbors int64 + Channels int64 + Capacity lightning.Satoshi + Neighbors []lightning.PubKey } // sortDistance sorts nodes by distance, distant neighbors, capacity, and channels @@ -56,19 +55,19 @@ type sortDistance []RelativeNode // Less is true if i is closer than j. func (s sortDistance) Less(i, j int) bool { - if s[i].distance != s[j].distance { - return s[i].distance < s[j].distance + if s[i].Distance != s[j].Distance { + return s[i].Distance < s[j].Distance } - if s[i].distantNeigbors != s[j].distantNeigbors { - return s[i].distantNeigbors < s[j].distantNeigbors + if s[i].DistantNeigbors != s[j].DistantNeigbors { + return s[i].DistantNeigbors < s[j].DistantNeigbors } - if s[i].capacity != s[j].capacity { - return s[i].capacity < s[j].capacity + if s[i].Capacity != s[j].Capacity { + return s[i].Capacity < s[j].Capacity } - return s[i].channels < s[j].channels + return s[i].Channels < s[j].Channels } // Swap nodes in slice. @@ -122,9 +121,6 @@ func (r Raiju) Candidates(ctx context.Context, request CandidatesRequest) ([]Rel return nil, err } - fmt.Fprintf(os.Stderr, "network contains %d nodes total\n", len(channelGraph.Nodes)) - fmt.Fprintf(os.Stderr, "filtering candidates by capacity: %d, channels: %d, distance: %d, distant neighbors: %d\n", request.MinCapacity, request.MinChannels, request.MinDistance, request.MinDistantNeighbors) - // initialize nodes map with static info nodes := make(map[lightning.PubKey]*RelativeNode, len(channelGraph.Nodes)) @@ -136,23 +132,23 @@ func (r Raiju) Candidates(ctx context.Context, request CandidatesRequest) ([]Rel // calculate node properties based on channels: neighbors, capacity, channels for _, e := range channelGraph.Edges { - if nodes[e.Node1].neighbors != nil { - nodes[e.Node1].neighbors = append(nodes[e.Node1].neighbors, e.Node2) + if nodes[e.Node1].Neighbors != nil { + nodes[e.Node1].Neighbors = append(nodes[e.Node1].Neighbors, e.Node2) } else { - nodes[e.Node1].neighbors = []lightning.PubKey{e.Node2} + nodes[e.Node1].Neighbors = []lightning.PubKey{e.Node2} } - if nodes[e.Node2].neighbors != nil { - nodes[e.Node2].neighbors = append(nodes[e.Node2].neighbors, e.Node1) + if nodes[e.Node2].Neighbors != nil { + nodes[e.Node2].Neighbors = append(nodes[e.Node2].Neighbors, e.Node1) } else { - nodes[e.Node2].neighbors = []lightning.PubKey{e.Node1} + nodes[e.Node2].Neighbors = []lightning.PubKey{e.Node1} } - nodes[e.Node1].capacity += e.Capacity - nodes[e.Node2].capacity += e.Capacity + nodes[e.Node1].Capacity += e.Capacity + nodes[e.Node2].Capacity += e.Capacity - nodes[e.Node1].channels++ - nodes[e.Node2].channels++ + nodes[e.Node1].Channels++ + nodes[e.Node2].Channels++ } // Add assumes to root node @@ -161,16 +157,16 @@ func (r Raiju) Candidates(ctx context.Context, request CandidatesRequest) ([]Rel return []RelativeNode{}, errors.New("candidate node does not exist") } - if nodes[request.PubKey].neighbors != nil { - nodes[request.PubKey].neighbors = append(nodes[request.PubKey].neighbors, c) + if nodes[request.PubKey].Neighbors != nil { + nodes[request.PubKey].Neighbors = append(nodes[request.PubKey].Neighbors, c) } else { - nodes[request.PubKey].neighbors = []lightning.PubKey{c} + nodes[request.PubKey].Neighbors = []lightning.PubKey{c} } - if nodes[c].neighbors != nil { - nodes[c].neighbors = append(nodes[c].neighbors, request.PubKey) + if nodes[c].Neighbors != nil { + nodes[c].Neighbors = append(nodes[c].Neighbors, request.PubKey) } else { - nodes[c].neighbors = []lightning.PubKey{request.PubKey} + nodes[c].Neighbors = []lightning.PubKey{request.PubKey} } } @@ -183,20 +179,20 @@ func (r Raiju) Candidates(ctx context.Context, request CandidatesRequest) ([]Rel // initialize search from root node's neighbors if n, ok := nodes[request.PubKey]; ok { // root node has no distance to self - n.distance = 0 + n.Distance = 0 // mark root as visited visited[n.PubKey] = true - neighbors = n.neighbors + neighbors = n.Neighbors } for len(neighbors) > 0 { next := make([]lightning.PubKey, 0) for _, n := range neighbors { if !visited[n] { - nodes[n].distance = distance + nodes[n].Distance = distance visited[n] = true - for _, neighbor := range nodes[n].neighbors { + for _, neighbor := range nodes[n].Neighbors { if !visited[neighbor] { next = append(next, neighbor) } @@ -218,22 +214,22 @@ func (r Raiju) Candidates(ctx context.Context, request CandidatesRequest) ([]Rel // calculate number of distant neighbors per node for node := range unfilteredSpan { var count int64 - for _, neighbor := range unfilteredSpan[node].neighbors { - if nodes[neighbor].distance > distantNeighborLimit { + for _, neighbor := range unfilteredSpan[node].Neighbors { + if nodes[neighbor].Distance > distantNeighborLimit { count++ } } - unfilteredSpan[node].distantNeigbors = count + unfilteredSpan[node].DistantNeigbors = count } // filter nodes by request conditions allCandidates := make([]RelativeNode, 0) for _, v := range unfilteredSpan { - if v.capacity >= request.MinCapacity && - v.channels >= request.MinChannels && - v.distance >= request.MinDistance && - v.distantNeigbors >= request.MinDistantNeighbors && + if v.Capacity >= request.MinCapacity && + v.Channels >= request.MinChannels && + v.Distance >= request.MinDistance && + v.DistantNeigbors >= request.MinDistantNeighbors && v.Updated.After(request.MinUpdated) { if request.Clearnet { if v.Clearnet() { @@ -252,61 +248,66 @@ func (r Raiju) Candidates(ctx context.Context, request CandidatesRequest) ([]Rel candidates = allCandidates[:request.Limit] } - printNodes(candidates) - return candidates, nil } // Fees to encourage a balanced channel. // -// Daemon mode continuously updates policies as channel liquidity changes. -func (r Raiju) Fees(ctx context.Context, daemon bool) (map[lightning.ChannelID]lightning.FeePPM, error) { +// Fees are initially set across all channels and then continuously updated as channel liquidity changes. +func (r Raiju) Fees(ctx context.Context) (chan map[lightning.ChannelID]lightning.FeePPM, chan error, error) { channels, err := r.l.ListChannels(ctx) if err != nil { - return map[lightning.ChannelID]lightning.FeePPM{}, err + return nil, nil, err } - updates, err := r.setFees(ctx, channels) + // buffer the channel for the first update + updates := make(chan map[lightning.ChannelID]lightning.FeePPM, 1) + errors := make(chan error) + // make sure updated at least once + u, err := r.setFees(ctx, channels) if err != nil { - return map[lightning.ChannelID]lightning.FeePPM{}, err + return nil, nil, err } - if daemon { - cc, ce, err := r.l.SubscribeChannelUpdates(ctx) - if err != nil { - return map[lightning.ChannelID]lightning.FeePPM{}, err - } + updates <- u + // listen for channel updates to keep fees in sync + cc, ce, err := r.l.SubscribeChannelUpdates(ctx) + if err != nil { + return nil, nil, err + } + + go func() { for { select { case channels = <-cc: - _, err = r.setFees(ctx, channels) + u, err = r.setFees(ctx, channels) if err != nil { - fmt.Fprintf(os.Stderr, "error setting fees %v\n", err) + errors <- fmt.Errorf("error setting fees: %w", err) + } else { + updates <- u } case err := <-ce: - fmt.Fprintf(os.Stderr, "error listening to channel updates %v\n", err) + errors <- fmt.Errorf("error listening to channel updates: %w", err) } } - } + }() - return updates, nil + return updates, errors, nil } // setFees on channels who's liquidity has changed, return updated channels and their new liquidity level. func (r Raiju) setFees(ctx context.Context, channels lightning.Channels) (map[lightning.ChannelID]lightning.FeePPM, error) { - updates := make(map[lightning.ChannelID]lightning.FeePPM) + updates := map[lightning.ChannelID]lightning.FeePPM{} // update channel fees based on liquidity, but only change if necessary for _, c := range channels { fee := r.f.Fee(c) if c.LocalFee != fee { - fmt.Fprintf(os.Stderr, "channel %s (%d) now has liquidity %g, setting fee to %g\n", c.RemoteNode.Alias, c.ChannelID, c.Liquidity(), fee) err := r.l.SetFees(ctx, c.ChannelID, fee) if err != nil { - fmt.Fprintf(os.Stderr, "error updating fees %v\n", err) - } else { - updates[c.ChannelID] = fee + return map[lightning.ChannelID]lightning.FeePPM{}, err } + updates[c.ChannelID] = fee } } @@ -331,37 +332,33 @@ func (r Raiju) Rebalance(ctx context.Context, outChannelID lightning.ChannelID, var totalFeePaid lightning.Satoshi for percentRebalanced < maxPercent { - fmt.Fprintf(os.Stderr, "attempting rebalance %d sats out of %s (%d) to %s with a %g max fee ppm...\n", amount, c.RemoteNode.Alias, outChannelID, lastHopPubKey, maxFee) // create and pay invoice invoice, err := r.l.AddInvoice(ctx, lightning.Satoshi(amount)) if err != nil { - fmt.Fprintf(os.Stderr, "rebalance failed\n") - return percentRebalanced, totalFeePaid, nil + return 0, 0, fmt.Errorf("error creating circular rebalance invoice: %w", err) } feePaid, err := r.l.SendPayment(ctx, invoice, outChannelID, lastHopPubKey, maxFee) + // not expecting rebalance payments to work all that often, so just short circuit and return what has been done if err != nil { - fmt.Fprintf(os.Stderr, "rebalance failed\n") return percentRebalanced, totalFeePaid, nil } - fmt.Fprintf(os.Stderr, "rebalance success %d sats out of %s (%d) to %s for a %d sats fee\n", amount, c.RemoteNode.Alias, outChannelID, lastHopPubKey, feePaid) percentRebalanced += stepPercent totalFeePaid += feePaid - fmt.Fprintf(os.Stderr, "rebalance has moved %f percent of max %f percent of the channel capacity\n", percentRebalanced, maxPercent) } return percentRebalanced, totalFeePaid, nil } -// RebalanceAll channels. -func (r Raiju) RebalanceAll(ctx context.Context, stepPercent float64, maxPercent float64, maxFee lightning.FeePPM) error { +// RebalanceAll high local liquidity channels into low liquidity channels, return percent rebalanced per channel attempted. +func (r Raiju) RebalanceAll(ctx context.Context, stepPercent float64, maxPercent float64, maxFee lightning.FeePPM) (map[lightning.ChannelID]float64, error) { local, err := r.l.GetInfo(ctx) if err != nil { - return err + return map[lightning.ChannelID]float64{}, err } channels, err := r.l.ListChannels(ctx) if err != nil { - return err + return map[lightning.ChannelID]float64{}, err } hlcs, llcs := r.f.RebalanceChannels(channels) @@ -372,10 +369,10 @@ func (r Raiju) RebalanceAll(ctx context.Context, stepPercent float64, maxPercent }) var totalFeePaid lightning.Satoshi + rebalanced := map[lightning.ChannelID]float64{} // Roll through high liquidity channels and try to push things through the low liquidity ones. for _, h := range hlcs { - fmt.Fprintf(os.Stderr, "channel %d with %s has high liquidity, attempting to rebalancing into low liquidity channels\n", h.ChannelID, h.RemoteNode.Alias) percentRebalanced := float64(0) // reshuffle low liquidity channels each time @@ -393,24 +390,23 @@ func (r Raiju) RebalanceAll(ctx context.Context, stepPercent float64, maxPercent // to rebalance and then a standard payment cancels out the liquidity ul, err := r.l.GetChannel(ctx, l.ChannelID) if err != nil { - return err + return map[lightning.ChannelID]float64{}, err } potentialLocal := lightning.Satoshi(float64(h.Capacity) * maxPercent) if r.f.PotentialFee(ul, potentialLocal) != r.f.Fee(ul) { - // don't really care if error or not, just continue on - p, f, _ := r.Rebalance(ctx, h.ChannelID, lastHopPubkey, stepPercent, (maxPercent - percentRebalanced), maxFee) + p, f, err := r.Rebalance(ctx, h.ChannelID, lastHopPubkey, stepPercent, (maxPercent - percentRebalanced), maxFee) + if err != nil { + return map[lightning.ChannelID]float64{}, err + } + percentRebalanced += p totalFeePaid += f - } else { - fmt.Fprintf(os.Stderr, "channel %d with %s would no longer have low liquidity, skipping\n", ul.ChannelID, ul.RemoteNode.Alias) } } - fmt.Fprintf(os.Stderr, "rebalanced %f percent of channel %d with %s\n", percentRebalanced, h.ChannelID, h.RemoteNode.Alias) + rebalanced[h.ChannelID] = percentRebalanced } - fmt.Fprintf(os.Stderr, "rebalanced channels paying a total fee of %d sats\n", totalFeePaid) - - return nil + return rebalanced, nil } // Reaper calculates inefficient channels which should be closed. @@ -444,7 +440,5 @@ func (r Raiju) Reaper(ctx context.Context) (lightning.Channels, error) { } } - printChannels(inefficient) - return inefficient, nil } diff --git a/raiju_test.go b/raiju_test.go index 54c4d2d..0990186 100644 --- a/raiju_test.go +++ b/raiju_test.go @@ -144,11 +144,11 @@ func TestRaiju_Candidates(t *testing.T) { Updated: updated, Addresses: []string{clearnetAddress}, }, - distance: 3, - distantNeigbors: 0, - channels: 1, - capacity: 1, - neighbors: []lightning.PubKey{pubKeyC}, + Distance: 3, + DistantNeigbors: 0, + Channels: 1, + Capacity: 1, + Neighbors: []lightning.PubKey{pubKeyC}, }, { Node: lightning.Node{ @@ -157,11 +157,11 @@ func TestRaiju_Candidates(t *testing.T) { Updated: updated, Addresses: []string{clearnetAddress}, }, - distance: 2, - distantNeigbors: 1, - channels: 2, - capacity: 2, - neighbors: []lightning.PubKey{pubKeyB, pubKeyD}, + Distance: 2, + DistantNeigbors: 1, + Channels: 2, + Capacity: 2, + Neighbors: []lightning.PubKey{pubKeyB, pubKeyD}, }, }, wantErr: false, @@ -279,11 +279,11 @@ func TestRaiju_Candidates(t *testing.T) { Updated: updated, Addresses: []string{clearnetAddress}, }, - distance: 3, - distantNeigbors: 0, - channels: 2, - capacity: 2, - neighbors: []lightning.PubKey{pubKeyC, pubKeyE}, + Distance: 3, + DistantNeigbors: 0, + Channels: 2, + Capacity: 2, + Neighbors: []lightning.PubKey{pubKeyC, pubKeyE}, }, }, wantErr: false, @@ -327,11 +327,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 0, - distantNeigbors: 0, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 0, + DistantNeigbors: 0, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, { Node: lightning.Node{ @@ -340,11 +340,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 1, - distantNeigbors: 0, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 1, + DistantNeigbors: 0, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, }, @@ -364,11 +364,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 1, - distantNeigbors: 0, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 1, + DistantNeigbors: 0, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, { Node: lightning.Node{ @@ -377,11 +377,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 1, - distantNeigbors: 1, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 1, + DistantNeigbors: 1, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, }, @@ -401,11 +401,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 1, - distantNeigbors: 1, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 1, + DistantNeigbors: 1, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, { Node: lightning.Node{ @@ -414,11 +414,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 1, - distantNeigbors: 1, - channels: 0, - capacity: 1, - neighbors: []lightning.PubKey{}, + Distance: 1, + DistantNeigbors: 1, + Channels: 0, + Capacity: 1, + Neighbors: []lightning.PubKey{}, }, }, @@ -438,11 +438,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 1, - distantNeigbors: 1, - channels: 0, - capacity: 1, - neighbors: []lightning.PubKey{}, + Distance: 1, + DistantNeigbors: 1, + Channels: 0, + Capacity: 1, + Neighbors: []lightning.PubKey{}, }, { Node: lightning.Node{ @@ -451,11 +451,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 1, - distantNeigbors: 1, - channels: 1, - capacity: 1, - neighbors: []lightning.PubKey{}, + Distance: 1, + DistantNeigbors: 1, + Channels: 1, + Capacity: 1, + Neighbors: []lightning.PubKey{}, }, }, @@ -475,11 +475,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 0, - distantNeigbors: 0, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 0, + DistantNeigbors: 0, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, { Node: lightning.Node{ @@ -488,11 +488,11 @@ func Test_sortDistance_Less(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 0, - distantNeigbors: 0, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 0, + DistantNeigbors: 0, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, }, @@ -527,19 +527,19 @@ func Test_sortDistance_Swap(t *testing.T) { s: []RelativeNode{ { Node: lightning.Node{}, - distance: 0, - distantNeigbors: 0, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 0, + DistantNeigbors: 0, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, { Node: lightning.Node{}, - distance: 0, - distantNeigbors: 0, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 0, + DistantNeigbors: 0, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, }, args: args{ @@ -571,11 +571,11 @@ func Test_sortDistance_Len(t *testing.T) { Updated: updated, Addresses: []string{}, }, - distance: 0, - distantNeigbors: 0, - channels: 0, - capacity: 0, - neighbors: []lightning.PubKey{}, + Distance: 0, + DistantNeigbors: 0, + Channels: 0, + Capacity: 0, + Neighbors: []lightning.PubKey{}, }, }, want: 1, @@ -718,10 +718,11 @@ func TestRaiju_RebalanceAll(t *testing.T) { name string fields fields args args + want map[lightning.ChannelID]float64 wantErr bool }{ { - name: "happy rebalance all", + name: "rebalance all only one channel", fields: fields{ l: &lightningerMock{ AddInvoiceFunc: func(ctx context.Context, amount lightning.Satoshi) (lightning.Invoice, error) { @@ -754,6 +755,7 @@ func TestRaiju_RebalanceAll(t *testing.T) { maxPercent: 5, maxFee: 10, }, + want: map[lightning.ChannelID]float64{}, wantErr: false, }, } @@ -762,9 +764,13 @@ func TestRaiju_RebalanceAll(t *testing.T) { r := Raiju{ l: tt.fields.l, } - if err := r.RebalanceAll(tt.args.ctx, tt.args.stepPercent, tt.args.maxPercent, tt.args.maxFee); (err != nil) != tt.wantErr { + got, err := r.RebalanceAll(tt.args.ctx, tt.args.stepPercent, tt.args.maxPercent, tt.args.maxFee) + if (err != nil) != tt.wantErr { t.Errorf("Raiju.RebalanceAll() error = %v, wantErr %v", err, tt.wantErr) } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Raiju.RebalanceAll() = %v, want %v", got, tt.want) + } }) } } @@ -848,8 +854,7 @@ func TestRaiju_Fees(t *testing.T) { f LiquidityFees } type args struct { - ctx context.Context - daemon bool + ctx context.Context } tests := []struct { name string @@ -905,12 +910,12 @@ func TestRaiju_Fees(t *testing.T) { }, }, f: LiquidityFees{ - thresholds: []float64{80, 20}, - fees: []lightning.FeePPM{5, 10, 100}, + Thresholds: []float64{80, 20}, + Fees: []lightning.FeePPM{5, 10, 100}, }, }, args: args{ - daemon: false, + ctx: context.Background(), }, want: map[lightning.ChannelID]lightning.FeePPM{ lightning.ChannelID(1): lightning.FeePPM(100), @@ -924,13 +929,21 @@ func TestRaiju_Fees(t *testing.T) { l: tt.fields.l, f: tt.fields.f, } - got, err := r.Fees(tt.args.ctx, tt.args.daemon) + + uc, ec, err := r.Fees(tt.args.ctx) if (err != nil) != tt.wantErr { t.Errorf("Raiju.Fees() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Raiju.Fees() = %v, want %v", got, tt.want) + + select { + case got := <-uc: + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Raiju.Fees() = %v, want %v", got, tt.want) + } + case err := <-ec: + t.Errorf("Raiju.Fees() error = %v, wantErr %v", err, tt.wantErr) + return } }) } @@ -951,15 +964,15 @@ func TestNew(t *testing.T) { args: args{ l: nil, r: LiquidityFees{ - thresholds: []float64{80, 20}, - fees: []lightning.FeePPM{}, + Thresholds: []float64{80, 20}, + Fees: []lightning.FeePPM{}, }, }, want: Raiju{ l: nil, f: LiquidityFees{ - thresholds: []float64{80, 20}, - fees: []lightning.FeePPM{}, + Thresholds: []float64{80, 20}, + Fees: []lightning.FeePPM{}, }, }, }, diff --git a/view/view.go b/view/view.go new file mode 100644 index 0000000..5c4e4a8 --- /dev/null +++ b/view/view.go @@ -0,0 +1,47 @@ +package view + +import ( + "github.com/nyonson/raiju" + "github.com/nyonson/raiju/lightning" + "github.com/rodaine/table" +) + +// PrintNodes in table formatted list. +func PrintNodes(nodes []raiju.RelativeNode) error { + tbl := table.New("Pubkey", "Alias", "Distance", "Distant Neighbors", "Capacity (BTC)", "Channels", "Updated", "Addresses") + + for _, v := range nodes { + tbl.AddRow(v.PubKey, v.Alias, v.Distance, v.DistantNeigbors, lightning.Satoshi(v.Capacity).BTC(), v.Channels, v.Updated, v.Addresses) + } + tbl.Print() + + return nil +} + +// PrintChannels in table formatted list. +func PrintChannels(channels lightning.Channels) error { + tbl := table.New("Channel ID", "Alias", "Capacity (BTC)") + + for _, c := range channels { + tbl.AddRow(c.ChannelID, c.RemoteNode.Alias, lightning.Satoshi(c.Capacity).BTC()) + } + + tbl.Print() + + return nil +} + +// PrintSettings to output. +func PrintFees(lf raiju.LiquidityFees) error { + tbl := table.New("Local Liquidity Threshold Percent", "Fee PPM") + + for i := 0; i < len(lf.Thresholds); i++ { + tbl.AddRow(lf.Thresholds[i], lf.Fees[i]) + } + + tbl.AddRow(0, lf.Fees[len(lf.Fees)-1]) + + tbl.Print() + + return nil +}