Compare commits

...

49 Commits

Author SHA1 Message Date
rkfg fb578adc01 Update changelog 11 months ago
rkfg 1e8858c41c Typo 11 months ago
rkfg 0771bd50b8 Add missing json/toml parameters for rel amounts 11 months ago
rkfg e6c593cbb2 Update changelog 1 year ago
rkfg 85ce47602b Fix no payment timeout after probing 1 year ago
rkfg baac26db5b Update LICENSE 1 year ago
rkfg 5b0031ecc8 Update changelog 1 year ago
rkfg 76cdc2efd6 Update LICENSE 1 year ago
rkfg 3467c7edbe
Merge pull request #56 from ziggie1984/rapid-rebalance-strategy 1 year ago
rkfg f839791e2b Fix crash on FEE_INSUFFICIENT 1 year ago
ziggie 56017f52a7
Add Decrease Strategy for Rapid Rebalances 1 year ago
rkfg efa39b4cc1 Release v1.12.0 1 year ago
rkfg 5eafce0853
Merge pull request #53 from ziggie1984/max_amt_on_route 1 year ago
rkfg c44da07f35
Merge pull request #54 from ziggie1984/channel-reserve 1 year ago
rkfg a3c86f4e28
Merge pull request #55 from bennyhodl/patch-1 1 year ago
₿en 5b9161c2e7
Missing tilde 1 year ago
ziggie 2b6c4061ea
cap rebalance amount before we overshoot the max possible amount on the route when rapid rebalancing 1 year ago
ziggie f299a0bfe3
take channel-reserve into account when determining the rebalance amt 1 year ago
rkfg 9a4f062085 Invalidate MC cache when no pairs left 1 year ago
rkfg 6e163725b1 Update README.md 1 year ago
rkfg f31d1d6f51 Update changelog 1 year ago
rkfg 9a5116eef8
Merge pull request #52 from ziggie1984/channel-age 1 year ago
rkfg 00151033e9 Get rid of global var 1 year ago
rkfg ff679c977d Reword descriptions 1 year ago
rkfg fe4caa232c
Merge pull request #49 from ziggie1984/insufficient-fee 1 year ago
ziggie bfe17d0ce1
add the option to exclude channels by channelage 1 year ago
rkfg f7b9ca5689
Merge pull request #48 from ziggie1984/tiny-improvement-rebalancer 2 years ago
ziggie a52771ddcc
reuse route when fees insufficient and add rapid rebalance fee check 2 years ago
ziggie fb0f3cbec1
Adding more verbose output and save some rapid rebalances in some cases 2 years ago
rkfg ec8e055340 Update changelog 2 years ago
rkfg e41b17b863
Merge pull request #47 from ziggie1984/refactor-rebalancer 2 years ago
ziggie 9b55697957
refactor rebalancer 2 years ago
rkfg 9193aaed5d Update changelog 2 years ago
rkfg 410566cb36 Fix rapid rebalance total amount calculation 2 years ago
rkfg 41c49fcbab Update changelog 2 years ago
rkfg 8b9d8c61c1
Merge pull request #46 from ziggie1984/docker-and-releaser 2 years ago
rkfg 4b872214be
Merge pull request #45 from ziggie1984/accelerator-rapid-rebalance
Fixes https://github.com/rkfg/regolancer/issues/43
2 years ago
ziggie ee3af3dffe add accelerator 2 years ago
rkfg 4116c3094c Fix fee ppm calculation 2 years ago
rkfg b5960e8f39
Merge pull request #42 from ziggie1984/flock-payment 2 years ago
rkfg 189f124597 Spelling 2 years ago
rkfg 381538d43b
Merge pull request #41 from ziggie1984/rapidrebalance-amount 2 years ago
rkfg 91acf21086 Fix initial fee in rapid rebalance 2 years ago
rkfg 7cb051fc97 Simplify rapid rebalance result 2 years ago
ziggie 200c42fe8b
add goreleaser and dockerfile
add goreleaser decription

add docker to README
2 years ago
ziggie 5a3a41afae
add fee output 2 years ago
ziggie cefbff413d
add exclusive lock when writing to Statfile 2 years ago
ziggie 98068a105d
add amount when rapid rebalancing 2 years ago
rkfg df27799b33 Update README.md 2 years ago

3
.gitignore vendored

@ -2,4 +2,5 @@
*.toml
*.json
*.cert
regolancer*
regolancer*
dist/

@ -0,0 +1,126 @@
before:
hooks:
- go mod tidy
- go mod verify
- go mod download
builds:
- goos:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
- arm
- arm64
goarm:
- 6
env:
- CGO_ENABLED=0
binary: regolancer
checksum:
name_template: '{{ tolower .ProjectName }}_{{.Version}}_checksums.txt'
snapshot:
name_template: SNAPSHOT-{{ .Commit }}
archives:
- name_template: "{{ tolower .ProjectName }}_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
darwin: macOS
linux: Linux
windows: Windows
freebsd: FreeBSD
files:
- LICENSE
- README.md
- CHANGELOG.md
- config.toml.sample
- config.json.sample
format: tar.gz
format_overrides:
- goos: windows
format: zip
changelog:
# Set this to true if you don't want any changelog at all.
# Warning: this will also ignore any changelog files passed via `--release-notes`,
# and will render an empty changelog.
# This may result in an empty release notes on GitHub/GitLab/Gitea.
skip: false
# dockers:
# # https://goreleaser.com/customization/docker/
# - use: buildx
# goos: linux
# goarch: amd64
# image_templates:
# - "ziggie1984/{{ .ProjectName }}:{{ .Version }}-amd64"
# - "ziggie1984/{{ .ProjectName }}:latest-amd64"
# build_flag_templates:
# - "--platform=linux/amd64"
# - "--label=org.opencontainers.image.created={{.Date}}"
# - "--label=org.opencontainers.image.title={{.ProjectName}}"
# - "--label=org.opencontainers.image.revision={{.FullCommit}}"
# - "--label=org.opencontainers.image.version={{.Version}}"
# - use: buildx
# goos: linux
# goarch: arm64
# image_templates:
# - "ziggie1984/{{ .ProjectName }}:{{ .Version }}-arm64v8"
# - "ziggie1984/{{ .ProjectName }}:latest-arm64v8"
# build_flag_templates:
# - "--platform=linux/arm64/v8"
# - "--label=org.opencontainers.image.created={{.Date}}"
# - "--label=org.opencontainers.image.title={{.ProjectName}}"
# - "--label=org.opencontainers.image.revision={{.FullCommit}}"
# - "--label=org.opencontainers.image.version={{.Version}}"
# - use: buildx
# goos: linux
# goarch: arm
# goarm: 6
# image_templates:
# - "ziggie1984/{{ .ProjectName }}:{{ .Version }}-armv6"
# - "ziggie1984/{{ .ProjectName }}:latest-armv6"
# build_flag_templates:
# - "--platform=linux/arm/v6"
# - "--label=org.opencontainers.image.created={{.Date}}"
# - "--label=org.opencontainers.image.title={{.ProjectName}}"
# - "--label=org.opencontainers.image.revision={{.FullCommit}}"
# - "--label=org.opencontainers.image.version={{.Version}}"
# - use: buildx
# goos: linux
# goarch: arm
# goarm: 7
# image_templates:
# - "ziggie1984/{{ .ProjectName }}:{{ .Version }}-armv7"
# - "ziggie1984/{{ .ProjectName }}:latest-armv7"
# build_flag_templates:
# - "--platform=linux/arm/v7"
# - "--label=org.opencontainers.image.created={{.Date}}"
# - "--label=org.opencontainers.image.title={{.ProjectName}}"
# - "--label=org.opencontainers.image.revision={{.FullCommit}}"
# - "--label=org.opencontainers.image.version={{.Version}}"
# docker_manifests:
# # https://goreleaser.com/customization/docker_manifest/
# - name_template: ziggie1984/{{ .ProjectName }}:{{ .Version }}
# image_templates:
# - ziggie1984/{{ .ProjectName }}:{{ .Version }}-amd64
# - ziggie1984/{{ .ProjectName }}:{{ .Version }}-arm64v8
# - ziggie1984/{{ .ProjectName }}:{{ .Version }}-armv6
# - ziggie1984/{{ .ProjectName }}:{{ .Version }}-armv7
# - name_template: ziggie1984/{{ .ProjectName }}:latest
# image_templates:
# - ziggie1984/{{ .ProjectName }}:latest-amd64
# - ziggie1984/{{ .ProjectName }}:latest-arm64v8
# - ziggie1984/{{ .ProjectName }}:latest-armv6
# - ziggie1984/{{ .ProjectName }}:latest-armv7

@ -2,8 +2,61 @@
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0.html).
## [1.12.3]
### Fixed
- Relative amount parameters worked only from CLI, now they can be specified in
the configs too
## [1.12.2]
### Fixed
- Payment after probing used the global rebalancing context instead of attempt
context which can result in waiting for the payment to fail for too long (6
hours by default)
## [1.12.1]
### Changed
- Rapid rebalance now goes all the way down to min amount to squeeze all
available liquidity on the route
### Fixed
- Silent crash with zero exit code on FEE_INSUFFICIENT in some cases
## [1.12.0]
### Added
- Check route for max htlc during rapid rebalance and limit the max rebalancing
amount to not hit that cap
- 2% safety margin for channel reserve and commitment fee to prevent failures due to hitting that limit
## [1.11.0]
### Added
- Exclude too young channels by their age in blocks: give liquidity a chance to
move to the other side by itself. You can now set the minimum channel age to
be considered for rebalance.
### Changed
- Rapid rebalance now skips some steps if the channel is already constrained by
liquidity
### Fixed
- Sudden fee changes are now properly handled: we retry rebalance and also see
if the new fee is still within the limit
## [1.10.2]
### Fixed
- Rapid rebalance summary now shows the correct total fee
## Changed
- Functions doing high-level rebalancing logic are refactored to a separate file
## [1.10.1]
### Fixed
- Rapid rebalance summary now shows the correct total amount
## [1.10.0]
### Added
- Rapid rebalance is now accelerated, it tries to double the amount until it
fails, then the amount is halved until it becomes lower than the initial one
- Goreleaser and Docker configs
- If any rapid rebalances succeeded, the total amount and fees are displayed at
the end
- Stat file (CSV) is now also flock'ed to prevent accidental corruption if
multiple instances try to update it
### Fixed
- Fees in route print and success message are now calculated from the target
balance (without fees) so for example 50 sat fee and 1 000 000 sat amount
would be shown as 50ppm, before it was 49ppm.
## [1.9.2]
### Added
- Exit codes for failed rebalances (2 for global timeout and 1 for all

@ -0,0 +1,38 @@
FROM golang:1.19.2-alpine as builder
# Pass a tag, branch or a commit using build-arg. This allows a docker
# image to be built from a specified Git state. The default image
# will use the Git tip of master by default.
ARG checkout="master"
ARG git_url="https://github.com/rkfg/regolancer.git"
# Install dependencies and build the binaries.
RUN apk add --no-cache git \
&& git clone $git_url /go/src/github.com/regolancer \
&& cd /go/src/github.com/regolancer \
&& git checkout $checkout \
&& go install
# Start a new, final image.
FROM alpine as final
RUN apk --no-cache add \
bash \
jq
WORKDIR /app
COPY --from=builder /go/bin/regolancer /app
VOLUME [ "/root/.lnd" ]
ENTRYPOINT ["./regolancer"]

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 rkfg
Copyright (c) 2022-2023 rkfg, ziggie1984 and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

@ -24,7 +24,7 @@ rebalance-lnd](https://github.com/accumulator/rebalance-lnd).
- timeouts can be customized
- JSON/TOML config file to set some defaults you prefer
- optional route probing using binary search to rebalance a smaller amount
- optional rapid rebalancing using the same route for further rebalances
- optional rapid rebalancing using the same route for further rebalances
unitl route is depleted in case a rebalance succeeds
- data caching to speed up alias resolution, quickly skip failing channel pairs
etc.
@ -62,8 +62,8 @@ in `~/go/bin/linux_arm`.
```
Config:
-f, --config config file path
-f, --config config file path
Node Connection:
-c, --connect connect to lnd using host:port
-t, --tlscert path to tls.cert to connect
@ -88,6 +88,7 @@ Common:
-e, --exclude-channel (DEPRECATED) don't use this channel at all (can be specified multiple times)
-d, --exclude-node (DEPRECATED) don't use this node for routing (can be specified multiple times)
--exclude don't use this node or your channel for routing (can be specified multiple times)
--exclude-channel-age don't use channels opened less than this number of blocks ago
--to try only this channel or node as target (should satisfy other constraints too; can be specified multiple times)
--from try only this channel or node as source (should satisfy other constraints too; can be specified multiple times)
--fail-tolerance a payment that differs from the prior attempt by this ppm will be cancelled
@ -97,7 +98,7 @@ Common:
earn for routing out of the target channel)
--econ-ratio-max-ppm limits the max fee ppm for a rebalance when using econ ratio
-F, --fee-limit-ppm don't consider the target channel fee and use this max fee ppm instead (can rebalance at a loss, be careful)
-l, --lost-profit also consider the outbound channel fees when looking for profitable routes so that outbound_fee+inbound_fee < route_fee
-l, --lost-profit also consider the source channel fee when looking for profitable routes so that route_fee < target_fee * econ_ratio - source_fee
Node Cache:
--node-cache-filename save and load other nodes information to this file, improves cold start performance
@ -195,6 +196,34 @@ payment is then done along this route and it should succeed. If, for whatever
reason, it doesn't (liquidity shifted somewhere unexpectedly) the cycle
continues.
# Docker Setup
In general its recommanded to run regolancer in a normal environment because it is
so easy to install as mentioned above. However if you want to run it in a docker
we also provide the Dockerfile so that you can easily get started.
Build the container or pull it:
`docker build -t regolancer .` or `docker pull ziggie1984/regolancer`
Now you can use regolancer with your lnd instance:
`docker run --rm --add-host=host.docker.internal:host-gateway -it -v /home/lnd:/root/.lnd regolancer --connect host.docker.internal:10009`
The above command assumes /home/lnd is your lnd configuration directory. Please adjust as required.
If you want to use a config file either copy the file into the mounted volume (/home/lnd) or mount a separate volume. Then just add the `--config /root/.lnd/config.toml` parameter to your start command
**Note for Umbrel/Umbrel-OS users**
`docker run --rm --network=umbrel_main_network -it -v /home/umbrel/umbrel/app-data/lightning/data/lnd:/root/.lnd regolancer --connect 10.21.21.9:10009`
Optionally you can create an alias in your shell's environment file like so:
alias regolancer="docker run --rm --network=umbrel_main_network -it -v /home/umbrel/umbrel/app-data/lightning/data/lnd:/root/.lnd regolancer --connect 10.21.21.9:10009"
For older versions of Umbrel please use `/home/umbrel/umbrel/lnd` instead of `/home/umbrel/umbrel/app-data/lightning/data/lnd`
# What's wrong with the other rebalancers
While I liked probing in `bos`, it has many downsides: gives up quickly on
@ -258,6 +287,7 @@ during route construction. You can also disable them (it only happens on your
end so you'll be able to receive liquidity but not send it) but it hurts your
score on various sites so better not to do it. Increase fees or lower max_htlc and
you'll be good. You can set multiple brackets with multiple limits like:
- 20% local balance => set max_htlc to 0.1 of channel capacity (so it can
process ≈2 payments max or more smaller payments)
- 10% local balance => set max_htlc to 0.01 of channel capacity (small payments
@ -276,6 +306,7 @@ accept contributions and suggestions though! For now I implemented almost
everything I needed, maybe except a couple of timeouts being configurable. But I
don't see much need for that as of now. The main goals and motivation for this
project were:
- make it reliable and robust so I don't have to babysit it (stop/restart if it
hangs, crashes or gives up early)
- make it fast and lightweight, don't stress `lnd` too much as it all should run
@ -288,4 +319,4 @@ project were:
# Feedback and contact
We have a Matrix room to discuss this program and solve issues, feel free to
join [#regolancer:matrix.org](https://matrix.to/#/#regolancer:matrix.org)!
join [#regolancer:matrix.org](https://matrix.to/#/#regolancer:matrix.org)!

@ -30,8 +30,12 @@ func (r *regolancer) loadNodeCache(filename string, exp int, doLock bool) error
if doLock {
log.Printf("Loading node cache from %s", filename)
l := lock()
l.RLock()
err := l.RLock()
defer l.Unlock()
if err != nil {
return fmt.Errorf("error taking shared lock on file %s: %s", filename, err)
}
}
f, err := os.Open(filename)
if err != nil {
@ -61,11 +65,15 @@ func (r *regolancer) saveNodeCache(filename string, exp int) error {
log.Printf("Saving node cache to %s", filename)
l := lock()
l.Lock()
err := l.Lock()
defer l.Unlock()
if err != nil {
return fmt.Errorf("error taking exclusive lock on file %s: %s", filename, err)
}
old := regolancer{nodeCache: map[string]cachedNodeInfo{}}
err := old.loadNodeCache(filename, exp, false)
err = old.loadNodeCache(filename, exp, false)
if err != nil {
logErrorF("Error merging cache, saving anew: %s", err)

@ -64,6 +64,10 @@ func (r *regolancer) getChannelCandidates(fromPerc, toPerc, amount int64) error
for _, c := range r.channels {
if params.ExcludeChannelAge != 0 && uint64(r.blockHeight)-getChannelAge(c.ChanId) < params.ExcludeChannelAge {
continue
}
if _, ok := r.excludeBoth[c.ChanId]; ok {
continue
}
@ -92,7 +96,11 @@ func (r *regolancer) getChannelCandidates(fromPerc, toPerc, amount int64) error
}
}
}
return nil
if len(r.channelPairs) > 0 {
return nil
} else {
return fmt.Errorf("no channelpairs available for rebalance")
}
}
func min(args ...int64) (result int64) {
@ -107,6 +115,13 @@ func min(args ...int64) (result int64) {
func (r *regolancer) pickChannelPair(amount, minAmount int64,
relFromAmount, relToAmount float64) (from uint64, to uint64, maxAmount int64, err error) {
// Channel Reserve we have to account for when determine the amount to
// rebalance. The normal reserve is 1% of the channel capacity in the current
// lightning protocol, though we also have to take the commitment fee into account
// when building up the commitment tx so we take 2% here to not run in those edge cases.
const channelReserve = 0.02
if len(r.channelPairs) == 0 {
if !r.routeFound || len(r.failureCache) == 0 {
return 0, 0, 0, errors.New("no routes")
@ -117,7 +132,7 @@ func (r *regolancer) pickChannelPair(amount, minAmount int64,
r.channelPairs[k] = v.channelPair
delete(r.failureCache, k)
}
r.mcCache = map[string]int64{}
r.routeFound = false
}
@ -134,11 +149,11 @@ func (r *regolancer) pickChannelPair(amount, minAmount int64,
}
fromChan = pair[0]
toChan = pair[1]
maxFrom := fromChan.LocalBalance
maxFrom := fromChan.LocalBalance - int64(float64(fromChan.Capacity)*channelReserve)
if relFromAmount > 0 {
maxFrom = min(maxFrom, int64(float64(fromChan.Capacity)*relFromAmount)-fromChan.RemoteBalance)
}
maxTo := toChan.RemoteBalance
maxTo := toChan.RemoteBalance - int64(float64(fromChan.Capacity)*channelReserve)
if relToAmount > 0 {
maxTo = min(maxTo, int64(float64(toChan.Capacity)*relToAmount)-toChan.LocalBalance)
}
@ -147,7 +162,9 @@ func (r *regolancer) pickChannelPair(amount, minAmount int64,
} else {
maxAmount = min(maxFrom, maxTo, amount)
}
if maxAmount < minAmount {
// we need to also fail the route when maxAmount is zero
// this can happen when rapid-rebalancing.
if maxAmount < minAmount || maxAmount == 0 {
r.addFailedRoute(fromChan.ChanId, toChan.ChanId)
return r.pickChannelPair(amount, minAmount, relFromAmount, relToAmount)
}
@ -196,6 +213,12 @@ func parseScid(chanId string) int64 {
}
func getChannelAge(chanId uint64) uint64 {
shortChanId := lnwire.NewShortChanIDFromInt(chanId)
return uint64(shortChanId.BlockHeight)
}
func (r *regolancer) getChannelForPeer(ctx context.Context, node []byte) []*lnrpc.Channel {
channels, err := r.lnClient.ListChannels(ctx, &lnrpc.ListChannelsRequest{ActiveOnly: true, PublicOnly: true, Peer: node})

@ -9,6 +9,7 @@ require (
github.com/jessevdk/go-flags v1.5.0
github.com/lightninglabs/lndclient v0.15.1-0
github.com/lightningnetwork/lnd v0.15.1-beta.rc1
github.com/mattn/go-runewidth v0.0.14
golang.org/x/sys v0.1.0
)
@ -83,7 +84,6 @@ require (
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mholt/archiver/v3 v3.5.0 // indirect
github.com/miekg/dns v1.1.43 // indirect

@ -0,0 +1,33 @@
# How to Create Releases with goreleaser
Before you can use goreleaser you first need to install it: https://goreleaser.com/install/
In case you want to create a test release just create a snapshot build with:
`goreleaser release --snapshot --rm-dist`
This will only create a release version locally.
One can only build all the target or only one specific target:
`GOOS=linux GOARCH=amd64 goreleaser build --rm-dist`
Currently the CHANGE.LOG of the goreleaser is enabled to remove it go to the `.goreleaser.yaml` and change the setting.
If you verified that the snapshot version is good to go than you can create a final release
First you need to get a github token with at least the privilige of write:packages
`export GITHUB_TOKEN="YOUR_GH_TOKEN"`
detailed information how to create a release with a speicific tag can be found here: https://goreleaser.com/quick-start/
```
git tag -a v0.1.0 -m "Release Comment"
goreleaser release
```
Currently Docker Releases are turned off, but can be decided otherwise

@ -79,6 +79,9 @@ func (r *regolancer) info(ctx context.Context) error {
}
}
fmt.Println()
if params.ExcludeChannelAge != 0 {
fmt.Printf("Channel age needs to be >= %s blocks\n", hiWhiteColor(params.ExcludeChannelAge))
}
fmt.Printf("Fail tolerance: %s ppm\n", formatAmt(int64(params.FailTolerance)))
printBooleanOption("Rapid rebalance", params.AllowRapidRebalance)
printBooleanOption("Lost profit accounting", params.LostProfit)

@ -3,7 +3,6 @@ package main
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"log"
@ -34,8 +33,8 @@ type configParams struct {
ToPerc int64 `long:"pto" description:"channels with less than this outbound liquidity percentage will be considered as target channels" json:"pto" toml:"pto"`
Perc int64 `short:"p" long:"perc" description:"use this value as both pfrom and pto from above" json:"perc" toml:"perc"`
Amount int64 `short:"a" long:"amount" description:"amount to rebalance" json:"amount" toml:"amount"`
RelAmountTo float64 `long:"rel-amount-to" description:"calculate amount as the target channel capacity fraction (for example, 0.2 means you want to achieve at most 20% target channel local balance)"`
RelAmountFrom float64 `long:"rel-amount-from" description:"calculate amount as the source channel capacity fraction (for example, 0.2 means you want to achieve at most 20% source channel remote balance)"`
RelAmountTo float64 `long:"rel-amount-to" description:"calculate amount as the target channel capacity fraction (for example, 0.2 means you want to achieve at most 20% target channel local balance)" json:"rel_amount_to" toml:"rel_amount_to"`
RelAmountFrom float64 `long:"rel-amount-from" description:"calculate amount as the source channel capacity fraction (for example, 0.2 means you want to achieve at most 20% source channel remote balance)" json:"rel_amount_from" toml:"rel_amount_from"`
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"`
AllowRapidRebalance bool `long:"allow-rapid-rebalance" description:"if a rebalance succeeds the route will be used for further rebalances until criteria for channels is not satifsied" json:"allow_rapid_rebalance" toml:"allow_rapid_rebalance"`
MinAmount int64 `long:"min-amount" description:"if probing is enabled this will be the minimum amount to try" json:"min_amount" toml:"min_amount"`
@ -46,6 +45,7 @@ type configParams struct {
ExcludeChannels []string `short:"e" long:"exclude-channel" description:"(DEPRECATED) 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:"(DEPRECATED) don't use this node for routing (can be specified multiple times)" json:"exclude_nodes" toml:"exclude_nodes"`
Exclude []string `long:"exclude" description:"don't use this node or your channel for routing (can be specified multiple times)" json:"exclude" toml:"exclude"`
ExcludeChannelAge uint64 `long:"exclude-channel-age" description:"don't use channels opened less than this number of blocks ago" json:"exclude_channel_age" toml:"exclude_channel_age"`
To []string `long:"to" description:"try only this channel or node as target (should satisfy other constraints too; can be specified multiple times)" json:"to" toml:"to"`
From []string `long:"from" description:"try only this channel or node as source (should satisfy other constraints too; can be specified multiple times)" json:"from" toml:"from"`
FailTolerance int64 `long:"fail-tolerance" description:"a payment that differs from the prior attempt by this ppm will be cancelled" json:"fail_tolerance" toml:"fail_tolerance"`
@ -84,6 +84,7 @@ type regolancer struct {
lnClient lnrpc.LightningClient
routerClient routerrpc.RouterClient
myPK string
blockHeight uint32
channels []*lnrpc.Channel
fromChannels []*lnrpc.Channel
fromChannelId map[uint64]struct{}
@ -105,7 +106,10 @@ type regolancer struct {
}
func loadConfig() {
flags.NewParser(&cfgParams, flags.None).Parse()
_, err := flags.NewParser(&cfgParams, flags.None).Parse()
if err != nil {
log.Fatalf("Error when parsing command line options: %s", err)
}
if cfgParams.Config == "" {
return
@ -168,202 +172,6 @@ func convertChanStringToInt(chanIds []string) (channels []uint64) {
}
func tryRebalance(ctx context.Context, r *regolancer, attempt *int) (err error,
repeat bool) {
attemptCtx, attemptCancel := context.WithTimeout(ctx, time.Minute*time.Duration(params.TimeoutAttempt))
defer attemptCancel()
from, to, amt, err := r.pickChannelPair(params.Amount, params.MinAmount, params.RelAmountFrom, params.RelAmountTo)
if err != nil {
log.Printf(errColor("Error during picking channel: %s"), err)
return err, false
}
routeCtx, routeCtxCancel := context.WithTimeout(attemptCtx, time.Second*time.Duration(params.TimeoutRoute))
defer routeCtxCancel()
routes, fee, err := r.getRoutes(routeCtx, from, to, amt*1000)
if err != nil {
if routeCtx.Err() == context.DeadlineExceeded {
log.Print(errColor("Timed out looking for a route"))
return err, false
}
r.addFailedRoute(from, to)
return err, true
}
routeCtxCancel()
for _, route := range routes {
log.Printf("Attempt %s, amount: %s (max fee: %s sat | %s ppm )",
hiWhiteColorF("#%d", *attempt), hiWhiteColor(amt), formatFee(fee), formatFeePPM(amt*1000, fee))
r.printRoute(attemptCtx, route)
err = r.pay(attemptCtx, amt, params.MinAmount, route, params.ProbeSteps)
if err == nil {
if params.AllowRapidRebalance {
_, err := tryRapidRebalance(ctx, r, from, to, route, amt)
if err != nil {
log.Printf("Rapid rebalance failed with %s", err)
} else {
log.Printf("Finished rapid rebalancing")
}
}
return nil, false
}
if retryErr, ok := err.(ErrRetry); ok {
amt = retryErr.amount
log.Printf("Trying to rebalance again with %s", hiWhiteColor(amt))
probedRoute, err := r.rebuildRoute(attemptCtx, route, amt)
if err != nil {
log.Printf("Error rebuilding the route for probed payment: %s", errColor(err))
} else {
err = r.pay(ctx, amt, 0, probedRoute, 0)
if err == nil {
if params.AllowRapidRebalance && params.MinAmount > 0 {
_, err := tryRapidRebalance(ctx, r, from, to, probedRoute, amt)
if err != nil {
log.Printf("Rapid rebalance failed with %s", err)
} else {
log.Printf("Finished rapid rebalancing")
}
}
return nil, false
} else {
r.invalidateInvoice(amt)
log.Printf("Probed rebalance failed with error: %s", errColor(err))
}
}
}
*attempt++
}
attemptCancel()
if attemptCtx.Err() == context.DeadlineExceeded {
log.Print(errColor("Attempt timed out"))
}
return nil, true
}
func tryRapidRebalance(ctx context.Context, r *regolancer, from, to uint64, route *lnrpc.Route, amt int64) (successfullAtempts int, err error) {
rapidAttempt := 0
for {
log.Printf("Rapid rebalance attempt %s", hiWhiteColor(rapidAttempt+1))
cTo, err := r.getChanInfo(ctx, to)
if err != nil {
logErrorF("Error fetching target channel: %s", err)
return rapidAttempt, err
}
cFrom, err := r.getChanInfo(ctx, from)
if err != nil {
logErrorF("Error fetching source channel: %s", err)
return rapidAttempt, err
}
fromPeer, _ := hex.DecodeString(cFrom.Node1Pub)
if cFrom.Node1Pub == r.myPK {
fromPeer, _ = hex.DecodeString(cFrom.Node2Pub)
}
fromChan, err := r.lnClient.ListChannels(ctx, &lnrpc.ListChannelsRequest{ActiveOnly: true, PublicOnly: true, Peer: fromPeer})
if err != nil {
logErrorF("Error fetching source channel: %s", err)
return rapidAttempt, err
}
toPeer, _ := hex.DecodeString(cTo.Node1Pub)
if cTo.Node1Pub == r.myPK {
toPeer, _ = hex.DecodeString(cTo.Node2Pub)
}
toChan, err := r.lnClient.ListChannels(ctx, &lnrpc.ListChannelsRequest{ActiveOnly: true, PublicOnly: true, Peer: toPeer})
if err != nil {
logErrorF("Error fetching target channel: %s", err)
return rapidAttempt, err
}
for k := range r.fromChannelId {
delete(r.fromChannelId, k)
}
r.fromChannelId = makeChanSet([]uint64{from})
for k := range r.toChannelId {
delete(r.toChannelId, k)
}
r.toChannelId = makeChanSet([]uint64{to})
r.channels = r.channels[:0]
r.fromChannels = r.fromChannels[:0]
r.toChannels = r.toChannels[:0]
r.channels = append(r.channels, toChan.Channels...)
r.channels = append(r.channels, fromChan.Channels...)
for k := range r.failureCache {
delete(r.failureCache, k)
}
for k := range r.channelPairs {
delete(r.channelPairs, k)
}
err = r.getChannelCandidates(params.FromPerc, params.ToPerc, amt)
if err != nil {
logErrorF("Error selecting channel candidates: %s", err)
return rapidAttempt, err
}
from, to, amt, err = r.pickChannelPair(amt, params.MinAmount, params.RelAmountFrom, params.RelAmountTo)
if err != nil {
log.Printf(errColor("Error during picking channel: %s"), err)
return rapidAttempt, err
}
log.Printf("rapid fire starting with amount %s", hiWhiteColor(amt))
route, err = r.rebuildRoute(ctx, route, amt)
if err != nil {
log.Printf(errColor("Error building route: %s"), err)
return rapidAttempt, err
}
attemptCtx, attemptCancel := context.WithTimeout(ctx, time.Minute*time.Duration(params.TimeoutAttempt))
defer attemptCancel()
err = r.pay(attemptCtx, amt, params.MinAmount, route, 0)
attemptCancel()
if attemptCtx.Err() == context.DeadlineExceeded {
log.Print(errColor("Rapid rebalance attempt timed out"))
return rapidAttempt, attemptCtx.Err()
}
if err != nil {
log.Printf("Rebalance failed with %s", err)
break
} else {
rapidAttempt++
}
}
log.Printf("%s rapid rebalances were successful\n", hiWhiteColor(rapidAttempt))
return rapidAttempt, nil
}
func preflightChecks(params *configParams) error {
if params.Version {
printVersion()
@ -448,6 +256,7 @@ func preflightChecks(params *configParams) error {
if params.TimeoutRoute == 0 {
params.TimeoutRoute = 30
}
return nil
}
@ -515,6 +324,7 @@ func main() {
log.Fatal(err)
}
r.myPK = info.IdentityPubkey
r.blockHeight = info.BlockHeight
err = r.getChannels(infoCtx)
if err != nil {
log.Fatal("Error listing own channels: ", err)
@ -598,7 +408,7 @@ func main() {
}()
for {
err, retry := tryRebalance(mainCtx, &r, &attempt)
err, retry := r.tryRebalance(mainCtx, &attempt)
if mainCtx.Err() == context.DeadlineExceeded {
log.Println(errColor("Rebalancing timed out"))
exitCode = 2

@ -1,8 +1,10 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"math"
"github.com/lightningnetwork/lnd/lnrpc"
)
@ -31,3 +33,29 @@ func (r *regolancer) validateRoute(route *lnrpc.Route) error {
}
return nil
}
func (r *regolancer) maxAmountOnRoute(ctx context.Context, route *lnrpc.Route) (uint64, error) {
var capAmountMsat uint64 = math.MaxInt64
for _, h := range route.Hops {
edge, err := r.getChanInfo(ctx, h.ChanId)
if err != nil {
return 0, err
}
policyTo := edge.Node1Policy
if h.PubKey != edge.Node2Pub {
policyTo = edge.Node2Policy
}
if policyTo.MaxHtlcMsat <= 0 {
continue
}
if capAmountMsat > policyTo.MaxHtlcMsat {
capAmountMsat = policyTo.MaxHtlcMsat
}
}
return capAmountMsat, nil
}

@ -19,7 +19,10 @@ func (e ErrRetry) Error() string {
return fmt.Sprintf("retry payment with %d sats", e.amount)
}
var ErrProbeFailed = fmt.Errorf("probe failed")
var (
ErrProbeFailed = fmt.Errorf("probe failed")
ErrFeeExceeded = fmt.Errorf("fee-limit exceeded")
)
func (r *regolancer) createInvoice(ctx context.Context, amount int64) (result *lnrpc.AddInvoiceResponse, err error) {
var ok bool
@ -38,10 +41,16 @@ func (r *regolancer) invalidateInvoice(amount int64) {
delete(r.invoiceCache, amount)
}
func (r *regolancer) pay(ctx context.Context, amount int64, minAmount int64,
func (r *regolancer) pay(ctx context.Context, amount int64, minAmount int64, maxFeeMsat int64,
route *lnrpc.Route, probeSteps int) error {
fmt.Println()
defer fmt.Println()
if route.TotalFeesMsat > maxFeeMsat {
log.Printf("fee on the route exceeds our limits: %s ppm (max fee %s ppm)", formatFeePPM(amount*1000, route.TotalFeesMsat), formatFeePPM(amount*1000, maxFeeMsat))
return ErrFeeExceeded
}
invoice, err := r.createInvoice(ctx, amount)
if err != nil {
log.Printf("Error creating invoice: %s", err)
@ -64,6 +73,7 @@ func (r *regolancer) pay(ctx context.Context, amount int64, minAmount int64,
Route: route,
})
if err != nil {
logErrorF("error sending payment %s", err)
return err
}
if result.Status == lnrpc.HTLCAttempt_FAILED {
@ -79,6 +89,7 @@ func (r *regolancer) pay(ctx context.Context, amount int64, minAmount int64,
return fmt.Errorf("error: %s @ %d", result.Failure.Code.String(),
result.Failure.FailureSourceIndex)
}
prevHop := route.Hops[result.Failure.FailureSourceIndex-1]
failedHop := route.Hops[result.Failure.FailureSourceIndex]
nodeCtx, cancel := context.WithTimeout(ctx, time.Second*time.Duration(params.TimeoutInfo))
@ -98,6 +109,22 @@ func (r *regolancer) pay(ctx context.Context, amount int64, minAmount int64,
node2name = node2.Node.Alias
}
log.Printf("%s %s ⇒ %s", faintWhiteColor(result.Failure.Code.String()), cyanColor(node1name), cyanColor(node2name))
if result.Failure.Code == lnrpc.Failure_FEE_INSUFFICIENT || result.Failure.Code == lnrpc.Failure_INCORRECT_CLTV_EXPIRY {
failedHop := route.Hops[result.Failure.FailureSourceIndex-1]
updatedRoute, err := r.rebuildRoute(ctx, route, amount)
if err == nil {
updatedHop := updatedRoute.Hops[result.Failure.FailureSourceIndex-1]
// compare hops to make sure we do not loop endlessly
if !compareHops(failedHop, updatedHop) {
log.Printf("received channelupdate after failure, trying again with amt %s and fee %s ppm",
hiWhiteColor(amount), formatFeePPM(amount*1000, updatedRoute.TotalFeesMsat))
return r.pay(ctx, amount, minAmount, maxFeeMsat, updatedRoute, probeSteps)
}
} else {
log.Printf("error rebuilding the route: %s", err)
}
}
if result.Failure.Code == lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE {
r.addFailedChan(node1.Node.PubKey, node2.Node.PubKey, prevHop.
AmtToForwardMsat)
@ -127,9 +154,18 @@ func (r *regolancer) pay(ctx context.Context, amount int64, minAmount int64,
return fmt.Errorf("error: %s @ %d", result.Failure.Code.String(), result.Failure.FailureSourceIndex)
} else {
log.Printf("Success! Paid %s in fees, %s ppm",
formatFee(result.Route.TotalFeesMsat), formatFeePPM(result.Route.TotalAmtMsat, result.Route.TotalFeesMsat))
formatFee(result.Route.TotalFeesMsat), formatFeePPM(result.Route.TotalAmtMsat-result.Route.TotalFeesMsat, result.Route.TotalFeesMsat))
if r.statFilename != "" {
_, err := os.Stat(r.statFilename)
l := lock()
err := l.Lock()
defer l.Unlock()
if err != nil {
return fmt.Errorf("error taking exclusive lock on file %s: %s", r.statFilename, err)
}
_, err = os.Stat(r.statFilename)
f, ferr := os.OpenFile(r.statFilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if ferr != nil {
logErrorF("Error saving rebalance stats to %s: %s", r.statFilename, ferr)

@ -0,0 +1,328 @@
package main
import (
"context"
"encoding/hex"
"errors"
"log"
"time"
"github.com/lightningnetwork/lnd/lnrpc"
)
type rebalanceResult struct {
successfulAttempts int
failedAttempts int
successfulAmt int64
paidFeeMsat int64
}
const (
increaseAmtRapidRebalance string = "increase"
decreaseAmtRapidRebalance string = "decrease"
)
func (r *regolancer) tryRebalance(ctx context.Context, attempt *int) (err error,
repeat bool) {
attemptCtx, attemptCancel := context.WithTimeout(ctx, time.Minute*time.Duration(params.TimeoutAttempt))
defer attemptCancel()
from, to, amt, err := r.pickChannelPair(params.Amount, params.MinAmount, params.RelAmountFrom, params.RelAmountTo)
if err != nil {
log.Printf(errColor("Error during picking channel: %s"), err)
return err, false
}
routeCtx, routeCtxCancel := context.WithTimeout(attemptCtx, time.Second*time.Duration(params.TimeoutRoute))
defer routeCtxCancel()
routes, maxFeeMsat, err := r.getRoutes(routeCtx, from, to, amt*1000)
if err != nil {
if routeCtx.Err() == context.DeadlineExceeded {
log.Print(errColor("Timed out looking for a route"))
return err, false
}
r.addFailedRoute(from, to)
return err, true
}
routeCtxCancel()
for _, route := range routes {
log.Printf("Attempt %s, amount: %s (max fee: %s sat | %s ppm )",
hiWhiteColorF("#%d", *attempt), hiWhiteColor(amt), formatFee(maxFeeMsat), formatFeePPM(amt*1000, maxFeeMsat))
r.printRoute(attemptCtx, route)
err = r.pay(attemptCtx, amt, params.MinAmount, maxFeeMsat, route, params.ProbeSteps)
if err == nil {
if params.AllowRapidRebalance {
rebalanceResult, _ := r.tryRapidRebalance(ctx, route)
if rebalanceResult.successfulAttempts > 0 || rebalanceResult.failedAttempts > 0 {
log.Printf("%s rapid rebalances were successful, total amount: %s (fee: %s sat | %s ppm) - Failed Attempts: %s\n",
hiWhiteColor(rebalanceResult.successfulAttempts), hiWhiteColor(rebalanceResult.successfulAmt),
formatFee(rebalanceResult.paidFeeMsat), formatFeePPM(rebalanceResult.successfulAmt*1000, rebalanceResult.paidFeeMsat),
hiWhiteColor(rebalanceResult.failedAttempts))
}
log.Printf("Finished rapid rebalancing")
}
return nil, false
}
if retryErr, ok := err.(ErrRetry); ok {
amt = retryErr.amount
log.Printf("Trying to rebalance again with %s", hiWhiteColor(amt))
probedRoute, err := r.rebuildRoute(attemptCtx, route, amt)
if err != nil {
log.Printf("Error rebuilding the route for probed payment: %s", errColor(err))
} else {
err = r.pay(attemptCtx, amt, 0, maxFeeMsat, probedRoute, 0)
if err == nil {
return nil, false
} else {
r.invalidateInvoice(amt)
log.Printf("Probed rebalance failed with error: %s", errColor(err))
}
}
}
*attempt++
}
attemptCancel()
if attemptCtx.Err() == context.DeadlineExceeded {
log.Print(errColor("Attempt timed out"))
}
return nil, true
}
func (r *regolancer) tryRapidRebalance(ctx context.Context, route *lnrpc.Route) (result rebalanceResult, err error) {
var (
amt int64 = (route.TotalAmtMsat - route.TotalFeesMsat) / 1000
from uint64 = getSource(route)
to uint64 = getTarget(route)
// Need to save the route and amount locally because we are changing it via the accelerator
// In case we reuse the route it will lead to a situation where no route is found
// the route variable will be overwritten and we are loosing the information
routeLocal *lnrpc.Route
amtLocal int64 = amt
accelerator int64 = 1
hittingTheWall bool
exitEarly bool
capReached bool
maxAmountOnRouteMsat uint64
minAmount uint64
rebalanceStrategy string = increaseAmtRapidRebalance
)
result.successfulAttempts = 0
result.failedAttempts = 0
// Include Initial Rebalance
result.successfulAmt = amt
result.paidFeeMsat = route.TotalFeesMsat
maxAmountOnRouteMsat, err = r.maxAmountOnRoute(ctx, route)
if err != nil {
return result, err
}
if params.MinAmount > 0 {
minAmount = uint64(params.MinAmount)
} else {
minAmount = 10000
}
Loop:
for {
switch rebalanceStrategy {
case increaseAmtRapidRebalance:
if hittingTheWall {
accelerator >>= 1
// In case we encounter that we are already constrained
// by the liquidity on the channels we are waiting for
// the accelerator to go below this amount to save
// already failed rebalances
if amtLocal < accelerator*amt && amtLocal > int64(minAmount) {
continue
}
} else if !capReached {
// we only increase the amount if the max Amount on the
// route is still not reached
accelerator <<= 1
}
if uint64(accelerator*amt) < maxAmountOnRouteMsat/1000 {
amtLocal = accelerator * amt
} else if !capReached {
capReached = true
log.Printf("Max amount on route reached capping amount at %s sats "+
"| max amount on route (max htlc size) %s sats\n", infoColor(amtLocal), infoColor(maxAmountOnRouteMsat/1000))
}
// We reached the initial amount again.
// now we switch to the decreasing strategy.
// We half the amount on every step we go down.
if accelerator < 1 {
accelerator = 2
rebalanceStrategy = decreaseAmtRapidRebalance
amtLocal = amt / accelerator
if amtLocal < int64(minAmount) {
break Loop
}
}
case decreaseAmtRapidRebalance:
accelerator <<= 1
if amtLocal < amt/accelerator {
continue
}
amtLocal = amt / accelerator
if amtLocal < int64(minAmount) {
break Loop
}
}
if exitEarly {
break Loop
}
log.Printf("Rapid rebalance attempt %s, amount: %s\n", hiWhiteColor(result.successfulAttempts+1), hiWhiteColor(amtLocal))
cTo, err := r.getChanInfo(ctx, to)
if err != nil {
logErrorF("Error fetching target channel: %s", err)
return result, err
}
cFrom, err := r.getChanInfo(ctx, from)
if err != nil {
logErrorF("Error fetching source channel: %s", err)
return result, err
}
fromPeer, _ := hex.DecodeString(cFrom.Node1Pub)
if cFrom.Node1Pub == r.myPK {
fromPeer, _ = hex.DecodeString(cFrom.Node2Pub)
}
fromChan, err := r.lnClient.ListChannels(ctx, &lnrpc.ListChannelsRequest{ActiveOnly: true, PublicOnly: true, Peer: fromPeer})
if err != nil {
logErrorF("Error fetching source channel: %s", err)
return result, err
}
toPeer, _ := hex.DecodeString(cTo.Node1Pub)
if cTo.Node1Pub == r.myPK {
toPeer, _ = hex.DecodeString(cTo.Node2Pub)
}
toChan, err := r.lnClient.ListChannels(ctx, &lnrpc.ListChannelsRequest{ActiveOnly: true, PublicOnly: true, Peer: toPeer})
if err != nil {
logErrorF("Error fetching target channel: %s", err)
return result, err
}
for k := range r.fromChannelId {
delete(r.fromChannelId, k)
}
r.fromChannelId = makeChanSet([]uint64{from})
for k := range r.toChannelId {
delete(r.toChannelId, k)
}
r.toChannelId = makeChanSet([]uint64{to})
r.channels = r.channels[:0]
r.fromChannels = r.fromChannels[:0]
r.toChannels = r.toChannels[:0]
r.channels = append(append(r.channels, toChan.Channels...),
fromChan.Channels...)
for k := range r.failureCache {
delete(r.failureCache, k)
}
for k := range r.channelPairs {
delete(r.channelPairs, k)
}
err = r.getChannelCandidates(params.FromPerc, params.ToPerc, amtLocal)
if err != nil {
logErrorF("Error selecting channel candidates: %s", err)
return result, err
}
amtLocalTemp := amtLocal
_, _, amtLocal, err = r.pickChannelPair(amtLocal, params.MinAmount, params.RelAmountFrom, params.RelAmountTo)
if err != nil {
log.Printf(errColor("Error during picking channel: %s"), err)
hittingTheWall = true
// We are not returning an error here because
// in we still could rebalance an amount in the
// decreasing strategy.
// return result, err
continue
}
if amtLocalTemp > amtLocal {
log.Printf("Rapid fire starting with actual amount: %s (could be lower than the attempted amount in case there is less liquidity available on the channel)", hiWhiteColor(amtLocal))
// We are already using maximum available liquidity so we can begin decreasing amounts again.
hittingTheWall = true
// This is needed so we do not test further amounts
// when in the decreasing strategy.
if rebalanceStrategy == decreaseAmtRapidRebalance {
exitEarly = true
}
}
routeLocal, err = r.rebuildRoute(ctx, route, amtLocal)
if err != nil {
log.Printf(errColor("Error building route: %s"), err)
return result, err
}
attemptCtx, attemptCancel := context.WithTimeout(ctx, time.Minute*time.Duration(params.TimeoutAttempt))
defer attemptCancel()
// make sure we account for fees when increasing the rebalance amount
maxFeeMsat, _, err := r.calcFeeMsat(ctx, from, to, amtLocal*1000)
if err != nil {
log.Printf(errColor("Error calculating fee: %s"), err)
return result, err
}
err = r.pay(attemptCtx, amtLocal, params.MinAmount, maxFeeMsat, routeLocal, 0)
// In case we are already decreasing the amount we can exit early because
// for even smaller amounts the fee will be higher (reason is the basefee).
if rebalanceStrategy == decreaseAmtRapidRebalance && errors.Is(err, ErrFeeExceeded) {
exitEarly = true
}
attemptCancel()
if attemptCtx.Err() == context.DeadlineExceeded {
log.Print(errColor("Rapid rebalance attempt timed out"))
return result, attemptCtx.Err()
}
if err != nil {
log.Printf("Rebalance failed with %s", err)
log.Println()
result.failedAttempts++
hittingTheWall = true
} else {
result.successfulAttempts++
result.successfulAmt += amtLocal
result.paidFeeMsat += routeLocal.TotalFeesMsat
}
}
return result, nil
}

@ -147,7 +147,7 @@ func (r *regolancer) printRoute(ctx context.Context, route *lnrpc.Route) {
}
errs := ""
fmt.Printf("%s %s sat | %s ppm\n", faintWhiteColor("Total fee:"),
formatFee(route.TotalFeesMsat), formatFeePPM(route.TotalAmtMsat, route.TotalFeesMsat))
formatFee(route.TotalFeesMsat), formatFeePPM(route.TotalAmtMsat-route.TotalFeesMsat, route.TotalFeesMsat))
for i, hop := range route.Hops {
cached := ""
if params.NodeCacheInfo {
@ -305,3 +305,27 @@ func (r *regolancer) makeNodeList(nodes []string) error {
}
return nil
}
func getSource(route *lnrpc.Route) uint64 {
if len(route.Hops) > 0 {
return route.Hops[0].ChanId
}
return 0
}
func getTarget(route *lnrpc.Route) uint64 {
if len(route.Hops) > 0 {
return route.Hops[len(route.Hops)-1].ChanId
}
return 0
}
func compareHops(hop1 *lnrpc.Hop, hop2 *lnrpc.Hop) bool {
if hop1 == nil || hop2 == nil {
return false
}
return hop1.ChanId == hop2.ChanId &&
hop1.FeeMsat == hop2.FeeMsat &&
hop1.Expiry == hop2.Expiry
}

Loading…
Cancel
Save