From f8915ed59f0be8ca0f7bdc3f42fb845909c4e377 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Fri, 3 Jul 2026 03:10:24 +0000 Subject: [PATCH] catchup: per-protocol dispatch for bitcoin, cosmos, and ripple Extend catchup.sh with protocol-aware branches (bitcoin height-gain, cosmos CometBFT timestamp via timestamp.sh, ripple validated-ledger seq) and explicit degradation for closed RPC / unsupported protocols. Reuse sync-status/check-health idioms for protocol resolution and parsing. Co-authored-by: Cursor --- catchup.sh | 493 ++++++++++++++++++++++++++++++++++++++++++++++++++- timestamp.sh | 44 ++++- 2 files changed, 534 insertions(+), 3 deletions(-) diff --git a/catchup.sh b/catchup.sh index 6e0da88d..c0da7196 100755 --- a/catchup.sh +++ b/catchup.sh @@ -25,13 +25,502 @@ s_to_human_readable() { seconds=$((seconds % 3600)) local minutes=$((seconds / 60)) seconds=$((seconds % 60)) - + printf "%d days, %02d hours, %02d minutes, %02d seconds\n" $days $hours $minutes $seconds } BASEPATH="$(dirname "$0")" source $BASEPATH/.env 2>/dev/null || true +compose_base="$1" +seconds_to_measure=${2:-10} + +if [ -z "$compose_base" ]; then + echo -e "${RED}Error: compose file path (sans .yml) required${NC}" + exit 1 +fi + +compose_yml="$BASEPATH/${compose_base}.yml" +if [ ! -f "$compose_yml" ]; then + echo -e "${RED}Error: compose file not found: $compose_yml${NC}" + exit 1 +fi + +if [ -n "$NO_SSL" ]; then + PROTO="http" + DOMAIN="${DOMAIN:-0.0.0.0}" +else + PROTO="https" +fi + +# Resolve traefik RPC path from compose labels (first non-blacklisted path). +compose_rpc_path() { + local pathlist path + pathlist=$(grep -oP "stripprefix\.prefixes.*?/\K[^\"]+" "$compose_yml" 2>/dev/null) + for path in $pathlist; do + echo "$path" + return 0 + done + return 1 +} + +compose_rpc_url() { + local path + path=$(compose_rpc_path) || return 1 + echo "$PROTO://$DOMAIN/$path" +} + +compose_chain_slug() { + grep -oP '^\s*chain:\s*\K\S+' "$compose_yml" 2>/dev/null | head -1 +} + +# Registry protocol via x-upstreams chain label; path-substring fallback for legacy composes. +resolve_protocol() { + local chain_slug protocol path + chain_slug=$(compose_chain_slug) + protocol="" + if [ -n "$chain_slug" ]; then + protocol=$($BASEPATH/reference-rpc-endpoint.sh --protocol "$chain_slug" 2>/dev/null) || protocol="" + fi + if [ -z "$protocol" ]; then + path=$(compose_rpc_path 2>/dev/null) || path="" + if echo "$path" | grep -qi "starknet"; then + protocol="starknet" + elif echo "$path" | grep -qi "aztec"; then + protocol="aztec" + elif echo "$path" | grep -qiE "bitcoin|digibyte|dogecoin|dash"; then + protocol="bitcoin" + elif echo "$path" | grep -qi "ripple"; then + protocol="ripple" + elif echo "$path" | grep -qiE "cosmos|babylon|haqq|gaiad|tac"; then + protocol="cosmos" + else + protocol="eth" + fi + fi + echo "$protocol" +} + +# Bitcoin-family RPC URL (traefik path) and basic-auth from x-upstreams (fallback username:password). +bitcoin_rpc_url() { + compose_rpc_url +} + +bitcoin_rpc_auth() { + # Prefer creds from compose x-upstreams basic-auth; per-chain rpcauth may differ on-host. + local user pass + user=$(grep -A5 'basic-auth:' "$compose_yml" 2>/dev/null | grep -oP '^\s*username:\s*\K\S+' | head -1) + pass=$(grep -A5 'basic-auth:' "$compose_yml" 2>/dev/null | grep -oP '^\s*password:\s*\K\S+' | head -1) + [ -z "$user" ] && user="username" + [ -z "$pass" ] && pass="password" + echo "${user}:${pass}" +} + +bitcoin_rpc_call() { + local method="$1" + local params="${2:-[]}" + local url auth + url=$(bitcoin_rpc_url) || return 1 + auth=$(bitcoin_rpc_auth) + curl -L --ipv4 -m 5 -s -u "$auth" -X POST -H "Content-Type: application/json" \ + --data "{\"jsonrpc\":\"1.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" "$url" +} + +bitcoin_parse_blocks_headers() { + local info="$1" + local blocks headers + blocks=$(echo "$info" | jq -r '.result.blocks // empty' 2>/dev/null) + headers=$(echo "$info" | jq -r '.result.headers // empty' 2>/dev/null) + if [[ ! "$blocks" =~ ^[0-9]+$ ]] || [[ ! "$headers" =~ ^[0-9]+$ ]]; then + return 1 + fi + echo "$blocks $headers" +} + +bitcoin_reference_height() { + local chain_slug="$1" + local ref_urls ref_url resp height + ref_urls=$($BASEPATH/reference-rpc-endpoint.sh --chain "$chain_slug" 2>/dev/null) || return 1 + ref_url=$(echo "$ref_urls" | awk '{print $1}') + [ -z "$ref_url" ] && return 1 + resp=$(curl -L --ipv4 -m 5 -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"1.0","id":1,"method":"getblockcount","params":[]}' "$ref_url") + if [ -z "$resp" ] || echo "$resp" | grep -qi 'Unknown network'; then + return 2 + fi + height=$(echo "$resp" | jq -r '.result // empty' 2>/dev/null) + if [[ ! "$height" =~ ^[0-9]+$ ]]; then + return 2 + fi + echo "$height" +} + +bitcoin_pick_seed_peer() { + local chain_slug="$1" path + path=$(compose_rpc_path 2>/dev/null) || path="" + if echo "$chain_slug $path" | grep -qi "digibyte"; then + echo "seed.digibyte.co:12024" + return 0 + fi + if echo "$chain_slug $path" | grep -qi "dogecoin"; then + # TODO: dogecoin wire seeds + return 1 + fi + if echo "$chain_slug $path" | grep -qi "dash"; then + # TODO: dash wire seeds + return 1 + fi + # Bitcoin mainnet / testnet / cash family default + echo "seed.bitcoin.sipa.be:8333" +} + +bitcoin_try_addnode() { + local chain_slug="$1" peer resp + peer="" + local ref_urls ref_url pinfo + ref_urls=$($BASEPATH/reference-rpc-endpoint.sh --chain "$chain_slug" 2>/dev/null) || ref_urls="" + ref_url=$(echo "$ref_urls" | awk '{print $1}') + if [ -n "$ref_url" ]; then + pinfo=$(curl -L --ipv4 -m 5 -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"1.0","id":1,"method":"getpeerinfo","params":[]}' "$ref_url" 2>/dev/null) + peer=$(echo "$pinfo" | jq -r '.result[0].addr // empty' 2>/dev/null) + fi + if [ -z "$peer" ] || [ "$peer" = "null" ]; then + peer=$(bitcoin_pick_seed_peer "$chain_slug") || return 0 + fi + resp=$(bitcoin_rpc_call "addnode" "[\"$peer\", \"onetry\"]") + if echo "$resp" | jq -e '.error == null' >/dev/null 2>&1; then + echo "Issued harmless addnode onetry to $peer" + fi +} + +report_block_catchup_rate() { + local gap_change="$1" + local interval="$2" + local block_time_ms="$3" + local gap_remaining="$4" + local label="${5:-blocks}" + + local block_time_sec realtime_units effective catchup_rate + block_time_sec=$(echo "scale=6; $block_time_ms / 1000" | bc) + realtime_units=$(echo "scale=6; $interval / $block_time_sec" | bc) + effective=$(echo "scale=6; $realtime_units - $gap_change" | bc) + catchup_rate=$(echo "scale=3; $effective / $realtime_units" | bc) + + echo "First measurement: $label behind = $((gap_remaining - gap_change)) $label" + echo "Second measurement: $label behind = $gap_remaining $label" + echo "Gap change in $interval seconds: $gap_change $label" + echo "Calculated catchup rate: ${catchup_rate}x realtime" + + if (( $(echo "$catchup_rate < 0" | bc -l) )); then + local falling_behind_rate + falling_behind_rate=$(echo "scale=2; -1 * $catchup_rate" | bc) + echo -e "${RED}Node is FALLING BEHIND by ${falling_behind_rate}x realtime${NC}" + echo -e "${RED}The gap increased by $gap_change $label over $interval seconds${NC}" + return 1 + elif (( $(echo "$catchup_rate < 1" | bc -l) )); then + echo -e "${YELLOW}WARNING: Node is processing at ${catchup_rate}x realtime${NC}" + echo -e "${YELLOW}This is too slow - node will never catch up to chain head${NC}" + return 2 + else + echo -e "${GREEN}Node is processing at ${catchup_rate}x realtime${NC}" + local time_to_catchup + time_to_catchup=$(echo "scale=0; $gap_remaining * $block_time_sec / ($catchup_rate - 1)" | bc) + echo -e "${GREEN}Estimated time until synced:${NC}" + s_to_human_readable "$time_to_catchup" + return 0 + fi +} + +bitcoin_catchup() { + local chain_slug="$1" + local interval="$2" + local block_time_ms info1 info2 blocks1 headers1 blocks2 headers2 + local ref1 ref2 use_pseudo=false gap1 gap2 gap_change + + block_time_ms=$($BASEPATH/reference-rpc-endpoint.sh --block-time-ms "$chain_slug" 2>/dev/null) || block_time_ms="600000" + + echo "Checking sync status for $compose_base (bitcoin-family)..." + info1=$(bitcoin_rpc_call "getblockchaininfo") + if [ -z "$info1" ]; then + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + fi + read -r blocks1 headers1 < <(bitcoin_parse_blocks_headers "$info1") || { + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + } + + ref1=$(bitcoin_reference_height "$chain_slug") + local ref_rc1=$? + if [ "$ref_rc1" -eq 2 ]; then + use_pseudo=true + ref1=$headers1 + echo "Reference RPC unavailable (Unknown network) — using local headers as pseudo-reference" + elif [ "$ref_rc1" -ne 0 ]; then + use_pseudo=true + ref1=$headers1 + echo "Reference RPC unavailable — using local headers as pseudo-reference" + fi + + gap1=$((ref1 - blocks1)) + echo "Current chain head is $gap1 blocks behind reference" + s_to_human_readable $(echo "scale=0; $gap1 * $block_time_ms / 1000" | bc) + + echo "Measuring progress over $interval seconds..." + sleep "$interval" + + info2=$(bitcoin_rpc_call "getblockchaininfo") + if [ -z "$info2" ]; then + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + fi + read -r blocks2 headers2 < <(bitcoin_parse_blocks_headers "$info2") || { + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + } + + if $use_pseudo; then + ref2=$headers2 + else + ref2=$(bitcoin_reference_height "$chain_slug") + local ref_rc2=$? + if [ "$ref_rc2" -ne 0 ]; then + ref2=$headers2 + use_pseudo=true + fi + fi + + gap2=$((ref2 - blocks2)) + gap_change=$((gap2 - gap1)) + + report_block_catchup_rate "$gap_change" "$interval" "$block_time_ms" "$gap2" "blocks" + local rc=$? + if [ "$rc" -eq 1 ] && [ -n "$chain_slug" ]; then + bitcoin_try_addnode "$chain_slug" + fi + exit "$rc" +} + +ripple_rpc_call() { + local url + url=$(compose_rpc_url) || return 1 + curl -L --ipv4 -m 5 -s -X POST -H "Content-Type: application/json" \ + --data '{"method":"server_info"}' "$url" +} + +ripple_validated_seq() { + local resp="$1" seq + seq=$(echo "$resp" | jq -r '.result.info.validated_ledger.seq // .info.validated_ledger.seq // empty' 2>/dev/null) + if [[ ! "$seq" =~ ^[0-9]+$ ]]; then + return 1 + fi + echo "$seq" +} + +ripple_reference_seq() { + local chain_slug="$1" ref_urls ref_url resp + ref_urls=$($BASEPATH/reference-rpc-endpoint.sh --chain "$chain_slug" 2>/dev/null) || return 1 + ref_url=$(echo "$ref_urls" | awk '{print $1}') + [ -z "$ref_url" ] && return 1 + resp=$(curl -L --ipv4 -m 5 -s -X POST -H "Content-Type: application/json" \ + --data '{"method":"server_info"}' "$ref_url") + ripple_validated_seq "$resp" +} + +ripple_catchup() { + local chain_slug="$1" + local interval="$2" + local block_time_ms resp1 resp2 seq1 seq2 ref1 ref2 gap1 gap2 gap_change + + block_time_ms=$($BASEPATH/reference-rpc-endpoint.sh --block-time-ms "$chain_slug" 2>/dev/null) || block_time_ms="4000" + + echo "Checking sync status for $compose_base (ripple)..." + resp1=$(ripple_rpc_call) + if [ -z "$resp1" ]; then + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + fi + seq1=$(ripple_validated_seq "$resp1") || { + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + } + + ref1=$(ripple_reference_seq "$chain_slug") || ref1="" + if [ -z "$ref1" ]; then + gap1=0 + echo "Reference RPC unavailable — measuring local validated-ledger progress only" + else + gap1=$((ref1 - seq1)) + echo "Current chain head is $gap1 ledgers behind reference" + s_to_human_readable $(echo "scale=0; $gap1 * $block_time_ms / 1000" | bc) + fi + + echo "Measuring progress over $interval seconds..." + sleep "$interval" + + resp2=$(ripple_rpc_call) + if [ -z "$resp2" ]; then + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + fi + seq2=$(ripple_validated_seq "$resp2") || { + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + } + + ref2=$(ripple_reference_seq "$chain_slug") || ref2="" + if [ -z "$ref1" ] || [ -z "$ref2" ]; then + local seq_gain=$((seq2 - seq1)) + local realtime_units effective catchup_rate + realtime_units=$(echo "scale=6; $interval * 1000 / $block_time_ms" | bc) + effective=$(echo "scale=6; $seq_gain" | bc) + catchup_rate=$(echo "scale=3; $effective / $realtime_units" | bc) + echo "Validated ledger gain in $interval seconds: $seq_gain ledgers" + echo "Calculated catchup rate: ${catchup_rate}x realtime" + if (( $(echo "$catchup_rate < 1" | bc -l) )); then + if (( $(echo "$catchup_rate < 0" | bc -l) )); then + echo -e "${RED}Node is FALLING BEHIND${NC}" + exit 1 + fi + echo -e "${YELLOW}WARNING: Node is processing at ${catchup_rate}x realtime${NC}" + exit 2 + fi + echo -e "${GREEN}Node is processing at ${catchup_rate}x realtime${NC}" + exit 0 + fi + + gap2=$((ref2 - seq2)) + gap_change=$((gap2 - gap1)) + + report_block_catchup_rate "$gap_change" "$interval" "$block_time_ms" "$gap2" "ledgers" + exit $? +} + +cosmos_catching_up() { + local url status catching_up + url=$(compose_rpc_url) || return 1 + status=$(curl -L --ipv4 -m 5 -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","id":1,"method":"status"}' "$url") + catching_up=$(echo "$status" | jq -r '.result.sync_info.catching_up // .sync_info.catching_up // empty' 2>/dev/null) + echo "$catching_up" +} + +cosmos_catchup() { + local catching_up + + echo "Checking sync status for $compose_base (cosmos/cometbft)..." + catching_up=$(cosmos_catching_up) + if [ "$catching_up" = "true" ]; then + echo "CometBFT reports catching_up=true (node is still syncing)" + elif [ -z "$catching_up" ]; then + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + fi + + # Standard timestamp-lag algorithm via timestamp.sh (cosmos /status source). + echo "Debug: Running ./timestamp.sh $compose_base" + timestamp_output=$(./timestamp.sh "$compose_base" 2>&1) + timestamp_exit_code=$? + + echo "Debug: timestamp.sh output: '$timestamp_output'" + echo "Debug: timestamp.sh exit code: $timestamp_exit_code" + + latest_block_timestamp_decimal=$(echo "$timestamp_output" | grep -o '[0-9]\+' | head -1) + + if [[ $timestamp_exit_code -ne 0 || -z "$latest_block_timestamp_decimal" ]]; then + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + fi + + current_time=$(date +%s) + time_difference=$((current_time - latest_block_timestamp_decimal)) + + echo "Current chain head is $time_difference seconds behind real time" + if [ "$catching_up" = "true" ]; then + echo "(catching_up=true)" + fi + s_to_human_readable $time_difference + + echo "Measuring progress over $seconds_to_measure seconds..." + sleep $seconds_to_measure + + catching_up=$(cosmos_catching_up) + if [ "$catching_up" = "true" ]; then + echo "CometBFT still reports catching_up=true" + fi + + echo "Debug: Running ./timestamp.sh $compose_base (second measurement)" + timestamp_output=$(./timestamp.sh "$compose_base" 2>&1) + timestamp_exit_code=$? + + echo "Debug: timestamp.sh output (second): '$timestamp_output'" + echo "Debug: timestamp.sh exit code (second): $timestamp_exit_code" + + latest_block_timestamp2=$(echo "$timestamp_output" | grep -o '[0-9]\+' | head -1) + + if [[ $timestamp_exit_code -ne 0 || -z "$latest_block_timestamp2" ]]; then + echo -e "${RED}rpc-not-open (expected mid-sync for stage-sync/bootstrap clients — avalanchego, erigon/reth stage-sync, statesync bootstraps); re-run at chainhead${NC}" + exit 1 + fi + + current_time2=$(date +%s) + time_difference2=$((current_time2 - latest_block_timestamp2)) + + gap_change=$((time_difference2 - time_difference)) + + effective_catchup_seconds=$((seconds_to_measure - gap_change)) + catchup_rate=$(echo "scale=3; $effective_catchup_seconds / $seconds_to_measure" | bc) + + echo "First measurement: Time behind = $time_difference seconds" + echo "Second measurement: Time behind = $time_difference2 seconds" + if [ "$catching_up" = "true" ]; then + echo "(catching_up=true)" + fi + echo "Gap change in $seconds_to_measure seconds: $gap_change seconds" + echo "Calculated catchup rate: $catchup_rate× realtime" + + if (( $(echo "$catchup_rate < 0" | bc -l) )); then + falling_behind_rate=$(echo "scale=2; -1 * $catchup_rate" | bc) + echo -e "${RED}Node is FALLING BEHIND by $falling_behind_rate× realtime${NC}" + echo -e "${RED}The gap increased by $gap_change seconds over $seconds_to_measure seconds${NC}" + exit 1 + elif (( $(echo "$catchup_rate < 1" | bc -l) )); then + echo -e "${YELLOW}WARNING: Node is processing at $catchup_rate× realtime${NC}" + echo -e "${YELLOW}This is too slow - node will never catch up to chain head${NC}" + exit 2 + else + echo -e "${GREEN}Node is processing at $catchup_rate× realtime${NC}" + time_to_catchup=$(echo "scale=0; $time_difference2 / ($catchup_rate - 1)" | bc) + echo -e "${GREEN}Estimated time until synced:${NC}" + s_to_human_readable $time_to_catchup + exit 0 + fi +} + +protocol=$(resolve_protocol) +chain_slug=$(compose_chain_slug) + +case "$protocol" in + bitcoin) + bitcoin_catchup "$chain_slug" "$seconds_to_measure" + ;; + ripple) + ripple_catchup "$chain_slug" "$seconds_to_measure" + ;; + cosmos) + cosmos_catchup + ;; + eth|starknet|aztec) + ;; + *) + echo "unsupported protocol $protocol for catchup" + exit 1 + ;; +esac + +# --- eth / starknet / aztec: original timestamp-lag path (unchanged) --- + seconds_to_measure=${2:-10} # First measurement @@ -55,7 +544,7 @@ fi current_time=$(date +%s) time_difference=$((current_time - latest_block_timestamp_decimal)) -# Check for reasonable time difference +# Check for reasonable time difference echo "Current chain head is $time_difference seconds behind real time" s_to_human_readable $time_difference diff --git a/timestamp.sh b/timestamp.sh index cb80cf74..ba27a1ed 100755 --- a/timestamp.sh +++ b/timestamp.sh @@ -16,7 +16,15 @@ else PROTO="https" fi -pathlist=$(cat $BASEPATH/$1.yml | grep -oP "stripprefix\.prefixes.*?/\K[^\"]+") +compose_yml="$BASEPATH/$1.yml" +pathlist=$(cat "$compose_yml" | grep -oP "stripprefix\.prefixes.*?/\K[^\"]+") + +# Resolve protocol from x-upstreams chain label (reuse sync-status / catchup idiom). +chain_slug=$(grep -oP '^\s*chain:\s*\K\S+' "$compose_yml" 2>/dev/null | head -1) +protocol="" +if [ -n "$chain_slug" ]; then + protocol=$($BASEPATH/reference-rpc-endpoint.sh --protocol "$chain_slug" 2>/dev/null) || protocol="" +fi for path in $pathlist; do include=true @@ -30,6 +38,40 @@ for path in $pathlist; do RPC_URL="$PROTO://$DOMAIN/$path" response_file=$(mktemp) + path_protocol="$protocol" + if [ -z "$path_protocol" ]; then + if echo "$path" | grep -qi "starknet"; then + path_protocol="starknet" + elif echo "$path" | grep -qi "aztec"; then + path_protocol="aztec" + elif echo "$path" | grep -qiE "cosmos|babylon|haqq|gaiad|tac"; then + path_protocol="cosmos" + else + path_protocol="eth" + fi + fi + + if [ "$path_protocol" = "cosmos" ]; then + http_status_code=$(curl -L --ipv4 -m 1 -s -X POST -w "%{http_code}" -o "$response_file" \ + -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","id":1,"method":"status"}' "$RPC_URL") + if [ $? -eq 0 ] && [[ $http_status_code -eq 200 ]]; then + response=$(cat "$response_file") + latest_block_time=$(echo "$response" | jq -r '.result.sync_info.latest_block_time // .sync_info.latest_block_time' 2>/dev/null) + rm -f "$response_file" + if [ -z "$latest_block_time" ] || [ "$latest_block_time" = "null" ]; then + exit 1 + fi + latest_block_timestamp_decimal=$(date -d "$latest_block_time" +%s 2>/dev/null) + if [ -z "$latest_block_timestamp_decimal" ]; then + exit 1 + fi + echo "$latest_block_timestamp_decimal" + exit 0 + fi + rm -f "$response_file" + exit 1 + fi + # Detect Starknet vs Ethereum vs Aztec based on path if echo "$path" | grep -qi "starknet"; then rpc_method='{"jsonrpc":"2.0","method":"starknet_getBlockWithTxHashes","params":["latest"],"id":1}'