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