Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions scripts/check_balance.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# RustChain wallet balance check with consistent error handling.
#
# Exit codes (documented scheme):
# 0 = success (balance fetched and printed)
# 1 = usage error (missing/invalid wallet address)
# 2 = network error (DNS/connect/timeout)
# 3 = bad response (non-200 HTTP, malformed JSON, missing field)
# 4 = wallet not found (HTTP 404 from the RPC)
#
# The script NEVER prints a balance it did not actually receive. Every
# failure path prints a distinct error to stderr and exits non-zero.
set -euo pipefail

NODE_URL="${RUSTCHAIN_NODE_URL:-https://rustchain.org}"
CURL_TIMEOUT="${RUSTCHAIN_CURL_TIMEOUT:-15}"

usage() {
echo "Usage: $0 <wallet_address>" >&2
echo "Checks the RTC balance of a wallet via the RustChain RPC." >&2
echo "Exit codes: 0 ok, 1 usage, 2 network, 3 bad response, 4 wallet not found." >&2
exit 1
}

[ $# -eq 1 ] || usage
WALLET="$1"

# Basic address sanity check (RTC addresses are 32-64 base58-ish chars).
if ! [[ "$WALLET" =~ ^[A-Za-z0-9]{20,64}$ ]]; then
echo "ERROR: invalid wallet address format: '$WALLET'" >&2
exit 1
fi

err_file="$(mktemp)"
code_file="$(mktemp)"
trap 'rm -f "$err_file" "$code_file"' EXIT

# 1) Network layer: curl connection failures are distinct network errors (exit 2).
body_file="$(mktemp)"
trap 'rm -f "$err_file" "$code_file" "$body_file"' EXIT

if ! http_out="$(curl -sS --max-time "$CURL_TIMEOUT" -w '%{http_code}' -o "$body_file" "$NODE_URL/wallet/balance?miner_id=$WALLET" 2>"$err_file")"; then
rc=$?
msg="$(cat "$err_file")"
if grep -qiE 'could not resolve|connection refused|connection reset|timed out|timeout|no route|failed to connect|could not connect|server' "$err_file" 2>/dev/null; then
echo "ERROR: network error - ${msg:-curl exit $rc}" >&2
exit 2
fi
echo "ERROR: request failed (curl exit $rc) - $msg" >&2
exit 3
fi

code="$http_out"

# 2) HTTP layer: 404 = wallet not found (exit 4); other non-200 = bad response (exit 3).
if [ "$code" = "404" ]; then
echo "ERROR: wallet '$WALLET' not found (HTTP 404)" >&2
exit 4
fi
if [ "$code" != "200" ]; then
echo "ERROR: server returned HTTP $code" >&2
exit 3
fi

# 3) Payload layer: strict JSON parse + required field check (exit 3).
if ! parsed="$(python3 -c '
import json, sys
d = json.load(open(sys.argv[1]))
if "amount_rtc" not in d and "balance_rtc" not in d:
raise SystemExit("missing balance field")
print(json.dumps({"wallet_id": sys.argv[2], "amount_rtc": d.get("amount_rtc", d.get("balance_rtc"))}))
' "$body_file" "$WALLET" 2>"$code_file")"; then
echo "ERROR: bad response - $(cat "$code_file")" >&2
exit 3
fi

echo "$parsed"
exit 0
58 changes: 58 additions & 0 deletions scripts/test_check_balance.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Tests for scripts/check_balance.sh - mocked HTTP, no real network.
# Run from the repo root: bash scripts/test_check_balance.sh
set -uo pipefail
cd "$(dirname "$0")/.."

PORT="${TEST_PORT:-18111}"
FAILURES=0
check() { # check <desc> <expected_rc> <actual_rc>
if [ "$3" -ne "$2" ]; then
echo "FAIL: $1 (expected rc=$2, got rc=$3)" >&2
FAILURES=$((FAILURES + 1))
else
echo "PASS: $1 (rc=$3)"
fi
}

# Mock RPC on $PORT
python3 - "$PORT" <<'EOF' &
import http.server, socketserver, sys
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if "goodwallet1234567890" in self.path:
body = b'{"amount_rtc": "42.5"}'; self.send_response(200)
elif "missingwallet12345678" in self.path:
body = b''; self.send_response(404)
elif "malformedwallet123456" in self.path:
body = b'not json'; self.send_response(200)
elif "emptyfieldwallet12345" in self.path:
body = b'{"hello": "world"}'; self.send_response(200)
else:
body = b''; self.send_response(500)
self.send_header("Content-Length", str(len(body))); self.end_headers(); self.wfile.write(body)
def log_message(self, *a): pass
class T(socketserver.ThreadingMixIn, http.server.HTTPServer): pass
T.allow_reuse_address = True
T(("127.0.0.1", int(sys.argv[1])), H).serve_forever()
EOF
SERVER_PID=$!
trap 'kill $SERVER_PID 2>/dev/null || true' EXIT
sleep 1

export RUSTCHAIN_NODE_URL="http://127.0.0.1:$PORT"
export RUSTCHAIN_CURL_TIMEOUT="3"
S=scripts/check_balance.sh
chmod +x "$S"

"$S" >/dev/null 2>&1; check "no args -> usage" 1 $?
"$S" '!!bad' >/dev/null 2>&1; check "bad address format" 1 $?
"$S" missingwallet12345678 >/dev/null 2>&1; check "404 -> wallet not found" 4 $?
"$S" malformedwallet123456 >/dev/null 2>&1; check "malformed JSON -> bad response" 3 $?
"$S" emptyfieldwallet12345 >/dev/null 2>&1; check "missing field -> bad response" 3 $?
out="$("$S" goodwallet1234567890 2>/dev/null)"; check "success -> rc 0" 0 $?
echo "$out" | grep -q '"amount_rtc": "42.5"' && echo "PASS: balance output" || { echo "FAIL: balance output" >&2; FAILURES=$((FAILURES + 1)); }
RUSTCHAIN_NODE_URL="http://127.0.0.1:1" "$S" goodwallet1234567890 >/dev/null 2>&1; check "unreachable -> network error" 2 $?

echo
if [ "$FAILURES" -eq 0 ]; then echo "ALL TESTS PASSED"; else echo "$FAILURES TEST(S) FAILED"; exit 1; fi