Files
proxmox/scripts/verify/sync-blockscout-address-labels-from-registry.sh
defiQUG 7ac74f432b chore: sync docs, config schemas, scripts, and meta task alignment
- Institutional / JVMTM / reserve-provenance / GRU transport + standards JSON
- Validation and verify scripts (Blockscout labels, x402, GRU preflight, P1 local path)
- Wormhole wiring in AGENTS, MCP_SETUP, MASTER_INDEX, 04-configuration README
- Meta docs, integration gaps, live verification log, architecture updates
- CI validate-config workflow updates

Operator/LAN items, submodule working trees, and public token-aggregation edge
routes remain follow-up (see TODOS_CONSOLIDATED P1).

Made-with: Cursor
2026-03-31 22:31:39 -07:00

285 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Sync address labels from DBIS institutional registry JSON into Blockscout.
# Default: print the planned action only. Use --apply to write.
#
# Supported modes:
# http - POST JSON to a Blockscout-compatible label endpoint
# db - write primary labels directly into Blockscout Postgres address_names
# auto - prefer HTTP if the route exists; otherwise fall back to DB sync
#
# Registry shape: config/dbis-institutional/schemas/address-registry-entry.schema.json
#
# Env (HTTP mode):
# BLOCKSCOUT_BASE_URL default https://explorer.d-bis.org
# BLOCKSCOUT_LABEL_PATH default /api/v1/labels
# BLOCKSCOUT_API_KEY optional Bearer token if the endpoint requires it
#
# Env (DB mode):
# BLOCKSCOUT_DB_SSH_HOST default root@192.168.11.12
# BLOCKSCOUT_DB_CT_VMID default 5000
# BLOCKSCOUT_DB_CONTAINER default blockscout-postgres
# BLOCKSCOUT_DB_USER default blockscout
# BLOCKSCOUT_DB_NAME default blockscout
#
# Usage:
# bash scripts/verify/sync-blockscout-address-labels-from-registry.sh file1.json [file2.json ...]
# bash scripts/verify/sync-blockscout-address-labels-from-registry.sh --from-dir config/dbis-institutional/registry
# bash scripts/verify/sync-blockscout-address-labels-from-registry.sh --apply --mode=db --from-dir config/dbis-institutional/registry
#
set -euo pipefail
APPLY=0
FROM_DIR=""
SYNC_MODE="${BLOCKSCOUT_SYNC_MODE:-auto}"
while [[ $# -gt 0 ]]; do
case "$1" in
--apply) APPLY=1; shift ;;
--from-dir=*)
FROM_DIR="${1#*=}"
shift
;;
--from-dir)
FROM_DIR="${2:?}"
shift 2
;;
--mode=*)
SYNC_MODE="${1#*=}"
shift
;;
--mode)
SYNC_MODE="${2:?}"
shift 2
;;
-h|--help)
sed -n '2,42p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*) break ;;
esac
done
case "$SYNC_MODE" in
auto|http|db) ;;
*)
echo "error: --mode must be one of: auto, http, db" >&2
exit 1
;;
esac
BASE_URL="${BLOCKSCOUT_BASE_URL:-https://explorer.d-bis.org}"
LABEL_PATH="${BLOCKSCOUT_LABEL_PATH:-/api/v1/labels}"
URL="${BASE_URL%/}${LABEL_PATH}"
BLOCKSCOUT_DB_SSH_HOST="${BLOCKSCOUT_DB_SSH_HOST:-root@192.168.11.12}"
BLOCKSCOUT_DB_CT_VMID="${BLOCKSCOUT_DB_CT_VMID:-5000}"
BLOCKSCOUT_DB_CONTAINER="${BLOCKSCOUT_DB_CONTAINER:-blockscout-postgres}"
BLOCKSCOUT_DB_USER="${BLOCKSCOUT_DB_USER:-blockscout}"
BLOCKSCOUT_DB_NAME="${BLOCKSCOUT_DB_NAME:-blockscout}"
files=()
if [[ -n "$FROM_DIR" ]]; then
if [[ ! -d "$FROM_DIR" ]]; then
echo "error: --from-dir not a directory: $FROM_DIR" >&2
exit 1
fi
while IFS= read -r -d '' f; do
files+=("$f")
done < <(find "$FROM_DIR" -maxdepth 1 -name '*.json' -print0 2>/dev/null || true)
else
files=("$@")
fi
if [[ ${#files[@]} -eq 0 ]]; then
echo "usage: $0 [--apply] [--mode auto|http|db] [--from-dir DIR] <registry.json> [...]" >&2
echo " or: REGISTRY_DIR=... $0 --from-dir \"\$REGISTRY_DIR\"" >&2
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "error: jq is required" >&2
exit 1
fi
sql_quote() {
printf "%s" "$1" | sed "s/'/''/g"
}
probe_http_sync() {
local tmp status
tmp=$(mktemp)
status=$(curl -sS -o "$tmp" -w '%{http_code}' -X POST "$URL" -H 'Content-Type: application/json' --data '{}' || true)
local body
body=$(cat "$tmp")
rm -f "$tmp"
# 2xx/4xx except 404 means the route exists and reached a handler.
if [[ "$status" =~ ^(200|201|202|204|400|401|403|405|409|415|422)$ ]]; then
return 0
fi
if [[ "$status" == "404" && "$body" == *'"error":"Not found"'* ]]; then
return 1
fi
return 1
}
run_db_sql() {
local sql="$1"
ssh "$BLOCKSCOUT_DB_SSH_HOST" \
"pct exec ${BLOCKSCOUT_DB_CT_VMID} -- docker exec -i ${BLOCKSCOUT_DB_CONTAINER} psql -U ${BLOCKSCOUT_DB_USER} -d ${BLOCKSCOUT_DB_NAME} -v ON_ERROR_STOP=1 -f -" \
<<<"$sql"
}
emit_http() {
local display="$1"
local address="$2"
local label="$3"
local ltype="$4"
local body
body=$(jq -nc --arg a "$address" --arg l "$label" --arg t "$ltype" '{address:$a,label:$l,type:$t}')
if [[ "$APPLY" -ne 1 ]]; then
echo "PLAN mode=http file=$display"
echo " POST $URL"
echo " $body"
return 0
fi
local hdr=()
if [[ -n "${BLOCKSCOUT_API_KEY:-}" ]]; then
hdr=(-H "Authorization: Bearer ${BLOCKSCOUT_API_KEY}" -H "Content-Type: application/json")
else
hdr=(-H "Content-Type: application/json")
fi
echo "POST $display -> $URL"
curl -fsS "${hdr[@]}" -X POST "$URL" -d "$body" >/dev/null
echo "ok http $address"
}
emit_db() {
local display="$1"
local address="$2"
local label="$3"
local ltype="$4"
local normalized_address="${address#0x}"
normalized_address="${normalized_address#0X}"
normalized_address=$(printf '%s' "$normalized_address" | tr '[:upper:]' '[:lower:]')
if [[ ! "$normalized_address" =~ ^[0-9a-f]{40}$ ]]; then
echo "skip (invalid address): $display" >&2
return 0
fi
local metadata
metadata=$(jq -nc \
--arg source "registry" \
--arg registryFile "$display" \
--arg labelType "$ltype" \
'{source:$source,registryFile:$registryFile,labelType:$labelType}')
local sql
sql=$(cat <<SQL
INSERT INTO public.address_names (
address_hash,
name,
"primary",
inserted_at,
updated_at,
metadata
)
VALUES (
decode('$(sql_quote "$normalized_address")', 'hex'),
'$(sql_quote "$label")',
true,
NOW(),
NOW(),
'$(sql_quote "$metadata")'::jsonb
)
ON CONFLICT (address_hash) WHERE "primary" = true
DO UPDATE SET
name = EXCLUDED.name,
updated_at = EXCLUDED.updated_at,
metadata = COALESCE(public.address_names.metadata, '{}'::jsonb) || COALESCE(EXCLUDED.metadata, '{}'::jsonb);
SQL
)
if [[ "$APPLY" -ne 1 ]]; then
echo "PLAN mode=db file=$display"
echo " SSH $BLOCKSCOUT_DB_SSH_HOST -> CT $BLOCKSCOUT_DB_CT_VMID -> ${BLOCKSCOUT_DB_NAME}.public.address_names"
echo " address=$address label=$label type=$ltype"
return 0
fi
echo "UPSERT $display -> ${BLOCKSCOUT_DB_NAME}.public.address_names"
run_db_sql "$sql" >/dev/null
echo "ok db $address"
}
emit_one() {
local file="$1"
local display="${2:-$file}"
local mode="$3"
local blob
blob=$(jq -e . "$file" 2>/dev/null) || { echo "skip (invalid JSON): $display" >&2; return 0; }
local status address label ltype
status=$(echo "$blob" | jq -r '.status // "active"')
[[ "$status" == "active" ]] || { echo "skip (status=$status): $display" >&2; return 0; }
address=$(echo "$blob" | jq -r '.address // empty')
label=$(echo "$blob" | jq -r '.blockscout.label // empty')
ltype=$(echo "$blob" | jq -r '.blockscout.labelType // "contract"')
if [[ -z "$address" || -z "$label" ]]; then
echo "skip (missing address or blockscout.label): $display" >&2
return 0
fi
case "$mode" in
http) emit_http "$display" "$address" "$label" "$ltype" ;;
db) emit_db "$display" "$address" "$label" "$ltype" ;;
*)
echo "error: unsupported mode: $mode" >&2
return 1
;;
esac
}
SELECTED_MODE="$SYNC_MODE"
if [[ "$SYNC_MODE" == "auto" && "$APPLY" -eq 1 ]]; then
if probe_http_sync; then
SELECTED_MODE="http"
else
SELECTED_MODE="db"
fi
fi
for f in "${files[@]}"; do
[[ -f "$f" ]] || { echo "skip (not a file): $f" >&2; continue; }
if jq -e 'type == "object" and (.address|type=="string")' "$f" &>/dev/null; then
emit_one "$f" "$f" "$SELECTED_MODE" || exit 1
elif jq -e 'type == "array"' "$f" &>/dev/null; then
tmpdir=$(mktemp -d)
len=$(jq 'length' "$f")
for ((i = 0; i < len; i++)); do
jq ".[$i]" "$f" >"$tmpdir/single.json"
emit_one "$tmpdir/single.json" "$f (item $i)" "$SELECTED_MODE" || { rm -rf "$tmpdir"; exit 1; }
done
rm -rf "$tmpdir"
else
echo "skip (not object or array of objects): $f" >&2
fi
done
if [[ "$APPLY" -ne 1 ]]; then
echo ""
echo "Dry run only. Re-run with --apply. Use --mode=db for this self-hosted Blockscout when /api/v1 labels is not available."
else
echo ""
echo "Completed in mode=$SELECTED_MODE."
fi