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 }