github.com/trezor/blockbook@v0.4.1-0.20240328132726-e9a08582ee2c/contrib/scripts/check-and-generate-port-registry.go (about)

     1  // usr/bin/go run $0 $@ ; exit
     2  package main
     3  
     4  import (
     5  	"bytes"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"math"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  )
    15  
    16  const (
    17  	inputDir   = "configs/coins"
    18  	outputFile = "docs/ports.md"
    19  )
    20  
    21  // PortInfo contains backend and blockbook ports
    22  type PortInfo struct {
    23  	CoinName              string
    24  	BlockbookInternalPort uint16
    25  	BlockbookPublicPort   uint16
    26  	BackendRPCPort        uint16
    27  	BackendServicePorts   map[string]uint16
    28  }
    29  
    30  // PortInfoSlice is self describing
    31  type PortInfoSlice []*PortInfo
    32  
    33  // Config contains coin configuration
    34  type Config struct {
    35  	Coin struct {
    36  		Name  string `json:"name"`
    37  		Label string `json:"label"`
    38  		Alias string `json:"alias"`
    39  	}
    40  	Ports     map[string]uint16 `json:"ports"`
    41  	Blockbook struct {
    42  		PackageName string `json:"package_name"`
    43  	}
    44  }
    45  
    46  func checkPorts() int {
    47  	ports := make(map[uint16][]string)
    48  	status := 0
    49  
    50  	files, err := os.ReadDir(inputDir)
    51  	if err != nil {
    52  		panic(err)
    53  	}
    54  
    55  	for _, fi := range files {
    56  		if fi.IsDir() || fi.Name()[0] == '.' {
    57  			continue
    58  		}
    59  
    60  		path := filepath.Join(inputDir, fi.Name())
    61  		f, err := os.Open(path)
    62  		if err != nil {
    63  			panic(fmt.Errorf("%s: %s", path, err))
    64  		}
    65  		defer f.Close()
    66  
    67  		v := Config{}
    68  		d := json.NewDecoder(f)
    69  		err = d.Decode(&v)
    70  		if err != nil {
    71  			panic(fmt.Errorf("%s: json: %s", path, err))
    72  		}
    73  
    74  		if _, ok := v.Ports["blockbook_internal"]; !ok {
    75  			fmt.Printf("%s (%s): missing blockbook_internal port\n", v.Coin.Name, v.Coin.Alias)
    76  			status = 1
    77  		}
    78  		if _, ok := v.Ports["blockbook_public"]; !ok {
    79  			fmt.Printf("%s (%s): missing blockbook_public port\n", v.Coin.Name, v.Coin.Alias)
    80  			status = 1
    81  		}
    82  		if _, ok := v.Ports["backend_rpc"]; !ok {
    83  			fmt.Printf("%s (%s): missing backend_rpc port\n", v.Coin.Name, v.Coin.Alias)
    84  			status = 1
    85  		}
    86  
    87  		for _, port := range v.Ports {
    88  			// ignore duplicities caused by configs that do not serve blockbook directly (consensus layers)
    89  			if port > 0 && v.Blockbook.PackageName == "" {
    90  				ports[port] = append(ports[port], v.Coin.Alias)
    91  			}
    92  		}
    93  	}
    94  
    95  	for port, coins := range ports {
    96  		if len(coins) > 1 {
    97  			fmt.Printf("port %d: registered by %q\n", port, coins)
    98  			status = 1
    99  		}
   100  	}
   101  
   102  	if status != 0 {
   103  		fmt.Println("Got some errors")
   104  	}
   105  	return status
   106  }
   107  
   108  func main() {
   109  	output := "stdout"
   110  	if len(os.Args) > 1 {
   111  		if len(os.Args) == 2 && os.Args[1] == "-w" {
   112  			output = outputFile
   113  		} else {
   114  			fmt.Fprintf(os.Stderr, "Usage: %s [-w]\n", filepath.Base(os.Args[0]))
   115  			fmt.Fprintf(os.Stderr, "    -w    write output to %s instead of stdout\n", outputFile)
   116  			os.Exit(1)
   117  		}
   118  	}
   119  
   120  	status := checkPorts()
   121  	if status != 0 {
   122  		os.Exit(status)
   123  	}
   124  
   125  	slice, err := loadPortInfo(inputDir)
   126  	if err != nil {
   127  		panic(err)
   128  	}
   129  
   130  	sortPortInfo(slice)
   131  
   132  	err = writeMarkdown(output, slice)
   133  	if err != nil {
   134  		panic(err)
   135  	}
   136  }
   137  
   138  func loadPortInfo(dir string) (PortInfoSlice, error) {
   139  	files, err := os.ReadDir(dir)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	items := make(PortInfoSlice, 0, len(files))
   145  
   146  	for _, fi := range files {
   147  		if fi.IsDir() || fi.Name()[0] == '.' {
   148  			continue
   149  		}
   150  
   151  		path := filepath.Join(dir, fi.Name())
   152  		f, err := os.Open(path)
   153  		if err != nil {
   154  			return nil, fmt.Errorf("%s: %s", path, err)
   155  		}
   156  		defer f.Close()
   157  
   158  		v := Config{}
   159  		d := json.NewDecoder(f)
   160  		err = d.Decode(&v)
   161  		if err != nil {
   162  			return nil, fmt.Errorf("%s: json: %s", path, err)
   163  		}
   164  
   165  		// skip configs that do not have blockbook (consensus layers)
   166  		if v.Blockbook.PackageName == "" {
   167  			continue
   168  		}
   169  		name := v.Coin.Label
   170  		// exceptions when to use Name instead of Label so that the table looks good
   171  		if len(name) == 0 || strings.Contains(v.Coin.Name, "Ethereum") || strings.Contains(v.Coin.Name, "Archive") {
   172  			name = v.Coin.Name
   173  		}
   174  		item := &PortInfo{CoinName: name, BackendServicePorts: map[string]uint16{}}
   175  		for k, p := range v.Ports {
   176  			if p == 0 {
   177  				continue
   178  			}
   179  
   180  			switch k {
   181  			case "blockbook_internal":
   182  				item.BlockbookInternalPort = p
   183  			case "blockbook_public":
   184  				item.BlockbookPublicPort = p
   185  			case "backend_rpc":
   186  				item.BackendRPCPort = p
   187  			default:
   188  				if len(k) > 8 && k[:8] == "backend_" {
   189  					item.BackendServicePorts[k[8:]] = p
   190  				}
   191  			}
   192  		}
   193  
   194  		items = append(items, item)
   195  	}
   196  
   197  	return items, nil
   198  }
   199  
   200  func sortPortInfo(slice PortInfoSlice) {
   201  	// normalizes values in order to sort zero values at the bottom of the slice
   202  	normalize := func(a, b uint16) (uint16, uint16) {
   203  		if a == 0 {
   204  			a = math.MaxUint16
   205  		}
   206  		if b == 0 {
   207  			b = math.MaxUint16
   208  		}
   209  		return a, b
   210  	}
   211  
   212  	// sort values by BlockbookPublicPort, then by BackendRPCPort and finally by
   213  	// CoinName; zero values are sorted at the bottom of the slice
   214  	sort.Slice(slice, func(i, j int) bool {
   215  		a, b := normalize(slice[i].BlockbookPublicPort, slice[j].BlockbookPublicPort)
   216  
   217  		if a < b {
   218  			return true
   219  		}
   220  		if a > b {
   221  			return false
   222  		}
   223  
   224  		a, b = normalize(slice[i].BackendRPCPort, slice[j].BackendRPCPort)
   225  
   226  		if a < b {
   227  			return true
   228  		}
   229  		if a > b {
   230  			return false
   231  		}
   232  
   233  		return strings.Compare(slice[i].CoinName, slice[j].CoinName) == -1
   234  	})
   235  }
   236  
   237  func writeMarkdown(output string, slice PortInfoSlice) error {
   238  	var (
   239  		buf bytes.Buffer
   240  		err error
   241  	)
   242  
   243  	fmt.Fprintf(&buf, "# Registry of ports\n\n")
   244  
   245  	header := []string{"coin", "blockbook public", "blockbook internal", "backend rpc", "backend service ports (zmq)"}
   246  	writeTable(&buf, header, slice)
   247  
   248  	fmt.Fprintf(&buf, "\n> NOTE: This document is generated from coin definitions in `configs/coins` using command `go run contrib/scripts/check-and-generate-port-registry.go -w`.\n")
   249  
   250  	out := os.Stdout
   251  	if output != "stdout" {
   252  		out, err = os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
   253  		if err != nil {
   254  			return err
   255  		}
   256  		defer out.Close()
   257  	}
   258  
   259  	n, err := out.Write(buf.Bytes())
   260  	if err != nil {
   261  		return err
   262  	}
   263  	if n < len(buf.Bytes()) {
   264  		return io.ErrShortWrite
   265  	}
   266  
   267  	return nil
   268  }
   269  
   270  func writeTable(w io.Writer, header []string, slice PortInfoSlice) {
   271  	rows := make([][]string, len(slice))
   272  	for i, item := range slice {
   273  		row := make([]string, len(header))
   274  		row[0] = item.CoinName
   275  		if item.BlockbookPublicPort > 0 {
   276  			row[1] = fmt.Sprintf("%d", item.BlockbookPublicPort)
   277  		}
   278  		if item.BlockbookInternalPort > 0 {
   279  			row[2] = fmt.Sprintf("%d", item.BlockbookInternalPort)
   280  		}
   281  		if item.BackendRPCPort > 0 {
   282  			row[3] = fmt.Sprintf("%d", item.BackendRPCPort)
   283  		}
   284  
   285  		svcPorts := make([]string, 0, len(item.BackendServicePorts))
   286  		for k, v := range item.BackendServicePorts {
   287  			var s string
   288  			if k == "message_queue" {
   289  				s = fmt.Sprintf("%d", v)
   290  			} else {
   291  				s = fmt.Sprintf("%d %s", v, k)
   292  			}
   293  			svcPorts = append(svcPorts, s)
   294  		}
   295  
   296  		sort.Strings(svcPorts)
   297  		row[4] = strings.Join(svcPorts, ", ")
   298  
   299  		rows[i] = row
   300  	}
   301  
   302  	padding := make([]int, len(header))
   303  	for column := range header {
   304  		padding[column] = len(header[column])
   305  
   306  		for _, row := range rows {
   307  			padding[column] = maxInt(padding[column], len(row[column]))
   308  		}
   309  	}
   310  
   311  	content := make([][]string, 0, len(rows)+2)
   312  
   313  	content = append(content, paddedRow(header, padding))
   314  	content = append(content, delim("-", padding))
   315  
   316  	for _, row := range rows {
   317  		content = append(content, paddedRow(row, padding))
   318  	}
   319  
   320  	for _, row := range content {
   321  		fmt.Fprintf(w, "|%s|\n", strings.Join(row, "|"))
   322  	}
   323  }
   324  
   325  func maxInt(a, b int) int {
   326  	if a > b {
   327  		return a
   328  	}
   329  	return b
   330  }
   331  
   332  func paddedRow(row []string, padding []int) []string {
   333  	out := make([]string, len(row))
   334  	for i := 0; i < len(row); i++ {
   335  		format := fmt.Sprintf(" %%-%ds ", padding[i])
   336  		out[i] = fmt.Sprintf(format, row[i])
   337  	}
   338  	return out
   339  }
   340  
   341  func delim(str string, padding []int) []string {
   342  	out := make([]string, len(padding))
   343  	for i := 0; i < len(padding); i++ {
   344  		out[i] = strings.Repeat(str, padding[i]+2)
   345  	}
   346  	return out
   347  }