ovtools 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/assets/common/bond-inspect.sh +368 -0
- package/assets/common/config.sh +263 -0
- package/assets/common/extract-data.sh +197 -0
- package/assets/common/harden-host.sh +37 -0
- package/assets/common/install-deps.sh +93 -0
- package/assets/common/node-enroll.sh +65 -0
- package/assets/common/node-setup.sh +150 -0
- package/assets/common/node-start.sh +90 -0
- package/assets/common/node-stop.sh +18 -0
- package/assets/common/node-upgrade.sh +45 -0
- package/assets/common/pre-run.sh +497 -0
- package/assets/common/promote-validator.sh +104 -0
- package/assets/deploy/10-prepare-host.sh +98 -0
- package/assets/deploy/20-install-toolchain.sh +93 -0
- package/assets/deploy/30-setup-node.sh +82 -0
- package/assets/deploy/40-start-node.sh +75 -0
- package/assets/deploy/50-verify.sh +259 -0
- package/assets/deploy/60-enroll.sh +304 -0
- package/assets/deploy/70-promote.sh +152 -0
- package/assets/deploy/lib.sh +398 -0
- package/assets/manage/10-bond-increase.sh +149 -0
- package/assets/manage/10-diagnose-node.sh +169 -0
- package/assets/manage/20-exit-validator.sh +259 -0
- package/assets/manage/25-recover-node.sh +291 -0
- package/assets/manage/30-status.sh +64 -0
- package/assets/manage/40-exit-withdraw.sh +133 -0
- package/assets/manage/bond_inc.sh +318 -0
- package/assets/manage/dashboard_monitor/app.js +1544 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/base.css +113 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/content.css +133 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/controls.css +229 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/data.css +374 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/icons.css +35 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/motion.css +57 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/shell.css +51 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/sidebar.css +242 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/topbar.css +138 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/fonts.css +30 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/synteq-ui-v2.css +25 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-dark.css +38 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-light.css +36 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/tokens.css +61 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/js/synteq-ui-v2.js +869 -0
- package/assets/manage/dashboard_monitor/deploy-dashboard.sh +230 -0
- package/assets/manage/dashboard_monitor/favicon.svg +13 -0
- package/assets/manage/dashboard_monitor/index.html +1197 -0
- package/assets/manage/dashboard_monitor/nginx-octra.conf +76 -0
- package/assets/manage/dashboard_monitor/octra-dashboard.service +29 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.cron +12 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.service +16 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.timer +12 -0
- package/assets/manage/dashboard_monitor/prune-node-junk.sh +329 -0
- package/assets/manage/dashboard_monitor/server.py +1887 -0
- package/assets/manage/dashboard_monitor/ssl-certbot.sh +90 -0
- package/assets/manage/dashboard_monitor/styles.css +327 -0
- package/assets/manage/lib.sh +347 -0
- package/assets/manifest.json +143 -0
- package/assets/migrate/00-backup-identity.sh +67 -0
- package/assets/migrate/01-prepare-target.sh +145 -0
- package/assets/migrate/02-authorize-pull.sh +43 -0
- package/assets/migrate/03-pull-state.sh +102 -0
- package/assets/migrate/04-stop-source.sh +85 -0
- package/assets/migrate/05-finalize-target.sh +140 -0
- package/assets/migrate/06-start-target.sh +57 -0
- package/assets/migrate/07-verify.sh +237 -0
- package/assets/migrate/08-rollback.sh +54 -0
- package/assets/migrate/lib.sh +149 -0
- package/assets/upgrade/10-preflight.sh +120 -0
- package/assets/upgrade/20-upgrade.sh +142 -0
- package/assets/upgrade/30-manual-upgrade.sh +129 -0
- package/assets/upgrade/40-storage-prune.sh +65 -0
- package/assets/upgrade/50-status.sh +58 -0
- package/assets/upgrade/lib.sh +232 -0
- package/bin/ovtools.js +32 -0
- package/dist/api/checks.d.ts +6 -0
- package/dist/api/checks.d.ts.map +1 -0
- package/dist/api/checks.js +173 -0
- package/dist/api/checks.js.map +1 -0
- package/dist/api/hostkeys.d.ts +10 -0
- package/dist/api/hostkeys.d.ts.map +1 -0
- package/dist/api/hostkeys.js +58 -0
- package/dist/api/hostkeys.js.map +1 -0
- package/dist/api/jobs.d.ts +33 -0
- package/dist/api/jobs.d.ts.map +1 -0
- package/dist/api/jobs.js +483 -0
- package/dist/api/jobs.js.map +1 -0
- package/dist/api/nodes.d.ts +16 -0
- package/dist/api/nodes.d.ts.map +1 -0
- package/dist/api/nodes.js +126 -0
- package/dist/api/nodes.js.map +1 -0
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +443 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +301 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +76 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +150 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/base.d.ts +52 -0
- package/dist/orchestrator/base.d.ts.map +1 -0
- package/dist/orchestrator/base.js +219 -0
- package/dist/orchestrator/base.js.map +1 -0
- package/dist/orchestrator/deploy.d.ts +33 -0
- package/dist/orchestrator/deploy.d.ts.map +1 -0
- package/dist/orchestrator/deploy.js +313 -0
- package/dist/orchestrator/deploy.js.map +1 -0
- package/dist/orchestrator/manage.d.ts +28 -0
- package/dist/orchestrator/manage.d.ts.map +1 -0
- package/dist/orchestrator/manage.js +267 -0
- package/dist/orchestrator/manage.js.map +1 -0
- package/dist/orchestrator/migrate.d.ts +31 -0
- package/dist/orchestrator/migrate.d.ts.map +1 -0
- package/dist/orchestrator/migrate.js +442 -0
- package/dist/orchestrator/migrate.js.map +1 -0
- package/dist/ssh/client.d.ts +39 -0
- package/dist/ssh/client.d.ts.map +1 -0
- package/dist/ssh/client.js +418 -0
- package/dist/ssh/client.js.map +1 -0
- package/dist/test/core.test.d.ts +2 -0
- package/dist/test/core.test.d.ts.map +1 -0
- package/dist/test/core.test.js +188 -0
- package/dist/test/core.test.js.map +1 -0
- package/dist/types.d.ts +267 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/assets.d.ts +28 -0
- package/dist/utils/assets.d.ts.map +1 -0
- package/dist/utils/assets.js +583 -0
- package/dist/utils/assets.js.map +1 -0
- package/dist/utils/browser.d.ts +2 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +23 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/dashboard.d.ts +16 -0
- package/dist/utils/dashboard.d.ts.map +1 -0
- package/dist/utils/dashboard.js +177 -0
- package/dist/utils/dashboard.js.map +1 -0
- package/dist/utils/helpers.d.ts +29 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +204 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/scripts.d.ts +60 -0
- package/dist/utils/scripts.d.ts.map +1 -0
- package/dist/utils/scripts.js +174 -0
- package/dist/utils/scripts.js.map +1 -0
- package/dist/utils/state.d.ts +12 -0
- package/dist/utils/state.d.ts.map +1 -0
- package/dist/utils/state.js +44 -0
- package/dist/utils/state.js.map +1 -0
- package/dist/utils/tar.d.ts +11 -0
- package/dist/utils/tar.d.ts.map +1 -0
- package/dist/utils/tar.js +93 -0
- package/dist/utils/tar.js.map +1 -0
- package/dist/utils/update.d.ts +9 -0
- package/dist/utils/update.d.ts.map +1 -0
- package/dist/utils/update.js +134 -0
- package/dist/utils/update.js.map +1 -0
- package/dist/utils/websocket.d.ts +10 -0
- package/dist/utils/websocket.d.ts.map +1 -0
- package/dist/utils/websocket.js +203 -0
- package/dist/utils/websocket.js.map +1 -0
- package/package.json +55 -4
- package/public/app.js +2712 -0
- package/public/assets/v2.2/css/base.css +113 -0
- package/public/assets/v2.2/css/components/content.css +133 -0
- package/public/assets/v2.2/css/components/controls.css +229 -0
- package/public/assets/v2.2/css/components/data.css +373 -0
- package/public/assets/v2.2/css/components/icons.css +35 -0
- package/public/assets/v2.2/css/components/motion.css +57 -0
- package/public/assets/v2.2/css/components/shell.css +51 -0
- package/public/assets/v2.2/css/components/sidebar.css +242 -0
- package/public/assets/v2.2/css/components/topbar.css +138 -0
- package/public/assets/v2.2/css/fonts.css +30 -0
- package/public/assets/v2.2/css/synteq-ui-v2.css +25 -0
- package/public/assets/v2.2/css/theme-dark.css +38 -0
- package/public/assets/v2.2/css/theme-light.css +36 -0
- package/public/assets/v2.2/css/tokens.css +61 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
- package/public/assets/v2.2/js/synteq-ui-v2.js +869 -0
- package/public/favicon.svg +7 -0
- package/public/index.html +1080 -0
- package/public/styles.css +552 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ovtools contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ovtools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deploy, migrate and manage Octra validators from one Node.js CLI and web console.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Deploy** a fresh VPS as an observer or validator.
|
|
8
|
+
- **Migrate** a running validator to a new VPS with no manual copying.
|
|
9
|
+
- **Manage** an active node: bond, exit, update, diagnose, recover, prune, and
|
|
10
|
+
deploy the read-only monitoring dashboard.
|
|
11
|
+
- **Web console** with live logs, node status, precondition checks and guided
|
|
12
|
+
enrollment/activation/exit steps.
|
|
13
|
+
- **Resumable** operations: long steps keep running on the VPS and a retry
|
|
14
|
+
continues where the last one stopped.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js 18 or newer on your computer (Windows, Linux or macOS).
|
|
19
|
+
- A Debian/Ubuntu VPS with root SSH access.
|
|
20
|
+
|
|
21
|
+
No extra SSH client or runtime is required on your computer.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g ovtools
|
|
27
|
+
ovtools # start the console and open the browser
|
|
28
|
+
ovtools --version
|
|
29
|
+
ovtools help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Running `ovtools` with no arguments starts the web console on
|
|
33
|
+
`http://127.0.0.1:9091` and opens your browser. It first checks npm for a newer
|
|
34
|
+
release: when one exists it prints the update command and exits; otherwise the
|
|
35
|
+
console starts.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Deploy a fresh validator
|
|
41
|
+
ovtools deploy --ip <vps-ip> --password '<root-password>' --bond-oct 1
|
|
42
|
+
|
|
43
|
+
# Deploy an observer
|
|
44
|
+
ovtools deploy --ip <vps-ip> --key ~/.ssh/id_ed25519 --role observer
|
|
45
|
+
|
|
46
|
+
# Migrate an existing validator
|
|
47
|
+
ovtools migrate --old-ip <old-ip> --new-ip <new-ip> \
|
|
48
|
+
--old-password '<old-password>' --new-password '<new-password>'
|
|
49
|
+
|
|
50
|
+
# Manage a running node
|
|
51
|
+
ovtools manage --ip <vps-ip> --password '<root-password>' --phase status
|
|
52
|
+
ovtools manage --ip <vps-ip> --password '<root-password>' --phase bond --amount 100
|
|
53
|
+
ovtools manage --ip <vps-ip> --password '<root-password>' --phase dashboard
|
|
54
|
+
ovtools upgrade --ip <vps-ip> --password '<root-password>' --phase upgrade
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Run `ovtools help` for all options and phases.
|
|
58
|
+
|
|
59
|
+
## Web console
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
ovtools web # start and open the browser
|
|
63
|
+
ovtools web --no-open # start without opening a browser
|
|
64
|
+
# http://127.0.0.1:9091
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The console listens on localhost by default. If port 9091 is busy it picks the
|
|
68
|
+
next free port. To require an API token:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
OVTOOLS_TOKEN='<secret>' ovtools web
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Where data lives
|
|
75
|
+
|
|
76
|
+
`~/.ovtools` holds run logs, resumable step state, downloaded identity backups,
|
|
77
|
+
pasted keys/certificates and pinned SSH host keys.
|
|
78
|
+
|
|
79
|
+
Passwords are kept in memory only and are never written to disk.
|
|
80
|
+
|
|
81
|
+
## Assets on the VPS
|
|
82
|
+
|
|
83
|
+
The console copies its deployment scripts to the VPS automatically. By default it
|
|
84
|
+
clones this repository (`https://github.com/m-tq/ovtools.git`) on the VPS and uses
|
|
85
|
+
only the task it needs. If cloning is not possible, it falls back to a local
|
|
86
|
+
transfer automatically.
|
|
87
|
+
|
|
88
|
+
To use a fork:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ovtools config --assets-repo https://github.com/<you>/ovtools.git
|
|
92
|
+
ovtools config
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm install
|
|
99
|
+
npm run build
|
|
100
|
+
npm test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Two terminals for the web console:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run dev # rebuild on save
|
|
107
|
+
npm run dev:web # restart the console on http://127.0.0.1:9099
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Security
|
|
111
|
+
|
|
112
|
+
- Passwords stay in memory; keys and certificates are stored with restricted
|
|
113
|
+
permissions.
|
|
114
|
+
- Deployment assets run as root on the target VPS. Only point `--assets-repo`
|
|
115
|
+
at a repository you control.
|
|
116
|
+
- SSH host keys are pinned. A reinstalled VPS is accepted once, logged and re-pinned.
|
|
117
|
+
- The web console binds to `127.0.0.1` unless you choose another host.
|
|
118
|
+
- One runtime dependency: `ssh2`.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ===================================================================
|
|
3
|
+
# bond-inspect.sh - explain a bond_inc.sh refusal without signing anything
|
|
4
|
+
#
|
|
5
|
+
# bond_inc.sh keeps exactly ONE signed increase in
|
|
6
|
+
# $OCTRA_DATA_DIR/validator-control/bond-increase.json
|
|
7
|
+
# Every later run compares that record with the arguments and the live
|
|
8
|
+
# registry; anything that differs is refused with
|
|
9
|
+
# event = bond_inc status = refused reason = "saved increase differs;
|
|
10
|
+
# inspect before requesting another increase"
|
|
11
|
+
# This script is read-only (no key is used, no transaction is submitted). It
|
|
12
|
+
# prints the saved record, the live registry view and a field-by-field verdict
|
|
13
|
+
# so the next command can be chosen on purpose.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# sudo bash bond-inspect.sh # inspect the saved increase as-is
|
|
17
|
+
# sudo bash bond-inspect.sh -a 20 # ... and compare with a planned +20 OCT
|
|
18
|
+
# sudo bash bond-inspect.sh -a 20 -w /opt/octra/libv_litecore/.keys/validator/wallet.json
|
|
19
|
+
#
|
|
20
|
+
# Options:
|
|
21
|
+
# -a, --amount <oct> the amount you would pass to `bond_inc.sh -a`
|
|
22
|
+
# -w, --wallet <path> wallet.json (default: <config dir>/wallet.json)
|
|
23
|
+
# --config <path> node.env (default: $NODE_SRC_DIR/.keys/validator/node.env)
|
|
24
|
+
# ===================================================================
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
source "$(dirname "$0")/config.sh"
|
|
27
|
+
|
|
28
|
+
if [[ $EUID -ne 0 ]]; then
|
|
29
|
+
err "Run as root: sudo bash $0"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
AMOUNT=""
|
|
34
|
+
CONFIG="${NODE_SRC_DIR}/.keys/validator/node.env"
|
|
35
|
+
WALLET="${NODE_SRC_DIR}/.keys/validator/wallet.json"
|
|
36
|
+
|
|
37
|
+
while (( $# )); do
|
|
38
|
+
case "$1" in
|
|
39
|
+
-a|--amount) AMOUNT="${2:-}"; shift 2 ;;
|
|
40
|
+
-w|--wallet) WALLET="${2:-}"; shift 2 ;;
|
|
41
|
+
--config) CONFIG="${2:-}"; shift 2 ;;
|
|
42
|
+
-h|--help) sed -n '3,23p' "$0"; exit 0 ;;
|
|
43
|
+
*) err "Unknown argument: $1"; exit 1 ;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
[[ -f "$CONFIG" ]] || { err "node.env not found: $CONFIG"; exit 1; }
|
|
48
|
+
[[ -f "$WALLET" ]] || { err "wallet not found: $WALLET"; exit 1; }
|
|
49
|
+
|
|
50
|
+
TMP=$(mktemp /tmp/bond-inspect.XXXXXX.py)
|
|
51
|
+
cleanup() { rm -f "$TMP"; }
|
|
52
|
+
trap cleanup EXIT
|
|
53
|
+
|
|
54
|
+
cat > "$TMP" <<'PY'
|
|
55
|
+
# Read-only inspection of a saved bond increase. Mirrors the guards of
|
|
56
|
+
# controls/lib (validator_enroll.committed_enrollment, bond_inc.identity,
|
|
57
|
+
# bond_inc.receipt) instead of importing them: the node modules are re-checked
|
|
58
|
+
# by bond_inc.sh itself when the real payment is signed.
|
|
59
|
+
import json
|
|
60
|
+
import re
|
|
61
|
+
import sys
|
|
62
|
+
import time
|
|
63
|
+
import urllib.request
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
|
|
66
|
+
FEE = 1000
|
|
67
|
+
STAGES = {"prepared", "sent", "accepted"}
|
|
68
|
+
|
|
69
|
+
def say(text = ""):
|
|
70
|
+
print("[bond] " + text if text else "[bond] ")
|
|
71
|
+
|
|
72
|
+
def fail(text):
|
|
73
|
+
print("[bond] ERROR: " + text, file = sys.stderr)
|
|
74
|
+
raise SystemExit(1)
|
|
75
|
+
|
|
76
|
+
def oct_from_raw(raw):
|
|
77
|
+
if type(raw) is not int or raw < 0:
|
|
78
|
+
return "?"
|
|
79
|
+
whole, fraction = divmod(raw, 1000000)
|
|
80
|
+
return (f"{whole}.{fraction:06d}".rstrip("0").rstrip(".") or "0")
|
|
81
|
+
|
|
82
|
+
def parse_env(path):
|
|
83
|
+
values = {}
|
|
84
|
+
for line in path.read_text(encoding = "utf-8").splitlines():
|
|
85
|
+
line = line.strip()
|
|
86
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
87
|
+
continue
|
|
88
|
+
key, _, value = line.partition("=")
|
|
89
|
+
value = value.strip()
|
|
90
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
91
|
+
value = value[1:-1]
|
|
92
|
+
values[key.strip()] = value
|
|
93
|
+
return values
|
|
94
|
+
|
|
95
|
+
def call(url, method, params, *, missing = False, timeout = 10):
|
|
96
|
+
body = json.dumps({"jsonrpc": "2.0", "id": int(time.time() * 1000) % 1000000,
|
|
97
|
+
"method": method, "params": params}).encode("utf-8")
|
|
98
|
+
request = urllib.request.Request(url, data = body, method = "POST", headers = {
|
|
99
|
+
"Accept": "application/json", "Content-Type": "application/json",
|
|
100
|
+
"User-Agent": "octra-validator-controls/1"})
|
|
101
|
+
try:
|
|
102
|
+
with urllib.request.urlopen(request, timeout = timeout) as response:
|
|
103
|
+
payload = json.loads(response.read())
|
|
104
|
+
except Exception as error:
|
|
105
|
+
fail("the local node RPC is unreachable at " + url + " (" + str(error) + ")")
|
|
106
|
+
if not isinstance(payload, dict):
|
|
107
|
+
fail("invalid RPC response from " + url)
|
|
108
|
+
if payload.get("error") is not None:
|
|
109
|
+
error = payload["error"]
|
|
110
|
+
if missing and isinstance(error, dict) and type(error.get("code")) is int and error["code"] == 112:
|
|
111
|
+
return None
|
|
112
|
+
fail("RPC " + method + " rejected: " + json.dumps(error, separators = (",", ":")))
|
|
113
|
+
if "result" not in payload:
|
|
114
|
+
fail("RPC " + method + " returned no result")
|
|
115
|
+
if missing and payload["result"] is None:
|
|
116
|
+
return None
|
|
117
|
+
return payload["result"]
|
|
118
|
+
|
|
119
|
+
def raw_amount(text):
|
|
120
|
+
if not re.fullmatch(r"[0-9]+(?:\.[0-9]{1,6})?", text):
|
|
121
|
+
fail("amount must be OCT with at most six decimal places")
|
|
122
|
+
whole, _, fraction = text.partition(".")
|
|
123
|
+
return int(whole) * 1000000 + int((fraction or "").ljust(6, "0"))
|
|
124
|
+
|
|
125
|
+
def integer(value):
|
|
126
|
+
if type(value) is int:
|
|
127
|
+
return value
|
|
128
|
+
if isinstance(value, str) and value.isascii() and value.isdecimal():
|
|
129
|
+
return int(value)
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def main():
|
|
133
|
+
config = Path(sys.argv[1])
|
|
134
|
+
requested_text = (sys.argv[2] or "").strip()
|
|
135
|
+
wallet_path = Path(sys.argv[3])
|
|
136
|
+
node_dir = Path(sys.argv[4]).expanduser() if len(sys.argv) > 4 else Path("/opt/octra/libv_litecore")
|
|
137
|
+
values = parse_env(config)
|
|
138
|
+
if "OCTRA_DATA_DIR" not in values:
|
|
139
|
+
fail("OCTRA_DATA_DIR is missing from " + str(config))
|
|
140
|
+
port = values.get("OCTRA_API_PORT", "8080")
|
|
141
|
+
chain = values.get("OCTRA_CHAIN_ID", "")
|
|
142
|
+
data = Path(values["OCTRA_DATA_DIR"])
|
|
143
|
+
control = data / "validator-control"
|
|
144
|
+
url = "http://127.0.0.1:" + port + "/rpc"
|
|
145
|
+
try:
|
|
146
|
+
wallet = json.loads(wallet_path.read_text(encoding = "utf-8"))
|
|
147
|
+
address, public = wallet["address"], wallet["pub"]
|
|
148
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
149
|
+
fail("invalid wallet file: " + str(wallet_path))
|
|
150
|
+
requested = raw_amount(requested_text) if requested_text else None
|
|
151
|
+
if requested is not None and requested < 1000000:
|
|
152
|
+
fail("bond increase must be at least 1 OCT")
|
|
153
|
+
|
|
154
|
+
say("config : " + str(config))
|
|
155
|
+
say("control dir : " + str(control))
|
|
156
|
+
say("rpc : " + url)
|
|
157
|
+
|
|
158
|
+
runtime = call(url, "octra_runtimeVersion", [])
|
|
159
|
+
enrollment = call(url, "octra_validatorEnrollment", [])
|
|
160
|
+
balance = call(url, "octra_balance", [address])
|
|
161
|
+
if not isinstance(runtime, dict) or not isinstance(enrollment, dict) or not isinstance(balance, dict):
|
|
162
|
+
fail("the local node returned an unexpected RPC shape")
|
|
163
|
+
head = integer(enrollment.get("head_epoch"))
|
|
164
|
+
bond = integer(enrollment.get("bond"))
|
|
165
|
+
bonded = integer(enrollment.get("bonded_epoch"))
|
|
166
|
+
ready = integer(enrollment.get("ready_epoch"))
|
|
167
|
+
nonce = integer(balance.get("nonce"))
|
|
168
|
+
pending = integer(balance.get("pending_nonce"))
|
|
169
|
+
funds = integer(balance.get("balance_raw"))
|
|
170
|
+
|
|
171
|
+
say("chain : " + str(chain) + (" (local node matches)"
|
|
172
|
+
if runtime.get("chain_id") == chain else " (local node chain DIFFERS: " + str(runtime.get("chain_id")) + ")"))
|
|
173
|
+
say("validator : " + address + (" (local node matches)" if runtime.get("validator") == address
|
|
174
|
+
else " (local node serves " + str(runtime.get("validator")) + ")"))
|
|
175
|
+
say(f"registry : state={enrollment.get('state')} head={head} bond={oct_from_raw(bond)} OCT "
|
|
176
|
+
f"({bond}) bonded_epoch={bonded} ready_epoch={ready}")
|
|
177
|
+
say(f"nonces : nonce={nonce} pending_nonce={pending}"
|
|
178
|
+
+ ("" if nonce == pending else " <- a transaction of this wallet is still pending"))
|
|
179
|
+
say(f"balance : {oct_from_raw(funds)} OCT ({funds})")
|
|
180
|
+
|
|
181
|
+
path = control / "bond-increase.json"
|
|
182
|
+
archives = sorted(control.glob("bond-increase-*.json")) if control.is_dir() else []
|
|
183
|
+
record = None
|
|
184
|
+
if path.is_file():
|
|
185
|
+
try:
|
|
186
|
+
record = json.loads(path.read_text(encoding = "utf-8"))
|
|
187
|
+
except (OSError, ValueError) as error:
|
|
188
|
+
fail("the saved increase is not valid JSON: " + str(path) + " (" + str(error) + ")")
|
|
189
|
+
if not isinstance(record, dict):
|
|
190
|
+
fail("the saved increase is not a JSON object: " + str(path))
|
|
191
|
+
say("saved : " + str(path))
|
|
192
|
+
else:
|
|
193
|
+
say("saved : none (" + str(path) + ")")
|
|
194
|
+
for archive in archives:
|
|
195
|
+
say(" leftover archive: " + archive.name)
|
|
196
|
+
if archives:
|
|
197
|
+
fail("bond_inc.sh refuses with \"saved increase missing; restore control records\" "
|
|
198
|
+
"while an archive exists and bond-increase.json does not")
|
|
199
|
+
|
|
200
|
+
user = values.get("NODE_USER", "octra")
|
|
201
|
+
command = "su - " + user + " -c \"cd '" + str(node_dir) + "' && sh ./bond_inc.sh"
|
|
202
|
+
if record is None:
|
|
203
|
+
amount_hint = oct_from_raw(requested) if requested is not None else "<OCT>"
|
|
204
|
+
say("")
|
|
205
|
+
say("verdict : no saved increase yet - the next run signs a fresh payment")
|
|
206
|
+
if nonce is not None and pending is not None and nonce != pending:
|
|
207
|
+
say("blocked : the mempool still holds a transaction of this wallet "
|
|
208
|
+
f"(nonce {nonce} vs pending {pending}); nothing can be signed")
|
|
209
|
+
if requested is not None and type(funds) is int and funds < requested + FEE:
|
|
210
|
+
say(f"blocked : balance {oct_from_raw(funds)} OCT does not cover "
|
|
211
|
+
f"{oct_from_raw(requested)} OCT + {oct_from_raw(FEE)} OCT fee")
|
|
212
|
+
say("next : " + command + " -a " + amount_hint + " -w '" + str(wallet_path) + "'\"")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
saved = integer(record.get("amount"))
|
|
217
|
+
tx = record.get("tx") if isinstance(record.get("tx"), dict) else {}
|
|
218
|
+
say(f"record : amount={oct_from_raw(saved)} OCT ({saved}) stage={record.get('stage')} "
|
|
219
|
+
f"before={oct_from_raw(record.get('before'))} OCT head={record.get('head')} "
|
|
220
|
+
f"bonded={record.get('bonded')} ready={record.get('ready')}")
|
|
221
|
+
say(f"record tx : {record.get('tx_hash')} nonce={tx.get('nonce')} ou={tx.get('ou')} "
|
|
222
|
+
f"timestamp={tx.get('timestamp')}")
|
|
223
|
+
|
|
224
|
+
state = None
|
|
225
|
+
observed = call(url, "octra_transaction", [record["tx_hash"]], missing = True) \
|
|
226
|
+
if isinstance(record.get("tx_hash"), str) else None
|
|
227
|
+
if isinstance(observed, dict):
|
|
228
|
+
state = observed.get("status")
|
|
229
|
+
differ = [name for name, want in (("tx_hash", record.get("tx_hash")),
|
|
230
|
+
("from", tx.get("from")), ("amount_raw", tx.get("amount")),
|
|
231
|
+
("nonce", tx.get("nonce")), ("op_type", tx.get("op_type")))
|
|
232
|
+
if observed.get(name) != want]
|
|
233
|
+
if observed.get("to_", observed.get("to")) != tx.get("to_"):
|
|
234
|
+
differ.append("to")
|
|
235
|
+
say("receipt : status=" + str(state)
|
|
236
|
+
+ (" fields match" if not differ else " fields DIFFER: " + ",".join(differ))
|
|
237
|
+
+ (" epoch=" + str(observed.get("epoch")) if observed.get("epoch") is not None else ""))
|
|
238
|
+
if "to_" not in observed:
|
|
239
|
+
say("note : this node returns the field `to`, while bond_inc.sh receipt() reads")
|
|
240
|
+
say(" `to_`, so a re-run or --new refuses with \"bond transaction")
|
|
241
|
+
say(" receipt differs\" once the identity checks pass")
|
|
242
|
+
else:
|
|
243
|
+
say("receipt : not on chain (the payment was never accepted)")
|
|
244
|
+
|
|
245
|
+
def rows_for(target):
|
|
246
|
+
rows = []
|
|
247
|
+
for key, want in (("chain", chain), ("address", address), ("pubkey", public),
|
|
248
|
+
("amount", target), ("bonded", bonded), ("ready", ready)):
|
|
249
|
+
actual = record.get(key)
|
|
250
|
+
rows.append((type(actual) is type(want) and actual == want, key, actual, want))
|
|
251
|
+
before = record.get("before")
|
|
252
|
+
rows.append((type(before) is int and before >= 1, "before", before, "int >= 1"))
|
|
253
|
+
saved_head = record.get("head")
|
|
254
|
+
rows.append((type(saved_head) is int and type(head) is int and 0 <= saved_head <= head,
|
|
255
|
+
"head", saved_head, "<= " + str(head)))
|
|
256
|
+
rows.append((record.get("stage") in STAGES, "stage", record.get("stage"),
|
|
257
|
+
"prepared|sent|accepted"))
|
|
258
|
+
return rows
|
|
259
|
+
|
|
260
|
+
def report(rows, label):
|
|
261
|
+
bad = [row for row in rows if not row[0]]
|
|
262
|
+
if not bad:
|
|
263
|
+
say(label + ": all " + str(len(rows)) + " identity checks match")
|
|
264
|
+
return True
|
|
265
|
+
say(label + ": " + str(len(bad)) + "/" + str(len(rows)) + " identity checks differ")
|
|
266
|
+
for _, key, actual, want in bad:
|
|
267
|
+
if key == "amount":
|
|
268
|
+
say(f" amount: saved {oct_from_raw(actual)} OCT ({actual}) != "
|
|
269
|
+
f"wanted {oct_from_raw(want)} OCT ({want})")
|
|
270
|
+
else:
|
|
271
|
+
say(f" {key}: saved {actual!r} != registry/argument {want!r}")
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
target = requested if requested is not None else saved
|
|
275
|
+
plain_rows = rows_for(target)
|
|
276
|
+
plain = report(plain_rows, "plain run")
|
|
277
|
+
plain_bad = {row[1] for row in plain_rows if not row[0]}
|
|
278
|
+
if requested is not None and requested != saved:
|
|
279
|
+
new = report(rows_for(saved), "--new run")
|
|
280
|
+
else:
|
|
281
|
+
new = plain
|
|
282
|
+
say("")
|
|
283
|
+
|
|
284
|
+
if plain and state == "confirmed":
|
|
285
|
+
say("verdict : the saved increase is already confirmed (bond "
|
|
286
|
+
+ oct_from_raw(bond) + " OCT on chain); nothing was signed and nothing changed")
|
|
287
|
+
say("next : to add MORE bond you must use --new:")
|
|
288
|
+
say(" " + command + " -a "
|
|
289
|
+
+ (oct_from_raw(requested) if requested is not None else "<OCT>")
|
|
290
|
+
+ " -w '" + str(wallet_path) + "' --new\"")
|
|
291
|
+
return
|
|
292
|
+
if plain and state in {"pending", "staging"}:
|
|
293
|
+
say("verdict : the saved payment is " + str(state) + " - it is already in flight, never sign again")
|
|
294
|
+
say("next : wait for tx " + str(record.get("tx_hash")) + " to reach status confirmed")
|
|
295
|
+
return
|
|
296
|
+
if plain and state in {"rejected", "dropped"}:
|
|
297
|
+
say("verdict : the saved payment was " + str(state) + " on chain, nothing was added")
|
|
298
|
+
say("next : archive the record OUTSIDE the control dir and sign a fresh payment:")
|
|
299
|
+
say(" install -d -m 700 " + str(data) + ".bond-archive")
|
|
300
|
+
say(" mv '" + str(path) + "' " + str(data) + ".bond-archive/bond-increase."
|
|
301
|
+
+ str(int(time.time())) + ".json")
|
|
302
|
+
say(" then: " + command + " -a "
|
|
303
|
+
+ (oct_from_raw(requested) if requested is not None else oct_from_raw(saved))
|
|
304
|
+
+ " -w '" + str(wallet_path) + "'\"")
|
|
305
|
+
return
|
|
306
|
+
elif plain and record.get("stage") != "prepared":
|
|
307
|
+
say("verdict : the saved payment was submitted but its outcome is unknown")
|
|
308
|
+
say("next : " + command + " -a " + oct_from_raw(saved) + " -w '" + str(wallet_path)
|
|
309
|
+
+ "' --retry\" (resubmits the SAME transaction)")
|
|
310
|
+
return
|
|
311
|
+
elif plain and record.get("stage") == "prepared":
|
|
312
|
+
say("verdict : the saved payment was signed but never submitted")
|
|
313
|
+
say("next : " + command + " -a " + oct_from_raw(saved) + " -w '" + str(wallet_path)
|
|
314
|
+
+ "'\" (submits the saved payment)")
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
if new and state != "confirmed":
|
|
318
|
+
say("verdict : the previous increase is NOT confirmed yet (receipt "
|
|
319
|
+
+ str(state or "absent") + "), so --new refuses until it is")
|
|
320
|
+
say("next : wait - watch `sh controls/stat.sh | grep validator_bond` and the receipt of")
|
|
321
|
+
say(" " + str(record.get("tx_hash")))
|
|
322
|
+
elif new:
|
|
323
|
+
if type(funds) is int and requested is not None and funds < requested + FEE:
|
|
324
|
+
say(f"verdict : ready for --new, but the balance {oct_from_raw(funds)} OCT does not cover "
|
|
325
|
+
f"{oct_from_raw(requested)} OCT + fee")
|
|
326
|
+
elif nonce is not None and pending is not None and nonce != pending:
|
|
327
|
+
say("verdict : ready for --new, but the mempool still holds a transaction "
|
|
328
|
+
f"(nonce {nonce} vs pending {pending}); wait for it to land")
|
|
329
|
+
else:
|
|
330
|
+
say("verdict : the previous increase is confirmed - a new one can be signed")
|
|
331
|
+
say("next : " + command + " -a "
|
|
332
|
+
+ (oct_from_raw(requested) if requested is not None else "<OCT>")
|
|
333
|
+
+ " -w '" + str(wallet_path) + "' --new\"")
|
|
334
|
+
if isinstance(observed, dict) and "to_" not in observed:
|
|
335
|
+
say("warning : --new still goes through receipt(), which reads `to_`; if it is")
|
|
336
|
+
say(" refused with \"bond transaction receipt differs\", archive the saved")
|
|
337
|
+
say(" record outside the control dir and sign a fresh increase instead")
|
|
338
|
+
else:
|
|
339
|
+
if plain_bad == {"amount"}:
|
|
340
|
+
say("verdict : the saved payment is for " + oct_from_raw(saved) + " OCT and the amount")
|
|
341
|
+
say(" passed is different - bond_inc.sh treats it as the SAME operation")
|
|
342
|
+
say(" (a different amount needs --new once the saved tx is confirmed)")
|
|
343
|
+
else:
|
|
344
|
+
say("verdict : the saved record no longer matches this node/registry (see the")
|
|
345
|
+
say(" differing fields above), so it cannot be replayed")
|
|
346
|
+
if state in {"pending", "staging"}:
|
|
347
|
+
say("next : keep the file, wait for tx " + str(record.get("tx_hash")))
|
|
348
|
+
elif state == "confirmed":
|
|
349
|
+
say("next : the saved payment is confirmed, so the file can be archived OUTSIDE the")
|
|
350
|
+
say(" control dir (a bond-increase-*.json left behind makes bond_inc.sh refuse")
|
|
351
|
+
say(" with \"saved increase missing; restore control records\"):")
|
|
352
|
+
say(" install -d -m 700 " + str(data) + ".bond-archive")
|
|
353
|
+
say(" mv '" + str(path) + "' " + str(data) + ".bond-archive/bond-increase."
|
|
354
|
+
+ str(int(time.time())) + ".json")
|
|
355
|
+
say(" then re-run bond_inc.sh for the new amount (no --new needed)")
|
|
356
|
+
else:
|
|
357
|
+
say("next : do NOT delete the file yet - check the tx first: "
|
|
358
|
+
+ str(record.get("tx_hash")))
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
try:
|
|
362
|
+
main()
|
|
363
|
+
except (OSError, KeyError, ValueError, TypeError) as error:
|
|
364
|
+
fail(str(error))
|
|
365
|
+
PY
|
|
366
|
+
|
|
367
|
+
python3 -B "$TMP" "$CONFIG" "$AMOUNT" "$WALLET" "$NODE_SRC_DIR"
|
|
368
|
+
|