- 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
285 lines
7.9 KiB
Bash
Executable File
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
|