11#! /bin/bash
2+ set -o pipefail
23
3- # Dynamic Supernode Version Checker Script
4- # Fetches live supernode data from Lumera testnet API
5- # Usage: ./check_versions.sh <latest_version>
6- # Example: ./check_versions.sh v2.2.6
4+ # Dynamic Supernode Version + Uptime Inventory
5+ # Fetches supernode list from Lumera LCD and queries each node's /api/v1/status.
6+ # Prints a breakdown sorted by version string (descending).
7+ #
8+ # Usage: ./check_versions.sh (no args)
79
8- if [ $# -eq 0 ]; then
9- echo " Usage: $0 <latest_version>"
10- echo " Example: $0 v2.2.6"
11- exit 1
12- fi
10+ TIMEOUT=10
11+ API_URL=" https://lcd.testnet.lumera.io/LumeraProtocol/lumera/supernode/list_super_nodes?pagination.limit=1000&pagination.count_total=true"
1312
14- LATEST_VERSION=" $1 "
1513TOTAL_CHECKED=0
16- LATEST_VERSION_COUNT=0
1714UNREACHABLE_COUNT=0
18- TIMEOUT=2
19- API_URL=" https://lcd.testnet.lumera.io/LumeraProtocol/lumera/supernode/list_super_nodes?pagination.limit=1000&pagination.count_total=true"
15+ INVALID_COUNT=0
2016
21- # Arrays to track failed calls
2217FAILED_CALLS=()
2318INVALID_RESPONSES=()
2419
20+ declare -A VERSION_COUNTS
21+ declare -A VERSION_UPTIME_SECS_SUM
22+
23+ if ! command -v jq > /dev/null 2>&1 ; then
24+ echo " ERROR: jq is required. Install with: sudo apt-get install -y jq"
25+ exit 1
26+ fi
27+
2528echo " Fetching supernode list from Lumera API..."
2629echo " =============================================================================="
27-
28- # Fetch supernode data from API
2930echo " Fetching from: $API_URL "
30- SUPERNODE_DATA=$( curl -s --connect-timeout 10 --max-time 30 \
31- -H " User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" \
32- -H " Accept: application/json, text/plain, */*" \
33- -H " Accept-Language: en-US,en;q=0.9" \
34- -H " Accept-Encoding: gzip, deflate, br" \
35- -H " Connection: keep-alive" \
36- -H " Upgrade-Insecure-Requests: 1" \
37- " $API_URL " )
31+
32+ SUPERNODE_DATA=$( curl -s --connect-timeout " $TIMEOUT " --max-time 30 \
33+ --compressed \
34+ -H " User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" \
35+ -H " Accept: application/json, text/plain, */*" \
36+ " $API_URL " )
3837CURL_STATUS=$?
3938
4039if [ $CURL_STATUS -ne 0 ]; then
41- echo " ERROR: curl failed with exit code $CURL_STATUS "
42- exit 1
40+ echo " ERROR: curl failed with exit code $CURL_STATUS "
41+ exit 1
4342fi
4443
4544if [ -z " $SUPERNODE_DATA " ]; then
46- echo " ERROR: Empty response from API"
47- exit 1
45+ echo " ERROR: Empty response from API"
46+ exit 1
4847fi
4948
50- # Debug: Show first 200 characters of response
5149echo " API Response preview: $( echo " $SUPERNODE_DATA " | head -c 200) ..."
5250
53- # Check if response is valid JSON
54- if ! echo " $SUPERNODE_DATA " | jq empty 2> /dev/null; then
55- echo " ERROR: Invalid JSON response from API"
56- echo " Full response:"
57- echo " $SUPERNODE_DATA "
58- exit 1
51+ if ! echo " $SUPERNODE_DATA " | jq empty > /dev/null 2>&1 ; then
52+ echo " ERROR: Invalid JSON response from API"
53+ echo " Full response:"
54+ echo " $SUPERNODE_DATA "
55+ exit 1
5956fi
6057
61- # Check if jq is available for JSON parsing
62- if ! command -v jq & > /dev/null; then
63- echo " ERROR: jq is required for JSON parsing but not installed"
64- echo " Please install jq: sudo apt-get install jq (Ubuntu/Debian) or brew install jq (macOS)"
65- exit 1
66- fi
67-
68- # Parse JSON and extract supernodes with their latest IP addresses
6958echo " Parsing supernode data and extracting latest IP addresses..."
7059
71- # Create temporary file to store processed supernode data
7260TEMP_FILE=$( mktemp)
73-
74- # Extract supernode data and find latest IP for each
75- echo " $SUPERNODE_DATA " | jq -r ' .supernodes[] |
76- # Find the address with highest height for each supernode
77- (.prev_ip_addresses | sort_by(.height | tonumber) | reverse | .[0].address) as $latest_ip |
78- .supernode_account + "," + $latest_ip' > " $TEMP_FILE "
79-
80- SUPERNODE_COUNT=$( wc -l < " $TEMP_FILE " )
61+ echo " $SUPERNODE_DATA " | jq -r '
62+ .supernodes[]
63+ | .supernode_account as $acct
64+ | ((.prev_ip_addresses // [])
65+ | sort_by(.height|tonumber)
66+ | last
67+ | .address) as $ip
68+ | select($ip != null and ($ip|tostring) != "")
69+ | "\($acct),\($ip)"
70+ ' > " $TEMP_FILE "
71+
72+ SUPERNODE_COUNT=$( wc -l < " $TEMP_FILE " | awk ' {print $1}' )
8173echo " Found $SUPERNODE_COUNT supernodes to check"
82- echo " Starting version checks (target version: $LATEST_VERSION ) "
74+ echo " Starting version + uptime inventory "
8375echo " =============================================================================="
8476
85- # Process each supernode
8677while IFS=' ,' read -r ACCOUNT LATEST_IP; do
87- TOTAL_CHECKED=$(( TOTAL_CHECKED + 1 ))
88-
89- # Skip empty lines
90- [ -z " $ACCOUNT " ] && continue
91-
92- # Clean up the IP first - remove all whitespace and trailing spaces
93- CLEAN_IP=$( echo " $LATEST_IP " | sed -E ' s/[[:space:]]+/ /g' | sed -E ' s/^[[:space:]]+|[[:space:]]+$//' )
94-
95- # Convert port from 4444 to 8002 for status API, handle various formats
96- STATUS_ENDPOINT=$( echo " $CLEAN_IP " | sed -E '
97- s/:4444$/:8002/
98- s/:443$/:8002/
99- /^https?:\/\//!s/^/http:\/\//
100- /:[0-9]+$/!s/$/:8002/
101- ' )
102-
103- # For URLs that already have http/https, extract just the domain:port part
104- if [[ " $STATUS_ENDPOINT " =~ ^https? :// ]]; then
105- # Extract domain from URL and add :8002
106- DOMAIN=$( echo " $STATUS_ENDPOINT " | sed -E ' s|^https?://([^/]+).*|\1|' )
107- STATUS_ENDPOINT=" http://$DOMAIN "
108- # Add port if not present
109- if [[ ! " $STATUS_ENDPOINT " =~ :[0-9]+$ ]]; then
110- STATUS_ENDPOINT=" $STATUS_ENDPOINT :8002"
111- fi
112- fi
113-
114- # Make HTTP request to status API
115- RESPONSE=$( curl -s --connect-timeout $TIMEOUT --max-time $TIMEOUT " $STATUS_ENDPOINT /api/v1/status" 2> /dev/null)
116- CURL_EXIT_CODE=$?
117-
118- if [ $CURL_EXIT_CODE -eq 0 ] && [ ! -z " $RESPONSE " ]; then
119- # Extract version from response
120- VERSION=$( echo " $RESPONSE " | jq -r ' .version' 2> /dev/null)
121-
122- if [ ! -z " $VERSION " ] && [ " $VERSION " != " null" ]; then
123- if [ " $VERSION " = " $LATEST_VERSION " ]; then
124- echo " ✓ $ACCOUNT ($STATUS_ENDPOINT ): $VERSION (LATEST)"
125- LATEST_VERSION_COUNT=$(( LATEST_VERSION_COUNT + 1 ))
126- else
127- echo " ○ $ACCOUNT ($STATUS_ENDPOINT ): $VERSION "
128- fi
129- else
130- echo " ✗ $ACCOUNT ($STATUS_ENDPOINT ): ERROR (invalid response format)"
131- INVALID_RESPONSES+=(" $ACCOUNT ($STATUS_ENDPOINT )" )
132- UNREACHABLE_COUNT=$(( UNREACHABLE_COUNT + 1 ))
133- fi
78+ [ -z " $ACCOUNT " ] && continue
79+ TOTAL_CHECKED=$(( TOTAL_CHECKED + 1 ))
80+
81+ CLEAN_IP=$( echo " $LATEST_IP " | sed -E ' s/^[[:space:]]+|[[:space:]]+$//g' )
82+ HOSTPORT=$( echo " $CLEAN_IP " | sed -E ' s|^https?://||' )
83+ if ! echo " $HOSTPORT " | grep -qE ' :[0-9]+$' ; then
84+ HOSTPORT=" ${HOSTPORT} :4444"
85+ fi
86+ HOSTPORT=$( echo " $HOSTPORT " | sed -E ' s/:4444$/:8002/; s/:443$/:8002/' )
87+ if ! echo " $HOSTPORT " | grep -qE ' :[0-9]+$' ; then
88+ HOSTPORT=" ${HOSTPORT} :8002"
89+ fi
90+
91+ STATUS_ENDPOINT=" http://${HOSTPORT} /api/v1/status"
92+ RESPONSE=$( curl -s --connect-timeout " $TIMEOUT " --max-time " $TIMEOUT " " $STATUS_ENDPOINT " )
93+ CURL_EXIT_CODE=$?
94+
95+ if [ $CURL_EXIT_CODE -eq 0 ] && [ -n " $RESPONSE " ]; then
96+ VERSION=$( echo " $RESPONSE " | jq -r ' try .version // empty' 2> /dev/null)
97+ UPTIME_SECONDS=$( echo " $RESPONSE " | jq -r ' try .uptime_seconds // empty' 2> /dev/null)
98+
99+ if [ -n " $VERSION " ]; then
100+ VERSION_COUNTS[" $VERSION " ]=$(( ${VERSION_COUNTS["$VERSION"]:- 0} + 1 ))
101+
102+ UPTIME_HOURS_STR=" n/a"
103+ if [[ -n " $UPTIME_SECONDS " && " $UPTIME_SECONDS " =~ ^[0-9]+$ ]]; then
104+ UPTIME_HOURS_STR=$( awk -v s=" $UPTIME_SECONDS " ' BEGIN{printf "%.2f", s/3600}' )
105+ VERSION_UPTIME_SECS_SUM[" $VERSION " ]=$(( ${VERSION_UPTIME_SECS_SUM["$VERSION"]:- 0} + UPTIME_SECONDS ))
106+ fi
107+
108+ echo " • $ACCOUNT (http://$HOSTPORT ): version=$VERSION , uptime_hours=$UPTIME_HOURS_STR "
134109 else
135- # Determine specific error type
136- if [ $CURL_EXIT_CODE -eq 6 ]; then
137- ERROR_MSG=" DNS resolution failed"
138- elif [ $CURL_EXIT_CODE -eq 7 ]; then
139- ERROR_MSG=" connection refused"
140- elif [ $CURL_EXIT_CODE -eq 28 ]; then
141- ERROR_MSG=" timeout"
142- else
143- ERROR_MSG=" curl error code $CURL_EXIT_CODE "
144- fi
145-
146- echo " ✗ $ACCOUNT ($STATUS_ENDPOINT ): UNREACHABLE ($ERROR_MSG )"
147- FAILED_CALLS+=(" $ACCOUNT ($STATUS_ENDPOINT ) - $ERROR_MSG " )
148- UNREACHABLE_COUNT=$(( UNREACHABLE_COUNT + 1 ))
110+ echo " ✗ $ACCOUNT (http://$HOSTPORT ): ERROR (invalid response format)"
111+ INVALID_RESPONSES+=(" $ACCOUNT (http://$HOSTPORT )" )
112+ INVALID_COUNT=$(( INVALID_COUNT + 1 ))
149113 fi
150-
114+ else
115+ case $CURL_EXIT_CODE in
116+ 6) ERROR_MSG=" DNS resolution failed" ;;
117+ 7) ERROR_MSG=" connection refused" ;;
118+ 28) ERROR_MSG=" timeout" ;;
119+ * ) ERROR_MSG=" curl error code $CURL_EXIT_CODE " ;;
120+ esac
121+ echo " ✗ $ACCOUNT (http://$HOSTPORT ): UNREACHABLE ($ERROR_MSG )"
122+ FAILED_CALLS+=(" $ACCOUNT (http://$HOSTPORT ) - $ERROR_MSG " )
123+ UNREACHABLE_COUNT=$(( UNREACHABLE_COUNT + 1 ))
124+ fi
151125done < " $TEMP_FILE "
152126
153- # Cleanup temp files
154- rm " $TEMP_FILE "
155- rm -rf " $TEMP_DIR "
127+ rm -f " $TEMP_FILE "
156128
157129echo " =============================================================================="
158130echo " SUMMARY:"
159131echo " Total supernodes checked: $TOTAL_CHECKED "
160- echo " Latest version ($LATEST_VERSION ): $LATEST_VERSION_COUNT "
161- echo " Other versions: $(( TOTAL_CHECKED - LATEST_VERSION_COUNT - UNREACHABLE_COUNT)) "
162- echo " Unreachable: $UNREACHABLE_COUNT "
163-
164- if [ $(( TOTAL_CHECKED - UNREACHABLE_COUNT)) -gt 0 ]; then
165- echo " Percentage with latest version: $(( LATEST_VERSION_COUNT * 100 / (TOTAL_CHECKED - UNREACHABLE_COUNT) )) %"
132+ REACHABLE_VALID=0
133+
134+ # Sort by version string (descending, natural sort)
135+ TMP_SORT=$( mktemp)
136+ for v in " ${! VERSION_COUNTS[@]} " ; do
137+ c=${VERSION_COUNTS["$v"]}
138+ echo -e " ${v} \t${c} " >> " $TMP_SORT "
139+ REACHABLE_VALID=$(( REACHABLE_VALID + c))
140+ done
141+
142+ if [ -s " $TMP_SORT " ]; then
143+ echo " Version breakdown (sorted by version desc):"
144+ sort -V -r " $TMP_SORT " | awk -F' \t' ' {printf " - %s: %s\n", $1, $2}'
166145else
167- echo " Percentage: N/A (all unreachable)"
146+ echo " Version breakdown: (none reachable with valid response)"
147+ fi
148+ rm -f " $TMP_SORT "
149+
150+ echo " Invalid responses: $INVALID_COUNT "
151+ echo " Unreachable: $UNREACHABLE_COUNT "
152+ echo " Reachable & valid: $REACHABLE_VALID "
153+
154+ TMP_SORT2=$( mktemp)
155+ for v in " ${! VERSION_COUNTS[@]} " ; do
156+ c=${VERSION_COUNTS["$v"]}
157+ sum=${VERSION_UPTIME_SECS_SUM["$v"]:- 0}
158+ if [ " $c " -gt 0 ] && [ " $sum " -gt 0 ]; then
159+ avg_hours=$( awk -v s=" $sum " -v n=" $c " ' BEGIN{printf "%.2f", (s/n)/3600}' )
160+ else
161+ avg_hours=" n/a"
162+ fi
163+ echo -e " ${v} \t${avg_hours} " >> " $TMP_SORT2 "
164+ done
165+
166+ if [ -s " $TMP_SORT2 " ]; then
167+ echo " "
168+ echo " Average uptime by version (hours, sorted by version desc):"
169+ sort -V -r " $TMP_SORT2 " | awk -F' \t' ' {printf " - %s: %s (avg)\n", $1, $2}'
168170fi
171+ rm -f " $TMP_SORT2 "
169172
170- # List all failed calls
171173if [ ${# FAILED_CALLS[@]} -gt 0 ] || [ ${# INVALID_RESPONSES[@]} -gt 0 ]; then
172- echo " "
173- echo " FAILED CALLS DETAILS:"
174- echo " ====================="
175-
176- if [ ${# FAILED_CALLS[@]} -gt 0 ]; then
177- echo " Connection failures (${# FAILED_CALLS[@]} ):"
178- for failed in " ${FAILED_CALLS[@]} " ; do
179- echo " - $failed "
180- done
181- fi
182-
183- if [ ${# INVALID_RESPONSES[@]} -gt 0 ]; then
184- echo " Invalid response format (${# INVALID_RESPONSES[@]} ):"
185- for invalid in " ${INVALID_RESPONSES[@]} " ; do
186- echo " - $invalid "
187- done
188- fi
189- fi
174+ echo " "
175+ echo " FAILED CALLS DETAILS:"
176+ echo " ====================="
177+ if [ ${# FAILED_CALLS[@]} -gt 0 ]; then
178+ echo " Connection failures (${# FAILED_CALLS[@]} ):"
179+ for failed in " ${FAILED_CALLS[@]} " ; do
180+ echo " - $failed "
181+ done
182+ fi
183+ if [ ${# INVALID_RESPONSES[@]} -gt 0 ]; then
184+ echo " Invalid response format (${# INVALID_RESPONSES[@]} ):"
185+ for invalid in " ${INVALID_RESPONSES[@]} " ; do
186+ echo " - $invalid "
187+ done
188+ fi
189+ fi
0 commit comments