mirror of https://github.com/guggero/chantools
Initial commit
commit
2774bcee41
@ -0,0 +1 @@
|
||||
/chansummary
|
@ -0,0 +1,24 @@
|
||||
run:
|
||||
# timeout for analysis
|
||||
deadline: 4m
|
||||
|
||||
# Linting uses a lot of memory. Keep it under control by only running a single
|
||||
# worker.
|
||||
concurrency: 1
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
# Don't report about shadowed variables
|
||||
check-shadowing: false
|
||||
gofmt:
|
||||
# simplify code: gofmt with `-s` option, true by default
|
||||
simplify: true
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
# Init functions are used by loggers throughout the codebase.
|
||||
- gochecknoinits
|
||||
|
||||
# Global variables are used by loggers.
|
||||
- gochecknoglobals
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Oliver Gugger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,49 @@
|
||||
PKG := github.com/guggero/chansummary
|
||||
|
||||
GOTEST := GO111MODULE=on go test -v
|
||||
|
||||
GO_BIN := ${GOPATH}/bin
|
||||
|
||||
GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
||||
GOLIST := go list $(PKG)/... | grep -v '/vendor/'
|
||||
|
||||
LINT_BIN := $(GO_BIN)/golangci-lint
|
||||
LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
LINT_COMMIT := v1.18.0
|
||||
LINT = $(LINT_BIN) run -v
|
||||
|
||||
DEPGET := cd /tmp && GO111MODULE=on go get -v
|
||||
GOBUILD := GO111MODULE=on go build -v
|
||||
GOINSTALL := GO111MODULE=on go install -v
|
||||
GOTEST := GO111MODULE=on go test -v
|
||||
XARGS := xargs -L 1
|
||||
|
||||
TEST_FLAGS = -test.timeout=20m
|
||||
|
||||
UNIT := $(GOLIST) | $(XARGS) env $(GOTEST) $(TEST_FLAGS)
|
||||
|
||||
default: build
|
||||
|
||||
$(LINT_BIN):
|
||||
@$(call print, "Fetching linter")
|
||||
$(DEPGET) $(LINT_PKG)@$(LINT_COMMIT)
|
||||
|
||||
unit:
|
||||
@$(call print, "Running unit tests.")
|
||||
$(UNIT)
|
||||
|
||||
build:
|
||||
@$(call print, "Building chansummary.")
|
||||
$(GOBUILD) $(PKG)/cmd/chansummary
|
||||
|
||||
install:
|
||||
@$(call print, "Installing chansummary.")
|
||||
$(GOINSTALL) $(PKG)/cmd/chansummary
|
||||
|
||||
fmt:
|
||||
@$(call print, "Formatting source.")
|
||||
gofmt -l -w -s $(GOFILES_NOVENDOR)
|
||||
|
||||
lint: $(LINT_BIN)
|
||||
@$(call print, "Linting source.")
|
||||
$(LINT)
|
@ -0,0 +1,7 @@
|
||||
# Channel summary
|
||||
|
||||
This tool works with the output of lnd's `listchannels` command and creates
|
||||
a summary of the on-chain state of these channels.
|
||||
|
||||
**WARNING**: This tool will query public block explorer APIs, your privacy
|
||||
might not be preserved. Use at your own risk.
|
@ -0,0 +1,80 @@
|
||||
package chansummary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type chainApi struct {
|
||||
baseUrl string
|
||||
}
|
||||
|
||||
type transaction struct {
|
||||
Vin []*vin `json:"vin"`
|
||||
Vout []*vout `json:"vout"`
|
||||
}
|
||||
|
||||
type vin struct {
|
||||
Tixid string `json:"txid"`
|
||||
Vout int `json:"vout"`
|
||||
Prevout *vout `json:"prevout"`
|
||||
}
|
||||
|
||||
type vout struct {
|
||||
ScriptPubkey string `json:"scriptpubkey"`
|
||||
ScriptPubkeyAsm string `json:"scriptpubkey_asm"`
|
||||
ScriptPubkeyType string `json:"scriptpubkey_type"`
|
||||
ScriptPubkeyAddr string `json:"scriptpubkey_addr"`
|
||||
Value uint64 `json:"value"`
|
||||
outspend *outspend
|
||||
}
|
||||
|
||||
type outspend struct {
|
||||
Spent bool `json:"spent"`
|
||||
Txid string `json:"txid"`
|
||||
Vin int `json:"vin"`
|
||||
Status *status `json:"status"`
|
||||
}
|
||||
|
||||
type status struct {
|
||||
Confirmed bool `json:"confirmed"`
|
||||
BlockHeight int `json:"block_height"`
|
||||
BlockHash string `json:"block_hash"`
|
||||
}
|
||||
|
||||
func (a *chainApi) Transaction(txid string) (*transaction, error) {
|
||||
tx := &transaction{}
|
||||
err := Fetch(fmt.Sprintf("%s/tx/%s", a.baseUrl, txid), tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for idx, vout := range tx.Vout {
|
||||
url := fmt.Sprintf(
|
||||
"%s/tx/%s/outspend/%d", a.baseUrl, txid, idx,
|
||||
)
|
||||
outspend := outspend{}
|
||||
err := Fetch(url, &outspend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vout.outspend = &outspend
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func Fetch(url string, target interface{}) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body := new(bytes.Buffer)
|
||||
_, err = body.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body.Bytes(), target)
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
package chansummary
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type channel struct {
|
||||
RemotePubkey string `json:"remote_pubkey"`
|
||||
ChannelPoint string `json:"channel_point"`
|
||||
Capacity string `json:"capacity"`
|
||||
Initiator bool `json:"initiator"`
|
||||
LocalBalance string `json:"local_balance"`
|
||||
RemoteBalance string `json:"remote_balance"`
|
||||
}
|
||||
|
||||
func (c *channel) FundingTXID() string {
|
||||
parts := strings.Split(c.ChannelPoint, ":")
|
||||
if len(parts) != 2 {
|
||||
panic(fmt.Errorf("channel point not in format <txid>:<idx>"))
|
||||
}
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
func (c *channel) FundingTXIndex() int {
|
||||
parts := strings.Split(c.ChannelPoint, ":")
|
||||
if len(parts) != 2 {
|
||||
panic(fmt.Errorf("channel point not in format <txid>:<idx>"))
|
||||
}
|
||||
return parseInt(parts[1])
|
||||
}
|
||||
|
||||
func (c *channel) localBalance() uint64 {
|
||||
return uint64(parseInt(c.LocalBalance))
|
||||
}
|
||||
|
||||
func (c *channel) remoteBalance() uint64 {
|
||||
return uint64(parseInt(c.RemoteBalance))
|
||||
}
|
||||
|
||||
func collectChanSummary(cfg *config, channels []*channel) error {
|
||||
var (
|
||||
chansClosed = 0
|
||||
chansOpen = 0
|
||||
valueUnspent = uint64(0)
|
||||
valueSalvage = uint64(0)
|
||||
valueSafe = uint64(0)
|
||||
)
|
||||
|
||||
chainApi := &chainApi{baseUrl: cfg.ApiUrl}
|
||||
|
||||
for idx, channel := range channels {
|
||||
tx, err := chainApi.Transaction(channel.FundingTXID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outspend := tx.Vout[channel.FundingTXIndex()].outspend
|
||||
if outspend.Spent {
|
||||
chansClosed++
|
||||
|
||||
s, f, err := reportOutspend(chainApi, channel, outspend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valueSalvage += s
|
||||
valueSafe += f
|
||||
} else {
|
||||
chansOpen++
|
||||
valueUnspent += channel.localBalance()
|
||||
}
|
||||
|
||||
if idx%50 == 0 {
|
||||
fmt.Printf("Queried channel %d of %d.\n", idx,
|
||||
len(channels))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Finished scanning.\nClosed channels: %d\nOpen channels: "+
|
||||
"%d\nSats in open channels: %d\nSats that can possibly be "+
|
||||
"salvaged: %d\nSats in co-op close channels: %d\n", chansClosed,
|
||||
chansOpen, valueUnspent, valueSalvage, valueSafe)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func reportOutspend(api *chainApi, ch *channel, os *outspend) (uint64, uint64,
|
||||
error) {
|
||||
|
||||
spendTx, err := api.Transaction(os.Txid)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
numSpent := 0
|
||||
salvageBalance := uint64(0)
|
||||
safeBalance := uint64(0)
|
||||
for _, vout := range spendTx.Vout {
|
||||
if vout.outspend.Spent {
|
||||
numSpent++
|
||||
}
|
||||
}
|
||||
if numSpent != len(spendTx.Vout) {
|
||||
fmt.Printf("Channel %s spent by %s:%d which has %d outputs of "+
|
||||
"which %d are spent:\n", ch.ChannelPoint, os.Txid,
|
||||
os.Vin, len(spendTx.Vout), numSpent)
|
||||
var utxo []*vout
|
||||
for _, vout := range spendTx.Vout {
|
||||
if !vout.outspend.Spent {
|
||||
utxo = append(utxo, vout)
|
||||
}
|
||||
}
|
||||
|
||||
if salvageable(ch, utxo) {
|
||||
salvageBalance += utxo[0].Value
|
||||
|
||||
outs := spendTx.Vout
|
||||
|
||||
switch {
|
||||
case len(outs) == 1 &&
|
||||
outs[0].ScriptPubkeyType == "v0_p2wpkh" &&
|
||||
outs[0].outspend.Spent == false:
|
||||
|
||||
safeBalance += utxo[0].Value
|
||||
|
||||
case len(outs) == 2 &&
|
||||
outs[0].ScriptPubkeyType == "v0_p2wpkh" &&
|
||||
outs[1].ScriptPubkeyType == "v0_p2wpkh":
|
||||
|
||||
safeBalance += utxo[0].Value
|
||||
}
|
||||
} else {
|
||||
for idx, vout := range spendTx.Vout {
|
||||
if !vout.outspend.Spent {
|
||||
fmt.Printf("UTXO %d of type %s with "+
|
||||
"value %d\n", idx,
|
||||
vout.ScriptPubkeyType,
|
||||
vout.Value)
|
||||
}
|
||||
}
|
||||
fmt.Printf("Local balance: %s\n", ch.LocalBalance)
|
||||
fmt.Printf("Remote balance: %s\n", ch.RemoteBalance)
|
||||
fmt.Printf("Initiator: %v\n", ch.Initiator)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return salvageBalance, safeBalance, nil
|
||||
}
|
||||
|
||||
func salvageable(ch *channel, utxo []*vout) bool {
|
||||
return ch.localBalance() == utxo[0].Value ||
|
||||
ch.remoteBalance() == 0
|
||||
}
|
||||
|
||||
func parseInt(str string) int {
|
||||
index, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error parsing '%s' as int: %v", str, err))
|
||||
}
|
||||
return index
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/guggero/chansummary"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := chansummary.Main(); err != nil {
|
||||
fmt.Printf("Error running chansummary: %v\n", err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
module github.com/guggero/chansummary
|
||||
|
||||
require github.com/jessevdk/go-flags v1.4.0
|
||||
|
||||
go 1.13
|
@ -0,0 +1,2 @@
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
@ -0,0 +1,54 @@
|
||||
package chansummary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultApiUrl = "https://blockstream.info/api"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)"`
|
||||
}
|
||||
|
||||
type fileContent struct {
|
||||
Channels []*channel `json:"channels"`
|
||||
}
|
||||
|
||||
func Main() error {
|
||||
var (
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
// Parse command line.
|
||||
config := &config{
|
||||
ApiUrl: defaultApiUrl,
|
||||
}
|
||||
if args, err = flags.Parse(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("exactly one file argument needed")
|
||||
}
|
||||
file := args[0]
|
||||
|
||||
// Read file and parse into channel.
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
channels := fileContent{}
|
||||
err = decoder.Decode(&channels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return collectChanSummary(config, channels.Channels)
|
||||
}
|
Loading…
Reference in New Issue