mirror of https://github.com/rkfg/regolancer
Compare commits
49 Commits
Author | SHA1 | Date |
---|---|---|
|
fb578adc01 | 11 months ago |
|
1e8858c41c | 11 months ago |
|
0771bd50b8 | 11 months ago |
|
e6c593cbb2 | 1 year ago |
|
85ce47602b | 1 year ago |
|
baac26db5b | 1 year ago |
|
5b0031ecc8 | 1 year ago |
|
76cdc2efd6 | 1 year ago |
|
3467c7edbe | 1 year ago |
|
f839791e2b | 1 year ago |
|
56017f52a7 | 1 year ago |
|
efa39b4cc1 | 1 year ago |
|
5eafce0853 | 1 year ago |
|
c44da07f35 | 1 year ago |
|
a3c86f4e28 | 1 year ago |
|
5b9161c2e7 | 1 year ago |
|
2b6c4061ea | 1 year ago |
|
f299a0bfe3 | 1 year ago |
|
9a4f062085 | 1 year ago |
|
6e163725b1 | 1 year ago |
|
f31d1d6f51 | 1 year ago |
|
9a5116eef8 | 1 year ago |
|
00151033e9 | 1 year ago |
|
ff679c977d | 1 year ago |
|
fe4caa232c | 1 year ago |
|
bfe17d0ce1 | 1 year ago |
|
f7b9ca5689 | 2 years ago |
|
a52771ddcc | 2 years ago |
|
fb0f3cbec1 | 2 years ago |
|
ec8e055340 | 2 years ago |
|
e41b17b863 | 2 years ago |
|
9b55697957 | 2 years ago |
|
9193aaed5d | 2 years ago |
|
410566cb36 | 2 years ago |
|
41c49fcbab | 2 years ago |
|
8b9d8c61c1 | 2 years ago |
|
4b872214be | 2 years ago |
|
ee3af3dffe | 2 years ago |
|
4116c3094c | 2 years ago |
|
b5960e8f39 | 2 years ago |
|
189f124597 | 2 years ago |
|
381538d43b | 2 years ago |
|
91acf21086 | 2 years ago |
|
7cb051fc97 | 2 years ago |
|
200c42fe8b | 2 years ago |
|
5a3a41afae | 2 years ago |
|
cefbff413d | 2 years ago |
|
98068a105d | 2 years ago |
|
df27799b33 | 2 years ago |
@ -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
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue