2019-05-19 14:52:17 +00:00
|
|
|
// Prometheus exporter for Ethereum address balances.
|
|
|
|
// Forked from https://github.com/hunterlong/ethexporter
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"context"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"math/big"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
|
"github.com/ethereum/go-ethereum/ethclient"
|
2019-05-20 18:05:04 +00:00
|
|
|
"github.com/ethereum/go-ethereum/params"
|
2024-02-15 05:46:47 +00:00
|
|
|
_ "github.com/prysmaticlabs/prysm/v5/runtime/maxprocs"
|
2020-04-13 04:11:09 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2019-05-19 14:52:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
allWatching []*Watching
|
|
|
|
loadSeconds float64
|
|
|
|
totalLoaded int64
|
|
|
|
eth *ethclient.Client
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
port = flag.Int("port", 9090, "Port to serve /metrics")
|
|
|
|
web3URL = flag.String("web3-provider", "https://goerli.prylabs.net", "Web3 URL to access information about ETH1")
|
|
|
|
prefix = flag.String("prefix", "", "Metrics prefix.")
|
|
|
|
addressFilePath = flag.String("addresses", "", "File path to addresses text file.")
|
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
if *addressFilePath == "" {
|
|
|
|
log.Println("--addresses is required")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err := OpenAddresses(*addressFilePath)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = ConnectionToGeth(*web3URL)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check address balances
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
totalLoaded = 0
|
|
|
|
t1 := time.Now()
|
|
|
|
fmt.Printf("Checking %v wallets...\n", len(allWatching))
|
|
|
|
for _, v := range allWatching {
|
2021-01-25 21:27:30 +00:00
|
|
|
v.Balance = EthBalance(v.Address).String()
|
2019-05-19 14:52:17 +00:00
|
|
|
totalLoaded++
|
|
|
|
}
|
|
|
|
t2 := time.Now()
|
|
|
|
loadSeconds = t2.Sub(t1).Seconds()
|
|
|
|
fmt.Printf("Finished checking %v wallets in %0.0f seconds, sleeping for %v seconds.\n", len(allWatching), loadSeconds, 15)
|
|
|
|
time.Sleep(15 * time.Second)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
block := CurrentBlock()
|
|
|
|
|
|
|
|
fmt.Printf("ETHexporter has started on port %v using web3 server: %v at block #%v\n", *port, *web3URL, block)
|
|
|
|
|
|
|
|
http.HandleFunc("/metrics", MetricsHTTP)
|
|
|
|
http.HandleFunc("/reload", ReloadHTTP)
|
2023-05-12 15:51:20 +00:00
|
|
|
srv := &http.Server{
|
|
|
|
Addr: fmt.Sprintf("127.0.0.1:%d", *port),
|
|
|
|
ReadHeaderTimeout: 3 * time.Second,
|
|
|
|
}
|
|
|
|
log.Fatal(srv.ListenAndServe())
|
2019-05-19 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Watching address wrapper
|
|
|
|
type Watching struct {
|
|
|
|
Name string
|
|
|
|
Address string
|
|
|
|
Balance string
|
|
|
|
}
|
|
|
|
|
|
|
|
// ConnectionToGeth - Connect to remote server.
|
|
|
|
func ConnectionToGeth(url string) error {
|
|
|
|
var err error
|
|
|
|
eth, err = ethclient.Dial(url)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-01-25 21:27:30 +00:00
|
|
|
// EthBalance from remote server.
|
|
|
|
func EthBalance(address string) *big.Float {
|
2019-05-19 14:52:17 +00:00
|
|
|
balance, err := eth.BalanceAt(context.TODO(), common.HexToAddress(address), nil)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Error fetching ETH Balance for address: %v\n", address)
|
|
|
|
}
|
|
|
|
return ToEther(balance)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CurrentBlock in ETH1.
|
|
|
|
func CurrentBlock() uint64 {
|
|
|
|
block, err := eth.BlockByNumber(context.TODO(), nil)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Error fetching current block height: %v\n", err)
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return block.NumberU64()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ToEther from Wei.
|
|
|
|
func ToEther(o *big.Int) *big.Float {
|
2019-05-20 18:05:04 +00:00
|
|
|
wei := big.NewFloat(0)
|
|
|
|
wei.SetInt(o)
|
|
|
|
return new(big.Float).Quo(wei, big.NewFloat(params.Ether))
|
2019-05-19 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// MetricsHTTP - HTTP response handler for /metrics.
|
2020-10-12 08:11:05 +00:00
|
|
|
func MetricsHTTP(w http.ResponseWriter, _ *http.Request) {
|
2022-08-26 11:49:50 +00:00
|
|
|
allOut := make([]string, 0, len(allWatching))
|
2019-05-19 14:52:17 +00:00
|
|
|
total := big.NewFloat(0)
|
|
|
|
for _, v := range allWatching {
|
|
|
|
if v.Balance == "" {
|
|
|
|
v.Balance = "0"
|
|
|
|
}
|
|
|
|
bal := big.NewFloat(0)
|
|
|
|
bal.SetString(v.Balance)
|
|
|
|
total.Add(total, bal)
|
|
|
|
allOut = append(allOut, fmt.Sprintf("%veth_balance{name=\"%v\",address=\"%v\"} %v", *prefix, v.Name, v.Address, v.Balance))
|
|
|
|
}
|
2020-10-04 15:03:10 +00:00
|
|
|
allOut = append(allOut,
|
|
|
|
fmt.Sprintf("%veth_balance_total %0.18f", *prefix, total),
|
|
|
|
fmt.Sprintf("%veth_load_seconds %0.2f", *prefix, loadSeconds),
|
|
|
|
fmt.Sprintf("%veth_loaded_addresses %v", *prefix, totalLoaded),
|
|
|
|
fmt.Sprintf("%veth_total_addresses %v", *prefix, len(allWatching)))
|
|
|
|
|
2020-04-13 04:11:09 +00:00
|
|
|
if _, err := fmt.Fprintln(w, strings.Join(allOut, "\n")); err != nil {
|
|
|
|
logrus.WithError(err).Error("Failed to write metrics")
|
|
|
|
}
|
2019-05-19 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ReloadHTTP reloads the addresses from disk.
|
|
|
|
func ReloadHTTP(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
if err := OpenAddresses(*addressFilePath); err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
log.Println("Reloaded addresses")
|
|
|
|
}
|
|
|
|
|
|
|
|
// OpenAddresses from text file (name:address)
|
|
|
|
func OpenAddresses(filename string) error {
|
2021-08-15 15:24:13 +00:00
|
|
|
file, err := os.Open(filename) // #nosec G304
|
2019-05-19 14:52:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-04-13 04:11:09 +00:00
|
|
|
defer func() {
|
|
|
|
if err := file.Close(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}()
|
2019-05-19 14:52:17 +00:00
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
allWatching = []*Watching{}
|
|
|
|
for scanner.Scan() {
|
|
|
|
object := strings.Split(scanner.Text(), ":")
|
|
|
|
if common.IsHexAddress(object[1]) {
|
|
|
|
w := &Watching{
|
|
|
|
Name: object[0],
|
|
|
|
Address: object[1],
|
|
|
|
}
|
|
|
|
allWatching = append(allWatching, w)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|