2022-08-20 14:11:45 +00:00
package main
import (
"context"
2022-08-20 17:43:51 +00:00
"encoding/json"
2022-08-20 14:11:45 +00:00
"log"
"math/rand"
"os"
2022-09-28 15:08:32 +00:00
"strings"
2022-08-20 14:11:45 +00:00
"time"
2022-09-28 15:08:32 +00:00
"github.com/BurntSushi/toml"
2022-08-20 14:11:45 +00:00
"github.com/jessevdk/go-flags"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
)
2022-08-20 17:43:51 +00:00
var mainParams struct {
Config string ` short:"f" long:"config" description:"config file path" `
}
2022-08-20 14:11:45 +00:00
var params struct {
2022-08-21 14:16:00 +00:00
Config string ` short:"f" long:"config" description:"config file path" `
2022-08-21 08:26:50 +00:00
Connect string ` short:"c" long:"connect" description:"connect to lnd using host:port" json:"connect" `
TLSCert string ` short:"t" long:"tlscert" description:"path to tls.cert to connect" required:"false" json:"tlscert" `
2022-09-28 15:08:32 +00:00
MacaroonDir string ` long:"macaroon-dir" description:"path to the macaroon directory" required:"false" json:"macaroon_dir" toml:"macaroon_dir" `
MacaroonFilename string ` long:"macaroon-filename" description:"macaroon filename" json:"macaroon_filename" toml:"macaroon_filename" `
2022-08-21 08:26:50 +00:00
Network string ` short:"n" long:"network" description:"bitcoin network to use" json:"network" `
2022-09-28 15:08:32 +00:00
FromPerc int64 ` long:"pfrom" description:"channels with less than this inbound liquidity percentage will be considered as source channels" json:"pfrom" toml:"pfrom" `
ToPerc int64 ` long:"pto" description:"channels with less than this outbound liquidity percentage will be considered as target channels" json:"pto" toml:"pto" `
2022-08-21 08:26:50 +00:00
Perc int64 ` short:"p" long:"perc" description:"use this value as both pfrom and pto from above" json:"perc" `
Amount int64 ` short:"a" long:"amount" description:"amount to rebalance" json:"amount" `
2022-09-28 15:08:32 +00:00
EconRatio float64 ` short:"r" long:"econ-ratio" description:"economical ratio for fee limit calculation as a multiple of target channel fee (for example, 0.5 means you want to pay at max half the fee you might earn for routing out of the target channel)" json:"econ_ratio" toml:"econ_ratio" `
2022-10-01 15:06:44 +00:00
FeeLimitPPM int64 ` short:"F" long:"fee-limit-ppm" description:"don't consider the target channel fee and use this max fee ppm instead (can rebalance at a loss, be careful)" json:"fee_limit_ppm" toml:"fee_limit_ppm" `
2022-10-01 13:02:19 +00:00
LostProfit bool ` short:"l" long:"lost-profit" description:"also consider the outbound channel fees when looking for profitable routes so that outbound_fee+inbound_fee < route_fee" json:"lost_profit" toml:"lost_profit" `
2022-09-28 15:08:32 +00:00
ProbeSteps int ` short:"b" long:"probe-steps" description:"if the payment fails at the last hop try to probe lower amount using this many steps" json:"probe_steps" toml:"probe_steps" `
MinAmount int64 ` long:"min-amount" description:"if probing is enabled this will be the minimum amount to try" json:"min_amount" toml:"min_amount" `
ExcludeChannelsIn [ ] uint64 ` short:"i" long:"exclude-channel-in" description:"don't use this channel as incoming (can be specified multiple times)" json:"exclude_channels_in" toml:"exclude_channels_in" `
ExcludeChannelsOut [ ] uint64 ` short:"o" long:"exclude-channel-out" description:"don't use this channel as outgoing (can be specified multiple times)" json:"exclude_channels_out" toml:"exclude_channels_out" `
ExcludeChannels [ ] uint64 ` short:"e" long:"exclude-channel" description:"don't use this channel at all (can be specified multiple times)" json:"exclude_channels" toml:"exclude_channels" `
ExcludeNodes [ ] string ` short:"d" long:"exclude-node" description:"don't use this node for routing (can be specified multiple times)" json:"exclude_nodes" toml:"exclude_nodes" `
2022-09-29 16:03:23 +00:00
ToChannel uint64 ` long:"to" description:"try only this channel as target (should satisfy other constraints too)" json:"to" toml:"to" `
FromChannel uint64 ` long:"from" description:"try only this channel as source (should satisfy other constraints too)" json:"from" toml:"from" `
2022-09-28 15:08:32 +00:00
StatFilename string ` short:"s" long:"stat" description:"save successful rebalance information to the specified CSV file" json:"stat" toml:"stat" `
2022-08-20 14:11:45 +00:00
}
2022-08-27 14:40:27 +00:00
type failedRoute struct {
channelPair [ 2 ] * lnrpc . Channel
expiration * time . Time
}
2022-08-20 14:11:45 +00:00
type regolancer struct {
2022-08-21 10:20:52 +00:00
lnClient lnrpc . LightningClient
routerClient routerrpc . RouterClient
myPK string
channels [ ] * lnrpc . Channel
fromChannels [ ] * lnrpc . Channel
fromChannelId uint64
toChannels [ ] * lnrpc . Channel
toChannelId uint64
2022-08-27 14:40:27 +00:00
channelPairs map [ string ] [ 2 ] * lnrpc . Channel
2022-08-21 10:20:52 +00:00
nodeCache map [ string ] * lnrpc . NodeInfo
chanCache map [ uint64 ] * lnrpc . ChannelEdge
2022-08-27 14:40:27 +00:00
failureCache map [ string ] failedRoute
2022-08-21 10:20:52 +00:00
excludeIn map [ uint64 ] struct { }
excludeOut map [ uint64 ] struct { }
excludeBoth map [ uint64 ] struct { }
excludeNodes [ ] [ ] byte
2022-08-25 17:03:37 +00:00
statFilename string
2022-09-03 00:50:00 +00:00
routeFound bool
2022-08-20 14:11:45 +00:00
}
2022-08-20 17:43:51 +00:00
func loadConfig ( ) {
2022-08-21 14:16:00 +00:00
flags . NewParser ( & mainParams , flags . PrintErrors | flags . IgnoreUnknown ) . Parse ( )
2022-09-28 15:08:32 +00:00
2022-08-20 17:43:51 +00:00
if mainParams . Config == "" {
return
}
2022-09-28 15:08:32 +00:00
if strings . Contains ( mainParams . Config , ".toml" ) {
_ , err := toml . DecodeFile ( mainParams . Config , & params )
if err != nil {
log . Fatalf ( "Error opening config file %s: %s" , mainParams . Config , err )
}
2022-08-20 17:43:51 +00:00
} else {
2022-09-28 15:08:32 +00:00
f , err := os . Open ( mainParams . Config )
2022-08-20 17:43:51 +00:00
if err != nil {
2022-09-28 15:08:32 +00:00
log . Fatalf ( "Error opening config file %s: %s" , mainParams . Config , err )
} else {
defer f . Close ( )
err = json . NewDecoder ( f ) . Decode ( & params )
if err != nil {
log . Fatalf ( "Error reading config file %s: %s" , mainParams . Config , err )
}
2022-08-20 17:43:51 +00:00
}
}
}
2022-08-22 08:44:28 +00:00
func tryRebalance ( ctx context . Context , r * regolancer , invoice * * lnrpc . AddInvoiceResponse ,
attempt * int ) ( err error , repeat bool ) {
2022-09-11 14:36:10 +00:00
from , to , amt , err := r . pickChannelPair ( params . Amount , params . MinAmount )
2022-08-22 08:44:28 +00:00
if err != nil {
log . Printf ( errColor ( "Error during picking channel: %s" ) , err )
return err , false
}
2022-08-23 12:15:25 +00:00
routeCtx , routeCtxCancel := context . WithTimeout ( ctx , time . Second * 30 )
defer routeCtxCancel ( )
2022-10-01 13:02:19 +00:00
routes , fee , err := r . getRoutes ( routeCtx , from , to , amt * 1000 )
2022-08-20 19:19:48 +00:00
if err != nil {
2022-08-22 08:44:28 +00:00
if routeCtx . Err ( ) == context . DeadlineExceeded {
log . Print ( errColor ( "Timed out looking for a route" ) )
return err , false
}
r . addFailedRoute ( from , to )
return err , true
2022-08-20 19:19:48 +00:00
}
2022-08-22 08:44:28 +00:00
routeCtxCancel ( )
2022-08-20 19:19:48 +00:00
if params . Amount == 0 || * invoice == nil {
2022-08-22 08:44:28 +00:00
* invoice , err = r . createInvoice ( ctx , from , to , amt )
2022-08-20 19:19:48 +00:00
if err != nil {
2022-08-21 09:51:05 +00:00
log . Printf ( "Error creating invoice: %s" , err )
2022-08-22 08:44:28 +00:00
return err , true
2022-08-20 19:19:48 +00:00
}
}
for _ , route := range routes {
log . Printf ( "Attempt %s, amount: %s (max fee: %s)" , hiWhiteColorF ( "#%d" , * attempt ) ,
hiWhiteColor ( amt ) , hiWhiteColor ( fee / 1000 ) )
2022-08-22 08:44:28 +00:00
r . printRoute ( ctx , route )
2022-08-22 20:01:46 +00:00
err = r . pay ( ctx , * invoice , amt , params . MinAmount , route , params . ProbeSteps )
2022-08-20 19:19:48 +00:00
if err == nil {
2022-08-22 08:44:28 +00:00
return nil , false
2022-08-20 19:19:48 +00:00
}
if retryErr , ok := err . ( ErrRetry ) ; ok {
amt = retryErr . amount
log . Printf ( "Trying to rebalance again with %s" , hiWhiteColor ( amt ) )
2022-08-22 08:44:28 +00:00
probedInvoice , err := r . createInvoice ( ctx , from , to , amt )
2022-08-20 19:19:48 +00:00
if err != nil {
2022-08-21 09:51:05 +00:00
log . Printf ( "Error creating invoice: %s" , err )
2022-08-22 08:44:28 +00:00
return err , true
2022-08-20 19:19:48 +00:00
}
2022-08-22 08:44:28 +00:00
probedRoute , err := r . rebuildRoute ( ctx , route , amt )
2022-08-20 19:19:48 +00:00
if err != nil {
log . Printf ( "Error rebuilding the route for probed payment: %s" , errColor ( err ) )
} else {
2022-08-22 20:01:46 +00:00
err = r . pay ( ctx , probedInvoice , amt , 0 , probedRoute , 0 )
2022-08-20 19:19:48 +00:00
if err == nil {
2022-08-22 08:44:28 +00:00
return nil , false
2022-08-20 19:19:48 +00:00
} else {
log . Printf ( "Probed rebalance failed with error: %s" , errColor ( err ) )
}
}
}
* attempt ++
}
2022-08-22 08:44:28 +00:00
return nil , true
2022-08-20 19:19:48 +00:00
}
2022-08-20 14:11:45 +00:00
func main ( ) {
2022-08-20 17:43:51 +00:00
rand . Seed ( time . Now ( ) . UnixNano ( ) )
loadConfig ( )
_ , err := flags . NewParser ( & params , flags . Default | flags . IgnoreUnknown ) . Parse ( )
2022-08-20 14:11:45 +00:00
if err != nil {
os . Exit ( 1 )
}
2022-08-20 17:43:51 +00:00
if params . Connect == "" {
params . Connect = "127.0.0.1:10009"
}
if params . MacaroonFilename == "" {
params . MacaroonFilename = "admin.macaroon"
}
2022-08-20 18:12:19 +00:00
if params . Network == "" {
params . Network = "mainnet"
}
2022-08-20 17:43:51 +00:00
if params . FromPerc == 0 {
params . FromPerc = 50
}
if params . ToPerc == 0 {
params . ToPerc = 50
}
2022-10-01 15:06:44 +00:00
if params . EconRatio == 0 && params . FeeLimitPPM == 0 {
2022-08-20 17:43:51 +00:00
params . EconRatio = 1
}
2022-08-20 14:11:45 +00:00
if params . Perc > 0 {
params . FromPerc = params . Perc
params . ToPerc = params . Perc
}
2022-08-22 20:01:46 +00:00
if params . MinAmount > 0 && params . MinAmount > params . Amount {
log . Fatal ( "Minimum amount should be more than amount" )
}
2022-08-20 14:11:45 +00:00
conn , err := lndclient . NewBasicConn ( params . Connect , params . TLSCert , params . MacaroonDir , params . Network ,
lndclient . MacFilename ( params . MacaroonFilename ) )
if err != nil {
log . Fatal ( err )
}
2022-08-20 19:19:48 +00:00
r := regolancer {
nodeCache : map [ string ] * lnrpc . NodeInfo { } ,
chanCache : map [ uint64 ] * lnrpc . ChannelEdge { } ,
2022-08-27 14:40:27 +00:00
channelPairs : map [ string ] [ 2 ] * lnrpc . Channel { } ,
failureCache : map [ string ] failedRoute { } ,
2022-08-25 17:03:37 +00:00
statFilename : params . StatFilename ,
2022-08-20 19:19:48 +00:00
}
2022-08-20 14:11:45 +00:00
r . lnClient = lnrpc . NewLightningClient ( conn )
r . routerClient = routerrpc . NewRouterClient ( conn )
2022-08-22 10:59:38 +00:00
mainCtx , mainCtxCancel := context . WithTimeout ( context . Background ( ) , time . Hour * 6 )
defer mainCtxCancel ( )
infoCtx , infoCtxCancel := context . WithTimeout ( mainCtx , time . Second * 30 )
defer infoCtxCancel ( )
2022-08-20 19:19:48 +00:00
info , err := r . lnClient . GetInfo ( infoCtx , & lnrpc . GetInfoRequest { } )
2022-08-20 14:11:45 +00:00
if err != nil {
log . Fatal ( err )
}
r . myPK = info . IdentityPubkey
2022-08-20 19:19:48 +00:00
err = r . getChannels ( infoCtx )
2022-08-20 14:11:45 +00:00
if err != nil {
log . Fatal ( "Error listing own channels: " , err )
}
2022-08-21 10:20:52 +00:00
if params . FromChannel > 0 {
r . fromChannelId = params . FromChannel
}
if params . ToChannel > 0 {
r . toChannelId = params . ToChannel
}
2022-08-21 08:26:50 +00:00
r . excludeIn = makeChanSet ( params . ExcludeChannelsIn )
r . excludeOut = makeChanSet ( params . ExcludeChannelsOut )
r . excludeBoth = makeChanSet ( params . ExcludeChannels )
err = r . makeNodeList ( params . ExcludeNodes )
if err != nil {
log . Fatal ( "Error parsing excluded node list: " , err )
}
err = r . getChannelCandidates ( params . FromPerc , params . ToPerc , params . Amount )
2022-08-20 14:11:45 +00:00
if err != nil {
log . Fatal ( "Error choosing channels: " , err )
}
if len ( r . fromChannels ) == 0 {
log . Fatal ( "No source channels selected" )
}
if len ( r . toChannels ) == 0 {
log . Fatal ( "No target channels selected" )
}
2022-08-22 10:59:38 +00:00
infoCtxCancel ( )
2022-08-20 14:11:45 +00:00
var invoice * lnrpc . AddInvoiceResponse
attempt := 1
for {
2022-08-22 08:44:28 +00:00
attemptCtx , attemptCancel := context . WithTimeout ( mainCtx , time . Minute * 5 )
_ , retry := tryRebalance ( attemptCtx , & r , & invoice , & attempt )
attemptCancel ( )
2022-08-22 10:59:38 +00:00
if attemptCtx . Err ( ) == context . DeadlineExceeded {
log . Print ( errColor ( "Attempt timed out" ) )
2022-08-22 12:57:48 +00:00
invoice = nil // create a new invoice next time
2022-08-22 10:59:38 +00:00
}
if mainCtx . Err ( ) == context . DeadlineExceeded {
log . Println ( errColor ( "Rebalancing timed out" ) )
return
}
2022-08-22 08:44:28 +00:00
if ! retry {
2022-08-20 19:19:48 +00:00
return
2022-08-20 14:11:45 +00:00
}
}
}