├── .gitignore ├── README.md ├── backend ├── api.py ├── config.sample.py └── requirements.txt ├── docs ├── config │ ├── dashboard.json │ ├── default.config │ └── radix-babylon.service ├── scripts │ ├── switch-mode │ └── update-node └── validator_guide.md └── frontend ├── debug.bat ├── elm.json ├── src ├── ArchiveApi.elm ├── GatewayApi.elm ├── Main.elm ├── Page │ ├── Home.elm │ └── Validators.elm ├── Palette.elm ├── Ports.elm ├── UI.elm └── Utils.elm └── static ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── images └── Telegram_2019_simple_logo.svg ├── index.html └── site.webmanifest /.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff 3 | # elm-repl generated files 4 | repl-temp-* 5 | .idea 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fpstaking 2 | Website of Florian Pieper Staking 3 | -------------------------------------------------------------------------------- /backend/api.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | from fastapi import FastAPI 4 | import uvicorn 5 | from starlette.middleware.cors import CORSMiddleware 6 | from fastapi.encoders import jsonable_encoder 7 | from pydantic import BaseModel 8 | from fastapi_utils.tasks import repeat_every 9 | from config import API_HOST, API_PORT, ARCHIVE_ENDPOINT 10 | 11 | 12 | class JsonRpc(BaseModel): 13 | jsonrpc: str 14 | method: str 15 | params: dict 16 | id: int 17 | 18 | 19 | class Cache: 20 | def __init__(self): 21 | self.cached = {} 22 | 23 | def update(self): 24 | json_rpc = JsonRpc(jsonrpc="2.0", method="validators.get_next_epoch_set", params={"size": 200}, id=1) 25 | self.cached["validators.get_next_epoch_set"] = asyncio.run(archive_request(json_rpc)) 26 | print("Updated cache") 27 | 28 | def get(self, method: str): 29 | return self.cached.get(method) 30 | 31 | 32 | @repeat_every(seconds=15) 33 | def update_cache(): 34 | cache.update() 35 | 36 | 37 | cache = Cache() 38 | app = FastAPI(on_startup=[update_cache]) 39 | app.add_middleware( 40 | CORSMiddleware, 41 | allow_origins=['*'], 42 | allow_credentials=True, 43 | allow_methods=["*"], 44 | allow_headers=["*"], 45 | ) 46 | 47 | 48 | async def archive_request(json_rpc: JsonRpc): 49 | async with aiohttp.ClientSession() as session: 50 | async with session.post(ARCHIVE_ENDPOINT, json=jsonable_encoder(json_rpc)) as response: 51 | return await response.json() 52 | 53 | 54 | @app.post("/archive") 55 | async def archive(json_rpc: JsonRpc): 56 | if json_rpc.method == "validators.get_next_epoch_set": 57 | return cache.get("validators.get_next_epoch_set") 58 | return await archive_request(json_rpc) 59 | 60 | 61 | if __name__ == "__main__": 62 | uvicorn.run(app, host=API_HOST, port=API_PORT) 63 | -------------------------------------------------------------------------------- /backend/config.sample.py: -------------------------------------------------------------------------------- 1 | API_HOST = "127.0.0.1" 2 | API_PORT = 8080 3 | ARCHIVE_ENDPOINT = 'https://mainnet.radixdlt.com/archive' 4 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | fastapi-utils 3 | uvicorn[standard] 4 | aiohttp[speedups] 5 | -------------------------------------------------------------------------------- /docs/config/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "$datasource", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 1, 18 | "id": 15, 19 | "iteration": 1624485038447, 20 | "links": [], 21 | "panels": [ 22 | { 23 | "datasource": "$datasource", 24 | "fieldConfig": { 25 | "defaults": { 26 | "color": { 27 | "mode": "thresholds" 28 | }, 29 | "mappings": [ 30 | { 31 | "options": { 32 | "0": { 33 | "text": "DOWN" 34 | }, 35 | "1": { 36 | "text": "UP" 37 | } 38 | }, 39 | "type": "value" 40 | } 41 | ], 42 | "noValue": "?", 43 | "thresholds": { 44 | "mode": "absolute", 45 | "steps": [ 46 | { 47 | "color": "dark-red", 48 | "value": null 49 | }, 50 | { 51 | "color": "green", 52 | "value": 1 53 | } 54 | ] 55 | } 56 | }, 57 | "overrides": [] 58 | }, 59 | "gridPos": { 60 | "h": 5, 61 | "w": 5, 62 | "x": 0, 63 | "y": 0 64 | }, 65 | "id": 29, 66 | "options": { 67 | "colorMode": "value", 68 | "graphMode": "none", 69 | "justifyMode": "auto", 70 | "orientation": "auto", 71 | "reduceOptions": { 72 | "calcs": [ 73 | "lastNotNull" 74 | ], 75 | "fields": "", 76 | "values": false 77 | }, 78 | "text": {}, 79 | "textMode": "value" 80 | }, 81 | "pluginVersion": "8.0.3", 82 | "targets": [ 83 | { 84 | "exemplar": true, 85 | "expr": "up{instance=\"$instance\", job=\"$job\"}", 86 | "format": "time_series", 87 | "instant": true, 88 | "interval": "", 89 | "legendFormat": "", 90 | "refId": "A" 91 | } 92 | ], 93 | "title": "Status", 94 | "type": "stat" 95 | }, 96 | { 97 | "datasource": "$datasource", 98 | "fieldConfig": { 99 | "defaults": { 100 | "color": { 101 | "mode": "thresholds" 102 | }, 103 | "mappings": [ 104 | { 105 | "options": { 106 | "from": 1, 107 | "result": { 108 | "text": "Yes" 109 | }, 110 | "to": 1 111 | }, 112 | "type": "range" 113 | } 114 | ], 115 | "noValue": "?", 116 | "thresholds": { 117 | "mode": "absolute", 118 | "steps": [ 119 | { 120 | "color": "dark-red", 121 | "value": null 122 | }, 123 | { 124 | "color": "dark-blue", 125 | "value": 0 126 | } 127 | ] 128 | } 129 | }, 130 | "overrides": [] 131 | }, 132 | "gridPos": { 133 | "h": 5, 134 | "w": 5, 135 | "x": 5, 136 | "y": 0 137 | }, 138 | "id": 37, 139 | "options": { 140 | "colorMode": "value", 141 | "graphMode": "area", 142 | "justifyMode": "auto", 143 | "orientation": "auto", 144 | "reduceOptions": { 145 | "calcs": [ 146 | "last" 147 | ], 148 | "fields": "", 149 | "values": false 150 | }, 151 | "text": {}, 152 | "textMode": "auto" 153 | }, 154 | "pluginVersion": "8.0.3", 155 | "targets": [ 156 | { 157 | "exemplar": true, 158 | "expr": "rate(info_counters_sync_target_state_version{instance=\"$instance\", job=\"$job\"}[1m])", 159 | "hide": false, 160 | "instant": false, 161 | "interval": "", 162 | "legendFormat": "", 163 | "refId": "A" 164 | } 165 | ], 166 | "timeFrom": null, 167 | "timeShift": null, 168 | "title": "Versions / Second", 169 | "transformations": [], 170 | "type": "stat" 171 | }, 172 | { 173 | "datasource": "$datasource", 174 | "fieldConfig": { 175 | "defaults": { 176 | "color": { 177 | "mode": "thresholds" 178 | }, 179 | "mappings": [ 180 | { 181 | "options": { 182 | "0": { 183 | "text": "Desynced" 184 | } 185 | }, 186 | "type": "value" 187 | }, 188 | { 189 | "options": { 190 | "from": 1, 191 | "result": { 192 | "text": "Synced" 193 | }, 194 | "to": 1000000000 195 | }, 196 | "type": "range" 197 | } 198 | ], 199 | "noValue": "No metrics", 200 | "thresholds": { 201 | "mode": "absolute", 202 | "steps": [ 203 | { 204 | "color": "dark-red", 205 | "value": null 206 | }, 207 | { 208 | "color": "dark-blue", 209 | "value": 0 210 | } 211 | ] 212 | } 213 | }, 214 | "overrides": [] 215 | }, 216 | "gridPos": { 217 | "h": 5, 218 | "w": 5, 219 | "x": 10, 220 | "y": 0 221 | }, 222 | "id": 2, 223 | "options": { 224 | "colorMode": "value", 225 | "graphMode": "none", 226 | "justifyMode": "auto", 227 | "orientation": "auto", 228 | "reduceOptions": { 229 | "calcs": [ 230 | "last" 231 | ], 232 | "fields": "", 233 | "values": false 234 | }, 235 | "text": {}, 236 | "textMode": "value" 237 | }, 238 | "pluginVersion": "8.0.3", 239 | "targets": [ 240 | { 241 | "exemplar": true, 242 | "expr": "rate(info_counters_sync_target_state_version{instance=\"$instance\", job=\"$job\"}[$__rate_interval])", 243 | "hide": false, 244 | "instant": false, 245 | "interval": "", 246 | "legendFormat": "", 247 | "refId": "A" 248 | } 249 | ], 250 | "timeFrom": null, 251 | "timeShift": null, 252 | "transformations": [], 253 | "type": "stat" 254 | }, 255 | { 256 | "datasource": "$datasource", 257 | "fieldConfig": { 258 | "defaults": { 259 | "color": { 260 | "mode": "thresholds" 261 | }, 262 | "mappings": [], 263 | "thresholds": { 264 | "mode": "absolute", 265 | "steps": [ 266 | { 267 | "color": "dark-blue", 268 | "value": null 269 | } 270 | ] 271 | }, 272 | "unit": "dtdurationms" 273 | }, 274 | "overrides": [] 275 | }, 276 | "gridPos": { 277 | "h": 3, 278 | "w": 9, 279 | "x": 15, 280 | "y": 0 281 | }, 282 | "id": 22, 283 | "options": { 284 | "colorMode": "value", 285 | "graphMode": "none", 286 | "justifyMode": "auto", 287 | "orientation": "auto", 288 | "reduceOptions": { 289 | "calcs": [ 290 | "last" 291 | ], 292 | "fields": "", 293 | "values": false 294 | }, 295 | "text": {}, 296 | "textMode": "auto" 297 | }, 298 | "pluginVersion": "8.0.3", 299 | "targets": [ 300 | { 301 | "exemplar": true, 302 | "expr": "info_counters_time_duration{instance=\"$instance\", job=\"$job\"}", 303 | "instant": false, 304 | "interval": "", 305 | "legendFormat": "", 306 | "refId": "A" 307 | } 308 | ], 309 | "timeFrom": null, 310 | "timeShift": null, 311 | "title": "Uptime", 312 | "type": "stat" 313 | }, 314 | { 315 | "datasource": "$datasource", 316 | "fieldConfig": { 317 | "defaults": { 318 | "color": { 319 | "mode": "thresholds" 320 | }, 321 | "mappings": [], 322 | "noValue": "?", 323 | "thresholds": { 324 | "mode": "absolute", 325 | "steps": [ 326 | { 327 | "color": "dark-blue", 328 | "value": null 329 | } 330 | ] 331 | }, 332 | "unit": "none" 333 | }, 334 | "overrides": [] 335 | }, 336 | "gridPos": { 337 | "h": 2, 338 | "w": 9, 339 | "x": 15, 340 | "y": 3 341 | }, 342 | "id": 31, 343 | "options": { 344 | "colorMode": "value", 345 | "graphMode": "none", 346 | "justifyMode": "auto", 347 | "orientation": "auto", 348 | "reduceOptions": { 349 | "calcs": [ 350 | "last" 351 | ], 352 | "fields": "", 353 | "values": false 354 | }, 355 | "text": {}, 356 | "textMode": "name" 357 | }, 358 | "pluginVersion": "8.0.3", 359 | "targets": [ 360 | { 361 | "exemplar": true, 362 | "expr": "nodeinfo{instance=\"$instance\", job=\"$job\"}", 363 | "instant": true, 364 | "interval": "", 365 | "legendFormat": "{{ branch_and_commit }}", 366 | "refId": "A" 367 | } 368 | ], 369 | "timeFrom": null, 370 | "timeShift": null, 371 | "title": "Version", 372 | "type": "stat" 373 | }, 374 | { 375 | "collapsed": false, 376 | "datasource": "$datasource", 377 | "gridPos": { 378 | "h": 1, 379 | "w": 24, 380 | "x": 0, 381 | "y": 5 382 | }, 383 | "id": 10, 384 | "panels": [], 385 | "title": "Node", 386 | "type": "row" 387 | }, 388 | { 389 | "datasource": "$datasource", 390 | "fieldConfig": { 391 | "defaults": { 392 | "color": { 393 | "mode": "thresholds" 394 | }, 395 | "mappings": [ 396 | { 397 | "options": { 398 | "100001": { 399 | "text": "?" 400 | }, 401 | "100002": { 402 | "text": "?" 403 | } 404 | }, 405 | "type": "value" 406 | } 407 | ], 408 | "max": 100000, 409 | "min": 0, 410 | "noValue": "?", 411 | "thresholds": { 412 | "mode": "absolute", 413 | "steps": [ 414 | { 415 | "color": "green", 416 | "value": null 417 | }, 418 | { 419 | "color": "dark-blue", 420 | "value": 10000 421 | } 422 | ] 423 | } 424 | }, 425 | "overrides": [] 426 | }, 427 | "gridPos": { 428 | "h": 4, 429 | "w": 18, 430 | "x": 0, 431 | "y": 6 432 | }, 433 | "id": 7, 434 | "options": { 435 | "displayMode": "gradient", 436 | "orientation": "horizontal", 437 | "reduceOptions": { 438 | "calcs": [ 439 | "last" 440 | ], 441 | "fields": "", 442 | "values": false 443 | }, 444 | "showUnfilled": true, 445 | "text": {} 446 | }, 447 | "pluginVersion": "8.0.3", 448 | "targets": [ 449 | { 450 | "exemplar": true, 451 | "expr": "info_epochmanager_currentview_view{instance=\"$instance\", job=\"$job\"}", 452 | "format": "time_series", 453 | "instant": false, 454 | "interval": "", 455 | "legendFormat": "View", 456 | "refId": "A" 457 | } 458 | ], 459 | "title": "View In Epoch", 460 | "type": "bargauge" 461 | }, 462 | { 463 | "datasource": "$datasource", 464 | "fieldConfig": { 465 | "defaults": { 466 | "color": { 467 | "mode": "thresholds" 468 | }, 469 | "mappings": [], 470 | "thresholds": { 471 | "mode": "absolute", 472 | "steps": [ 473 | { 474 | "color": "dark-blue", 475 | "value": null 476 | } 477 | ] 478 | } 479 | }, 480 | "overrides": [] 481 | }, 482 | "gridPos": { 483 | "h": 4, 484 | "w": 6, 485 | "x": 18, 486 | "y": 6 487 | }, 488 | "id": 23, 489 | "options": { 490 | "colorMode": "value", 491 | "graphMode": "none", 492 | "justifyMode": "auto", 493 | "orientation": "horizontal", 494 | "reduceOptions": { 495 | "calcs": [ 496 | "lastNotNull" 497 | ], 498 | "fields": "", 499 | "values": false 500 | }, 501 | "text": {}, 502 | "textMode": "auto" 503 | }, 504 | "pluginVersion": "8.0.3", 505 | "targets": [ 506 | { 507 | "exemplar": true, 508 | "expr": "info_epochmanager_currentview_epoch{instance=\"$instance\", job=\"$job\"}", 509 | "format": "time_series", 510 | "instant": false, 511 | "interval": "", 512 | "legendFormat": "", 513 | "refId": "A" 514 | } 515 | ], 516 | "title": "Epoch", 517 | "type": "stat" 518 | }, 519 | { 520 | "datasource": "$datasource", 521 | "fieldConfig": { 522 | "defaults": { 523 | "color": { 524 | "mode": "thresholds" 525 | }, 526 | "mappings": [], 527 | "thresholds": { 528 | "mode": "absolute", 529 | "steps": [ 530 | { 531 | "color": "dark-blue", 532 | "value": null 533 | } 534 | ] 535 | } 536 | }, 537 | "overrides": [] 538 | }, 539 | "gridPos": { 540 | "h": 2, 541 | "w": 18, 542 | "x": 0, 543 | "y": 10 544 | }, 545 | "id": 20, 546 | "options": { 547 | "colorMode": "value", 548 | "graphMode": "area", 549 | "justifyMode": "center", 550 | "orientation": "auto", 551 | "reduceOptions": { 552 | "calcs": [ 553 | "last" 554 | ], 555 | "fields": "/.*/", 556 | "values": false 557 | }, 558 | "text": {}, 559 | "textMode": "name" 560 | }, 561 | "pluginVersion": "8.0.3", 562 | "targets": [ 563 | { 564 | "exemplar": true, 565 | "expr": "nodeinfo{instance=\"$instance\", job=\"$job\"}", 566 | "format": "time_series", 567 | "instant": true, 568 | "interval": "", 569 | "intervalFactor": 1, 570 | "legendFormat": "{{ owner_address }}", 571 | "refId": "A" 572 | } 573 | ], 574 | "timeFrom": null, 575 | "timeShift": null, 576 | "title": "Address", 577 | "transformations": [ 578 | { 579 | "id": "organize", 580 | "options": { 581 | "excludeByName": { 582 | "Time": true 583 | }, 584 | "indexByName": {}, 585 | "renameByName": {} 586 | } 587 | } 588 | ], 589 | "type": "stat" 590 | }, 591 | { 592 | "datasource": "$datasource", 593 | "fieldConfig": { 594 | "defaults": { 595 | "color": { 596 | "mode": "thresholds" 597 | }, 598 | "mappings": [ 599 | { 600 | "options": { 601 | "0": { 602 | "text": "Yes" 603 | } 604 | }, 605 | "type": "value" 606 | } 607 | ], 608 | "noValue": "No", 609 | "thresholds": { 610 | "mode": "absolute", 611 | "steps": [ 612 | { 613 | "color": "dark-blue", 614 | "value": null 615 | } 616 | ] 617 | } 618 | }, 619 | "overrides": [] 620 | }, 621 | "gridPos": { 622 | "h": 4, 623 | "w": 6, 624 | "x": 18, 625 | "y": 10 626 | }, 627 | "id": 4, 628 | "options": { 629 | "colorMode": "value", 630 | "graphMode": "none", 631 | "justifyMode": "auto", 632 | "orientation": "auto", 633 | "reduceOptions": { 634 | "calcs": [ 635 | "lastNotNull" 636 | ], 637 | "fields": "", 638 | "values": false 639 | }, 640 | "text": {}, 641 | "textMode": "auto" 642 | }, 643 | "pluginVersion": "8.0.3", 644 | "targets": [ 645 | { 646 | "exemplar": true, 647 | "expr": "nodeinfo{instance=\"$instance\", job=\"$job\", validator_registered=\"true\"}", 648 | "instant": true, 649 | "interval": "", 650 | "legendFormat": "", 651 | "refId": "A" 652 | } 653 | ], 654 | "timeFrom": null, 655 | "timeShift": null, 656 | "title": "Registered Validator", 657 | "type": "stat" 658 | }, 659 | { 660 | "datasource": "$datasource", 661 | "fieldConfig": { 662 | "defaults": { 663 | "color": { 664 | "mode": "thresholds" 665 | }, 666 | "mappings": [], 667 | "thresholds": { 668 | "mode": "absolute", 669 | "steps": [ 670 | { 671 | "color": "dark-blue", 672 | "value": null 673 | } 674 | ] 675 | } 676 | }, 677 | "overrides": [] 678 | }, 679 | "gridPos": { 680 | "h": 2, 681 | "w": 18, 682 | "x": 0, 683 | "y": 12 684 | }, 685 | "id": 24, 686 | "options": { 687 | "colorMode": "value", 688 | "graphMode": "area", 689 | "justifyMode": "auto", 690 | "orientation": "auto", 691 | "reduceOptions": { 692 | "calcs": [ 693 | "last" 694 | ], 695 | "fields": "/.*/", 696 | "values": false 697 | }, 698 | "text": {}, 699 | "textMode": "name" 700 | }, 701 | "pluginVersion": "8.0.3", 702 | "targets": [ 703 | { 704 | "exemplar": true, 705 | "expr": "nodeinfo{instance=\"$instance\", job=\"$job\"}", 706 | "format": "time_series", 707 | "instant": true, 708 | "interval": "", 709 | "legendFormat": "{{ own_validator_address }}", 710 | "refId": "A" 711 | } 712 | ], 713 | "timeFrom": null, 714 | "timeShift": null, 715 | "title": "Validator Address", 716 | "transformations": [ 717 | { 718 | "id": "organize", 719 | "options": { 720 | "excludeByName": { 721 | "Time": true, 722 | "vb1qv29t52l7nzunf83ykfe90qv5ffzky4eym647m43qku5dd38kya97tmktxt": false 723 | }, 724 | "indexByName": {}, 725 | "renameByName": {} 726 | } 727 | } 728 | ], 729 | "type": "stat" 730 | }, 731 | { 732 | "datasource": "$datasource", 733 | "fieldConfig": { 734 | "defaults": { 735 | "color": { 736 | "mode": "thresholds" 737 | }, 738 | "mappings": [], 739 | "thresholds": { 740 | "mode": "absolute", 741 | "steps": [ 742 | { 743 | "color": "dark-blue", 744 | "value": null 745 | } 746 | ] 747 | } 748 | }, 749 | "overrides": [] 750 | }, 751 | "gridPos": { 752 | "h": 2, 753 | "w": 18, 754 | "x": 0, 755 | "y": 14 756 | }, 757 | "id": 28, 758 | "options": { 759 | "colorMode": "value", 760 | "graphMode": "area", 761 | "justifyMode": "auto", 762 | "orientation": "auto", 763 | "reduceOptions": { 764 | "calcs": [ 765 | "lastNotNull" 766 | ], 767 | "fields": "", 768 | "values": false 769 | }, 770 | "text": {}, 771 | "textMode": "name" 772 | }, 773 | "pluginVersion": "8.0.3", 774 | "targets": [ 775 | { 776 | "exemplar": true, 777 | "expr": "nodeinfo{instance=\"$instance\", job=\"$job\"}", 778 | "instant": true, 779 | "interval": "", 780 | "legendFormat": "{{ key }}", 781 | "refId": "A" 782 | } 783 | ], 784 | "title": "Node Key", 785 | "transformations": [ 786 | { 787 | "id": "organize", 788 | "options": { 789 | "excludeByName": { 790 | "041455d15ff4c5c9a4f1259392bc0ca2522b12b926f55f6eb105b946b627b13a5fad3b6ff99339cc94c61840ba883b1ce927d8b640b854a22fb99be49b8f7874d3": false, 791 | "Time": true 792 | }, 793 | "indexByName": {}, 794 | "renameByName": {} 795 | } 796 | } 797 | ], 798 | "type": "stat" 799 | }, 800 | { 801 | "datasource": "$datasource", 802 | "fieldConfig": { 803 | "defaults": { 804 | "color": { 805 | "mode": "thresholds" 806 | }, 807 | "mappings": [ 808 | { 809 | "options": { 810 | "0": { 811 | "text": "Validating" 812 | } 813 | }, 814 | "type": "value" 815 | } 816 | ], 817 | "noValue": "Not Validating", 818 | "thresholds": { 819 | "mode": "absolute", 820 | "steps": [ 821 | { 822 | "color": "dark-blue", 823 | "value": null 824 | }, 825 | { 826 | "color": "dark-blue", 827 | "value": 0 828 | } 829 | ] 830 | } 831 | }, 832 | "overrides": [] 833 | }, 834 | "gridPos": { 835 | "h": 5, 836 | "w": 6, 837 | "x": 18, 838 | "y": 14 839 | }, 840 | "id": 38, 841 | "options": { 842 | "colorMode": "value", 843 | "graphMode": "area", 844 | "justifyMode": "auto", 845 | "orientation": "auto", 846 | "reduceOptions": { 847 | "calcs": [ 848 | "last" 849 | ], 850 | "fields": "", 851 | "values": false 852 | }, 853 | "text": {}, 854 | "textMode": "auto" 855 | }, 856 | "pluginVersion": "8.0.3", 857 | "targets": [ 858 | { 859 | "exemplar": true, 860 | "expr": "nodeinfo{instance=\"$instance\", job=\"$job\", is_in_validator_set=\"true\"}", 861 | "instant": true, 862 | "interval": "", 863 | "legendFormat": "", 864 | "refId": "A" 865 | } 866 | ], 867 | "timeFrom": null, 868 | "timeShift": null, 869 | "title": "Validator Status", 870 | "type": "stat" 871 | }, 872 | { 873 | "datasource": "$datasource", 874 | "fieldConfig": { 875 | "defaults": { 876 | "color": { 877 | "mode": "thresholds" 878 | }, 879 | "mappings": [], 880 | "noValue": "?", 881 | "thresholds": { 882 | "mode": "absolute", 883 | "steps": [ 884 | { 885 | "color": "dark-blue", 886 | "value": null 887 | } 888 | ] 889 | } 890 | }, 891 | "overrides": [] 892 | }, 893 | "gridPos": { 894 | "h": 3, 895 | "w": 9, 896 | "x": 0, 897 | "y": 16 898 | }, 899 | "id": 26, 900 | "options": { 901 | "colorMode": "value", 902 | "graphMode": "none", 903 | "justifyMode": "auto", 904 | "orientation": "auto", 905 | "reduceOptions": { 906 | "calcs": [ 907 | "last" 908 | ], 909 | "fields": "", 910 | "values": false 911 | }, 912 | "text": {}, 913 | "textMode": "auto" 914 | }, 915 | "pluginVersion": "8.0.3", 916 | "targets": [ 917 | { 918 | "exemplar": true, 919 | "expr": "balance_xrd{instance=\"$instance\", job=\"$job\"}", 920 | "instant": true, 921 | "interval": "", 922 | "legendFormat": "", 923 | "refId": "A" 924 | } 925 | ], 926 | "title": "XRD Balance", 927 | "type": "stat" 928 | }, 929 | { 930 | "datasource": "$datasource", 931 | "fieldConfig": { 932 | "defaults": { 933 | "color": { 934 | "mode": "thresholds" 935 | }, 936 | "mappings": [], 937 | "noValue": "?", 938 | "thresholds": { 939 | "mode": "absolute", 940 | "steps": [ 941 | { 942 | "color": "dark-blue", 943 | "value": null 944 | } 945 | ] 946 | } 947 | }, 948 | "overrides": [] 949 | }, 950 | "gridPos": { 951 | "h": 3, 952 | "w": 9, 953 | "x": 9, 954 | "y": 16 955 | }, 956 | "id": 30, 957 | "options": { 958 | "colorMode": "value", 959 | "graphMode": "none", 960 | "justifyMode": "auto", 961 | "orientation": "auto", 962 | "reduceOptions": { 963 | "calcs": [ 964 | "last" 965 | ], 966 | "fields": "", 967 | "values": false 968 | }, 969 | "text": {}, 970 | "textMode": "auto" 971 | }, 972 | "pluginVersion": "8.0.3", 973 | "targets": [ 974 | { 975 | "exemplar": true, 976 | "expr": "validator_total_stake{instance=\"$instance\", job=\"$job\"}", 977 | "interval": "", 978 | "legendFormat": "", 979 | "refId": "A" 980 | } 981 | ], 982 | "title": "XRD Staked", 983 | "type": "stat" 984 | }, 985 | { 986 | "datasource": "$datasource", 987 | "fieldConfig": { 988 | "defaults": { 989 | "color": { 990 | "mode": "thresholds" 991 | }, 992 | "mappings": [], 993 | "max": 1000, 994 | "min": 0, 995 | "noValue": "?", 996 | "thresholds": { 997 | "mode": "absolute", 998 | "steps": [ 999 | { 1000 | "color": "green", 1001 | "value": null 1002 | }, 1003 | { 1004 | "color": "dark-red", 1005 | "value": 1000 1006 | } 1007 | ] 1008 | } 1009 | }, 1010 | "overrides": [] 1011 | }, 1012 | "gridPos": { 1013 | "h": 4, 1014 | "w": 18, 1015 | "x": 0, 1016 | "y": 19 1017 | }, 1018 | "id": 40, 1019 | "options": { 1020 | "displayMode": "gradient", 1021 | "orientation": "horizontal", 1022 | "reduceOptions": { 1023 | "calcs": [ 1024 | "last" 1025 | ], 1026 | "fields": "", 1027 | "values": false 1028 | }, 1029 | "showUnfilled": true, 1030 | "text": {} 1031 | }, 1032 | "pluginVersion": "8.0.3", 1033 | "targets": [ 1034 | { 1035 | "exemplar": true, 1036 | "expr": "info_counters_mempool_count{instance=\"$instance\", job=\"$job\"}", 1037 | "instant": true, 1038 | "interval": "", 1039 | "legendFormat": "", 1040 | "refId": "A" 1041 | } 1042 | ], 1043 | "timeFrom": null, 1044 | "timeShift": null, 1045 | "title": "Mempool Size", 1046 | "type": "bargauge" 1047 | }, 1048 | { 1049 | "datasource": "$datasource", 1050 | "fieldConfig": { 1051 | "defaults": { 1052 | "color": { 1053 | "mode": "thresholds" 1054 | }, 1055 | "mappings": [], 1056 | "thresholds": { 1057 | "mode": "absolute", 1058 | "steps": [ 1059 | { 1060 | "color": "dark-blue", 1061 | "value": null 1062 | } 1063 | ] 1064 | } 1065 | }, 1066 | "overrides": [] 1067 | }, 1068 | "gridPos": { 1069 | "h": 4, 1070 | "w": 6, 1071 | "x": 18, 1072 | "y": 19 1073 | }, 1074 | "id": 41, 1075 | "options": { 1076 | "colorMode": "value", 1077 | "graphMode": "area", 1078 | "justifyMode": "auto", 1079 | "orientation": "auto", 1080 | "reduceOptions": { 1081 | "calcs": [ 1082 | "lastNotNull" 1083 | ], 1084 | "fields": "", 1085 | "values": false 1086 | }, 1087 | "text": {}, 1088 | "textMode": "auto" 1089 | }, 1090 | "pluginVersion": "8.0.3", 1091 | "targets": [ 1092 | { 1093 | "exemplar": true, 1094 | "expr": "rate(info_counters_mempool_add_success{instance=\"$instance\", job=\"$job\"}[$__rate_interval])", 1095 | "interval": "", 1096 | "legendFormat": "", 1097 | "refId": "A" 1098 | } 1099 | ], 1100 | "timeFrom": null, 1101 | "timeShift": null, 1102 | "title": "Mempool Additions / Second", 1103 | "type": "stat" 1104 | }, 1105 | { 1106 | "collapsed": false, 1107 | "datasource": "$datasource", 1108 | "gridPos": { 1109 | "h": 1, 1110 | "w": 24, 1111 | "x": 0, 1112 | "y": 23 1113 | }, 1114 | "id": 12, 1115 | "panels": [], 1116 | "title": "Radix Network", 1117 | "type": "row" 1118 | }, 1119 | { 1120 | "datasource": "$datasource", 1121 | "fieldConfig": { 1122 | "defaults": { 1123 | "color": { 1124 | "mode": "thresholds" 1125 | }, 1126 | "mappings": [], 1127 | "thresholds": { 1128 | "mode": "absolute", 1129 | "steps": [ 1130 | { 1131 | "color": "green", 1132 | "value": null 1133 | } 1134 | ] 1135 | } 1136 | }, 1137 | "overrides": [] 1138 | }, 1139 | "gridPos": { 1140 | "h": 5, 1141 | "w": 7, 1142 | "x": 0, 1143 | "y": 24 1144 | }, 1145 | "id": 33, 1146 | "options": { 1147 | "colorMode": "value", 1148 | "graphMode": "none", 1149 | "justifyMode": "auto", 1150 | "orientation": "auto", 1151 | "reduceOptions": { 1152 | "calcs": [ 1153 | "lastNotNull" 1154 | ], 1155 | "fields": "", 1156 | "values": false 1157 | }, 1158 | "text": {}, 1159 | "textMode": "auto" 1160 | }, 1161 | "pluginVersion": "8.0.3", 1162 | "targets": [ 1163 | { 1164 | "exemplar": true, 1165 | "expr": "total_peers{instance=\"$instance\", job=\"$job\"}", 1166 | "interval": "", 1167 | "legendFormat": "", 1168 | "refId": "A" 1169 | } 1170 | ], 1171 | "title": "Network Peers", 1172 | "type": "stat" 1173 | }, 1174 | { 1175 | "aliasColors": {}, 1176 | "bars": false, 1177 | "dashLength": 10, 1178 | "dashes": false, 1179 | "datasource": "$datasource", 1180 | "fill": 3, 1181 | "fillGradient": 0, 1182 | "gridPos": { 1183 | "h": 5, 1184 | "w": 17, 1185 | "x": 7, 1186 | "y": 24 1187 | }, 1188 | "hiddenSeries": false, 1189 | "id": 35, 1190 | "legend": { 1191 | "alignAsTable": false, 1192 | "avg": false, 1193 | "current": false, 1194 | "hideEmpty": false, 1195 | "hideZero": false, 1196 | "max": false, 1197 | "min": false, 1198 | "rightSide": false, 1199 | "show": false, 1200 | "total": false, 1201 | "values": false 1202 | }, 1203 | "lines": true, 1204 | "linewidth": 1, 1205 | "nullPointMode": "null", 1206 | "options": { 1207 | "alertThreshold": true 1208 | }, 1209 | "percentage": false, 1210 | "pluginVersion": "8.0.3", 1211 | "pointradius": 2, 1212 | "points": false, 1213 | "renderer": "flot", 1214 | "seriesOverrides": [], 1215 | "spaceLength": 10, 1216 | "stack": false, 1217 | "steppedLine": false, 1218 | "targets": [ 1219 | { 1220 | "exemplar": true, 1221 | "expr": "total_peers{instance=\"$instance\", job=\"$job\"}", 1222 | "interval": "", 1223 | "legendFormat": "Peers", 1224 | "refId": "A" 1225 | } 1226 | ], 1227 | "thresholds": [], 1228 | "timeFrom": null, 1229 | "timeRegions": [], 1230 | "timeShift": null, 1231 | "title": "Network Peers Over Time", 1232 | "tooltip": { 1233 | "shared": true, 1234 | "sort": 0, 1235 | "value_type": "individual" 1236 | }, 1237 | "type": "graph", 1238 | "xaxis": { 1239 | "buckets": null, 1240 | "mode": "time", 1241 | "name": null, 1242 | "show": true, 1243 | "values": [] 1244 | }, 1245 | "yaxes": [ 1246 | { 1247 | "$$hashKey": "object:566", 1248 | "decimals": 0, 1249 | "format": "none", 1250 | "label": null, 1251 | "logBase": 1, 1252 | "max": null, 1253 | "min": null, 1254 | "show": true 1255 | }, 1256 | { 1257 | "$$hashKey": "object:567", 1258 | "format": "short", 1259 | "label": null, 1260 | "logBase": 1, 1261 | "max": null, 1262 | "min": null, 1263 | "show": true 1264 | } 1265 | ], 1266 | "yaxis": { 1267 | "align": false, 1268 | "alignLevel": null 1269 | } 1270 | }, 1271 | { 1272 | "datasource": "$datasource", 1273 | "fieldConfig": { 1274 | "defaults": { 1275 | "color": { 1276 | "mode": "thresholds" 1277 | }, 1278 | "mappings": [], 1279 | "noValue": "Yet Unkown", 1280 | "thresholds": { 1281 | "mode": "absolute", 1282 | "steps": [ 1283 | { 1284 | "color": "green", 1285 | "value": null 1286 | } 1287 | ] 1288 | } 1289 | }, 1290 | "overrides": [] 1291 | }, 1292 | "gridPos": { 1293 | "h": 5, 1294 | "w": 7, 1295 | "x": 0, 1296 | "y": 29 1297 | }, 1298 | "id": 34, 1299 | "options": { 1300 | "colorMode": "value", 1301 | "graphMode": "none", 1302 | "justifyMode": "auto", 1303 | "orientation": "auto", 1304 | "reduceOptions": { 1305 | "calcs": [ 1306 | "last" 1307 | ], 1308 | "fields": "", 1309 | "values": false 1310 | }, 1311 | "text": {}, 1312 | "textMode": "auto" 1313 | }, 1314 | "pluginVersion": "8.0.3", 1315 | "targets": [ 1316 | { 1317 | "exemplar": true, 1318 | "expr": "total_validators{instance=\"$instance\", job=\"$job\"}", 1319 | "instant": true, 1320 | "interval": "", 1321 | "legendFormat": "", 1322 | "refId": "A" 1323 | } 1324 | ], 1325 | "title": "Network Validators", 1326 | "type": "stat" 1327 | }, 1328 | { 1329 | "aliasColors": {}, 1330 | "bars": false, 1331 | "dashLength": 10, 1332 | "dashes": false, 1333 | "datasource": "$datasource", 1334 | "fill": 3, 1335 | "fillGradient": 0, 1336 | "gridPos": { 1337 | "h": 5, 1338 | "w": 17, 1339 | "x": 7, 1340 | "y": 29 1341 | }, 1342 | "hiddenSeries": false, 1343 | "id": 36, 1344 | "legend": { 1345 | "avg": false, 1346 | "current": false, 1347 | "max": false, 1348 | "min": false, 1349 | "show": false, 1350 | "total": false, 1351 | "values": false 1352 | }, 1353 | "lines": true, 1354 | "linewidth": 1, 1355 | "nullPointMode": "null", 1356 | "options": { 1357 | "alertThreshold": true 1358 | }, 1359 | "percentage": false, 1360 | "pluginVersion": "8.0.3", 1361 | "pointradius": 2, 1362 | "points": false, 1363 | "renderer": "flot", 1364 | "seriesOverrides": [], 1365 | "spaceLength": 10, 1366 | "stack": false, 1367 | "steppedLine": false, 1368 | "targets": [ 1369 | { 1370 | "exemplar": true, 1371 | "expr": "total_validators{instance=\"$instance\", job=\"$job\"}", 1372 | "interval": "", 1373 | "legendFormat": "Validators", 1374 | "refId": "A" 1375 | } 1376 | ], 1377 | "thresholds": [], 1378 | "timeFrom": null, 1379 | "timeRegions": [], 1380 | "timeShift": null, 1381 | "title": "Network Validators Over Time", 1382 | "tooltip": { 1383 | "shared": true, 1384 | "sort": 0, 1385 | "value_type": "individual" 1386 | }, 1387 | "type": "graph", 1388 | "xaxis": { 1389 | "buckets": null, 1390 | "mode": "time", 1391 | "name": null, 1392 | "show": true, 1393 | "values": [] 1394 | }, 1395 | "yaxes": [ 1396 | { 1397 | "$$hashKey": "object:566", 1398 | "decimals": 0, 1399 | "format": "none", 1400 | "label": null, 1401 | "logBase": 1, 1402 | "max": null, 1403 | "min": null, 1404 | "show": true 1405 | }, 1406 | { 1407 | "$$hashKey": "object:567", 1408 | "format": "short", 1409 | "label": null, 1410 | "logBase": 1, 1411 | "max": null, 1412 | "min": null, 1413 | "show": true 1414 | } 1415 | ], 1416 | "yaxis": { 1417 | "align": false, 1418 | "alignLevel": null 1419 | } 1420 | }, 1421 | { 1422 | "alert": { 1423 | "alertRuleTags": {}, 1424 | "conditions": [ 1425 | { 1426 | "evaluator": { 1427 | "params": [ 1428 | 1 1429 | ], 1430 | "type": "lt" 1431 | }, 1432 | "operator": { 1433 | "type": "and" 1434 | }, 1435 | "query": { 1436 | "params": [ 1437 | "A", 1438 | "2m", 1439 | "now" 1440 | ] 1441 | }, 1442 | "reducer": { 1443 | "params": [], 1444 | "type": "diff" 1445 | }, 1446 | "type": "query" 1447 | } 1448 | ], 1449 | "executionErrorState": "alerting", 1450 | "for": "2m", 1451 | "frequency": "1m", 1452 | "handler": 1, 1453 | "name": "Proposal Made alert", 1454 | "noDataState": "alerting", 1455 | "notifications": [] 1456 | }, 1457 | "datasource": "grafanacloud--prom", 1458 | "fieldConfig": { 1459 | "defaults": { 1460 | "color": { 1461 | "mode": "palette-classic" 1462 | }, 1463 | "custom": { 1464 | "axisLabel": "", 1465 | "axisPlacement": "auto", 1466 | "barAlignment": 0, 1467 | "drawStyle": "line", 1468 | "fillOpacity": 0, 1469 | "gradientMode": "none", 1470 | "hideFrom": { 1471 | "legend": false, 1472 | "tooltip": false, 1473 | "viz": false 1474 | }, 1475 | "lineInterpolation": "stepAfter", 1476 | "lineWidth": 1, 1477 | "pointSize": 5, 1478 | "scaleDistribution": { 1479 | "type": "linear" 1480 | }, 1481 | "showPoints": "auto", 1482 | "spanNulls": false, 1483 | "stacking": { 1484 | "group": "A", 1485 | "mode": "none" 1486 | }, 1487 | "thresholdsStyle": { 1488 | "mode": "off" 1489 | } 1490 | }, 1491 | "mappings": [], 1492 | "thresholds": { 1493 | "mode": "absolute", 1494 | "steps": [ 1495 | { 1496 | "color": "green", 1497 | "value": null 1498 | }, 1499 | { 1500 | "color": "red", 1501 | "value": 80 1502 | } 1503 | ] 1504 | } 1505 | }, 1506 | "overrides": [] 1507 | }, 1508 | "gridPos": { 1509 | "h": 8, 1510 | "w": 12, 1511 | "x": 0, 1512 | "y": 34 1513 | }, 1514 | "id": 43, 1515 | "options": { 1516 | "legend": { 1517 | "calcs": [], 1518 | "displayMode": "list", 1519 | "placement": "bottom" 1520 | }, 1521 | "tooltip": { 1522 | "mode": "single" 1523 | } 1524 | }, 1525 | "targets": [ 1526 | { 1527 | "exemplar": true, 1528 | "expr": "info_counters_bft_proposals_made{instance=\"$instance\", job=\"$job\"}", 1529 | "interval": "", 1530 | "legendFormat": "", 1531 | "refId": "A" 1532 | } 1533 | ], 1534 | "thresholds": [ 1535 | { 1536 | "colorMode": "critical", 1537 | "op": "lt", 1538 | "value": 1, 1539 | "visible": true 1540 | } 1541 | ], 1542 | "title": "Proposal Made", 1543 | "type": "timeseries" 1544 | }, 1545 | { 1546 | "datasource": "$datasource", 1547 | "fieldConfig": { 1548 | "defaults": { 1549 | "color": { 1550 | "mode": "palette-classic" 1551 | }, 1552 | "custom": { 1553 | "axisLabel": "", 1554 | "axisPlacement": "auto", 1555 | "barAlignment": 0, 1556 | "drawStyle": "line", 1557 | "fillOpacity": 0, 1558 | "gradientMode": "none", 1559 | "hideFrom": { 1560 | "legend": false, 1561 | "tooltip": false, 1562 | "viz": false 1563 | }, 1564 | "lineInterpolation": "linear", 1565 | "lineWidth": 1, 1566 | "pointSize": 5, 1567 | "scaleDistribution": { 1568 | "type": "linear" 1569 | }, 1570 | "showPoints": "auto", 1571 | "spanNulls": false, 1572 | "stacking": { 1573 | "group": "A", 1574 | "mode": "none" 1575 | }, 1576 | "thresholdsStyle": { 1577 | "mode": "off" 1578 | } 1579 | }, 1580 | "mappings": [], 1581 | "thresholds": { 1582 | "mode": "absolute", 1583 | "steps": [ 1584 | { 1585 | "color": "green", 1586 | "value": null 1587 | }, 1588 | { 1589 | "color": "red", 1590 | "value": 80 1591 | } 1592 | ] 1593 | } 1594 | }, 1595 | "overrides": [] 1596 | }, 1597 | "gridPos": { 1598 | "h": 8, 1599 | "w": 12, 1600 | "x": 12, 1601 | "y": 34 1602 | }, 1603 | "id": 45, 1604 | "options": { 1605 | "legend": { 1606 | "calcs": [], 1607 | "displayMode": "list", 1608 | "placement": "bottom" 1609 | }, 1610 | "tooltip": { 1611 | "mode": "single" 1612 | } 1613 | }, 1614 | "targets": [ 1615 | { 1616 | "exemplar": true, 1617 | "expr": "info_counters_radix_engine_cur_epoch_missed_proposals{instance=\"$instance\", job=\"$job\"}", 1618 | "interval": "", 1619 | "legendFormat": "", 1620 | "queryType": "randomWalk", 1621 | "refId": "A" 1622 | } 1623 | ], 1624 | "title": "Missed Proposals", 1625 | "type": "timeseries" 1626 | } 1627 | ], 1628 | "refresh": "", 1629 | "schemaVersion": 30, 1630 | "style": "dark", 1631 | "tags": [], 1632 | "templating": { 1633 | "list": [ 1634 | { 1635 | "description": "If case you are running many nodes, use this var to target a single on", 1636 | "error": null, 1637 | "hide": 2, 1638 | "label": null, 1639 | "name": "instance", 1640 | "query": "localhost:3333", 1641 | "skipUrlSync": false, 1642 | "type": "constant" 1643 | }, 1644 | { 1645 | "allValue": null, 1646 | "current": { 1647 | "selected": true, 1648 | "text": "radix-mainnet-validator", 1649 | "value": "radix-mainnet-validator" 1650 | }, 1651 | "datasource": "grafanacloud--prom", 1652 | "definition": "label_values(job)", 1653 | "description": "Allows you to monitor different radix nodes in even different networks.", 1654 | "error": null, 1655 | "hide": 0, 1656 | "includeAll": false, 1657 | "label": "Radix Node", 1658 | "multi": false, 1659 | "name": "job", 1660 | "options": [], 1661 | "query": { 1662 | "query": "label_values(job)", 1663 | "refId": "StandardVariableQuery" 1664 | }, 1665 | "refresh": 1, 1666 | "regex": "/radix.*/", 1667 | "skipUrlSync": false, 1668 | "sort": 0, 1669 | "type": "query" 1670 | }, 1671 | { 1672 | "description": "Set your datasource", 1673 | "error": null, 1674 | "hide": 2, 1675 | "label": "Data Source", 1676 | "name": "datasource", 1677 | "query": "grafanacloud--prom", 1678 | "skipUrlSync": false, 1679 | "type": "constant" 1680 | } 1681 | ] 1682 | }, 1683 | "time": { 1684 | "from": "now-30m", 1685 | "to": "now" 1686 | }, 1687 | "timepicker": {}, 1688 | "timezone": "", 1689 | "title": "Radix Node Dashboard", 1690 | "uid": "radix_node_dashboard", 1691 | "version": 1 1692 | } -------------------------------------------------------------------------------- /docs/config/default.config: -------------------------------------------------------------------------------- 1 | network.id=1 2 | #network.genesis_data_file= 3 | network.p2p.seed_nodes=radix://node_rdx1qf2x63qx4jdaxj83kkw2yytehvvmu6r2xll5gcp6c9rancmrfsgfw0vnc65@52.212.35.209,radix://node_rdx1qgxn3eeldj33kd98ha6wkjgk4k77z6xm0dv7mwnrkefknjcqsvhuu4gc609@54.79.136.139,radix://node_rdx1qwrrnhzfu99fg3yqgk3ut9vev2pdssv7hxhff80msjmmcj968487uugc0t2@43.204.226.50,radix://node_rdx1q0gnmwv0fmcp7ecq0znff7yzrt7ggwrp47sa9pssgyvrnl75tvxmvj78u7t@52.21.106.232 4 | 5 | node.key.path=/etc/radix-babylon/node/secrets/node-keystore.ks 6 | db.location=/babylon-ledger 7 | 8 | network.host_ip=1.2.3.4 9 | 10 | api.core.bind_address= 11 | api.core.port= 12 | api.system.bind_address= 13 | api.system.port= 14 | api.prometheus.bind_address= 15 | api.prometheus.port= 16 | 17 | network.p2p.listen_port=30000 18 | network.p2p.broadcast_port=30000 19 | network.p2p.use_proxy_protocol=false 20 | 21 | db.local_transaction_execution_index.enable= 22 | db.account_change_index.enable= 23 | api.core.flags.enable_unbounded_endpoints= 24 | 25 | consensus.validator_address= 26 | 27 | genesis.use_olympia=true 28 | genesis.olympia.node_end_state_api_url=http://127.0.0.1:3400 29 | genesis.olympia.node_end_state_api_auth_user= 30 | genesis.olympia.node_end_state_api_auth_password= 31 | genesis.olympia.node_bech32_address= -------------------------------------------------------------------------------- /docs/config/radix-babylon.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Radix Babylon Validator 3 | After=local-fs.target 4 | After=network-online.target 5 | After=nss-lookup.target 6 | After=time-sync.target 7 | After=systemd-journald-dev-log.socket 8 | Wants=network-online.target 9 | 10 | [Service] 11 | EnvironmentFile=/etc/radix-babylon/node/secrets/environment 12 | User=radixdlt 13 | LimitNOFILE=65536 14 | LimitNPROC=65536 15 | LimitMEMLOCK=infinity 16 | WorkingDirectory=/etc/radix-babylon/node 17 | ExecStart=/etc/radix-babylon/node/bin/core 18 | SuccessExitStatus=143 19 | TimeoutStopSec=10 20 | Restart=on-failure 21 | 22 | [Install] 23 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /docs/scripts/switch-mode: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # VALIDATOR CHECKS WILL BE UPDATED AFTER MIGRATION 4 | # 5 | # HOST="http://localhost:3333" 6 | # 7 | # VALIDATOR_ADDRESS=$(curl -s -X POST "$HOST/key/list" -H "Content-Type: application/json" \ 8 | # -d '{"network_identifier": {"network": "mainnet"}}' \ 9 | # | jq -r ".public_keys[0].identifiers.validator_entity_identifier.address") 10 | # IS_VALIDATING=$(curl -s -X POST "$HOST/entity" -H "Content-Type: application/json" \ 11 | # -d "{\"network_identifier\": {\"network\": \"mainnet\"}, \"entity_identifier\": 12 | # {\"address\": \"$VALIDATOR_ADDRESS\", \"sub_entity\": {\"address\": \"system\"}}}" \ 13 | # | jq ".data_objects | any(.type == \"ValidatorBFTData\")") 14 | # 15 | # get_completed_proposals () { 16 | # curl -s -X POST "$HOST/entity" -H "Content-Type: application/json" \ 17 | # -d "{\"network_identifier\": {\"network\": \"mainnet\"}, \"entity_identifier\": 18 | # {\"address\": \"$VALIDATOR_ADDRESS\", \"sub_entity\": {\"address\": \"system\"}}}" \ 19 | # | jq ".data_objects[] | select(.type == \"ValidatorBFTData\") | .proposals_completed" 20 | # } 21 | 22 | IS_VALIDATING=false 23 | 24 | check_return_code () { 25 | if [[ $? -eq 0 ]] 26 | then 27 | echo "Successfully switched radix mode and restarted." 28 | else 29 | echo "Error: Failed to switch radix mode and restart." 30 | fi 31 | } 32 | 33 | switch_grafana () { 34 | if [[ ! -f /etc/grafana-agent.yaml ]] 35 | then 36 | return 37 | fi 38 | if 39 | sudo sed -i "s/$1/$2/g" /etc/grafana-agent.yaml && \ 40 | sudo systemctl restart grafana-agent 41 | then 42 | echo "Successfully switched grafana agent to $2 mode." 43 | else 44 | echo "Error: Failed to switch grafana agent to $2 mode." 45 | fi 46 | } 47 | 48 | if [[ "$1" == "validator" ]] 49 | then 50 | echo "Restarting Radix Node in validator mode ..." 51 | sudo systemctl stop radix-babylon && \ 52 | rm -f /etc/radix-babylon/node/secrets && \ 53 | ln -s /etc/radix-babylon/node/secrets-validator /etc/radix-babylon/node/secrets && \ 54 | sudo systemctl start radix-babylon 55 | check_return_code 56 | switch_grafana "fullnode" $1 57 | elif [[ "$1" == "fullnode" ]] 58 | then 59 | if [[ $IS_VALIDATING == true && "$2" != "force" ]] 60 | then 61 | PROPOSALS_COMPLETED=$(get_completed_proposals) 62 | echo "Wait until node completed proposal to minimise risk of a missed proposal ..." 63 | while (( $(get_completed_proposals) == PROPOSALS_COMPLETED)) || (( $(get_completed_proposals) == 0)) 64 | do 65 | echo "Waiting ..." 66 | sleep 1 67 | done 68 | echo "Validator completed proposal - updating now." 69 | fi 70 | echo "Restarting Radix Node in fullnode mode ..." 71 | sudo systemctl stop radix-babylon && \ 72 | rm -f /etc/radix-babylon/node/secrets && \ 73 | ln -s /etc/radix-babylon/node/secrets-fullnode /etc/radix-babylon/node/secrets && \ 74 | sudo systemctl start radix-babylon 75 | check_return_code 76 | switch_grafana "validator" $1 77 | else 78 | echo "Radix Node Switch Mode" 79 | echo "" 80 | echo "Usage:" 81 | echo " switch-mode fullnode Switch radix node to fullnode mode." 82 | echo " switch-mode validator Switch radix node to validator mode." 83 | fi 84 | -------------------------------------------------------------------------------- /docs/scripts/update-node: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # VALIDATOR CHECKS WILL BE UPDATED AFTER MIGRATION 4 | # 5 | # HOST="http://localhost:3333" 6 | # 7 | # VALIDATOR_ADDRESS=$(curl -s -X POST "$HOST/key/list" -H "Content-Type: application/json" \ 8 | # -d '{"network_identifier": {"network": "mainnet"}}' \ 9 | # | jq -r ".public_keys[0].identifiers.validator_entity_identifier.address") 10 | # IS_VALIDATING=$(curl -s -X POST "$HOST/entity" -H "Content-Type: application/json" \ 11 | # -d "{\"network_identifier\": {\"network\": \"mainnet\"}, \"entity_identifier\": 12 | # {\"address\": \"$VALIDATOR_ADDRESS\", \"sub_entity\": {\"address\": \"system\"}}}" \ 13 | # | jq ".data_objects | any(.type == \"ValidatorBFTData\")") 14 | # 15 | # get_completed_proposals () { 16 | # curl -s -X POST "$HOST/entity" -H "Content-Type: application/json" \ 17 | # -d "{\"network_identifier\": {\"network\": \"mainnet\"}, \"entity_identifier\": 18 | # {\"address\": \"$VALIDATOR_ADDRESS\", \"sub_entity\": {\"address\": \"system\"}}}" \ 19 | # | jq ".data_objects[] | select(.type == \"ValidatorBFTData\") | .proposals_completed" 20 | # } 21 | 22 | NODE_STATUS=$(curl -s localhost:3334/system/health | jq -r .status) 23 | IS_VALIDATING=false 24 | 25 | cd /opt/radix-babylon || exit 26 | 27 | echo "Checking for latest radix node-runner version ..." 28 | NODE_RUNNER_URL=$(curl -s https://api.github.com/repos/radixdlt/babylon-nodecli/releases/latest | \ 29 | jq -r '.assets[] | select(.browser_download_url|split("/")|last|test("^babylonnode-ubuntu-22.04")) | .browser_download_url') 30 | echo "Found node-runner url:" "$NODE_RUNNER_URL" 31 | NODE_RUNNER_FILE="$(basename "$NODE_RUNNER_URL")-$(echo "$NODE_RUNNER_URL" | awk -F/ '{print $(NF-1)}')" 32 | echo "Latest node-runner version: " "$NODE_RUNNER_FILE" 33 | 34 | if [[ ! -f $NODE_RUNNER_FILE ]] 35 | then 36 | echo "Downloading new node-runner version ..." 37 | curl -Lo "$NODE_RUNNER_FILE" "$NODE_RUNNER_URL" && \ 38 | chmod +x "$NODE_RUNNER_FILE" && \ 39 | rm -f "radixnode" && \ 40 | ln -s "/opt/radix-babylon/$NODE_RUNNER_FILE" radixnode 41 | else 42 | echo "Radix node-runner already up to date." 43 | fi 44 | 45 | cd /opt/radix-babylon/releases || exit 46 | 47 | echo "Checking for latest radix node version ..." 48 | NODE_URL=$(curl -s https://api.github.com/repos/radixdlt/babylon-node/releases/latest | \ 49 | jq -r '.assets[] | select(.browser_download_url|split("/")|last|test("^babylon-node-v[0-9.]*zip")) | .browser_download_url') 50 | echo "Found radix node url:" "$NODE_URL" 51 | NODE_ARCHIVE=$(basename "$NODE_URL") 52 | 53 | echo "Latest node version:" "$NODE_ARCHIVE" 54 | 55 | if [[ ! -f $NODE_ARCHIVE ]] 56 | then 57 | echo "Downloading new node version ..." 58 | curl -OL "$NODE_URL" && \ 59 | unzip "$NODE_ARCHIVE" 60 | fi 61 | 62 | NODE_EXTRACTED=$(unzip -Z1 "$NODE_ARCHIVE" | head -n 1) 63 | if [[ ${#NODE_EXTRACTED} == 0 ]] 64 | then 65 | echo "Error: Failed to read downloaded archive." 66 | exit 67 | fi 68 | 69 | if [[ $(find "$NODE_EXTRACTED" -type f | wc -l) == 0 ]] 70 | then 71 | echo "Error: no files extracted." 72 | exit 73 | fi 74 | 75 | DIR_BIN=/opt/radix-babylon/releases/${NODE_EXTRACTED}bin 76 | DIR_LIB=/opt/radix-babylon/releases/${NODE_EXTRACTED}lib 77 | DIR_JNI=/opt/radix-babylon/releases/${NODE_EXTRACTED}jni 78 | 79 | JNI_URL=$(curl -s https://api.github.com/repos/radixdlt/babylon-node/releases/latest | \ 80 | jq -r '.assets[] | select(.browser_download_url|split("/")|last|test("^babylon-node-rust-arch-linux-x86_64-release-v[0-9.]*zip")) | .browser_download_url') 81 | echo "Found jni node url:" "$JNI_URL" 82 | JNI_ARCHIVE=$(basename "$JNI_URL") 83 | 84 | echo "Latest JNI version:" "$JNI_ARCHIVE" 85 | 86 | if [[ ! -f $JNI_ARCHIVE ]] 87 | then 88 | echo "Downloading new JNI version ..." 89 | mkdir -p $DIR_JNI && \ 90 | curl -OL "$JNI_URL" && \ 91 | unzip "$JNI_ARCHIVE" -d $DIR_JNI 92 | fi 93 | 94 | JNI_EXTRACTED=$(unzip -Z1 "$JNI_ARCHIVE" | head -n 1) 95 | if [[ ${#JNI_EXTRACTED} == 0 ]] 96 | then 97 | echo "Error: Failed to read downloaded archive." 98 | exit 99 | fi 100 | 101 | if [[ $(find "$DIR_JNI/$JNI_EXTRACTED" -type f | wc -l) == 0 ]] 102 | then 103 | echo "Error: no files extracted." 104 | exit 105 | fi 106 | 107 | mkdir -p /etc/radix-babylon/node 108 | 109 | if [[ $DIR_BIN = $(readlink /etc/radix-babylon/node/bin) && \ 110 | $DIR_LIB = $(readlink /etc/radix-babylon/node/lib) && \ 111 | $DIR_JNI = $(readlink /etc/radix-babylon/node/jni) && \ 112 | "$1" != "force" ]] 113 | then 114 | echo "Radix node already up to date." 115 | exit 116 | fi 117 | 118 | echo "Installing new node version" "$NODE_ARCHIVE" "..." 119 | 120 | ## INSTALL - node not running 121 | 122 | if [[ $NODE_STATUS == "" ]] 123 | then 124 | if 125 | rm -f /etc/radix-babylon/node/bin && \ 126 | rm -f /etc/radix-babylon/node/lib && \ 127 | rm -f /etc/radix-babylon/node/jni && \ 128 | ln -s "$DIR_BIN" /etc/radix-babylon/node/bin && \ 129 | ln -s "$DIR_LIB" /etc/radix-babylon/node/lib && \ 130 | ln -s "$DIR_JNI" /etc/radix-babylon/node/jni 131 | then 132 | echo "Successfully installed node files." 133 | else 134 | echo "Error: Failed to install node files." 135 | fi 136 | exit 137 | fi 138 | 139 | ## UPDATE 140 | 141 | if [[ $IS_VALIDATING == true ]] 142 | then 143 | PROPOSALS_COMPLETED=$(get_completed_proposals) 144 | echo "Wait until node completed proposal to minimise risk of a missed proposal ..." 145 | while (( $(get_completed_proposals) == PROPOSALS_COMPLETED)) || (( $(get_completed_proposals) == 0)) 146 | do 147 | echo "Waiting ..." 148 | sleep 1 149 | done 150 | echo "Validator completed proposal - updating now." 151 | fi 152 | 153 | if 154 | sudo systemctl stop radix-babylon && \ 155 | rm -f /etc/radix-babylon/node/bin && \ 156 | rm -f /etc/radix-babylon/node/lib && \ 157 | rm -f /etc/radix-babylon/node/jni && \ 158 | ln -s "$DIR_BIN" /etc/radix-babylon/node/bin && \ 159 | ln -s "$DIR_LIB" /etc/radix-babylon/node/lib && \ 160 | ln -s "$DIR_JNI" /etc/radix-babylon/node/jni && \ 161 | sudo systemctl start radix-babylon 162 | then 163 | echo "Successfully installed node and restarted." 164 | else 165 | echo "Error: Failed to install and restart node." 166 | fi 167 | -------------------------------------------------------------------------------- /docs/validator_guide.md: -------------------------------------------------------------------------------- 1 | # Production-grade Standalone Validator Guide 2 | 3 | This guide presents a straightforward and focused guide to set up a production grade validator node. 4 | It won't discuss all possible ways to set up a standalone node. The guide is tested and based on Ubuntu 22.04. 5 | 6 | This is work in progress and some parts may change with the upcoming node releases. 7 | 8 | # Basic Setup 9 | 10 | ## Create User 11 | Create a user which you use instead of root (pick your own username). 12 | ``` 13 | adduser john 14 | ``` 15 | 16 | Add user to sudo group 17 | ``` 18 | adduser john sudo 19 | ``` 20 | 21 | Change user and go to home directory 22 | ``` 23 | su - john 24 | ``` 25 | 26 | Lock root password to disable root login via password 27 | (don't confuse with `-d` it removes the password and allows to login without a password) 28 | ``` 29 | sudo passwd -l root 30 | ``` 31 | 32 | ## Hostname 33 | 34 | You may want to set a different hostname to make distinguishing between your different nodes easier e.g.: 35 | ``` 36 | sudo hostnamectl set-hostname mainnet-1 37 | ``` 38 | 39 | ## SSH 40 | Based on https://withblue.ink/2016/07/15/stop-ssh-brute-force-attempts.html 41 | 42 | ### Public Key Authentication 43 | It is recommended to use ED25519 keys for SSH (same like Radix is using itself for signing transactions). 44 | Generate a key with a strong passphrase to protect it on your CLIENT system. 45 | 46 | On Linux: 47 | ``` 48 | ssh-keygen -t ed25519 49 | ``` 50 | On Windows PuTTYgen can be used to generate an ED25519 key. 51 | 52 | On the `SERVER` paste your generated public key (in OpenSSH format) into `authorized_keys`: 53 | ``` 54 | mkdir -p ~/.ssh && nano ~/.ssh/authorized_keys 55 | ``` 56 | 57 | Remove all "group" and "other" permissions and ensure ownership of `.ssh` is correct: 58 | ``` 59 | chmod -R go= ~/.ssh 60 | chown -R john:john ~/.ssh 61 | ``` 62 | 63 | Further details: 64 | - https://medium.com/risan/upgrade-your-ssh-key-to-ed25519-c6e8d60d3c54 65 | - https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys-on-ubuntu-20-04 66 | 67 | ### Secure Configuration 68 | To secure SSH we: 69 | - Change the port (use your own custom port instead of `1234`). 70 | Though this doesn't really make your node more secure, but stops a lot of low effort 'attacks' appearing in your log files. 71 | - Disable password authentication 72 | - Disable root login 73 | - Only allow our own user `john` to connect 74 | 75 | Modify or add the following settings to `/etc/ssh/sshd_config`. 76 | ``` 77 | sudo nano /etc/ssh/sshd_config 78 | ``` 79 | ``` 80 | Port 1234 81 | PasswordAuthentication no 82 | PermitRootLogin no 83 | AllowUsers john 84 | ``` 85 | 86 | 87 | ### Restart SSH 88 | To activate the changes restart the SSH service 89 | ``` 90 | sudo systemctl restart sshd 91 | ``` 92 | 93 | ## Firewall (using UFW) 94 | First, we ensure that safe defaults are set (they should be in a clean installation) 95 | ``` 96 | sudo ufw default deny incoming 97 | sudo ufw default allow outgoing 98 | ``` 99 | 100 | Second, we will only allow the custom SSH port and Radix network gossip on port 30000/tcp. 101 | ``` 102 | sudo ufw allow 1234/tcp 103 | sudo ufw allow 30000/tcp 104 | ``` 105 | 106 | Afterwards we enable the firewall and check the status. 107 | ``` 108 | sudo ufw enable 109 | sudo ufw status 110 | ``` 111 | 112 | Be careful and verify whether you can successfully open a new SSH connection before 113 | closing your existing session. Now after you ensured you didn't lock yourself out of your 114 | server we can continue with setting up the Radix node itself. 115 | 116 | ## Update System 117 | Update package repository and update system: 118 | ``` 119 | sudo apt update -y 120 | sudo apt-get dist-upgrade 121 | ``` 122 | 123 | ## Automatic system updates 124 | We want automatic unattended security updates (based on https://help.ubuntu.com/community/AutomaticSecurityUpdates) 125 | ``` 126 | sudo apt install unattended-upgrades 127 | sudo dpkg-reconfigure --priority=low unattended-upgrades 128 | ``` 129 | 130 | You can check whether it created the `/etc/apt/apt.conf.d/20auto-upgrades` file with the following content: 131 | ``` 132 | cat /etc/apt/apt.conf.d/20auto-upgrades 133 | ``` 134 | ``` 135 | APT::Periodic::Update-Package-Lists "1"; 136 | APT::Periodic::Unattended-Upgrade "1"; 137 | ``` 138 | 139 | If you want to configure optional email notifications you can check out this article 140 | https://linoxide.com/enable-automatic-updates-on-ubuntu-20-04/. 141 | 142 | 143 | ## Kernel live patching 144 | ``` 145 | Disclaimer: the kernel live patching section was tested on Ubuntu 20.04 and might need to be slightly adapted. 146 | ``` 147 | We will use `canonical-livepatch` for kernel live patching. 148 | First we need to check whether you are running the `linux-generic` kernel 149 | (or any of these `generic, lowlatency, aws, azure, oem, gcp, gke, gkeop` 150 | https://wiki.ubuntu.com/Kernel/Livepatch - then you can skip installing a different kernel 151 | and move to enabling `livepatch` directly). 152 | ``` 153 | uname -a 154 | ``` 155 | 156 | If you are not running linux-generic, you need to uninstall your current kernel 157 | (replace `linux-image-5.4.0-1040-kvm` with your kernel version) and then install linux-generic: 158 | https://www.reddit.com/r/Ubuntu/comments/7pujtv/difference_between_linuxgeneric_and_linuxkvm/ 159 | ``` 160 | dpkg --list | grep linux-image 161 | sudo apt-get remove --purge linux-image-5.4.0-1040-kvm 162 | sudo apt install linux-generic 163 | sudo update-grub 164 | sudo reboot 165 | ``` 166 | 167 | Attach the machine to your Ubuntu account and activate livepatch (register for a token on https://ubuntu.com/security/livepatch) 168 | ``` 169 | sudo ua attach 170 | sudo snap install canonical-livepatch 171 | sudo ua enable livepatch 172 | ``` 173 | 174 | To reinstall your old kernel (if linux-kvm was previously used) - uninstall linux-generic kernel like above and then: 175 | ``` 176 | sudo apt install linux-kvm 177 | ``` 178 | 179 | Check for status 180 | ``` 181 | sudo canonical-livepatch status --verbose 182 | ``` 183 | 184 | Troubleshooting: maybe reinstalling the kernel if necessary 185 | ``` 186 | sudo apt-get install --reinstall linux-generic 187 | ``` 188 | 189 | ## Shared Memory Read Only 190 | Based on https://www.techrepublic.com/article/how-to-enable-secure-shared-memory-on-ubuntu-server/ 191 | 192 | Add the following line `/etc/fstab`: 193 | ``` 194 | none /run/shm tmpfs defaults,ro 0 0 195 | ``` 196 | 197 | Enable changes 198 | ``` 199 | sudo mount -a 200 | sudo reboot 201 | ``` 202 | 203 | # Radix Node 204 | We install the Radix node based on the standalone instructions form the documentation 205 | https://docs.radixdlt.com/main/node-and-gateway/systemd-install-node.html. 206 | 207 | ## Dependencies 208 | Install the necessary dependencies and initiate randomness to securely generate keys. 209 | ``` 210 | sudo apt install -y rng-tools openjdk-17-jdk unzip jq curl 211 | sudo rngd -r /dev/random 212 | ``` 213 | 214 | ## Create Radix User 215 | Create a specific user for running the Radix node. The user is created with a locked password 216 | and can only be switched to via `sudo su - radixdlt` (if you have already created this user you can skip this). 217 | ``` 218 | sudo useradd radixdlt -m -s /bin/bash 219 | ``` 220 | 221 | ## Service Control 222 | Allow radix user to control the radix node service. 223 | ``` 224 | sudo sh -c 'cat > /etc/sudoers.d/radix-babylon << EOF 225 | radixdlt ALL= NOPASSWD: /bin/systemctl enable radix-babylon.service 226 | radixdlt ALL= NOPASSWD: /bin/systemctl restart radix-babylon.service 227 | radixdlt ALL= NOPASSWD: /bin/systemctl stop radix-babylon.service 228 | radixdlt ALL= NOPASSWD: /bin/systemctl start radix-babylon.service 229 | radixdlt ALL= NOPASSWD: /bin/systemctl reload radix-babylon.service 230 | radixdlt ALL= NOPASSWD: /bin/systemctl status radix-babylon.service 231 | radixdlt ALL= NOPASSWD: /bin/systemctl enable radix-babylon 232 | radixdlt ALL= NOPASSWD: /bin/systemctl restart radix-babylon 233 | radixdlt ALL= NOPASSWD: /bin/systemctl stop radix-babylon 234 | radixdlt ALL= NOPASSWD: /bin/systemctl start radix-babylon 235 | radixdlt ALL= NOPASSWD: /bin/systemctl reload radix-babylon 236 | radixdlt ALL= NOPASSWD: /bin/systemctl status radix-babylon 237 | radixdlt ALL= NOPASSWD: /bin/systemctl status radix-babylon 238 | radixdlt ALL= NOPASSWD: /bin/systemctl restart grafana-agent 239 | radixdlt ALL= NOPASSWD: /bin/sed -i s/fullnode/validator/g /etc/grafana-agent.yaml 240 | radixdlt ALL= NOPASSWD: /bin/sed -i s/validator/fullnode/g /etc/grafana-agent.yaml 241 | EOF' 242 | ``` 243 | 244 | 245 | ## Systemd Service 246 | Create the radixdlt-node service: 247 | ``` 248 | sudo curl -Lo /etc/systemd/system/radix-babylon.service \ 249 | https://raw.githubusercontent.com/fpieper/fpstaking/main/docs/config/radix-babylon.service 250 | ``` 251 | 252 | Also we enable the service at boot: 253 | ``` 254 | sudo systemctl enable radix-babylon 255 | ``` 256 | 257 | ## Create config and data directories 258 | We create the necessary directories and set the ownership: 259 | ``` 260 | sudo mkdir /etc/radix-babylon/ 261 | sudo chown radixdlt:radixdlt -R /etc/radix-babylon 262 | sudo mkdir /babylon-ledger 263 | sudo chown radixdlt:radixdlt /babylon-ledger 264 | sudo mkdir -p /opt/radix-babylon/releases 265 | sudo chown -R radixdlt:radixdlt /opt/radix-babylon 266 | ``` 267 | 268 | Add `/opt/radix-babylon` to `PATH`: 269 | ``` 270 | sudo sh -c 'cat > /etc/profile.d/radix-babylon.sh << EOF 271 | PATH=$PATH:/opt/radix-babylon 272 | EOF' 273 | ``` 274 | 275 | ## Install Node 276 | Switch to radixdlt user first 277 | ``` 278 | sudo su - radixdlt 279 | ``` 280 | 281 | I developed a seamless install and update script which downloads the last release 282 | from `https://github.com/radixdlt/babylon-node/releases` and waits until one proposal was made to 283 | restart the node to minimise the downtime. 284 | If the interval between proposals is higher than around 5 seconds then there will be zero missed proposals: 285 | ``` 286 | curl -Lo /opt/radix-babylon/update-node \ 287 | https://raw.githubusercontent.com/fpieper/fpstaking/main/docs/scripts/update-node && \ 288 | chmod +x /opt/radix-babylon/update-node 289 | ``` 290 | 291 | Installs or updates the radix node with the latest available version. 292 | ``` 293 | update-node 294 | ``` 295 | 296 | The argument `force` bypasses the check of the current installed version (mostly useful for testing). 297 | ``` 298 | update-node force 299 | ``` 300 | 301 | Change directory for following steps. 302 | ``` 303 | cd /etc/radix-babylon/node 304 | ``` 305 | 306 | ## Secrets 307 | Create secrets directories (one for validator and one for full node mode) 308 | ``` 309 | mkdir /etc/radix-babylon/node/secrets-validator 310 | mkdir /etc/radix-babylon/node/secrets-fullnode 311 | ``` 312 | 313 | ### Key Copy or Generation 314 | The idea is to have two folders with configurations for a validator and a fullnode setting with different keys. 315 | `/etc/radix-babylon/node/secrets-validator` contains the configuration for a validator. 316 | `/etc/radix-babylon/node/secrets-fullnode` contains the configuration for a fullnode. 317 | We will later to be able to switch between being a validator or fullnode. 318 | This is useful for failover scenarios. 319 | 320 | Either copy your already existing keyfiles `node-keystore.ks` to `/etc/radix-babylon/node/secrets-validator` or `/etc/radix-babylon/node/secrets-fullnode` or create a new keys. 321 | Use a password generator of your choice to generate a secure password, don't use your regular one because 322 | it will be written in plain text on disk and loaded as environment variable. 323 | ``` 324 | ./bin/keygen --keystore=secrets-validator/node-keystore.ks --password=YOUR_VALIDATOR_PASSWORD 325 | ./bin/keygen --keystore=secrets-fullnode/node-keystore.ks --password=YOUR_FULLNODE_PASSWORD 326 | ``` 327 | 328 | If you are migrating from Olympia you already have valid keyfiles here which you can copy: 329 | ``` 330 | cp /etc/radixdlt/node/secrets-validator/node-keystore.ks /etc/radix-babylon/node/secrets-validator/node-keystore.ks 331 | cp /etc/radixdlt/node/secrets-fullnode/node-keystore.ks /etc/radix-babylon/node/secrets-fullnode/node-keystore.ks 332 | ``` 333 | 334 | Don't forget to set the ownership and permissions (and switch user again) - if you have used sudo to copy etc: 335 | ``` 336 | sudo chown -R radixdlt:radixdlt /etc/radix-babylon/node/secrets-validator/ 337 | sudo chown -R radixdlt:radixdlt /etc/radix-babylon/node/secrets-fullnode/ 338 | sudo su - radixdlt 339 | cd /etc/radix-babylon/node 340 | ``` 341 | 342 | To achieve high uptime, it is important to also have a backup node for maintenance or failover. 343 | Your main and backup node will have the same validator key (node-keystore.ks), but they both have different fullnode keys 344 | (which leads to 3 different keys in total: 1 key used as validator, 2 keys used for the full nodes). 345 | Please also checkout this article for further details: https://docs.radixdlt.com/main/node-and-gateway/maintaining-uptime.html. 346 | 347 | ### Environment file 348 | Set java options, the previously used keystore password and the Rust JNI core lib. 349 | ``` 350 | cat > /etc/radix-babylon/node/secrets-validator/environment << EOF 351 | JAVA_OPTS="--enable-preview -server -Xms12g -Xmx12g -XX:MaxDirectMemorySize=2048m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseCompressedOops -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=jks -Djava.security.egd=file:/dev/urandom -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Djava.library.path=/etc/radix-babylon/node/jni" 352 | LD_PRELOAD=/etc/radix-babylon/node/jni/libcorerust.so 353 | RADIX_NODE_KEYSTORE_PASSWORD=YOUR_VALIDATOR_PASSWORD 354 | EOF 355 | 356 | cat > /etc/radix-babylon/node/secrets-fullnode/environment << EOF 357 | JAVA_OPTS="--enable-preview -server -Xms12g -Xmx12g -XX:MaxDirectMemorySize=2048m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseCompressedOops -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=jks -Djava.security.egd=file:/dev/urandom -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Djava.library.path=/etc/radix-babylon/node/jni" 358 | LD_PRELOAD=/etc/radix-babylon/node/jni/libcorerust.so 359 | RADIX_NODE_KEYSTORE_PASSWORD=YOUR_FULLNODE_PASSWORD 360 | EOF 361 | ``` 362 | 363 | ### Restrict Access To Secrets 364 | ``` 365 | chown -R radixdlt:radixdlt /etc/radix-babylon/node/secrets-validator 366 | chown -R radixdlt:radixdlt /etc/radix-babylon/node/secrets-fullnode 367 | chmod 500 /etc/radix-babylon/node/secrets-validator && chmod 400 /etc/radix-babylon/node/secrets-validator/* 368 | chmod 500 /etc/radix-babylon/node/secrets-fullnode && chmod 400 /etc/radix-babylon/node/secrets-fullnode/* 369 | ``` 370 | 371 | ## Node Configuration 372 | Create and adapt the node configuration to your needs. 373 | Especially, set the `network.host_ip` to your own IP (`curl ifconfig.me`) and 374 | bind both apis to localhost `127.0.0.1`. 375 | 376 | ``` 377 | curl -Lo /etc/radix-babylon/node/default.config \ 378 | https://raw.githubusercontent.com/fpieper/fpstaking/main/docs/config/default.config 379 | nano /etc/radix-babylon/node/default.config 380 | ``` 381 | 382 | You also may want set a `seed_node` from another region instead of the one from the `EU` above. 383 | 384 | If you want to run on stokenet (testnet) instead of mainnet, you can set `network.id=2` and use this seed nodes: 385 | ``` 386 | radix://node_tdx_2_1qv89yg0la2jt429vqp8sxtpg95hj637gards67gpgqy2vuvwe4s5ss0va2y@13.126.248.88,radix://node_tdx_2_1qvtd9ffdhxyg7meqggr2ezsdfgjre5aqs6jwk5amdhjg86xhurgn5c79t9t@13.210.209.103,radix://node_tdx_2_1qwfh2nn0zx8cut5fqfz6n7pau2f7vdyl89mypldnn4fwlhaeg2tvunp8s8h@54.229.126.97,radix://node_tdx_2_1qwz237kqdpct5l3yjhmna66uxja2ymrf3x6hh528ng3gtvnwndtn5rsrad4@3.210.187.161 387 | ``` 388 | 389 | For further detail and explanation check out the official documentation 390 | https://docs-babylon.radixdlt.com/main/node-and-gateway/systemd-install-node.html 391 | 392 | 393 | ### Olympia Migration 394 | Get the node address from your olympia node (needs to match the one you are connecting to): 395 | ``` 396 | curl -s localhost:4333/system/configuration | jq -r .networking.node_address 397 | ``` 398 | 399 | /etc/radix-babylon/node/default.config: 400 | ``` 401 | genesis.olympia.node_bech32_address=rn1q.... 402 | ``` 403 | 404 | ## Failover 405 | 406 | Until now the service the Radix node does not find the secrets (environment and key). 407 | Depending on whether we want to run the node in validator or full node mode we create a symbolic link 408 | to the corresponding directory. For example to run in validator mode: 409 | ``` 410 | /etc/radix-babylon/node/secrets -> /etc/radix-babylon/node/secrets-validator 411 | ``` 412 | 413 | To streamline this process of promoting in case of a failover from our primary node, I wrote a small script: 414 | ``` 415 | curl -Lo /opt/radix-babylon/switch-mode \ 416 | https://raw.githubusercontent.com/fpieper/fpstaking/main/docs/scripts/switch-mode && \ 417 | chmod +x /opt/radix-babylon/switch-mode 418 | ``` 419 | 420 | To switch the mode simply pass the mode as first argument. Possible modes are: `validator` and `fullnode` 421 | ``` 422 | switch-mode 423 | ``` 424 | 425 | For example: 426 | ``` 427 | switch-mode validator 428 | switch-mode fullnode 429 | ``` 430 | 431 | It also supports `force` in case you need to switch, but your validator isn't fully working or making proposals: 432 | ``` 433 | switch-mode fullnode force 434 | ``` 435 | 436 | For bootstrapping a new validator it is a good idea to start as a `fullnode` and then after full sync 437 | switch to `validator` mode because this also directly tests failover or promoting to validator works fine. 438 | 439 | Switching to fullnode waits for the next made proposal still in validator mode 440 | and stops immediately afterwards to minimise downtime (or specifically the missed proposals) 441 | 442 | For maintenance failover just open SSH connections to both of your servers side-by-side. 443 | 1. Switch to fullnode mode on your validator: `/opt/radix-babylon/switch-mode.sh fullnode` 444 | 2. Wait until switching mode was successful 445 | 3. Immediately switch to validator mode on your backup node: `/opt/radix-babylon/switch-mode.sh validator` 446 | 447 | ## Node-Runner CLI 448 | ``` 449 | This section is currently outdated and refers to the Olympia nodes. 450 | ``` 451 | The node-runner cli was already installed by the `update-node` script. 452 | We only just need to fit the following environment variables to our setup: 453 | ``` 454 | echo ' 455 | export NGINX_SUPERADMIN_PASSWORD="" 456 | export NGINX_ADMIN_PASSWORD="" 457 | export NGINX_METRICS_PASSWORD="" 458 | export NODE_END_POINT="http://localhost:3333"' >> ~/.bashrc 459 | ``` 460 | 461 | Now you need to logout and login back into the shell to enable the environment variables. 462 | 463 | The radix node-runner cli can afterwards be called with for example: 464 | ``` 465 | radixnode api system health 466 | ``` 467 | 468 | For further details checkout the official documentation https://github.com/radixdlt/node-runner. 469 | Though only use the `api` feature of the cli to interact with the node endpoints in 470 | an easier way and not the `setup`/`update`/`nginx`/`monitoring` commands, since these 471 | conflict with the minimal setup approach in this guide. 472 | 473 | 474 | ## Registering as a validator 475 | First of all we make sure that our node is running in `validator mode` to register the correct node key. 476 | ``` 477 | switch-mode validator 478 | ``` 479 | 480 | To register as validator please refer to the official documentation https://docs-babylon.radixdlt.com/main/node-and-gateway/register-as-validator.html Keep in mind to adapt the local port in `curl` commands if necessary. 481 | 482 | # Monitoring with Grafana Cloud 483 | ``` 484 | This section is currently outdated and refers to the Olympia nodes. 485 | ``` 486 | I can recommend watching this comprehensive introduction to Grafana Cloud 487 | https://grafana.com/go/webinar/intro-to-prometheus-and-grafana/. 488 | First, sign up for a Grafana Cloud free account and follow their quickstart introductions to install 489 | Grafana Agent on your node (via the automatic setup script). This basic setup is out of the scope of this guide. 490 | You can find the quickstart introductions to install the Grafana Agent under 491 | `Onboarding (lightning icon) / Walkthrough / Linux Server` and click on `Next: Configure Service`. 492 | The Grafana Agent is basically a stripped down Promotheus which is directly writing to Grafana Cloud instead of storing metrics locally 493 | (Grafana Agent behaves like having a built-in Promotheus). 494 | You should now have a working monitoring of your system load pushed to Grafana Cloud. 495 | 496 | ## Extending Grafana Agent Config 497 | Add the `scrape_configs` configuration to `etc/grafana-agent.yaml`: 498 | ``` 499 | sudo nano /etc/grafana-agent.yaml 500 | ``` 501 | ``` 502 | prometheus: 503 | configs: 504 | - name: integrations 505 | scrape_configs: 506 | - job_name: radix-mainnet-fullnode 507 | static_configs: 508 | - targets: ['localhost:3333'] 509 | metrics_path: /prometheus/metrics 510 | remote_write: 511 | - basic_auth: 512 | password: secret 513 | username: 123456 514 | url: https://prometheus-blocks-prod-us-central1.grafana.net/api/prom/push 515 | ``` 516 | 517 | The prefixes like `radix-mainnet` before `fullnode` or `validator` are arbitrary and can be used 518 | to have two dashboards (one for mainnet and one for stokenet) in the same Grafana Cloud account. 519 | 520 | Just set the template variable `job` to `radix-mainnet-validator` in your mainnet dashboard 521 | and `radix-stokenet-validator` in your stokenet dashboard. 522 | 523 | The switch-mode script replaces `fullnode` with `validator` and vice versa. 524 | Set `job_name` in the config above to e.g. `radix-mainnet-fullnode` if you are running in fullnode mode and 525 | `radix-mainnet-validator` if you are running as validator. 526 | 527 | And restart to activate the new settings: 528 | ``` 529 | sudo systemctl restart grafana-agent 530 | ``` 531 | 532 | ## Radix Dashboard 533 | 534 | I adapted the official `Radix Node Dashboard` 535 | https://github.com/radixdlt/node-runner/blob/main/monitoring/grafana/provisioning/dashboards/sample-node-dashboard.json 536 | and modified it a bit for usage in Grafana Cloud (including specific job names for `radix-validator` and `radix-fullnode` for failover). 537 | You can get the `dashboard.json` from https://github.com/fpieper/fpstaking/blob/main/docs/config/dashboard.json. 538 | You only need to replace `` with your own cloud name 539 | (three times, since it seems the alerts have problems to process a datasource template variable). 540 | It is a good idea to replace the values and variables in your JSON and then import the JSON as dashboard into Grafana Cloud. 541 | 542 | ## Alerts 543 | 544 | ### Spike.sh for phone calls 545 | To get phone proper notifications via phone calls in case of Grafana Alerts I am using Spike.sh. 546 | It only costs 7$/month and is working great. 547 | How you can configure Spike.sh as `Notification Channel` is described here: 548 | https://docs.spike.sh/integrations-guideline/integrate-spike-with-grafana. 549 | Afterwards you can select `Spike.sh` in your alert configurations. 550 | 551 | ### Grafana Alerts 552 | You can find the alerts by clicking on the panel title / Edit / Alert. 553 | 554 | I set an alert on the proposals made panel, which fires an alert if no proposal was made in the last 2 minutes. 555 | However, this needs a bit tuning for real world condition (worked fine in betanet conditions). 556 | 557 | You also need to set `Notifications` to `Spike.sh` (if you configured the `Notification Channel` above). 558 | Or any other notification channel if you prefer `PagerDuty` or `Telegram`. 559 | 560 | # More Hardening 561 | ## SSH 562 | - https://serverfault.com/questions/275669/ssh-sshd-how-do-i-set-max-login-attempts 563 | - Restrict access to the port: 564 | - use a VPN 565 | - only allow connections from a fix IP address 566 | ``` 567 | sudo ufw allow from 1.2.3.4 to any port ssh 568 | ``` 569 | 570 | ## Restrict Local Access (TTY1, etc) 571 | We can additionally restrict local access. 572 | However, this obviously leads results in that you won't be able to login without SSH in emergencies. 573 | (booting into recovery mode works with most virtual servers, but causes downtime). 574 | But since we have multiple backup servers this can be a fair trade-off. 575 | 576 | Uncomment or add in this file 577 | ``` 578 | sudo nano /etc/pam.d/login 579 | ``` 580 | the following line: 581 | ``` 582 | account required pam_access.so 583 | ``` 584 | 585 | Then uncomment or add in this file 586 | ``` 587 | sudo nano /etc/security/access.conf 588 | ``` 589 | the following line: 590 | ``` 591 | -:ALL:ALL 592 | ``` 593 | 594 | For further details: 595 | - https://linuxconfig.org/how-to-restrict-users-access-on-a-linux-machine 596 | 597 | # Logs & Status 598 | 599 | Shows radix node logs with colours: 600 | ``` 601 | sudo journalctl -f -u radix-babylon --output=cat 602 | ``` 603 | 604 | Shows node health (`BOOTING`, `SYNCING`, `UP`, `STALLED`, `OUT_OF_SYNC`, `BOOTING_PRE_GENESIS`) 605 | ``` 606 | curl -s localhost:3334/system/health | jq 607 | ``` 608 | 609 | Shows current validator information: 610 | ``` 611 | curl -s localhost:3334/system/identity | jq 612 | ``` 613 | 614 | Get network peers: 615 | ``` 616 | curl -s localhost:3334/system/peers | jq 617 | ``` 618 | 619 | Get node configuration: 620 | ``` 621 | curl -s localhost:3334/system/configuration | jq 622 | ``` 623 | 624 | 625 | # Babylon Migration 626 | To simplify the upgrade process we are going to run the migration in parallel to your current Olympia nodes. 627 | This requires however to have enough memory (32GB should be enough, 4 CPU cores should work (untested) but would recommend 8 to be on the safe side). 628 | 629 | ## Preparations 630 | Upgrade both nodes to Ubuntu 22.04 and node version 1.5.0 (if you didn't do that already). For details to upgrade the operating system see e.g.: https://jumpcloud.com/blog/how-to-upgrade-ubuntu-20-04-to-ubuntu-22-04 631 | 632 | 1. first upgrade your backup node (you can either update Ubuntu or the node first). The node can be updated as always using: 633 | `update-node` 634 | 2. switch your validator to the backup 635 | 3. upgrade your other node (currently the full node - also Ubuntu and Radix node) 636 | 637 | ## Olympia Node Configuration Change 638 | These changes need to be applied to both nodes - your fullnode and validator. 639 | 640 | ### Bind Olympia End State endpoint to localhost 641 | 642 | In your `/etc/radixdlt/node/default.config` add this line (if you are using an Olympia node on another server do not add this line or set `0.0.0.0`): 643 | ``` 644 | api.end-state.bind.address=127.0.0.1 645 | ``` 646 | 647 | ### Change network listen port 648 | To be able to run both nodes in parallel we set the Olympia listen port to `30001`: 649 | ``` 650 | sudo ufw allow 30001/tcp 651 | sudo ufw reload 652 | sudo ufw status 653 | sudo nano /etc/radixdlt/node/default.config 654 | ``` 655 | 656 | /etc/radixdlt/node/default.config: 657 | ``` 658 | api.port=4333 659 | network.p2p.listen_port=30001 660 | network.p2p.broadcast_port=30001 661 | ``` 662 | 663 | As fallback in case needed also update the port in the `/opt/radixdlt/switch-mode` script. 664 | ``` 665 | HOST="http://localhost:4333" 666 | ``` 667 | 668 | After successful Babylon migration you can remove this firewall rule again with: 669 | ``` 670 | sudo ufw delete allow 30001/tcp 671 | sudo ufw reload 672 | sudo ufw status 673 | ``` 674 | 675 | ### Increase assigned memory 676 | In `/etc/radixdlt/node/secrets-validator/environment` and `/etc/radixdlt/node/secrets-fullnode/environment` (both nodes) change the memory settings: 677 | ``` 678 | JAVA_OPTS="... -Xms12g -Xmx12g ..." 679 | ``` 680 | 681 | You probably need to also adjust the permissions: 682 | ``` 683 | sudo chmod +w /etc/radixdlt/node/secrets-validator/environment 684 | sudo chmod +w /etc/radixdlt/node/secrets-fullnode/environment 685 | 686 | sudo nano /etc/radixdlt/node/secrets-validator/environment 687 | sudo nano /etc/radixdlt/node/secrets-fullnode/environment 688 | 689 | sudo su radixdlt 690 | chmod 500 /etc/radixdlt/node/secrets-validator && chmod 400 /etc/radixdlt/node/secrets-validator/* 691 | chmod 500 /etc/radixdlt/node/secrets-fullnode && chmod 400 /etc/radixdlt/node/secrets-fullnode/* 692 | 693 | exit 694 | ``` 695 | 696 | ### Olympia PATH variables 697 | We need to remove the old PATH for Olympia by doing: 698 | ``` 699 | sudo rm /etc/profile.d/radixdlt.sh 700 | ``` 701 | This will take effect after logout / reboot. Until then better to use the explicit paths for the `switch-mode` and `update-node` script. 702 | 703 | ### Apply & verify new config 704 | Afterwards restart your validator/fullnode with (if you want to be safe against proposal misses use the switch-mode script which waits for an propsal and then safely restarts): 705 | ``` 706 | sudo systemctl restart radixdlt-node 707 | ``` 708 | 709 | Verify afterwards that the endpoint is working with (it is a ): 710 | ``` 711 | john@radixnode:~$ curl localhost:3400/olympia-end-state 712 | Invalid method, path exists for GET /olympia-end-state 713 | ``` 714 | 715 | ## Install Babylon 716 | The [Radix Node](#radix-node) section is updated to Babylon and will install the Babylon node in parallel to the Olympia node. 717 | Best to install it first on your backup node to see if everything works fine and then on your current validator. 718 | If you feel more comfortable you can also switch validators for that process (and always installing on the backup node). 719 | 720 | Besides that, you want to run the Olympia and Babylon node on one machine in the same mode. On one server you are running both in validator mode and on one machine both in fullnode mode. 721 | 722 | ## Verify Babylon node setup 723 | Finally we need to verify that the Babylon nodes are configured correctly and can connect to your (local) Olympia nodes. 724 | For that check the logs on both nodes with: 725 | ``` 726 | sudo journalctl -f -u radix-babylon --output=cat 727 | ``` 728 | 729 | This should give you log messages like these: 730 | ``` 731 | 2023-09-27T00:29:26,854 [INFO/OlympiaGenesisService/OlympiaGenesisService] - Querying the Olympia node http://127.0.0.1:3400 for genesis data (this may take a few minutes) 732 | 2023-09-27T00:29:26,899 [INFO/OlympiaGenesisService/OlympiaGenesisService] - Successfully connected to the Olympia mainnet node, but the end state hasn't yet been generated (will keep polling)... 733 | 2023-09-27T00:29:27,901 [INFO/OlympiaGenesisService/OlympiaGenesisService] - Querying the Olympia node http://127.0.0.1:3400 for genesis data (with test payload) (this may take a few minutes) 734 | ``` 735 | 736 | In this case everything is fine and you are ready for the Babylon migration. 737 | 738 | ## Take your front seats 739 | Babylon is a huge milestone for Radix - take a seat and enjoy your front seats in the migration ;) 740 | -------------------------------------------------------------------------------- /frontend/debug.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | cd %~dp0 4 | elm-live src/Main.elm --pushstate --dir=static --host=0.0.0.0 -- --output=static/main.js 5 | -------------------------------------------------------------------------------- /frontend/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "avh4/elm-color": "1.0.0", 11 | "cmditch/elm-bigint": "2.0.1", 12 | "cuducos/elm-format-number": "8.1.4", 13 | "dillonkearns/elm-markdown": "6.0.1", 14 | "elm/browser": "1.0.2", 15 | "elm/core": "1.0.5", 16 | "elm/html": "1.0.0", 17 | "elm/http": "2.0.0", 18 | "elm/json": "1.1.3", 19 | "elm/url": "1.0.0", 20 | "elm-community/typed-svg": "7.0.0", 21 | "gampleman/elm-visualization": "2.1.2", 22 | "icidasset/elm-material-icons": "7.0.0", 23 | "krisajenkins/remotedata": "6.0.1", 24 | "mdgriffith/elm-ui": "1.1.8", 25 | "noahzgordon/elm-color-extra": "1.0.2", 26 | "perzanko/elm-loading": "2.0.4" 27 | }, 28 | "indirect": { 29 | "elm/bytes": "1.0.8", 30 | "elm/file": "1.0.5", 31 | "elm/parser": "1.1.0", 32 | "elm/random": "1.0.0", 33 | "elm/regex": "1.0.0", 34 | "elm/svg": "1.0.1", 35 | "elm/time": "1.0.0", 36 | "elm/virtual-dom": "1.0.2", 37 | "elm-community/list-extra": "8.3.0", 38 | "elm-community/maybe-extra": "5.2.0", 39 | "folkertdev/elm-deque": "3.0.1", 40 | "folkertdev/one-true-path-experiment": "5.0.2", 41 | "folkertdev/svg-path-lowlevel": "3.0.0", 42 | "fredcy/elm-parseint": "2.0.1", 43 | "ianmackenzie/elm-1d-parameter": "1.0.1", 44 | "ianmackenzie/elm-float-extra": "1.1.0", 45 | "ianmackenzie/elm-geometry": "3.9.0", 46 | "ianmackenzie/elm-interval": "2.0.0", 47 | "ianmackenzie/elm-triangular-mesh": "1.1.0", 48 | "ianmackenzie/elm-units": "2.8.0", 49 | "ianmackenzie/elm-units-interval": "2.3.0", 50 | "ianmackenzie/elm-units-prefixed": "2.7.0", 51 | "justinmimbs/date": "3.2.1", 52 | "justinmimbs/time-extra": "1.1.0", 53 | "myrho/elm-round": "1.0.4", 54 | "rtfeldman/elm-css": "16.1.1", 55 | "rtfeldman/elm-hex": "1.0.0", 56 | "ryannhg/date-format": "2.3.0" 57 | } 58 | }, 59 | "test-dependencies": { 60 | "direct": {}, 61 | "indirect": {} 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/ArchiveApi.elm: -------------------------------------------------------------------------------- 1 | module ArchiveApi exposing (..) 2 | 3 | import BigInt exposing (BigInt) 4 | import Http exposing (Error, jsonBody) 5 | import Json.Decode as Decode exposing (Decoder, andThen, bool, fail, field, int, list, string, succeed) 6 | import Json.Decode.Pipeline exposing (hardcoded, required) 7 | import Json.Encode as Encode 8 | 9 | 10 | type alias Validator = 11 | { totalDelegatedStake : BigInt 12 | , uptimePercentage : Float 13 | , proposalsMissed : Int 14 | , address : String 15 | , infoUrl : String 16 | , ownerDelegation : BigInt 17 | , name : String 18 | , fee : Float 19 | , registered : Bool 20 | , ownerAddress : String 21 | , acceptsExternalStake : Bool 22 | , proposalCompleted : Int 23 | , group : Maybe Group 24 | , rank : Int 25 | , stakeShare : Float 26 | } 27 | 28 | 29 | type alias Group = 30 | { name : String 31 | , totalStake : BigInt 32 | , stakeShare : Float 33 | } 34 | 35 | 36 | type alias StakePosition = 37 | { amount : BigInt 38 | , validator : String 39 | } 40 | 41 | 42 | 43 | -- HTTP 44 | 45 | 46 | archiveEndpoint = 47 | "https://api.florianpieperstaking.com/archive" 48 | 49 | 50 | getValidatorsRequest : (Result Error (List Validator) -> msg) -> Cmd msg 51 | getValidatorsRequest msg = 52 | Http.post 53 | { url = archiveEndpoint 54 | , body = jsonBody getNextEpochSet 55 | , expect = Http.expectJson msg validatorsDecoder 56 | } 57 | 58 | 59 | getStakePositionsRequest : String -> (Result Error (List StakePosition) -> msg) -> Cmd msg 60 | getStakePositionsRequest address msg = 61 | Http.post 62 | { url = archiveEndpoint 63 | , body = jsonBody (getStakePositions address) 64 | , expect = Http.expectJson msg stakePositionsDecoder 65 | } 66 | 67 | 68 | bigIntDecoder : Decoder BigInt 69 | bigIntDecoder = 70 | string 71 | |> andThen 72 | (\value -> 73 | case BigInt.fromIntString value of 74 | Just number -> 75 | succeed number 76 | 77 | Nothing -> 78 | fail "Invalid BigInt" 79 | ) 80 | 81 | 82 | floatDecoder : Decoder Float 83 | floatDecoder = 84 | string 85 | |> andThen 86 | (\value -> 87 | case String.toFloat value of 88 | Just number -> 89 | succeed number 90 | 91 | Nothing -> 92 | fail "Invalid Float" 93 | ) 94 | 95 | 96 | validatorDecoder : Decoder Validator 97 | validatorDecoder = 98 | Decode.succeed Validator 99 | |> required "totalDelegatedStake" bigIntDecoder 100 | |> required "uptimePercentage" floatDecoder 101 | |> required "proposalsMissed" int 102 | |> required "address" string 103 | |> required "infoURL" string 104 | |> required "ownerDelegation" bigIntDecoder 105 | |> required "name" (Decode.map String.trim string) 106 | |> required "validatorFee" floatDecoder 107 | |> required "registered" bool 108 | |> required "ownerAddress" string 109 | |> required "isExternalStakeAccepted" bool 110 | |> required "proposalsCompleted" int 111 | |> hardcoded Nothing 112 | |> hardcoded 0 113 | |> hardcoded 0 114 | 115 | 116 | validatorsDecoder : Decoder (List Validator) 117 | validatorsDecoder = 118 | field "result" <| 119 | field "validators" <| 120 | list validatorDecoder 121 | 122 | 123 | stakePositionDecoder : Decoder StakePosition 124 | stakePositionDecoder = 125 | Decode.map2 StakePosition 126 | (field "amount" bigIntDecoder) 127 | (field "validator" string) 128 | 129 | 130 | stakePositionsDecoder : Decoder (List StakePosition) 131 | stakePositionsDecoder = 132 | field "result" <| 133 | list stakePositionDecoder 134 | 135 | 136 | getNextEpochSet : Encode.Value 137 | getNextEpochSet = 138 | Encode.object 139 | [ ( "jsonrpc", Encode.string "2.0" ) 140 | , ( "method", Encode.string "validators.get_next_epoch_set" ) 141 | , ( "params" 142 | , Encode.object 143 | [ ( "size", Encode.int 200 ) 144 | ] 145 | ) 146 | , ( "id", Encode.int 1 ) 147 | ] 148 | 149 | 150 | getStakePositions : String -> Encode.Value 151 | getStakePositions address = 152 | Encode.object 153 | [ ( "jsonrpc", Encode.string "2.0" ) 154 | , ( "method", Encode.string "account.get_stake_positions" ) 155 | , ( "params" 156 | , Encode.object 157 | [ ( "address", Encode.string address ) 158 | ] 159 | ) 160 | , ( "id", Encode.int 1 ) 161 | ] 162 | -------------------------------------------------------------------------------- /frontend/src/GatewayApi.elm: -------------------------------------------------------------------------------- 1 | module GatewayApi exposing (..) 2 | 3 | import BigInt exposing (BigInt) 4 | import Http exposing (Body, Error, Expect, header, jsonBody) 5 | import Json.Decode as Decode exposing (Decoder, andThen, at, bool, fail, field, float, int, list, string, succeed) 6 | import Json.Decode.Pipeline exposing (hardcoded, required, requiredAt) 7 | import Json.Encode as Encode 8 | 9 | 10 | type alias Validator = 11 | { totalDelegatedStake : BigInt 12 | , uptimePercentage : Float 13 | , proposalsMissed : Int 14 | , address : String 15 | , infoUrl : String 16 | , ownerDelegation : BigInt 17 | , name : String 18 | , fee : Float 19 | , registered : Bool 20 | , ownerAddress : String 21 | , acceptsExternalStake : Bool 22 | , proposalCompleted : Int 23 | , group : Maybe Group 24 | , rank : Int 25 | , stakeShare : Float 26 | } 27 | 28 | 29 | type alias Group = 30 | { name : String 31 | , totalStake : BigInt 32 | , stakeShare : Float 33 | } 34 | 35 | 36 | type alias StakePosition = 37 | { amount : BigInt 38 | , validator : String 39 | } 40 | 41 | 42 | 43 | -- HTTP 44 | 45 | 46 | gatewayRequest : { endpoint : String, body : Body, expect : Expect msg } -> Cmd msg 47 | gatewayRequest { endpoint, body, expect } = 48 | Http.request 49 | { method = "POST" 50 | , headers = [] --[ header "X-Radixdlt-Target-Gw-Api" "1.0.2" ] 51 | , url = "https://mainnet.clana.io/" ++ endpoint 52 | , body = body 53 | , expect = expect 54 | , timeout = Nothing 55 | , tracker = Nothing 56 | } 57 | 58 | 59 | getValidatorsRequest : (Result Error (List Validator) -> msg) -> Cmd msg 60 | getValidatorsRequest msg = 61 | gatewayRequest 62 | { endpoint = "validators" 63 | , body = jsonBody mainnetIdentifier 64 | , expect = Http.expectJson msg validatorsDecoder 65 | } 66 | 67 | 68 | getStakePositionsRequest : String -> (Result Error (List StakePosition) -> msg) -> Cmd msg 69 | getStakePositionsRequest address msg = 70 | gatewayRequest 71 | { endpoint = "account/stakes" 72 | , body = jsonBody (stakePositionsRequestBody address) 73 | , expect = Http.expectJson msg stakePositionsDecoder 74 | } 75 | 76 | 77 | bigIntDecoder : Decoder BigInt 78 | bigIntDecoder = 79 | string 80 | |> andThen 81 | (\value -> 82 | case BigInt.fromIntString value of 83 | Just number -> 84 | succeed number 85 | 86 | Nothing -> 87 | fail "Invalid BigInt" 88 | ) 89 | 90 | 91 | validatorDecoder : Decoder Validator 92 | validatorDecoder = 93 | Decode.succeed Validator 94 | |> requiredAt [ "stake", "value" ] bigIntDecoder 95 | |> requiredAt [ "info", "uptime", "uptime_percentage" ] float 96 | |> requiredAt [ "info", "uptime", "proposals_missed" ] int 97 | |> requiredAt [ "validator_identifier", "address" ] string 98 | |> requiredAt [ "properties", "url" ] string 99 | |> requiredAt [ "info", "owner_stake", "value" ] bigIntDecoder 100 | |> requiredAt [ "properties", "name" ] (Decode.map String.trim string) 101 | |> requiredAt [ "properties", "validator_fee_percentage" ] float 102 | |> requiredAt [ "properties", "registered" ] bool 103 | |> requiredAt [ "properties", "owner_account_identifier", "address" ] string 104 | |> requiredAt [ "properties", "external_stake_accepted" ] bool 105 | |> requiredAt [ "info", "uptime", "proposals_completed" ] int 106 | |> hardcoded Nothing 107 | |> hardcoded 0 108 | |> hardcoded 0 109 | 110 | 111 | validatorsDecoder : Decoder (List Validator) 112 | validatorsDecoder = 113 | field "validators" <| 114 | list validatorDecoder 115 | 116 | 117 | stakePositionDecoder : Decoder StakePosition 118 | stakePositionDecoder = 119 | Decode.map2 StakePosition 120 | (at [ "delegated_stake", "value" ] bigIntDecoder) 121 | (at [ "validator_identifier", "address" ] string) 122 | 123 | 124 | stakePositionsDecoder : Decoder (List StakePosition) 125 | stakePositionsDecoder = 126 | field "stakes" <| 127 | list stakePositionDecoder 128 | 129 | 130 | mainnetIdentifier : Encode.Value 131 | mainnetIdentifier = 132 | Encode.object 133 | [ ( "network_identifier" 134 | , Encode.object 135 | [ ( "network", Encode.string "mainnet" ) 136 | ] 137 | ) 138 | ] 139 | 140 | 141 | stakePositionsRequestBody : String -> Encode.Value 142 | stakePositionsRequestBody address = 143 | Encode.object 144 | [ ( "network_identifier" 145 | , Encode.object 146 | [ ( "network", Encode.string "mainnet" ) 147 | ] 148 | ) 149 | , ( "account_identifier" 150 | , Encode.object 151 | [ ( "address", Encode.string address ) 152 | ] 153 | ) 154 | ] 155 | -------------------------------------------------------------------------------- /frontend/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Browser 4 | import Browser.Events 5 | import Browser.Navigation as Nav 6 | import Element exposing (..) 7 | import Element.Background as Background 8 | import Element.Font as Font 9 | import Page.Home 10 | import Page.Validators 11 | import Palette exposing (..) 12 | import Ports 13 | import Url exposing (Url) 14 | import Url.Parser as Parser exposing ((), oneOf, parse, s, top) 15 | 16 | 17 | 18 | -- MAIN 19 | 20 | 21 | main : Program Flags Model Msg 22 | main = 23 | Browser.application 24 | { init = init 25 | , view = view 26 | , update = update 27 | , subscriptions = subscriptions 28 | , onUrlChange = UrlChanged 29 | , onUrlRequest = LinkClicked 30 | } 31 | 32 | 33 | 34 | -- MODEL 35 | 36 | 37 | type alias Model = 38 | { key : Nav.Key 39 | , url : Url.Url 40 | , page : PageModel 41 | , device : Device 42 | } 43 | 44 | 45 | type PageModel 46 | = NoPage 47 | | PageHome Page.Home.Model 48 | | PageValidators Page.Validators.Model 49 | 50 | 51 | type alias Flags = 52 | { width : Int 53 | , height : Int 54 | } 55 | 56 | 57 | init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg ) 58 | init flags url key = 59 | let 60 | device = 61 | Element.classifyDevice flags 62 | 63 | ( pageModel, pageCmd ) = 64 | initByUrl url 65 | in 66 | ( { key = key 67 | , url = url 68 | , page = pageModel 69 | , device = device 70 | } 71 | , pageCmd 72 | ) 73 | 74 | 75 | initPage : ( b, Cmd a ) -> (b -> c) -> (a -> msg) -> ( c, Cmd msg ) 76 | initPage pageInit pageModelConstructor pageMsgConstructor = 77 | let 78 | ( model, cmd ) = 79 | pageInit 80 | in 81 | ( pageModelConstructor model, Cmd.map pageMsgConstructor cmd ) 82 | 83 | 84 | initHome : ( PageModel, Cmd Msg ) 85 | initHome = 86 | initPage Page.Home.init PageHome HomeMsg 87 | 88 | 89 | initValidators : ( PageModel, Cmd Msg ) 90 | initValidators = 91 | initPage Page.Validators.init PageValidators ValidatorsMsg 92 | 93 | 94 | initByUrl : Url -> ( PageModel, Cmd Msg ) 95 | initByUrl url = 96 | let 97 | route = 98 | parse routeParser url 99 | in 100 | case route of 101 | Nothing -> 102 | initHome 103 | 104 | Just RouteHome -> 105 | initHome 106 | 107 | Just RouteValidators -> 108 | initValidators 109 | 110 | 111 | 112 | -- UPDATE 113 | 114 | 115 | type Msg 116 | = LinkClicked Browser.UrlRequest 117 | | UrlChanged Url.Url 118 | | DeviceClassified Device 119 | | HomeMsg Page.Home.Msg 120 | | ValidatorsMsg Page.Validators.Msg 121 | 122 | 123 | update : Msg -> Model -> ( Model, Cmd Msg ) 124 | update msg model = 125 | case ( msg, model.page ) of 126 | ( LinkClicked urlRequest, _ ) -> 127 | case urlRequest of 128 | Browser.Internal url -> 129 | ( model, Nav.pushUrl model.key (Url.toString url) ) 130 | 131 | Browser.External href -> 132 | ( model, Nav.load href ) 133 | 134 | ( UrlChanged url, _ ) -> 135 | let 136 | ( page, cmd ) = 137 | initByUrl url 138 | in 139 | ( { model | url = url, page = page } 140 | , Cmd.batch [ Ports.setMetaDescription "", cmd ] 141 | ) 142 | 143 | ( DeviceClassified device, _ ) -> 144 | ( { model | device = device }, Cmd.none ) 145 | 146 | ( HomeMsg subMsg, PageHome subModel ) -> 147 | Page.Home.update subMsg subModel 148 | |> updatePage HomeMsg PageHome model 149 | 150 | ( ValidatorsMsg subMsg, PageValidators subModel ) -> 151 | Page.Validators.update subMsg subModel 152 | |> updatePage ValidatorsMsg PageValidators model 153 | 154 | _ -> 155 | ( model, Cmd.none ) 156 | 157 | 158 | updatePage : 159 | (subMsg -> msg) 160 | -> (subModel -> pageModel) 161 | -> { model | page : pageModel } 162 | -> ( subModel, Cmd subMsg ) 163 | -> ( { model | page : pageModel }, Cmd msg ) 164 | updatePage msgConstructor modelConstructor model ( subModel, subMsg ) = 165 | ( { model | page = modelConstructor subModel }, Cmd.map msgConstructor subMsg ) 166 | 167 | 168 | 169 | -- ROUTE 170 | 171 | 172 | type Route 173 | = RouteHome 174 | | RouteValidators 175 | 176 | 177 | routeParser : Parser.Parser (Route -> a) a 178 | routeParser = 179 | oneOf 180 | [ Parser.map RouteHome top 181 | , Parser.map RouteValidators (s "validators") 182 | ] 183 | 184 | 185 | 186 | -- SUBSCRIPTIONS 187 | 188 | 189 | subscriptions : Model -> Sub Msg 190 | subscriptions model = 191 | Browser.Events.onResize <| 192 | \width height -> 193 | DeviceClassified (Element.classifyDevice { width = width, height = height }) 194 | 195 | 196 | 197 | -- VIEW 198 | -- VIEW MAIN 199 | 200 | 201 | viewPage : Device -> PageModel -> Element Msg 202 | viewPage device page = 203 | case page of 204 | NoPage -> 205 | none 206 | 207 | PageHome subModel -> 208 | Element.map HomeMsg <| Page.Home.view device subModel 209 | 210 | PageValidators subModel -> 211 | Element.map ValidatorsMsg <| Page.Validators.view device subModel 212 | 213 | 214 | 215 | -- Amatic SC 216 | 217 | 218 | view : Model -> Browser.Document Msg 219 | view model = 220 | { title = "Florian Pieper Staking" 221 | , body = 222 | [ layoutWith 223 | { options = 224 | [ focusStyle 225 | { borderColor = Nothing 226 | , backgroundColor = Nothing 227 | , shadow = Nothing 228 | } 229 | ] 230 | } 231 | [ Font.family 232 | [ Font.typeface "Roboto Mono" 233 | , Font.sansSerif 234 | ] 235 | , Background.color lightShades 236 | , Font.letterSpacing 1 237 | , Font.color <| blackAlpha 1 238 | , Font.size small 239 | ] 240 | <| 241 | column [ width fill ] 242 | [ viewPage model.device model.page 243 | ] 244 | ] 245 | } 246 | -------------------------------------------------------------------------------- /frontend/src/Page/Home.elm: -------------------------------------------------------------------------------- 1 | module Page.Home exposing (..) 2 | 3 | import BigInt exposing (BigInt) 4 | import Element exposing (..) 5 | import Element.Background as Background 6 | import Element.Border as Border 7 | import Element.Font as Font 8 | import Element.Input as Input 9 | import GatewayApi exposing (StakePosition, Validator, getStakePositionsRequest, getValidatorsRequest) 10 | import Html.Attributes 11 | import Http 12 | import Loading exposing (LoaderType(..), defaultConfig) 13 | import Material.Icons.Outlined exposing (build, cloud_off, face, favorite, language, notifications_active, paid, security) 14 | import Page.Validators exposing (addGroups) 15 | import Palette exposing (..) 16 | import RemoteData exposing (RemoteData(..)) 17 | import UI exposing (Icon, heading, icon, inputHint, sliderStyle, subHeading, thumb, viewContact, viewFactTable, viewFooter) 18 | import Utils exposing (bigIntDivToFloat, bigIntMulFloat, bigIntSum, formatWithDecimals, safeBigInt, toXRD) 19 | 20 | 21 | 22 | -- MODEL 23 | 24 | 25 | type alias Model = 26 | { validators : RemoteData Http.Error (List Validator) 27 | , stakedTokens : StakedTokens 28 | , stakedTokensRaw : String 29 | , totalStake : BigInt 30 | , validatorFee : Float 31 | , uptime : Float 32 | } 33 | 34 | 35 | type StakedTokens 36 | = StakeAmount Int 37 | | WalletAddress String (RemoteData Http.Error (List StakePosition)) 38 | 39 | 40 | 41 | -- INIT 42 | 43 | 44 | init : ( Model, Cmd Msg ) 45 | init = 46 | ( { validators = NotAsked 47 | , totalStake = BigInt.fromInt 0 48 | , stakedTokens = StakeAmount 0 49 | , stakedTokensRaw = "" 50 | , validatorFee = 0 51 | , uptime = 100 52 | } 53 | , getValidatorsRequest GotValidators 54 | ) 55 | 56 | 57 | 58 | -- UPDATE 59 | 60 | 61 | type Msg 62 | = TokensStakedChanged String 63 | | ValidatorFeeChanged Float 64 | | UptimeChanged Float 65 | | GotValidators (Result Http.Error (List Validator)) 66 | | GotStakePositions (Result Http.Error (List StakePosition)) 67 | 68 | 69 | update : Msg -> Model -> ( Model, Cmd Msg ) 70 | update msg model = 71 | case msg of 72 | TokensStakedChanged tokens -> 73 | let 74 | tokens_ = 75 | String.trim tokens 76 | in 77 | if String.length tokens_ == 0 then 78 | ( { model | stakedTokens = StakeAmount 0, stakedTokensRaw = tokens }, Cmd.none ) 79 | 80 | else if String.length tokens_ == 65 then 81 | ( { model | stakedTokens = WalletAddress tokens_ Loading, stakedTokensRaw = tokens }, getStakePositionsRequest tokens_ GotStakePositions ) 82 | 83 | else 84 | case String.toInt tokens_ of 85 | Just t -> 86 | ( { model | stakedTokens = StakeAmount t, stakedTokensRaw = tokens }, Cmd.none ) 87 | 88 | Nothing -> 89 | ( { model | stakedTokens = StakeAmount 0, stakedTokensRaw = tokens }, Cmd.none ) 90 | 91 | ValidatorFeeChanged fee -> 92 | ( { model | validatorFee = fee }, Cmd.none ) 93 | 94 | UptimeChanged uptime -> 95 | ( { model | uptime = uptime }, Cmd.none ) 96 | 97 | GotValidators validators -> 98 | case validators of 99 | Ok allValidators -> 100 | let 101 | validators_ = 102 | List.filter .registered allValidators 103 | in 104 | ( { model 105 | | validators = Success <| addGroups validators_ 106 | , totalStake = 107 | validators_ 108 | |> List.map .totalDelegatedStake 109 | |> List.take 100 110 | |> bigIntSum 111 | } 112 | , Cmd.none 113 | ) 114 | 115 | Err error -> 116 | ( { model | validators = Failure error }, Cmd.none ) 117 | 118 | GotStakePositions stakePositions -> 119 | case model.stakedTokens of 120 | StakeAmount _ -> 121 | ( model, Cmd.none ) 122 | 123 | WalletAddress address currentStakePositions -> 124 | let 125 | newStakePositions = 126 | case stakePositions of 127 | Ok stakePositions_ -> 128 | Success stakePositions_ 129 | 130 | Err error -> 131 | Failure error 132 | in 133 | ( { model 134 | | stakedTokens = WalletAddress address newStakePositions 135 | } 136 | , Cmd.none 137 | ) 138 | 139 | 140 | 141 | -- VIEW 142 | 143 | 144 | viewHeader : Device -> Model -> Element Msg 145 | viewHeader device model = 146 | column 147 | [ Font.center 148 | , spacing small 149 | , Font.size 48 150 | , paddingXY small 0 151 | , Background.color darkShades 152 | , Font.color white 153 | , Font.extraLight 154 | , paddingXY small 155 | (case device.class of 156 | Phone -> 157 | xLarge 158 | 159 | _ -> 160 | xxLarge 161 | ) 162 | , width fill 163 | , Border.shadow 164 | { offset = ( toFloat xxSmall, toFloat xxSmall ) 165 | , size = 0 166 | , blur = toFloat small 167 | , color = blackAlpha 0.4 168 | } 169 | ] 170 | [ paragraph [] [ text "Stake and maximise your rewards." ] 171 | ] 172 | 173 | 174 | viewBenefit : Device -> Icon Msg -> String -> List (Element Msg) -> Element Msg 175 | viewBenefit device icon_ title content = 176 | let 177 | ( widthBox, paddingBoxX, paddingBoxY ) = 178 | case device.class of 179 | Phone -> 180 | ( fill, small, normal ) 181 | 182 | _ -> 183 | ( minimum 450 fill, normal, normal ) 184 | in 185 | column [ alignTop, width widthBox, spacing xSmall, paddingXY paddingBoxX paddingBoxY ] 186 | [ row [] 187 | [ el [ Font.color mainBrand, width <| px 48 ] <| icon normal icon_ 188 | , el [ Font.size (small + xxSmall) ] <| text title 189 | ] 190 | , row [ width fill ] 191 | [ paragraph [ paddingEach { top = 0, bottom = 0, left = 48, right = 0 }, Font.size small ] 192 | content 193 | ] 194 | ] 195 | 196 | 197 | viewBenefits : Device -> Model -> Element Msg 198 | viewBenefits device model = 199 | let 200 | row_ = 201 | case device.class of 202 | Phone -> 203 | column 204 | 205 | _ -> 206 | wrappedRow 207 | in 208 | column [ spacing normal, width fill ] 209 | [ paragraph [ Font.center, width fill, paddingXY small 0, spacing small ] [ heading "Why stake your Radix with me?" ] 210 | , el [ width <| maximum 1000 fill, centerX ] <| 211 | row_ 212 | [ htmlAttribute <| Html.Attributes.style "justify-content" "center" 213 | ] 214 | [ viewBenefit device cloud_off "Decentralised" [ text "A core principle of decentralised ledgers (DLT) like Radix is decentralisation. That is why my validator is not hosted at common cloud providers but multiple smaller ones." ] 215 | , viewBenefit device paid "Low Fees" [ text "My validator fee is 3.4%. Low fees combined with high uptime ensure that your rewards are maximised. You can calculate your expected APY in my staking calculator." ] 216 | , viewBenefit device language "High availability" [ text "Multiple backup nodes in different data centers allow to maximise uptime. I also developed seamless upgrade and failover scripts to achieve zero maintenance downtime." ] 217 | , viewBenefit device favorite "Commitment" [ text "I want to make Radix a success and put a lot of effort into my validator. Also, I will only be staking on my own node and putting my money where my mouth is." ] 218 | , viewBenefit device notifications_active "Realtime Alerts" [ text "The validator is constantly monitored 24/7 in real time to immediately trigger alerts in case of outtakes to minimise downtime." ] 219 | , viewBenefit device 220 | build 221 | "Experience" 222 | [ text "I have a lot of experience in running production-grade servers as a software developer and also wrote a " 223 | , link [ Font.color cello, Font.semiBold, mouseOver [ Font.color mainBrand ] ] 224 | { url = "https://github.com/fpieper/fpstaking/blob/main/docs/validator_guide.md" 225 | , label = text "validator guide" 226 | } 227 | , text " to help other node-runners configuring their validator in a secure way." 228 | ] 229 | , viewBenefit device security "Secure" [ text "The validator and failover node are hardened with best security practices to minimise attack vectors and are protected against DDOS attacks." ] 230 | , viewBenefit device face "Transparent" [ text "Since I stumbled across Radix I had a lot of fun researching possible competitors and therefore am quite active and known in the community." ] 231 | ] 232 | ] 233 | 234 | 235 | viewStakingCalculator : Device -> Model -> Element Msg 236 | viewStakingCalculator device model = 237 | column 238 | [ centerX 239 | , spacing normal 240 | , padding 241 | (case device.class of 242 | Phone -> 243 | small 244 | 245 | _ -> 246 | large 247 | ) 248 | , Background.color mainBrand 249 | , Font.color darkShades 250 | , Border.rounded 5 251 | , Border.shadow 252 | { offset = ( toFloat xxSmall, toFloat xxSmall ) 253 | , size = 0 254 | , blur = toFloat xSmall 255 | , color = blackAlpha 0.1 256 | } 257 | ] 258 | [ paragraph [ Font.center ] 259 | [ subHeading "How much staking rewards do I earn?" 260 | ] 261 | , Input.text 262 | ((case model.stakedTokens of 263 | StakeAmount _ -> 264 | [ inputHint mainBrand <| text "XRD" ] 265 | 266 | _ -> 267 | [] 268 | ) 269 | ++ [ Background.color mainBrand 270 | , Border.color darkShades 271 | , Font.semiBold 272 | ] 273 | ) 274 | { onChange = TokensStakedChanged 275 | , text = 276 | model.stakedTokensRaw 277 | , placeholder = Nothing 278 | , label = Input.labelAbove [] <| text "Staked Tokens / Wallet Address" 279 | } 280 | , Input.slider 281 | sliderStyle 282 | { onChange = ValidatorFeeChanged 283 | , label = 284 | Input.labelAbove [] <| 285 | row [ width fill ] 286 | [ text "Validator Fee" 287 | , el [ Font.color darkShades, Font.semiBold, alignRight ] <| 288 | text <| 289 | formatWithDecimals 1 model.validatorFee 290 | ++ "%" 291 | ] 292 | , min = 0 293 | , max = 20 294 | , step = Just 0.1 295 | , value = model.validatorFee 296 | , thumb = 297 | thumb 298 | } 299 | , Input.slider 300 | sliderStyle 301 | { onChange = UptimeChanged 302 | , label = 303 | Input.labelAbove [] <| 304 | row [ width fill ] 305 | [ text "Uptime" 306 | , el [ Font.color darkShades, Font.semiBold, alignRight ] <| 307 | text <| 308 | formatWithDecimals 2 model.uptime 309 | ++ "%" 310 | ] 311 | , min = 98 312 | , max = 100 313 | , step = Just 0.02 314 | , value = model.uptime 315 | , thumb = 316 | thumb 317 | } 318 | , let 319 | tokensStaked = 320 | case model.stakedTokens of 321 | StakeAmount amount -> 322 | amount 323 | 324 | WalletAddress _ (Success positions) -> 325 | positions 326 | |> List.map (\p -> p.amount |> toXRD |> BigInt.toString |> String.toInt |> Maybe.withDefault 0) 327 | |> List.sum 328 | 329 | _ -> 330 | 0 331 | 332 | totalStaked = 333 | toXRD model.totalStake 334 | 335 | stakingShare = 336 | bigIntDivToFloat (BigInt.fromInt tokensStaked) totalStaked * 100 337 | 338 | feeFactor = 339 | 1 - (model.validatorFee / 100) 340 | 341 | uptimeFactor = 342 | ((model.uptime / 100) - 0.98) / 0.02 343 | 344 | stakingRewardsYearly = 345 | (stakingShare / 100) * 300000000 * feeFactor * uptimeFactor 346 | 347 | stakingRewardsMonthly = 348 | stakingRewardsYearly / 12 349 | 350 | stakingRewardsDaily = 351 | stakingRewardsYearly / 365 352 | 353 | apy = 354 | if tokensStaked == 0 then 355 | 0 356 | 357 | else 358 | (stakingRewardsYearly / toFloat tokensStaked) * 100 359 | in 360 | case model.stakedTokens of 361 | WalletAddress _ Loading -> 362 | el [ centerX, centerY ] <| 363 | html <| 364 | Loading.render 365 | DoubleBounce 366 | { defaultConfig | color = "#2E294E", size = toFloat large, speed = 1 } 367 | Loading.On 368 | 369 | _ -> 370 | row [ width fill, spacing normal ] 371 | [ viewFactTable [ width fill, spacing small ] 372 | [] 373 | [ { key = text "Total Staked" 374 | , value = el [ alignRight ] <| text <| formatWithDecimals 2 (bigIntDivToFloat totalStaked (safeBigInt "1000000000")) ++ "B XRD" 375 | } 376 | , { key = text "Staked" 377 | , value = el [ alignRight ] <| text <| String.fromInt tokensStaked ++ " XRD" 378 | } 379 | , { key = text "Rewards (yearly)" 380 | , value = el [ alignRight, Font.semiBold ] <| text <| String.fromInt (round stakingRewardsYearly) ++ " XRD" 381 | } 382 | , { key = text "Rewards (monthly)" 383 | , value = el [ alignRight, Font.semiBold ] <| text <| String.fromInt (round stakingRewardsMonthly) ++ " XRD" 384 | } 385 | , { key = text "Rewards (daily)" 386 | , value = el [ alignRight, Font.semiBold ] <| text <| String.fromInt (round stakingRewardsDaily) ++ " XRD" 387 | } 388 | , { key = text "APY" 389 | , value = el [ alignRight, Font.semiBold ] <| text <| formatWithDecimals 2 apy ++ " %" 390 | } 391 | ] 392 | ] 393 | ] 394 | 395 | 396 | viewUptime : Model -> Element Msg 397 | viewUptime model = 398 | let 399 | validator : Maybe Validator 400 | validator = 401 | case model.validators of 402 | Success validators -> 403 | List.filter (\v -> v.address == "rv1qfxktwkq9amdh678cxfynzt4zeua2tkh8nnrtcjpt7fyl0lmu8r3urllukm") validators |> List.head 404 | 405 | _ -> 406 | Nothing 407 | in 408 | column [ centerX, spacing normal ] 409 | [ el [ centerX ] <| heading "Uptime" 410 | , el [ centerX ] <| 411 | text <| 412 | case model.validators of 413 | Success validators -> 414 | validators 415 | |> List.filter (\v -> v.address == "rv1qfxktwkq9amdh678cxfynzt4zeua2tkh8nnrtcjpt7fyl0lmu8r3urllukm") 416 | |> List.head 417 | |> Maybe.map (\v -> formatWithDecimals 2 v.uptimePercentage ++ "%") 418 | |> Maybe.withDefault "" 419 | 420 | _ -> 421 | "" 422 | ] 423 | 424 | 425 | viewValidators : Element Msg 426 | viewValidators = 427 | column [ centerX, spacing normal ] 428 | [ el [ centerX, Font.center ] <| heading "Validators" 429 | , link 430 | [ centerX 431 | , Background.color darkShades 432 | , padding small 433 | , Font.color white 434 | , mouseOver [ alpha 0.95 ] 435 | , Border.rounded 5 436 | , Border.shadow 437 | { offset = ( toFloat xxSmall, toFloat xxSmall ) 438 | , size = 0 439 | , blur = toFloat small 440 | , color = blackAlpha 0.4 441 | } 442 | ] 443 | { url = "/validators" 444 | , label = text "Show All Radix Validators" 445 | } 446 | ] 447 | 448 | 449 | viewValidatorAddress : Element Msg 450 | viewValidatorAddress = 451 | column [ centerX, spacing normal ] 452 | [ el [ centerX, Font.center ] <| heading "Validator Address" 453 | , paragraph 454 | [ centerX 455 | , Border.color mainBrand 456 | , Border.width 3 457 | , Border.rounded 5 458 | , padding normal 459 | , width fill 460 | , mouseOver [ Background.color white ] 461 | , htmlAttribute <| Html.Attributes.style "word-break" "break-all" 462 | ] 463 | [ text "rv1qfxktwkq9amdh678cxfynzt4zeua2tkh8nnrtcjpt7fyl0lmu8r3urllukm" 464 | ] 465 | 466 | --} 467 | ] 468 | 469 | 470 | view : Device -> Model -> Element Msg 471 | view device model = 472 | column [ width fill, spacing xLarge, Font.color darkShades, paddingXY 0 0 ] 473 | [ viewHeader device model 474 | , viewBenefits device model 475 | , viewValidatorAddress 476 | , viewStakingCalculator device model 477 | , viewUptime model 478 | , viewValidators 479 | , viewContact 480 | , viewFooter 481 | ] 482 | -------------------------------------------------------------------------------- /frontend/src/Page/Validators.elm: -------------------------------------------------------------------------------- 1 | module Page.Validators exposing (..) 2 | 3 | import BigInt exposing (BigInt) 4 | import Color.Interpolate exposing (interpolate) 5 | import Dict exposing (Dict) 6 | import Element exposing (..) 7 | import Element.Background as Background 8 | import Element.Border as Border 9 | import Element.Font as Font 10 | import Element.Input as Input 11 | import FormatNumber 12 | import FormatNumber.Locales exposing (Decimals(..), usLocale) 13 | import GatewayApi exposing (Group, Validator, getValidatorsRequest) 14 | import Html exposing (Html) 15 | import Html.Attributes 16 | import Http exposing (emptyBody, jsonBody) 17 | import Json.Decode as Decode exposing (Decoder, andThen, bool, fail, field, int, list, string, succeed) 18 | import Json.Decode.Pipeline exposing (hardcoded, optional, required) 19 | import Json.Encode as Encode 20 | import Loading exposing (Config, LoaderType(..), defaultConfig) 21 | import Material.Icons exposing (check_circle, content_copy, dangerous, remove_circle, sentiment_neutral, sentiment_very_dissatisfied, sentiment_very_satisfied, warning) 22 | import Material.Icons.Outlined exposing (build, cloud_off, face, favorite, language, notifications_active, paid, security) 23 | import Material.Icons.Types exposing (Coloring) 24 | import Palette exposing (..) 25 | import RemoteData exposing (RemoteData(..)) 26 | import UI exposing (Icon, fromUiColor, heading, icon, inputHint, sliderStyle, subHeading, thumb, toUiColor, viewContact, viewFactTable, viewFooter) 27 | import Utils exposing (bigIntDivToFloat, bigIntMulFloat, bigIntSum, formatWithDecimals, safeBigInt, toXRD) 28 | 29 | 30 | 31 | -- MODEL 32 | 33 | 34 | type alias Model = 35 | { validators : RemoteData Http.Error (List Validator) 36 | , totalStake : BigInt 37 | , tokensStaked : Int 38 | , validatorFee : Float 39 | , uptime : Float 40 | } 41 | 42 | 43 | type alias GroupFull = 44 | { name : String 45 | , validators : List Validator 46 | , totalStake : BigInt 47 | } 48 | 49 | 50 | addGroups : List Validator -> List Validator 51 | addGroups validators = 52 | let 53 | validatorsByAddress : Dict String Validator 54 | validatorsByAddress = 55 | validators 56 | |> List.map (\v -> ( v.address, v )) 57 | |> Dict.fromList 58 | 59 | getValidators : List String -> List Validator 60 | getValidators addresses = 61 | addresses 62 | |> List.filterMap 63 | (\address -> Dict.get address validatorsByAddress) 64 | 65 | buildGroup : String -> List String -> GroupFull 66 | buildGroup name addresses = 67 | let 68 | groupValidators = 69 | getValidators addresses 70 | in 71 | { name = name 72 | , validators = groupValidators 73 | , totalStake = List.map .totalDelegatedStake groupValidators |> bigIntSum 74 | } 75 | 76 | groups = 77 | [ buildGroup "Artistizen" 78 | [ "rv1qdawnqw6l9dmsx287gvw3nl7tndx8agh9e0hmw24zdxnpdfp267exk8ljwu" 79 | , "rv1qtztn2t7smu8nn3gk9cnh9cl66ju7z9kvmatpqyx5h295v4304xmwnjf43n" 80 | , "rv1qwe3k036lrj79s06e8cx2kzcsrjmenxgug6d045jzer29sur3cfyv4r0a3t" 81 | 82 | --, "rv1qggt5w4g800k5w3du63g0j86u8ayaqkzdgyxfkw3uc93826zxef22gjr64m" 83 | --, "rv1q0m9329x6tywt3kggatrtr8ut8qvxqw9lsfw2w8phrvvv2qydw9xsk88slq" 84 | --, "rv1q0xq9jg3vcuvelflpdvkcvzvgtsd6k0tprf5u734nnl4xq06hecajc40rqw" 85 | ] 86 | , buildGroup "CaviarNine" 87 | [ "rv1qdfhzmygv2vmxuc4702pttrpkep0vkc06a64zlenmvujn2yvq2u3y93e8ky" 88 | , "rv1q29pg6kl80m43h0mewh8w8zfnhnpg50e3plwaexqx29vq2savjnzkdn89kp" 89 | ] 90 | 91 | {--, buildGroup "✅RadCrew" 92 | [ "rv1qgaftlzpdxaacv3jmf0x9vlys3ap0mngfea8gsedph4ckaya0gqe6h9mv6f" 93 | , "rv1qf53n265drur37lkqcun5sa8j0h0aqpvpuxh3r2nz7xzdskvwcy9zkpyh42" 94 | ]--} 95 | , buildGroup "ITS Australia and Ideomaker" 96 | [ "rv1qgk7asalvem6y06asnxwt8rgx05gvl9vzrldnkshvat3uysxmkx9gmkv7y2" 97 | , "rv1qg923hl7f725cs06eg5gmdcy4pfvpa8mfnjcw669traquj28c96z5w0rld0" 98 | ] 99 | 100 | {--, buildGroup "🔗 Radixnode.io" 101 | [ "rv1qwqvrexkz0qdgve9jxk4c8s35n9n4wucgm0m8pahgqyc2py6raf7g9uf9j7" 102 | , "rv1q0lskvu7awu4rgzju3pth6a7eqk3zazrnlqnxwqtehr7td4he2zfck5yruz" 103 | ]--} 104 | , buildGroup "RadixPool" 105 | [ "rv1q04u5zwtgffsqkvr08xqm6vpm3gwxh4uqwtjpx5p47ew0m0v8m5zs3m3jed" 106 | , "rv1q27pjz9zf4f37df493xzx6hgattjj6qdyn255u8a58eeyac7lg495xklqu7" 107 | ] 108 | , buildGroup "SKY" 109 | [ "rv1q2fj02guaut2k0fvxjngs77vlfr9mfrk7vmj6kvf22kwq7cww854jgjkhvf" 110 | , "rv1qfgl9cqd8cr5df54fahlkw9epxvmll0yvucndvt0ytt0t9nrmvs3zqeu75q" 111 | , "rv1q00l6tamghj5jzq6rzj7rh9yjyeaks2d207jnqm5d4lsgzutqte05d3kjv3" 112 | , "rv1qdwduz6jf7eghgmn7n7axtq54yrr0q4zttww2jghk8p8a6e4u2p0w4jnkvt" 113 | ] 114 | , buildGroup "StakeSafe" 115 | [ "rv1q0v80rfgsldx3zksfzurumdf5g3us8xa9sykf3fdevtrr557lgrmjv2cft5" 116 | , "rv1qdxg4r7ulqustdus02k8egkenfwknlvxs4hm2ynemp5knzykgwh2xfa53at" 117 | , "rv1qwsg60y9h6c0t0n93z70053jseygtd8n6ueg3tr7wn8krxv60fexcsnh0zq" 118 | ] 119 | , buildGroup "RadixFoundation" 120 | [ "rv1qgxn3eeldj33kd98ha6wkjgk4k77z6xm0dv7mwnrkefknjcqsvhuu30c6hh" 121 | , "rv1qwrrnhzfu99fg3yqgk3ut9vev2pdssv7hxhff80msjmmcj968487uc0c0nc" 122 | , "rv1qf2x63qx4jdaxj83kkw2yytehvvmu6r2xll5gcp6c9rancmrfsgfwttnczx" 123 | , "rv1qt2zy2cuf3ssx25zspkzg29qzlwf3tcdhud3908y8x5veytgv4ykwh9w0ty" 124 | , "rv1q0zdrxc7u6e296yjptd4yqdl3m9g5nk4zk97ta46rt2fuye05y38vcqdxul" 125 | , "rv1q0gnmwv0fmcp7ecq0znff7yzrt7ggwrp47sa9pssgyvrnl75tvxmvke8uxe" 126 | , "rv1q29j87g5s05vf8l7ele9543r6n4sda0kj548jnncx6jqscrl40vw6m3s805" 127 | , "rv1qt3t0tezvfqdyjzkyepmjh3rvp5mu3lfa824rq6kl3j2xlh9ptkq504xyuq" 128 | , "rv1qfhdl4zcu3tntaa9dpa4gmndcggrw9a97ee485sswstzxx9w7qsrqvp8j2n" 129 | , "rv1q0fnnp2ncmtkyyz4fz6q69x3hpnu95r8jndzh6zyxh0kr98v8t5fw6w4gj3" 130 | , "rv1qgzea7fs4f9jxj3s55uw98tsvujzef6uuwjxptea4zya2yywkgcm2m772ts" 131 | , "rv1q25v040ejdlwxy5lu4688la66w8l0kqh383psyaadqp58jmsz75wgqgk0j3" 132 | , "rv1q0c0frnkguvsynghlw84f82nfh82mpr6d2fxamlcn4pcpz8zsgcrcquhlk3" 133 | , "rv1qt22v09sl9ptdujtz8tz3g3dc4m93439jyq6jf2jwf5ea668r2ycvyuv7z5" 134 | , "rv1q0k9hxla6phcamc7laxa0juawg9t9vtspx6ux2s0nal07x8uk5ylzye7lhd" 135 | , "rv1qwxf0ytqa8damm3vr2jgq04nejev043xy509q0vkrhdhchs7n79fvewxsvv" 136 | , "rv1qfyk8r20jpwxjmzkvkkxu247vmffr4dl3vkqj9gsmcpp5f6mxpgxcscwj6m" 137 | , "rv1qgvahu9vl6fslx8m6stdherjkvztqxm60fhmctjl0ueqfhledd5ds8pnyty" 138 | , "rv1qdakpg9s90sha6z9q7rug43ucdyc82aqw6hlxcfr2t8rrse589se26mf90m" 139 | , "rv1qwp64k6s05kcl0c86cumezyeh4twzy7eduwz4enf35udhfhfvelkseawyu2" 140 | , "rv1qft2plmngnkn6f2xp2zcl944twzyylh4s50aa2q2n6wh37gnqtfwyz9as5w" 141 | , "rv1qdpavrvzvrsljlh7u7mxg3zqszcya9yrpxk9d77grhtm4cxgwhlh69a2hkm" 142 | , "rv1qgdue83wgezwrdsn2ngqnqpa74euykulngsqxernzkzcspkpula4kle75q6" 143 | , "rv1qgcw6z26qr3mslfjkz82s7qtmgqnugq9amsnl8jwqzxhtrax4mqk7qsl6vu" 144 | ] 145 | , buildGroup "AMR Node 🇷🇺 and RadStaking 🇷🇺" 146 | [ "rv1q03txhtq9d4v79len5jk65hzecgzdwqr94cu9pqd3v0r8dp923r9z37n7hw" 147 | , "rv1qf3hq39mnxx6ln5yfy5gmgnm2vuxvpjxm8rhymqahq09n4vntyk77a5nfre" 148 | ] 149 | , buildGroup "DogeCube and RadixDLT Staking" 150 | [ "rv1qgw68kqkryhgxvcvp04wfss0k76svxkqv3zvf57rcvrkuzdluu9ay4snzxc" 151 | , "rv1qd9wlc66dzssnkzwja2mrnxsdezzkmxg00xqzrkn4039zpghaj0rs43mz63" 152 | ] 153 | , buildGroup "XRDScan" 154 | [ "rv1q250v8v6a7s6594tlvv6ndkk880dc3mgxaqssa4jeet890qczty9vesjmvn" 155 | , "rv1qw00edymt8x7ch63s4ukyncgq6ggr2krsdlyg6rz5g6zgt4a4qyacl2a5hy" 156 | ] 157 | ] 158 | 159 | validatorGroup : Dict String GroupFull 160 | validatorGroup = 161 | groups 162 | |> List.map (\group -> List.map (\v -> ( v.address, group )) group.validators) 163 | |> List.concat 164 | |> Dict.fromList 165 | 166 | totalStake : BigInt 167 | totalStake = 168 | validators 169 | |> List.map .totalDelegatedStake 170 | |> bigIntSum 171 | in 172 | validators 173 | |> List.map 174 | (\v -> 175 | { v 176 | | group = 177 | Dict.get v.address validatorGroup 178 | |> Maybe.map 179 | (\groupFull -> 180 | Group groupFull.name 181 | groupFull.totalStake 182 | (bigIntDivToFloat groupFull.totalStake totalStake) 183 | ) 184 | , stakeShare = bigIntDivToFloat v.totalDelegatedStake totalStake 185 | } 186 | ) 187 | |> List.sortWith (\a b -> BigInt.compare a.totalDelegatedStake b.totalDelegatedStake) 188 | |> List.reverse 189 | |> List.indexedMap (\index v -> { v | rank = index + 1 }) 190 | 191 | 192 | 193 | -- INIT 194 | 195 | 196 | init : ( Model, Cmd Msg ) 197 | init = 198 | ( { validators = Loading 199 | , totalStake = BigInt.fromInt 0 200 | , tokensStaked = 0 201 | , validatorFee = 4 202 | , uptime = 100 203 | } 204 | , getValidatorsRequest GotValidators 205 | ) 206 | 207 | 208 | 209 | -- UPDATE 210 | 211 | 212 | type Msg 213 | = TokensStakedChanged String 214 | | ValidatorFeeChanged Float 215 | | UptimeChanged Float 216 | | GotValidators (Result Http.Error (List Validator)) 217 | 218 | 219 | update : Msg -> Model -> ( Model, Cmd Msg ) 220 | update msg model = 221 | case msg of 222 | TokensStakedChanged tokens -> 223 | ( if String.length tokens == 0 then 224 | { model | tokensStaked = 0 } 225 | 226 | else 227 | case String.toInt tokens of 228 | Just t -> 229 | { model | tokensStaked = t } 230 | 231 | Nothing -> 232 | model 233 | , Cmd.none 234 | ) 235 | 236 | ValidatorFeeChanged fee -> 237 | ( { model | validatorFee = fee }, Cmd.none ) 238 | 239 | UptimeChanged uptime -> 240 | ( { model | uptime = uptime }, Cmd.none ) 241 | 242 | GotValidators validators -> 243 | case validators of 244 | Ok allValidators -> 245 | let 246 | validators_ = 247 | List.filter .registered allValidators 248 | in 249 | ( { model 250 | | validators = Success <| addGroups validators_ 251 | , totalStake = 252 | validators_ 253 | |> List.map .totalDelegatedStake 254 | |> List.take 100 255 | |> bigIntSum 256 | } 257 | , Cmd.none 258 | ) 259 | 260 | Err error -> 261 | ( { model | validators = Failure error }, Cmd.none ) 262 | 263 | 264 | 265 | -- VIEW 266 | 267 | 268 | viewHeader : Device -> Model -> Element Msg 269 | viewHeader device model = 270 | column 271 | [ Font.center 272 | , spacing small 273 | , Font.size normal 274 | , paddingXY small 0 275 | , Background.color darkShades 276 | , Font.color white 277 | , Font.extraLight 278 | , paddingXY 0 279 | (case device.class of 280 | Phone -> 281 | small 282 | 283 | _ -> 284 | normal 285 | ) 286 | , width fill 287 | , Border.shadow 288 | { offset = ( toFloat xxSmall, toFloat xxSmall ) 289 | , size = 0 290 | , blur = toFloat small 291 | , color = blackAlpha 0.4 292 | } 293 | ] 294 | [ paragraph [] [ text "Radix Validators" ] 295 | ] 296 | 297 | 298 | viewStakingCalculator : Device -> Model -> Element Msg 299 | viewStakingCalculator device model = 300 | column 301 | [ centerX 302 | , spacing normal 303 | , padding 304 | (case device.class of 305 | Phone -> 306 | small 307 | 308 | _ -> 309 | large 310 | ) 311 | , Background.color mainBrand 312 | , Font.color darkShades 313 | , Border.rounded 5 314 | , Border.shadow 315 | { offset = ( toFloat xxSmall, toFloat xxSmall ) 316 | , size = 0 317 | , blur = toFloat xSmall 318 | , color = blackAlpha 0.1 319 | } 320 | ] 321 | [ paragraph [ Font.center ] 322 | [ subHeading "How much staking rewards do I earn?" 323 | ] 324 | , Input.text 325 | [ inputHint mainBrand <| text "XRD" 326 | , Background.color mainBrand 327 | , Border.color darkShades 328 | , Font.semiBold 329 | ] 330 | { onChange = TokensStakedChanged 331 | , text = 332 | String.fromInt model.tokensStaked 333 | |> (\t -> 334 | if t == "0" then 335 | "" 336 | 337 | else 338 | t 339 | ) 340 | , placeholder = Nothing 341 | , label = Input.labelAbove [] <| text "Staked Tokens / Wallet Address" 342 | } 343 | , Input.slider 344 | sliderStyle 345 | { onChange = ValidatorFeeChanged 346 | , label = 347 | Input.labelAbove [] <| 348 | row [ width fill ] 349 | [ text "Validator Fee" 350 | , el [ Font.color darkShades, Font.semiBold, alignRight ] <| 351 | text <| 352 | formatWithDecimals 1 model.validatorFee 353 | ++ "%" 354 | ] 355 | , min = 0 356 | , max = 20 357 | , step = Just 0.1 358 | , value = model.validatorFee 359 | , thumb = 360 | thumb 361 | } 362 | , Input.slider 363 | sliderStyle 364 | { onChange = UptimeChanged 365 | , label = 366 | Input.labelAbove [] <| 367 | row [ width fill ] 368 | [ text "Uptime" 369 | , el [ Font.color darkShades, Font.semiBold, alignRight ] <| 370 | text <| 371 | formatWithDecimals 2 model.uptime 372 | ++ "%" 373 | ] 374 | , min = 98 375 | , max = 100 376 | , step = Just 0.02 377 | , value = model.uptime 378 | , thumb = 379 | thumb 380 | } 381 | , let 382 | totalStaked = 383 | toXRD model.totalStake 384 | 385 | stakingShare = 386 | bigIntDivToFloat (BigInt.fromInt model.tokensStaked) totalStaked * 100 387 | 388 | feeFactor = 389 | 1 - (model.validatorFee / 100) 390 | 391 | uptimeFactor = 392 | ((model.uptime / 100) - 0.98) / 0.02 393 | 394 | stackingRewards = 395 | (stakingShare / 100) * 300000000 * feeFactor * uptimeFactor 396 | 397 | apy = 398 | if model.tokensStaked == 0 then 399 | 0 400 | 401 | else 402 | (stackingRewards / toFloat model.tokensStaked) * 100 403 | in 404 | row [ width fill, spacing normal ] 405 | [ viewFactTable [ width fill, spacing small ] 406 | [] 407 | [ { key = text "Total Staked" 408 | , value = el [ alignRight ] <| text <| formatWithDecimals 3 (bigIntDivToFloat totalStaked (safeBigInt "1000000000")) ++ "B XRD" 409 | } 410 | , { key = text "Rewards" 411 | , value = el [ alignRight, Font.semiBold ] <| text <| String.fromInt (round stackingRewards) ++ " XRD" 412 | } 413 | , { key = text "APY" 414 | , value = el [ alignRight, Font.semiBold ] <| text <| formatWithDecimals 2 apy ++ " %" 415 | } 416 | ] 417 | ] 418 | ] 419 | 420 | 421 | formatStake : BigInt -> String 422 | formatStake stake = 423 | stake 424 | |> toXRD 425 | |> BigInt.toString 426 | |> String.toFloat 427 | |> Maybe.withDefault 0 428 | |> FormatNumber.format { usLocale | decimals = Exact 0 } 429 | 430 | 431 | formatPercentage : Float -> String 432 | formatPercentage percentage = 433 | formatWithDecimals 2 (percentage * 100) ++ "%" 434 | 435 | 436 | nodeRunnerStake : Validator -> BigInt 437 | nodeRunnerStake validator = 438 | case validator.group of 439 | Nothing -> 440 | validator.totalDelegatedStake 441 | 442 | Just group -> 443 | group.totalStake 444 | 445 | 446 | shortAddress : String -> String 447 | shortAddress address = 448 | String.left 2 address 449 | ++ "…" 450 | ++ String.right 7 address 451 | 452 | 453 | type SortOrder 454 | = Ascending 455 | | Descending 456 | 457 | 458 | isOwnValidator : Validator -> Bool 459 | isOwnValidator validator = 460 | validator.address == "rv1qfxktwkq9amdh678cxfynzt4zeua2tkh8nnrtcjpt7fyl0lmu8r3urllukm" 461 | 462 | 463 | viewValidators : Device -> List Validator -> Color -> SortOrder -> Bool -> Element Msg 464 | viewValidators device validators zoneColor sortOrder combine = 465 | let 466 | sortedValidators = 467 | List.sortWith 468 | (\a b -> 469 | case 470 | if combine then 471 | BigInt.compare (nodeRunnerStake a) (nodeRunnerStake b) 472 | 473 | else 474 | BigInt.compare a.totalDelegatedStake b.totalDelegatedStake 475 | of 476 | LT -> 477 | if sortOrder == Ascending then 478 | LT 479 | 480 | else 481 | GT 482 | 483 | EQ -> 484 | EQ 485 | 486 | GT -> 487 | if sortOrder == Ascending then 488 | GT 489 | 490 | else 491 | LT 492 | ) 493 | validators 494 | 495 | cellPadding = 496 | paddingXY small small 497 | 498 | headerCell : List (Attribute msg) -> Element msg -> Element msg 499 | headerCell attributes content = 500 | el 501 | ([ Font.medium 502 | , cellPadding 503 | , Font.color <| whiteAlpha 0.9 504 | , Background.color zoneColor 505 | ] 506 | ++ attributes 507 | ) 508 | <| 509 | content 510 | 511 | stakeCell : BigInt -> Float -> Element Msg 512 | stakeCell stake stakeShare = 513 | row 514 | [ cellPadding 515 | , Font.alignRight 516 | , alignRight 517 | , centerX 518 | , centerY 519 | ] 520 | [ el [ alignRight ] <| 521 | text <| 522 | formatStake stake 523 | ++ " / " 524 | , el [ alignRight, Font.extraBold, Font.color zoneColor ] <| text <| formatPercentage stakeShare ++ "" 525 | ] 526 | in 527 | Element.indexedTable [ width <| maximum 1400 fill, centerX, Font.size 14 ] 528 | { data = sortedValidators 529 | , columns = 530 | [ { header = headerCell [] <| text "Rank" 531 | , width = shrink 532 | , view = 533 | \index validator -> 534 | el [ cellPadding, centerX, centerY ] <| text <| "#" ++ String.fromInt validator.rank 535 | } 536 | , { header = headerCell [] <| text "Validator" 537 | , width = fill 538 | , view = 539 | \index validator -> 540 | let 541 | trimmedName = 542 | if String.length validator.name > 25 then 543 | String.left 25 validator.name ++ "…" 544 | 545 | else 546 | validator.name 547 | in 548 | if String.isEmpty <| String.trim <| validator.infoUrl then 549 | el [ cellPadding, Font.medium, centerX, centerY ] <| text trimmedName 550 | 551 | else 552 | let 553 | style = 554 | if isOwnValidator validator then 555 | [ cellPadding 556 | , Font.medium 557 | , centerX 558 | , centerY 559 | , Background.color darkShades 560 | , padding small 561 | , Font.color white 562 | , mouseOver [ alpha 0.95 ] 563 | , Border.rounded 5 564 | , Border.shadow 565 | { offset = ( toFloat xxSmall, toFloat xxSmall ) 566 | , size = 0 567 | , blur = toFloat small 568 | , color = blackAlpha 0.4 569 | } 570 | ] 571 | 572 | else 573 | [ mouseOver [ Font.color zoneColor ], cellPadding, Font.medium, centerX, centerY ] 574 | in 575 | link style 576 | { url = validator.infoUrl 577 | , label = text trimmedName 578 | } 579 | } 580 | , { header = headerCell [ Font.alignRight ] <| text "Combined Operator Stake" 581 | , width = shrink 582 | , view = 583 | \index validator -> 584 | case validator.group of 585 | Nothing -> 586 | if combine then 587 | stakeCell validator.totalDelegatedStake validator.stakeShare 588 | 589 | else 590 | el [ cellPadding, Font.alignRight, centerX, centerY ] <| text "= validator" 591 | 592 | Just group -> 593 | stakeCell group.totalStake group.stakeShare 594 | } 595 | , { header = headerCell [ Font.alignRight ] <| text "Validator Stake" 596 | , width = shrink 597 | , view = 598 | \index validator -> 599 | case validator.group of 600 | Nothing -> 601 | if combine then 602 | el [ cellPadding, Font.alignRight, centerX, centerY ] <| text "= combined" 603 | 604 | else 605 | stakeCell validator.totalDelegatedStake validator.stakeShare 606 | 607 | Just group -> 608 | stakeCell validator.totalDelegatedStake validator.stakeShare 609 | } 610 | , { header = headerCell [ Font.alignRight ] <| text "Owner Stake" 611 | , width = shrink 612 | , view = 613 | \index validator -> 614 | el 615 | [ cellPadding 616 | , Font.alignRight 617 | , alignRight 618 | , centerX 619 | , centerY 620 | ] 621 | <| 622 | text <| 623 | formatStake validator.ownerDelegation 624 | } 625 | , { header = headerCell [ Font.alignRight ] <| text "Uptime" 626 | , width = shrink 627 | , view = 628 | \index validator -> 629 | el [ cellPadding, Font.alignRight, centerX, centerY ] <| 630 | text <| 631 | formatWithDecimals 2 validator.uptimePercentage 632 | ++ "%" 633 | } 634 | 635 | {--, { header = headerCell [ Font.alignRight ] <| text "Proposals Completed" 636 | , width = shrink 637 | , view = 638 | \index validator -> 639 | el [ cellPadding, Font.alignRight, centerX, centerY ] <| 640 | text <| 641 | String.fromInt validator.proposalCompleted 642 | } 643 | , { header = headerCell [ Font.alignRight ] <| text "Proposals Missed" 644 | , width = shrink 645 | , view = 646 | \index validator -> 647 | el [ cellPadding, Font.alignRight, centerX, centerY ] <| 648 | text <| 649 | String.fromInt validator.proposalsMissed 650 | }--} 651 | , { header = headerCell [ Font.alignRight ] <| text "Fee" 652 | , width = shrink 653 | , view = 654 | \index validator -> 655 | el [ cellPadding, Font.alignRight, centerX, centerY ] <| 656 | text <| 657 | formatWithDecimals 2 validator.fee 658 | ++ "%" 659 | } 660 | 661 | {--, { header = headerCell [ Font.alignRight ] <| text "Yearly Operator Income" 662 | , width = shrink 663 | , view = 664 | \index validator -> 665 | el [ cellPadding, Font.alignRight, centerX, centerY ] <| 666 | text <| 667 | FormatNumber.format { usLocale | decimals = Exact 0 } (validator.stakeShare * validator.fee * 100 * 30000) 668 | }--} 669 | , { header = headerCell [] <| text "Address" 670 | , width = shrink 671 | , view = 672 | \index validator -> 673 | row [ cellPadding, spacing xSmall, centerX, centerY ] 674 | [ text <| 675 | shortAddress validator.address 676 | , Input.button 677 | [ Background.color <| blackAlpha 0.04 678 | , htmlAttribute <| Html.Attributes.class "radix-address" 679 | , htmlAttribute <| Html.Attributes.attribute "data-clipboard-text" validator.address 680 | , Font.color <| blackAlpha 0.5 681 | , padding xSmall 682 | , Border.rounded 5 683 | , mouseOver 684 | [ Background.color <| blackAlpha 0.1 685 | ] 686 | ] 687 | { onPress = Nothing 688 | , label = icon 14 content_copy 689 | } 690 | ] 691 | } 692 | , { header = headerCell [] <| text "Open" 693 | , width = shrink 694 | , view = 695 | \index validator -> 696 | if validator.acceptsExternalStake then 697 | el [ cellPadding, Font.color malachite, centerX, centerY ] <| icon small check_circle 698 | 699 | else 700 | el [ cellPadding, Font.color crimson, centerX, centerY ] <| icon small remove_circle 701 | } 702 | ] 703 | } 704 | 705 | 706 | headingWithIcon : (Int -> Coloring -> Html msg) -> String -> Color -> Element msg 707 | headingWithIcon icon_ label_ color_ = 708 | row [ centerX, spacing small, Font.color color_ ] 709 | [ el [] <| icon 48 icon_ 710 | , el [ Font.size 48, Font.light ] <| text label_ 711 | ] 712 | 713 | 714 | viewValidatorZone : Device -> (Int -> Coloring -> Html Msg) -> String -> Color -> String -> List Validator -> List String -> SortOrder -> Bool -> Element Msg 715 | viewValidatorZone device icon_ headingLabel color_ emptyMessage validators description sortOrder combine = 716 | column [ width fill, spacing normal ] 717 | [ headingWithIcon icon_ headingLabel color_ 718 | , column [ spacing normal, centerX ] <| List.map (\t -> paragraph [ spacing small, centerX, width <| maximum 800 fill, Font.center ] [ text t ]) description 719 | , if List.isEmpty validators then 720 | text emptyMessage 721 | 722 | else 723 | viewValidators device validators color_ sortOrder combine 724 | ] 725 | 726 | 727 | viewValidatorZones : Device -> Model -> Element Msg 728 | viewValidatorZones device model = 729 | case model.validators of 730 | Loading -> 731 | el [ centerX, centerY ] <| 732 | html <| 733 | Loading.render 734 | DoubleBounce 735 | { defaultConfig | color = "#1CE67A", size = toFloat large, speed = 1 } 736 | Loading.On 737 | 738 | -- LoadingState 739 | Success validators -> 740 | let 741 | top100validators = 742 | List.filter (\v -> v.rank <= 100) validators 743 | 744 | beyond100validators = 745 | List.filter (\v -> v.rank > 100) validators 746 | in 747 | column [ width fill, spacing xLarge ] 748 | [ column [ width fill, spacing large ] 749 | [ let 750 | safeValidators = 751 | List.filter 752 | (\validator -> 753 | case validator.group of 754 | Just group -> 755 | group.stakeShare < 0.02 756 | 757 | Nothing -> 758 | validator.stakeShare < 0.02 759 | ) 760 | top100validators 761 | in 762 | viewValidatorZone device 763 | sentiment_very_satisfied 764 | "SAFE ZONE" 765 | malachite 766 | "No validators in safe zone currently." 767 | safeValidators 768 | [ "The validators in the safe zone do not have more than 2% of combined node operator stake and therefore are good candidates for increasing network decentralisation and security." ] 769 | Ascending 770 | True 771 | , let 772 | warningValidators = 773 | List.filter 774 | (\validator -> 775 | case validator.group of 776 | Just group -> 777 | 0.02 <= group.stakeShare && group.stakeShare < 0.03 778 | 779 | Nothing -> 780 | 0.02 <= validator.stakeShare && validator.stakeShare < 0.03 781 | ) 782 | top100validators 783 | in 784 | viewValidatorZone device 785 | sentiment_neutral 786 | "WARNING ZONE" 787 | portlandOrange 788 | "No validators in warning zone currently." 789 | warningValidators 790 | [ "The validators in the warning zone have quite some stake. Maybe better pick another validator of the safe zone." ] 791 | Ascending 792 | True 793 | , let 794 | dangerValidators = 795 | List.filter 796 | (\validator -> 797 | case validator.group of 798 | Just group -> 799 | group.stakeShare >= 0.03 800 | 801 | Nothing -> 802 | validator.stakeShare >= 0.03 803 | ) 804 | top100validators 805 | in 806 | viewValidatorZone device 807 | sentiment_very_dissatisfied 808 | "DANGER ZONE" 809 | crimson 810 | "No validators in danger zone currently." 811 | dangerValidators 812 | [ "The validators in the danger zone have too much stake and stakers should not select them to increase network security." 813 | , "Be warned: Your cat will die otherwise." 814 | ] 815 | Ascending 816 | True 817 | , viewValidatorZone device 818 | sentiment_neutral 819 | "NOT IN NEXT EPOCH" 820 | portlandOrange 821 | "No validators out of top 100 currently." 822 | beyond100validators 823 | [ "The following validators will be not be selected for the next epoch. You will not earn rewards in this case." ] 824 | Descending 825 | False 826 | ] 827 | ] 828 | 829 | Failure err -> 830 | paragraph [] 831 | [ text "Error loading validators!" 832 | ] 833 | 834 | NotAsked -> 835 | none 836 | 837 | 838 | viewStatistics : Model -> Element Msg 839 | viewStatistics model = 840 | case model.validators of 841 | Success _ -> 842 | el [ centerX ] <| text <| "Total Stake: " ++ formatStake model.totalStake ++ " XRD" 843 | 844 | _ -> 845 | none 846 | 847 | 848 | viewCallToAction : Element Msg 849 | viewCallToAction = 850 | link 851 | [ centerX 852 | , Background.color darkShades 853 | , padding small 854 | , Font.color white 855 | , mouseOver [ alpha 0.95 ] 856 | , Border.rounded 5 857 | , Border.shadow 858 | { offset = ( toFloat xxSmall, toFloat xxSmall ) 859 | , size = 0 860 | , blur = toFloat small 861 | , color = blackAlpha 0.4 862 | } 863 | ] 864 | { url = "/" 865 | , label = text "Stake on 🚀 Florian Pieper Staking" 866 | } 867 | 868 | 869 | view : Device -> Model -> Element Msg 870 | view device model = 871 | column [ width fill, spacing xLarge, Font.color darkShades, paddingXY 0 0 ] 872 | [ viewHeader device model 873 | , viewStatistics model 874 | 875 | --, viewStakingCalculator device model 876 | , viewCallToAction 877 | , viewValidatorZones device model 878 | , viewContact 879 | , viewFooter 880 | ] 881 | -------------------------------------------------------------------------------- /frontend/src/Palette.elm: -------------------------------------------------------------------------------- 1 | module Palette exposing (..) 2 | 3 | import Element exposing (Color, rgb, rgb255, rgba, rgba255) 4 | 5 | 6 | 7 | -- SPACING 8 | 9 | 10 | xxxSmall : Int 11 | xxxSmall = 12 | 2 13 | 14 | 15 | xxSmall : Int 16 | xxSmall = 17 | 4 18 | 19 | 20 | xSmall : Int 21 | xSmall = 22 | 8 23 | 24 | 25 | small : Int 26 | small = 27 | 16 28 | 29 | 30 | smallNormal : Int 31 | smallNormal = 32 | 24 33 | 34 | 35 | normal : Int 36 | normal = 37 | 32 38 | 39 | 40 | large : Int 41 | large = 42 | 64 43 | 44 | 45 | xLarge : Int 46 | xLarge = 47 | 128 48 | 49 | 50 | xxLarge : Int 51 | xxLarge = 52 | 256 53 | 54 | 55 | 56 | -- COLORS 57 | 58 | 59 | white : Color 60 | white = 61 | rgb 1 1 1 62 | 63 | 64 | whiteAlpha : Float -> Color 65 | whiteAlpha alpha = 66 | rgba 1 1 1 alpha 67 | 68 | 69 | black : Color 70 | black = 71 | rgb 0 0 0 72 | 73 | 74 | blackAlpha : Float -> Color 75 | blackAlpha alpha = 76 | rgba 0 0 0 alpha 77 | 78 | 79 | transparent : Color 80 | transparent = 81 | rgba 0 0 0 0 82 | 83 | 84 | 85 | -- colormind.io 86 | 87 | 88 | softPeach : Color 89 | softPeach = 90 | rgb255 251 248 249 91 | 92 | 93 | glacier : Color 94 | glacier = 95 | rgb255 127 186 193 96 | 97 | 98 | malachite : Color 99 | malachite = 100 | rgb255 28 230 122 101 | 102 | 103 | copperRust : Color 104 | copperRust = 105 | rgb255 154 88 77 106 | 107 | 108 | cello : Color 109 | cello = 110 | rgb255 31 54 93 111 | 112 | 113 | 114 | -- https://coolors.co/d7263d-f46036-00fb6b-2e294e-1b998b 115 | 116 | 117 | spaceCadet : Color 118 | spaceCadet = 119 | rgb255 46 41 78 120 | 121 | 122 | blueCrayola : Color 123 | blueCrayola = 124 | rgb255 0 117 242 125 | 126 | 127 | crimson : Color 128 | crimson = 129 | rgb255 215 38 61 130 | 131 | 132 | portlandOrange : Color 133 | portlandOrange = 134 | rgb255 244 96 54 135 | 136 | 137 | lightShades : Color 138 | lightShades = 139 | softPeach 140 | 141 | 142 | lightAccent : Color 143 | lightAccent = 144 | glacier 145 | 146 | 147 | mainBrand : Color 148 | mainBrand = 149 | malachite 150 | 151 | 152 | darkAccent : Color 153 | darkAccent = 154 | copperRust 155 | 156 | 157 | darkShades : Color 158 | darkShades = 159 | spaceCadet 160 | -------------------------------------------------------------------------------- /frontend/src/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Ports exposing (..) 2 | 3 | -- PORTS 4 | 5 | 6 | port setMetaDescription : String -> Cmd msg 7 | -------------------------------------------------------------------------------- /frontend/src/UI.elm: -------------------------------------------------------------------------------- 1 | module UI exposing (..) 2 | 3 | import Color as ElmColor 4 | import Element exposing (..) 5 | import Element.Background as Background 6 | import Element.Border as Border 7 | import Element.Font as Font 8 | import Element.Input as Input 9 | import Html 10 | import Material.Icons.Types exposing (Coloring(..)) 11 | import Palette exposing (..) 12 | 13 | 14 | 15 | -- ICONS 16 | 17 | 18 | type alias Icon msg = 19 | Int -> Coloring -> Html.Html msg 20 | 21 | 22 | icon : Int -> Icon msg -> Element msg 23 | icon size name = 24 | Element.html (name size Inherit) 25 | 26 | 27 | 28 | -- COLORS 29 | 30 | 31 | toUiColor : ElmColor.Color -> Color 32 | toUiColor color = 33 | color 34 | |> ElmColor.toRgba 35 | |> (\c -> rgba c.red c.green c.blue c.alpha) 36 | 37 | 38 | fromUiColor : Color -> ElmColor.Color 39 | fromUiColor color = 40 | color 41 | |> toRgb 42 | |> (\c -> ElmColor.fromRgba { red = c.red, green = c.green, blue = c.blue, alpha = c.alpha }) 43 | 44 | 45 | 46 | -- HEADING 47 | 48 | 49 | heading : String -> Element msg 50 | heading heading_ = 51 | paragraph [ Font.size normal ] [ text heading_ ] 52 | 53 | 54 | subHeading : String -> Element msg 55 | subHeading heading_ = 56 | paragraph [ Font.size smallNormal ] [ text heading_ ] 57 | 58 | 59 | 60 | -- INPUT 61 | 62 | 63 | inputHint : Color -> Element a -> Attribute a 64 | inputHint background hint = 65 | inFront <| 66 | el 67 | [ centerY 68 | , alignRight 69 | , Background.color background 70 | , paddingXY xSmall 0 71 | ] 72 | hint 73 | 74 | 75 | 76 | -- TABLE 77 | 78 | 79 | viewFactTable : List (Attribute a) -> List (Attribute a) -> List { f | key : Element a, value : Element a } -> Element a 80 | viewFactTable tableStyles cellStyles data = 81 | Element.table tableStyles 82 | { data = data 83 | , columns = 84 | [ { header = none 85 | , width = shrink 86 | , view = 87 | \i -> 88 | i.key 89 | } 90 | , { header = none 91 | , width = fill 92 | , view = 93 | \i -> 94 | el cellStyles <| i.value 95 | } 96 | ] 97 | } 98 | 99 | 100 | viewContact : Element msg 101 | viewContact = 102 | row [ centerX ] 103 | [ newTabLink [ centerX ] 104 | { url = "https://t.me/florianpieperstaking" 105 | , label = 106 | image 107 | [ width <| px large 108 | , mouseOver 109 | [ alpha 0.9 110 | ] 111 | ] 112 | { src = "images/Telegram_2019_simple_logo.svg", description = "Telegram logo" } 113 | } 114 | ] 115 | 116 | 117 | viewFooter : Element msg 118 | viewFooter = 119 | column 120 | [ centerX 121 | , paddingEach { top = 0, bottom = xLarge, left = normal, right = normal } 122 | , spacing small 123 | ] 124 | [ paragraph [ Font.center ] [ text "Radix Staking powered by Florian Pieper Staking" ] 125 | , el [ centerX ] <| text "2022" 126 | ] 127 | 128 | 129 | 130 | -- SLIDER 131 | 132 | 133 | sliderStyle = 134 | [ Element.height (Element.px 30) 135 | 136 | -- Here is where we're creating/styling the "track" 137 | , Element.behindContent 138 | (Element.el 139 | [ Element.width Element.fill 140 | , Element.height (Element.px 1) 141 | , Element.centerY 142 | , Background.color darkShades 143 | , Border.rounded 0 144 | ] 145 | Element.none 146 | ) 147 | ] 148 | 149 | 150 | thumb : Input.Thumb 151 | thumb = 152 | Input.thumb 153 | [ Element.width (Element.px 16) 154 | , Element.height (Element.px 16) 155 | , Border.rounded 8 156 | , Background.color darkShades 157 | ] 158 | -------------------------------------------------------------------------------- /frontend/src/Utils.elm: -------------------------------------------------------------------------------- 1 | module Utils exposing (..) 2 | 3 | import BigInt exposing (BigInt) 4 | 5 | 6 | roundBy : Int -> Float -> Float 7 | roundBy decimals number = 8 | let 9 | factor = 10 | 10.0 ^ toFloat decimals 11 | in 12 | toFloat (number * factor |> round) / factor 13 | 14 | 15 | formatWithDecimals : Int -> Float -> String 16 | formatWithDecimals decimals number = 17 | let 18 | number_ = 19 | roundBy decimals number 20 | in 21 | case number_ |> String.fromFloat |> String.split "." of 22 | [ n ] -> 23 | n ++ "." ++ String.repeat decimals "0" 24 | 25 | [ n, d ] -> 26 | n ++ "." ++ String.padRight decimals '0' d 27 | 28 | _ -> 29 | String.fromFloat number_ 30 | 31 | 32 | 33 | -- BIGINT 34 | 35 | 36 | safeBigInt : String -> BigInt 37 | safeBigInt input = 38 | input 39 | |> BigInt.fromIntString 40 | |> Maybe.withDefault (BigInt.fromInt 0) 41 | 42 | 43 | bigIntMulFloat : Float -> BigInt -> BigInt 44 | bigIntMulFloat rate number = 45 | BigInt.div 46 | (BigInt.mul (BigInt.fromInt <| round <| rate * 1000000000000) 47 | number 48 | ) 49 | (safeBigInt "1000000000000") 50 | 51 | 52 | bigIntDivToFloat : BigInt -> BigInt -> Float 53 | bigIntDivToFloat a b = 54 | (BigInt.div (BigInt.mul a (safeBigInt "1000000000000")) b 55 | |> BigInt.toString 56 | |> String.toFloat 57 | |> Maybe.withDefault 0 58 | ) 59 | / 1000000000000 60 | 61 | 62 | bigIntSum : List BigInt -> BigInt 63 | bigIntSum numbers = 64 | List.foldl (\fee acc -> BigInt.add fee acc) (BigInt.fromInt 0) numbers 65 | 66 | 67 | 68 | -- XRD 69 | 70 | 71 | toXRD : BigInt -> BigInt 72 | toXRD subUnits = 73 | BigInt.div subUnits (safeBigInt "1000000000000000000") 74 | -------------------------------------------------------------------------------- /frontend/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpieper/fpstaking/ea7138db244059ed6584d1dbcefc250a30947a1e/frontend/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpieper/fpstaking/ea7138db244059ed6584d1dbcefc250a30947a1e/frontend/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpieper/fpstaking/ea7138db244059ed6584d1dbcefc250a30947a1e/frontend/static/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpieper/fpstaking/ea7138db244059ed6584d1dbcefc250a30947a1e/frontend/static/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpieper/fpstaking/ea7138db244059ed6584d1dbcefc250a30947a1e/frontend/static/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpieper/fpstaking/ea7138db244059ed6584d1dbcefc250a30947a1e/frontend/static/favicon.ico -------------------------------------------------------------------------------- /frontend/static/images/Telegram_2019_simple_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 28 | 29 | 30 |
31 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} --------------------------------------------------------------------------------