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
This commit is contained in:
defiQUG
2026-03-31 22:31:39 -07:00
parent 00880304d4
commit 7ac74f432b
948 changed files with 47476 additions and 490 deletions

View File

@@ -19,7 +19,7 @@ fi
LXCS=(
"${RTGS_ORCH_VMID:-5805} ${RTGS_ORCH_HOSTNAME:-rtgs-orchestrator-1} ${RTGS_ORCH_IP:-192.168.11.93} 4096 2 24"
"${RTGS_FX_VMID:-5806} ${RTGS_FX_HOSTNAME:-rtgs-fx-1} ${RTGS_FX_IP:-192.168.11.94} 4096 2 24"
"${RTGS_FX_VMID:-5806} ${RTGS_FX_HOSTNAME:-rtgs-fx-1} ${RTGS_FX_IP:-192.168.11.99} 4096 2 24"
"${RTGS_LIQ_VMID:-5807} ${RTGS_LIQ_HOSTNAME:-rtgs-liquidity-1} ${RTGS_LIQ_IP:-192.168.11.95} 4096 2 24"
)

View File

@@ -20,6 +20,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
REPO_ROOT="$PROJECT_ROOT"
SMOM="${PROJECT_ROOT}/smom-dbis-138"
DRY_RUN=""
@@ -37,9 +38,21 @@ if [[ ! -f "$SMOM/.env" ]]; then
echo "Missing $SMOM/.env. Abort." >&2
exit 1
fi
set -a
source "$SMOM/.env"
set +a
if [[ -f "$SMOM/scripts/lib/deployment/dotenv.sh" ]]; then
# shellcheck disable=SC1090
source "$SMOM/scripts/lib/deployment/dotenv.sh"
load_deployment_env --repo-root "$SMOM"
else
had_nounset=0
if [[ $- == *u* ]]; then
had_nounset=1
set +u
fi
set -a
source "$SMOM/.env"
set +a
(( had_nounset )) && set -u
fi
# 2) RPC: Core (2101) only — no Public fallback for deployments
RPC="${RPC_URL_138:-http://192.168.11.211:8545}"
@@ -169,4 +182,4 @@ done
echo ""
echo "Running on-chain verification..."
"$PROJECT_ROOT/scripts/verify/check-contracts-on-chain-138.sh" "$RPC"
"$REPO_ROOT/scripts/verify/check-contracts-on-chain-138.sh" "$RPC"

View File

@@ -4,12 +4,19 @@
# - Remove .env.local on CT 7801; install .env with PORTAL_LOCAL_LOGIN_* + NEXTAUTH_SECRET.
# - Run sync-sankofa-portal-7801.sh (rebuild portal with updated auth.ts).
#
# Keycloak SSO: If repo .env defines KEYCLOAK_CLIENT_SECRET (and optional KEYCLOAK_URL / REALM /
# CLIENT_ID), those values are written into the pushed .env. After sync, sankofa-portal-merge-keycloak-env-from-repo.sh
# runs to mirror OIDC settings into .env.local as well. Without KEYCLOAK_CLIENT_SECRET in .env,
# use keycloak-sankofa-ensure-client-redirects*.sh then the merge script.
#
# Usage: ./scripts/deployment/enable-sankofa-portal-login-7801.sh [--dry-run]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=/dev/null
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" 2>/dev/null || true
# shellcheck source=/dev/null
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
PROXMOX_HOST="${PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}"
@@ -20,6 +27,9 @@ LOCAL_EMAIL="${PORTAL_LOCAL_LOGIN_EMAIL:-portal@sankofa.nexus}"
if [[ "${1:-}" == "--dry-run" ]]; then
echo "[DRY-RUN] Would patch Keycloak ${VMID_KC}, write .env on ${VMID_PORTAL}, sync portal"
if [[ -n "${KEYCLOAK_CLIENT_SECRET:-}" ]]; then
echo "[DRY-RUN] Would run sankofa-portal-merge-keycloak-env-from-repo.sh after sync (KEYCLOAK_CLIENT_SECRET is set)"
fi
exit 0
fi
@@ -34,10 +44,10 @@ NEXT_PUBLIC_GRAPHQL_WS_ENDPOINT=ws://192.168.11.50:4000/graphql-ws
NEXTAUTH_URL=https://portal.sankofa.nexus
NEXTAUTH_SECRET=${NEXTAUTH_SEC}
KEYCLOAK_URL=https://keycloak.sankofa.nexus
KEYCLOAK_REALM=master
KEYCLOAK_CLIENT_ID=sankofa-portal
KEYCLOAK_CLIENT_SECRET=
KEYCLOAK_URL=${KEYCLOAK_URL:-https://keycloak.sankofa.nexus}
KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-sankofa-portal}
KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-}
PORTAL_LOCAL_LOGIN_EMAIL=${LOCAL_EMAIL}
PORTAL_LOCAL_LOGIN_PASSWORD=${GEN_PASS}
@@ -83,12 +93,24 @@ echo ""
echo "📤 Syncing portal source + rebuild…"
bash "${SCRIPT_DIR}/sync-sankofa-portal-7801.sh"
if [[ -n "${KEYCLOAK_CLIENT_SECRET:-}" ]]; then
echo ""
echo "🔐 Mirroring Keycloak OIDC into portal .env + .env.local (merge script)…"
bash "${SCRIPT_DIR}/sankofa-portal-merge-keycloak-env-from-repo.sh"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Sign in at https://portal.sankofa.nexus (NEXTAUTH_URL)"
echo " Email: ${LOCAL_EMAIL}"
echo " Password: ${GEN_PASS}"
echo ""
echo "SSO: Add NPM host keycloak.sankofa.nexus → ${IP_KEYCLOAK:-192.168.11.52}:8080, then create Keycloak"
echo " confidential client sankofa-portal; set KEYCLOAK_CLIENT_SECRET in .env and re-sync."
if [[ -n "${KEYCLOAK_CLIENT_SECRET:-}" ]]; then
echo "SSO: Keycloak client secret was taken from repo .env; portal CT updated via merge script."
else
echo "SSO: No KEYCLOAK_CLIENT_SECRET in repo .env — local login only until you:"
echo " 1) NPM: keycloak.sankofa.nexus → ${IP_KEYCLOAK:-192.168.11.52}:8080"
echo " 2) ./scripts/deployment/keycloak-sankofa-ensure-client-redirects-via-proxmox-pct.sh"
echo " 3) ./scripts/deployment/sankofa-portal-merge-keycloak-env-from-repo.sh"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# Create or reset the Keycloak master-realm "admin" user directly in PostgreSQL (Keycloak 24 Quarkus
# has no bootstrap-admin CLI). Use when user_entity is empty or you must rotate the admin password.
#
# Requirements: SSH to Proxmox, pct to PostgreSQL CT (default 7803), sudo postgres psql on DB "keycloak".
# Does not print the password to stdout; writes it to a file you pass, or merges into repo .env.
#
# Usage:
# KEYCLOAK_ADMIN_PASSWORD='your-secure-value' ./scripts/deployment/keycloak-bootstrap-or-reset-master-admin-db.sh
# ./scripts/deployment/keycloak-bootstrap-or-reset-master-admin-db.sh # generates password → .env
#
# Env:
# PROXMOX_HOST (default 192.168.11.11), POSTGRES_CT_VMID (7803), KEYCLOAK_CT_VMID (7802)
# KEYCLOAK_ADMIN_USERNAME (default admin), KEYCLOAK_DB_NAME (keycloak)
# KEYCLOAK_ADMIN_PASSWORD — if unset, a random alphanumeric password is generated
# WRITE_ENV_FILE — path to .env to upsert KEYCLOAK_ADMIN + KEYCLOAK_ADMIN_PASSWORD (default: repo .env)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=/dev/null
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
PROXMOX_HOST="${PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}"
POSTGRES_CT_VMID="${POSTGRES_CT_VMID:-7803}"
KEYCLOAK_CT_VMID="${KEYCLOAK_CT_VMID:-${SANKOFA_KEYCLOAK_VMID:-7802}}"
ADMIN_USER="${KEYCLOAK_ADMIN:-admin}"
DB_NAME="${KEYCLOAK_DB_NAME:-keycloak}"
WRITE_ENV_FILE="${WRITE_ENV_FILE:-${PROJECT_ROOT}/.env}"
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
gen_pass() {
openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32
}
NEW_PASS="${KEYCLOAK_ADMIN_PASSWORD:-}"
if [[ -z "$NEW_PASS" ]]; then
NEW_PASS="$(gen_pass)"
fi
SQL_GEN="$(mktemp)"
trap 'rm -f "$SQL_GEN"' EXIT
python3 - "$NEW_PASS" "$ADMIN_USER" >"$SQL_GEN" <<'PY'
import json, base64, hashlib, os, sys, time, uuid
password, admin_user = sys.argv[1], sys.argv[2]
salt = os.urandom(16)
iters = 27500
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iters)
secret_data = json.dumps(
{
"value": base64.b64encode(dk).decode(),
"salt": base64.b64encode(salt).decode(),
"additionalParameters": {},
},
separators=(",", ":"),
)
credential_data = json.dumps(
{"hashIterations": iters, "algorithm": "pbkdf2-sha256", "additionalParameters": {}},
separators=(",", ":"),
)
ts = int(time.time() * 1000)
user_id = str(uuid.uuid4())
cred_id = str(uuid.uuid4())
def q(s: str) -> str:
return s.replace("'", "''")
sd, cd = q(secret_data), q(credential_data)
user_esc = q(admin_user)
print("BEGIN;")
print(
f"""
DO $do$
DECLARE
rid TEXT;
r_admin TEXT;
r_default TEXT;
uid TEXT;
n INT;
v_secret TEXT := '{sd}';
v_cred TEXT := '{cd}';
BEGIN
SELECT id INTO rid FROM realm WHERE name = 'master' LIMIT 1;
IF rid IS NULL THEN
RAISE EXCEPTION 'realm master not found';
END IF;
SELECT id INTO r_admin FROM keycloak_role
WHERE realm_id = rid AND name = 'admin' AND client IS NULL LIMIT 1;
SELECT id INTO r_default FROM keycloak_role
WHERE realm_id = rid AND name = 'default-roles-master' AND client IS NULL LIMIT 1;
IF r_admin IS NULL OR r_default IS NULL THEN
RAISE EXCEPTION 'missing admin or default-roles-master role';
END IF;
SELECT COUNT(*) INTO n FROM user_entity WHERE realm_id = rid AND username = '{user_esc}';
IF n = 0 THEN
INSERT INTO user_entity (
id, email, email_constraint, email_verified, enabled, realm_id, username, created_timestamp, not_before
) VALUES (
'{user_id}',
'{user_esc}@sankofa.nexus',
'{user_esc}@sankofa.nexus',
true,
true,
rid,
'{user_esc}',
{ts},
0
);
uid := '{user_id}';
INSERT INTO user_role_mapping (role_id, user_id) VALUES (r_admin, uid);
INSERT INTO user_role_mapping (role_id, user_id) VALUES (r_default, uid);
ELSE
SELECT id INTO uid FROM user_entity WHERE realm_id = rid AND username = '{user_esc}' LIMIT 1;
END IF;
DELETE FROM credential WHERE user_id = uid AND type = 'password';
INSERT INTO credential (id, salt, type, user_id, created_date, user_label, secret_data, credential_data, priority)
VALUES (
'{cred_id}',
NULL,
'password',
uid,
{ts},
NULL,
v_secret,
v_cred,
10
);
END
$do$;
"""
)
print("COMMIT;")
PY
ssh "${SSH_OPTS[@]}" "root@${PROXMOX_HOST}" \
"pct exec ${POSTGRES_CT_VMID} -- sudo -u postgres psql -d ${DB_NAME} -v ON_ERROR_STOP=1 -f -" <"$SQL_GEN"
ssh "${SSH_OPTS[@]}" "root@${PROXMOX_HOST}" \
"pct exec ${KEYCLOAK_CT_VMID} -- systemctl restart keycloak"
echo "[ok] Keycloak master admin user '${ADMIN_USER}' password set in DB; Keycloak restarted on CT ${KEYCLOAK_CT_VMID}."
if [[ -n "${WRITE_ENV_FILE}" ]]; then
python3 - "${WRITE_ENV_FILE}" "${NEW_PASS}" "${ADMIN_USER}" <<'PY'
import re
import sys
from pathlib import Path
path, password, admin_user = Path(sys.argv[1]), sys.argv[2], sys.argv[3]
text = path.read_text() if path.exists() else ""
def upsert_line(body: str, key: str, value: str) -> str:
line = f"{key}={value}"
if re.search(rf"^{re.escape(key)}=", body, flags=re.M):
return re.sub(rf"^{re.escape(key)}=.*$", line, body, flags=re.M, count=1)
if body and not body.endswith("\n"):
body += "\n"
return body + line + "\n"
text = upsert_line(text, "KEYCLOAK_ADMIN", admin_user)
text = upsert_line(text, "KEYCLOAK_ADMIN_PASSWORD", password)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
PY
echo "[ok] Updated ${WRITE_ENV_FILE} (KEYCLOAK_ADMIN, KEYCLOAK_ADMIN_PASSWORD)."
fi

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env bash
# Create or update Keycloak OIDC client (default sankofa-portal) with portal/admin redirect URIs.
# Runs Admin API against http://127.0.0.1:8080 inside the Keycloak CT (no LAN to NPM required).
# After a new client is created, repo .env gets KEYCLOAK_CLIENT_SECRET — push it to CT 7801 with
# ./scripts/deployment/sankofa-portal-merge-keycloak-env-from-repo.sh
#
# If the client is created, prints a JSON footer line for the operator .env:
# __SANKOFA_KEYCLOAK_FOOTER__{"created":true,"clientSecret":"..."}
#
# Loads repo .env. Env: PROXMOX_HOST, KEYCLOAK_CT_VMID (7802), KEYCLOAK_* .
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=/dev/null
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
if [ -f "$PROJECT_ROOT/.env" ]; then
set +u
set -a
# shellcheck source=/dev/null
source "$PROJECT_ROOT/.env" 2>/dev/null || true
set +a
set -u
fi
PROXMOX_HOST="${PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}"
KEYCLOAK_CT_VMID="${KEYCLOAK_CT_VMID:-${SANKOFA_KEYCLOAK_VMID:-7802}}"
REALM="${KEYCLOAK_REALM:-master}"
CLIENT_ID="${KEYCLOAK_CLIENT_ID:-sankofa-portal}"
ADMIN_USER="${KEYCLOAK_ADMIN:-admin}"
ADMIN_PASS="${KEYCLOAK_ADMIN_PASSWORD:-}"
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
if [ -z "$ADMIN_PASS" ]; then
echo "KEYCLOAK_ADMIN_PASSWORD is not set in .env" >&2
exit 1
fi
OUT="$(
ssh "${SSH_OPTS[@]}" "root@${PROXMOX_HOST}" \
"pct exec ${KEYCLOAK_CT_VMID} -- env KC_PASS=\"${ADMIN_PASS}\" ADMUSER=\"${ADMIN_USER}\" REALM=\"${REALM}\" CLIENT_ID=\"${CLIENT_ID}\" python3 -u -" <<'PY'
import json
import os
import secrets
import urllib.error
import urllib.parse
import urllib.request
base = "http://127.0.0.1:8080"
realm = os.environ["REALM"]
client_id = os.environ["CLIENT_ID"]
admin_user = os.environ["ADMUSER"]
password = os.environ["KC_PASS"]
desired_redirects = [
"https://portal.sankofa.nexus/*",
"https://portal.sankofa.nexus",
"https://admin.sankofa.nexus/*",
"https://admin.sankofa.nexus",
]
desired_origins = [
"https://portal.sankofa.nexus",
"https://admin.sankofa.nexus",
]
created_new = False
portal_secret = None
def post_form(url: str, data: dict) -> dict:
body = urllib.parse.urlencode(data).encode()
req = urllib.request.Request(url, data=body, method="POST")
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode())
tok = post_form(
f"{base}/realms/master/protocol/openid-connect/token",
{
"grant_type": "password",
"client_id": "admin-cli",
"username": admin_user,
"password": password,
},
)
access = tok.get("access_token")
if not access:
raise SystemExit(f"token failed: {tok}")
list_url = f"{base}/admin/realms/{realm}/clients?clientId={urllib.parse.quote(client_id)}"
r = urllib.request.Request(list_url, headers={"Authorization": f"Bearer {access}"})
with urllib.request.urlopen(r, timeout=60) as resp:
clients = json.loads(resp.read().decode())
if not clients:
portal_secret = secrets.token_urlsafe(48)
new_client = {
"clientId": client_id,
"name": "Sankofa Portal",
"enabled": True,
"protocol": "openid-connect",
"publicClient": False,
"standardFlowEnabled": True,
"implicitFlowEnabled": False,
"directAccessGrantsEnabled": False,
"serviceAccountsEnabled": False,
"redirectUris": desired_redirects,
"webOrigins": desired_origins,
"secret": portal_secret,
}
cr = urllib.request.Request(
f"{base}/admin/realms/{realm}/clients",
data=json.dumps(new_client).encode(),
headers={"Authorization": f"Bearer {access}", "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(cr, timeout=120) as resp:
if resp.getcode() not in (200, 201):
raise SystemExit(f"create client unexpected HTTP {resp.getcode()}")
except urllib.error.HTTPError as e:
err = e.read().decode() if e.fp else str(e)
raise SystemExit(f"POST client failed HTTP {e.code}: {err}") from e
created_new = True
r = urllib.request.Request(list_url, headers={"Authorization": f"Bearer {access}"})
with urllib.request.urlopen(r, timeout=60) as resp:
clients = json.loads(resp.read().decode())
if not clients:
raise SystemExit("client create did not persist")
internal_id = clients[0]["id"]
get_url = f"{base}/admin/realms/{realm}/clients/{internal_id}"
r2 = urllib.request.Request(get_url, headers={"Authorization": f"Bearer {access}"})
with urllib.request.urlopen(r2, timeout=60) as resp:
full = json.loads(resp.read().decode())
redirs = list(dict.fromkeys((full.get("redirectUris") or []) + desired_redirects))
origins = list(dict.fromkeys((full.get("webOrigins") or []) + desired_origins))
full["redirectUris"] = redirs
full["webOrigins"] = origins
if portal_secret and not full.get("secret"):
full["secret"] = portal_secret
put = urllib.request.Request(
get_url,
data=json.dumps(full).encode(),
headers={"Authorization": f"Bearer {access}", "Content-Type": "application/json"},
method="PUT",
)
try:
with urllib.request.urlopen(put, timeout=120) as resp:
code = resp.getcode()
except urllib.error.HTTPError as e:
err = e.read().decode() if e.fp else str(e)
raise SystemExit(f"PUT failed HTTP {e.code}: {err}") from e
if code not in (200, 204):
raise SystemExit(f"PUT unexpected HTTP {code}")
action = "Created" if created_new else "Updated"
print(f"{action} Keycloak client {client_id!r} (redirect URIs + web origins).", flush=True)
footer = {"created": bool(created_new)}
if portal_secret:
footer["clientSecret"] = portal_secret
print("__SANKOFA_KEYCLOAK_FOOTER__" + json.dumps(footer), flush=True)
PY
)"
echo "$OUT" | sed '/__SANKOFA_KEYCLOAK_FOOTER__/d'
FOOTER=$(echo "$OUT" | grep '^__SANKOFA_KEYCLOAK_FOOTER__' | sed 's/^__SANKOFA_KEYCLOAK_FOOTER__//' || true)
if [[ -n "$FOOTER" ]]; then
CREATED="$(echo "$FOOTER" | jq -r '.created // false')"
SEC="$(echo "$FOOTER" | jq -r '.clientSecret // empty')"
if [[ "$CREATED" == "true" ]] && [[ -n "$SEC" ]] && [[ "$SEC" != "null" ]]; then
python3 - "${PROJECT_ROOT}/.env" "${SEC}" <<'PY'
import re
import sys
from pathlib import Path
path, sec = Path(sys.argv[1]), sys.argv[2]
text = path.read_text() if path.exists() else ""
def upsert(body: str, key: str, value: str) -> str:
line = f"{key}={value}"
if re.search(rf"^{re.escape(key)}=", body, flags=re.M):
return re.sub(rf"^{re.escape(key)}=.*$", line, body, flags=re.M, count=1)
if body and not body.endswith("\n"):
body += "\n"
return body + line + "\n"
text = upsert(text, "KEYCLOAK_CLIENT_SECRET", sec)
path.write_text(text)
PY
echo "[ok] Wrote KEYCLOAK_CLIENT_SECRET to .env (portal Keycloak OIDC path enabled)." >&2
fi
fi

View File

@@ -36,9 +36,22 @@ fi
# 3) Load env for RPC and nonce checks (no secrets printed)
[[ -f "${PROJECT_ROOT}/config/ip-addresses.conf" ]] && source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
set -a
source "$SMOM/.env"
set +a
if [[ -f "$SMOM/scripts/lib/deployment/dotenv.sh" ]]; then
# shellcheck disable=SC1090
source "$SMOM/scripts/lib/deployment/dotenv.sh"
load_deployment_env --repo-root "$SMOM"
else
local_had_nounset=0
if [[ $- == *u* ]]; then
local_had_nounset=1
set +u
fi
set -a
# shellcheck disable=SC1090
source "$SMOM/.env"
set +a
(( local_had_nounset )) && set -u
fi
RPC="${RPC_URL_138:-http://192.168.11.211:8545}"
if [[ -z "${PRIVATE_KEY:-}" ]]; then

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Rotate the Chain 138 oracle publisher off the shared deployer key by provisioning
# a dedicated transmitter key, adding it to the oracle aggregator, updating CT 3500,
# and removing the legacy deployer transmitter after the new key confirms an update.
#
# Usage:
# bash scripts/deployment/rotate-oracle-publisher-transmitter.sh [--dry-run]
#
# Env overrides:
# PROXMOX_NODE_IP default 192.168.11.12
# ORACLE_VMID default 3500
# ORACLE_SECRET_FILE default ~/.secure-secrets/chain138-oracle-publisher.env
# ORACLE_AGGREGATOR_ADDRESS default 0x99b3511a2d315a497c8112c1fdd8d508d4b1e506
# ORACLE_FUND_WEI default 100000000000000000 (0.1 native token)
# NEW_ORACLE_PRIVATE_KEY optional pre-generated 0x-prefixed key
#
set -euo pipefail
DRY_RUN=0
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=1
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
had_nounset=0
if [[ $- == *u* ]]; then
had_nounset=1
set +u
fi
set -a
source scripts/lib/load-project-env.sh >/dev/null 2>&1
set +a
if [[ "$had_nounset" -eq 1 ]]; then
set -u
fi
RPC="${DEPLOY_RPC_URL_138:-${RPC_URL_138:-http://192.168.11.211:8545}}"
NODE_IP="${PROXMOX_NODE_IP:-192.168.11.12}"
ORACLE_VMID="${ORACLE_VMID:-3500}"
AGG="${ORACLE_AGGREGATOR_ADDRESS:-0x99b3511a2d315a497c8112c1fdd8d508d4b1e506}"
SECRET_FILE="${ORACLE_SECRET_FILE:-$HOME/.secure-secrets/chain138-oracle-publisher.env}"
DEPLOYER_ADDR="$(cast wallet address --private-key "$PRIVATE_KEY")"
NEW_KEY="${NEW_ORACLE_PRIVATE_KEY:-0x$(openssl rand -hex 32)}"
NEW_ADDR="$(cast wallet address --private-key "$NEW_KEY")"
ORACLE_FUND_WEI="${ORACLE_FUND_WEI:-100000000000000000}"
echo "Oracle transmitter rotation"
echo " node: $NODE_IP"
echo " vmid: $ORACLE_VMID"
echo " aggregator: $AGG"
echo " admin: $DEPLOYER_ADDR"
echo " new signer: $NEW_ADDR"
echo " secret file: $SECRET_FILE"
echo " fund wei: $ORACLE_FUND_WEI"
if [[ "$DRY_RUN" -eq 1 ]]; then
exit 0
fi
mkdir -p "$(dirname "$SECRET_FILE")"
umask 077
cat >"$SECRET_FILE" <<EOF
CHAIN138_ORACLE_PUBLISHER_PRIVATE_KEY=$NEW_KEY
CHAIN138_ORACLE_PUBLISHER_ADDRESS=$NEW_ADDR
CHAIN138_ORACLE_AGGREGATOR_ADDRESS=$AGG
CHAIN138_ORACLE_ROTATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
EOF
if [[ "$(cast call "$AGG" 'isTransmitter(address)(bool)' "$NEW_ADDR" --rpc-url "$RPC")" != "true" ]]; then
cast send "$AGG" 'addTransmitter(address)' "$NEW_ADDR" \
--rpc-url "$RPC" \
--private-key "$PRIVATE_KEY" \
--legacy \
--gas-price 1000000000 \
>/dev/null
fi
new_balance="$(cast balance "$NEW_ADDR" --rpc-url "$RPC")"
if [[ "$new_balance" -lt "$ORACLE_FUND_WEI" ]]; then
cast send "$NEW_ADDR" \
--value "$ORACLE_FUND_WEI" \
--rpc-url "$RPC" \
--private-key "$PRIVATE_KEY" \
--legacy \
--gas-price 1000000000 \
>/dev/null
fi
ssh -o BatchMode=yes -o StrictHostKeyChecking=no "root@$NODE_IP" "\
pct exec $ORACLE_VMID -- bash -lc 'set -euo pipefail; \
ENV=/opt/oracle-publisher/.env; \
grep -q \"^PRIVATE_KEY=\" \$ENV && sed -i \"s|^PRIVATE_KEY=.*|PRIVATE_KEY=$NEW_KEY|\" \$ENV || echo \"PRIVATE_KEY=$NEW_KEY\" >> \$ENV; \
systemctl restart oracle-publisher.service; \
systemctl is-active oracle-publisher.service >/dev/null'"
echo "Waiting for new transmitter to confirm an oracle update..."
tx_hash=""
for _ in {1..24}; do
line="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=no "root@$NODE_IP" "pct exec $ORACLE_VMID -- journalctl -u oracle-publisher.service -n 20 --no-pager | grep 'Transaction confirmed:' | tail -n 1" || true)"
if [[ -n "$line" ]]; then
tx_hash="$(printf '%s' "$line" | grep -oE '0x[a-fA-F0-9]{64}' | tail -n 1 || true)"
fi
if [[ -n "$tx_hash" ]]; then
tx_from="$(cast receipt "$tx_hash" --rpc-url "$RPC" | awk '/^from /{print $2}')"
if [[ "${tx_from,,}" == "${NEW_ADDR,,}" ]]; then
break
fi
fi
sleep 5
done
if [[ -z "$tx_hash" ]]; then
echo "ERROR: No confirmed oracle update observed from the new transmitter." >&2
exit 1
fi
tx_from="$(cast receipt "$tx_hash" --rpc-url "$RPC" | awk '/^from /{print $2}')"
if [[ "${tx_from,,}" != "${NEW_ADDR,,}" ]]; then
echo "ERROR: Latest confirmed oracle update was not sent by the new transmitter: $tx_from" >&2
exit 1
fi
if [[ "$(cast call "$AGG" 'isTransmitter(address)(bool)' "$DEPLOYER_ADDR" --rpc-url "$RPC")" == "true" ]]; then
cast send "$AGG" 'removeTransmitter(address)' "$DEPLOYER_ADDR" \
--rpc-url "$RPC" \
--private-key "$PRIVATE_KEY" \
--legacy \
--gas-price 1000000000 \
>/dev/null
fi
echo "Rotation complete."
echo " new transmitter: $NEW_ADDR"
echo " confirmed tx: $tx_hash"
echo " deployer signer removed from transmitter set."

View File

@@ -17,8 +17,32 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
REPO_ROOT="$PROJECT_ROOT"
SMOM="$PROJECT_ROOT/smom-dbis-138"
load_smom_env() {
if [[ -f "$SMOM/scripts/lib/deployment/dotenv.sh" ]]; then
# shellcheck disable=SC1090
source "$SMOM/scripts/lib/deployment/dotenv.sh"
load_deployment_env --repo-root "$SMOM"
return 0
fi
if [[ -f "$SMOM/.env" ]]; then
local had_nounset=0
if [[ $- == *u* ]]; then
had_nounset=1
set +u
fi
set -a
# shellcheck disable=SC1090
source "$SMOM/.env"
set +a
(( had_nounset )) && set -u
return 0
fi
return 1
}
DRY_RUN=""
SKIP_MIRROR=""
SKIP_MESH=""
@@ -82,8 +106,7 @@ if [[ -z "$SKIP_REGISTER_GRU" ]]; then
if [[ -n "$DRY_RUN" ]]; then
echo "[DRY-RUN] cd $SMOM && forge script script/deploy/RegisterGRUCompliantTokens.s.sol --rpc-url \$RPC_URL_138 --broadcast --private-key \$PRIVATE_KEY --with-gas-price 1000000000"
else
if [[ -f "$SMOM/.env" ]]; then
set -a; source "$SMOM/.env"; set +a
if load_smom_env; then
# Fallback: Register script expects CUSDT_ADDRESS_138/CUSDC_ADDRESS_138; use COMPLIANT_USDT/COMPLIANT_USDC if set
[[ -z "${CUSDT_ADDRESS_138:-}" && -n "${COMPLIANT_USDT:-}" ]] && export CUSDT_ADDRESS_138="$COMPLIANT_USDT"
[[ -z "${CUSDC_ADDRESS_138:-}" && -n "${COMPLIANT_USDC:-}" ]] && export CUSDC_ADDRESS_138="$COMPLIANT_USDC"
@@ -108,8 +131,8 @@ if [[ -z "$SKIP_VERIFY" ]]; then
if [[ -n "$DRY_RUN" ]]; then
echo "[DRY-RUN] $PROJECT_ROOT/scripts/verify/check-contracts-on-chain-138.sh"
else
[[ -f "$SMOM/.env" ]] && set -a && source "$SMOM/.env" && set +a
"$PROJECT_ROOT/scripts/verify/check-contracts-on-chain-138.sh" "${RPC_URL_138:-}" || true
load_smom_env || true
"$REPO_ROOT/scripts/verify/check-contracts-on-chain-138.sh" "${RPC_URL_138:-}" || true
fi
echo ""
else

View File

@@ -3,11 +3,12 @@
# See docs/07-ccip/CW_DEPLOY_AND_WIRE_RUNBOOK.md and docs/00-meta/CW_BRIDGE_TASK_LIST.md.
#
# Usage:
# ./scripts/deployment/run-cw-remaining-steps.sh [--dry-run] [--deploy] [--update-mapping] [--verify]
# ./scripts/deployment/run-cw-remaining-steps.sh [--dry-run] [--deploy] [--update-mapping] [--verify] [--verify-hard-peg]
# --dry-run Run deploy-cw in dry-run mode (print commands only).
# --deploy Run deploy-cw on all chains (requires RPC/PRIVATE_KEY in smom-dbis-138/.env).
# --update-mapping Update config/token-mapping-multichain.json from CWUSDT_*/CWUSDC_* in .env.
# --verify For each chain with CWUSDT_* set, check MINTER_ROLE/BURNER_ROLE on cW* for CW_BRIDGE_*.
# --verify-hard-peg Check Avalanche hard-peg bridge controls for cWUSDT/cWUSDC.
# With no options, runs --dry-run then --update-mapping (if any CWUSDT_* in .env).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -18,6 +19,7 @@ DRY_RUN=false
DO_DEPLOY=false
DO_UPDATE_MAPPING=false
DO_VERIFY=false
DO_VERIFY_HARD_PEG=false
for a in "$@"; do
case "$a" in
@@ -25,9 +27,10 @@ for a in "$@"; do
--deploy) DO_DEPLOY=true ;;
--update-mapping) DO_UPDATE_MAPPING=true ;;
--verify) DO_VERIFY=true ;;
--verify-hard-peg) DO_VERIFY_HARD_PEG=true ;;
esac
done
if ! $DRY_RUN && ! $DO_DEPLOY && ! $DO_UPDATE_MAPPING && ! $DO_VERIFY; then
if ! $DRY_RUN && ! $DO_DEPLOY && ! $DO_UPDATE_MAPPING && ! $DO_VERIFY && ! $DO_VERIFY_HARD_PEG; then
DRY_RUN=true
DO_UPDATE_MAPPING=true
fi
@@ -37,7 +40,9 @@ if [[ ! -f "$SMOM/.env" ]]; then
exit 1
fi
set -a
set +u
source "$SMOM/.env"
set -u
set +a
# Chain name (env suffix) -> chainId for 138 -> chain pairs
@@ -135,4 +140,57 @@ if $DO_VERIFY; then
done
fi
call_or_unavailable() {
local rpc="$1"
local address="$2"
local signature="$3"
shift 3
if [[ -z "$rpc" || -z "$address" ]]; then
printf 'unavailable\n'
return
fi
cast call "$address" "$signature" "$@" --rpc-url "$rpc" 2>/dev/null || printf 'legacy-or-unavailable\n'
}
if $DO_VERIFY_HARD_PEG; then
echo "=== Verify Avalanche hard-peg bridge state ==="
CHAIN138_L1_BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}"
AVAX_CW_BRIDGE="${CW_BRIDGE_AVALANCHE:-}"
RESERVE_VERIFIER="${CW_RESERVE_VERIFIER_CHAIN138:-}"
AVALANCHE_SELECTOR_VALUE="${AVALANCHE_SELECTOR:-6433500567565415381}"
CW_CANONICAL_USDT_ADDR="${CW_CANONICAL_USDT:-${COMPLIANT_USDT_ADDRESS:-${CUSDT_ADDRESS_138:-}}}"
CW_CANONICAL_USDC_ADDR="${CW_CANONICAL_USDC:-${COMPLIANT_USDC_ADDRESS:-${CUSDC_ADDRESS_138:-}}}"
echo " Chain 138 L1 bridge: $CHAIN138_L1_BRIDGE"
echo " Avalanche cW bridge: $AVAX_CW_BRIDGE"
echo " Reserve verifier: ${RESERVE_VERIFIER:-unconfigured}"
echo " Avalanche selector: $AVALANCHE_SELECTOR_VALUE"
if [[ -n "$CHAIN138_L1_BRIDGE" ]]; then
echo " L1 bridge reserveVerifier(): $(call_or_unavailable "$RPC_URL_138" "$CHAIN138_L1_BRIDGE" "reserveVerifier()(address)")"
fi
for entry in "cUSDT:$CW_CANONICAL_USDT_ADDR:$CW_MAX_OUTSTANDING_USDT_AVALANCHE" "cUSDC:$CW_CANONICAL_USDC_ADDR:$CW_MAX_OUTSTANDING_USDC_AVALANCHE"; do
IFS=":" read -r label token desired_cap <<<"$entry"
if [[ -z "$token" ]]; then
echo " $label: canonical token not set"
continue
fi
echo " $label supportedCanonicalToken(): $(call_or_unavailable "$RPC_URL_138" "$CHAIN138_L1_BRIDGE" "supportedCanonicalToken(address)(bool)" "$token")"
echo " $label maxOutstanding(): $(call_or_unavailable "$RPC_URL_138" "$CHAIN138_L1_BRIDGE" "maxOutstanding(address,uint64)(uint256)" "$token" "$AVALANCHE_SELECTOR_VALUE")"
if [[ -n "$desired_cap" ]]; then
echo " $label desired maxOutstanding env: $desired_cap"
fi
echo " $label tokenPairFrozen(): $(call_or_unavailable "$AVALANCHE_RPC_URL" "$AVAX_CW_BRIDGE" "tokenPairFrozen(address)(bool)" "$token")"
if [[ -n "$RESERVE_VERIFIER" ]]; then
echo " $label verifier tokenConfigs(): $(call_or_unavailable "$RPC_URL_138" "$RESERVE_VERIFIER" "tokenConfigs(address)(bool,address,bool,bool,bool)" "$token" | tr '\n' ' ' | xargs)"
echo " $label verifier getVerificationStatus(): $(call_or_unavailable "$RPC_URL_138" "$RESERVE_VERIFIER" "getVerificationStatus(address,uint64)((bool,bool,bool,bool,bool,bool,bool,bool,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))" "$token" "$AVALANCHE_SELECTOR_VALUE" | tr '\n' ' ' | xargs)"
fi
done
echo " Avalanche destinationFrozen(138): $(call_or_unavailable "$AVALANCHE_RPC_URL" "$AVAX_CW_BRIDGE" "destinationFrozen(uint64)(bool)" 138)"
fi
echo "Done. See docs/07-ccip/CW_DEPLOY_AND_WIRE_RUNBOOK.md for Phase E (relay and E2E)."

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Merge Keycloak OIDC settings from the operator repo .env into LXC 7801
# (/opt/sankofa-portal/.env and .env.local). Uses base64 over ssh for the client secret
# so special characters in KEYCLOAK_CLIENT_SECRET do not break the remote shell.
#
# Requires KEYCLOAK_CLIENT_SECRET (and loads repo .env via load-project-env when sourced
# from repo root, or export vars before calling).
#
# Run after creating the confidential client (e.g. keycloak-sankofa-ensure-client-redirects*.sh)
# or when rotating KEYCLOAK_CLIENT_SECRET.
#
# Usage:
# ./scripts/deployment/sankofa-portal-merge-keycloak-env-from-repo.sh
# ./scripts/deployment/sankofa-portal-merge-keycloak-env-from-repo.sh --dry-run
# ./scripts/deployment/sankofa-portal-merge-keycloak-env-from-repo.sh --no-restart
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=/dev/null
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" 2>/dev/null || true
# shellcheck source=/dev/null
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
PROXMOX_HOST="${PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}"
VMID="${SANKOFA_PORTAL_VMID:-7801}"
CT_DIR="${SANKOFA_PORTAL_CT_DIR:-/opt/sankofa-portal}"
SERVICE_NAME="${SANKOFA_PORTAL_SERVICE:-sankofa-portal}"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new)
KC_URL="${KEYCLOAK_URL:-https://keycloak.sankofa.nexus}"
KC_REALM="${KEYCLOAK_REALM:-master}"
KC_CID="${KEYCLOAK_CLIENT_ID:-sankofa-portal}"
DRY_RUN=false
NO_RESTART=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--no-restart) NO_RESTART=true ;;
esac
done
if [[ -z "${KEYCLOAK_CLIENT_SECRET:-}" ]]; then
echo "ERROR: KEYCLOAK_CLIENT_SECRET is not set. Add it to repo .env (from Keycloak admin or" >&2
echo " keycloak-sankofa-ensure-client-redirects-via-proxmox-pct.sh when the client is created)," >&2
echo " then: source scripts/lib/load-project-env.sh && $0" >&2
exit 1
fi
if $DRY_RUN; then
echo "[DRY-RUN] Would upsert KEYCLOAK_* on CT ${VMID} ${CT_DIR}/.env and .env.local"
echo "[DRY-RUN] KEYCLOAK_URL=${KC_URL} KEYCLOAK_REALM=${KC_REALM} KEYCLOAK_CLIENT_ID=${KC_CID}"
echo "[DRY-RUN] restart: $([[ "$NO_RESTART" == true ]] && echo no || echo yes)"
exit 0
fi
B64="$(printf '%s' "$KEYCLOAK_CLIENT_SECRET" | base64 -w0)"
ssh "${SSH_OPTS[@]}" "root@${PROXMOX_HOST}" \
"pct exec ${VMID} -- env KCSEC_B64='${B64}' KC_URL='${KC_URL}' KC_REALM='${KC_REALM}' KC_CID='${KC_CID}' CT_DIR='${CT_DIR}' python3 -" <<'PY'
import base64
import os
import re
from pathlib import Path
sec = base64.b64decode(os.environ["KCSEC_B64"]).decode("utf-8")
ct = Path(os.environ["CT_DIR"])
keys = {
"KEYCLOAK_URL": os.environ["KC_URL"],
"KEYCLOAK_REALM": os.environ["KC_REALM"],
"KEYCLOAK_CLIENT_ID": os.environ["KC_CID"],
"KEYCLOAK_CLIENT_SECRET": sec,
}
def upsert(text: str, k: str, v: str) -> str:
line = f"{k}={v}"
if re.search(rf"^{re.escape(k)}=", text, flags=re.M):
return re.sub(rf"^{re.escape(k)}=.*$", line, text, flags=re.M, count=1)
if text and not text.endswith("\n"):
text += "\n"
return text + line + "\n"
for fname in (".env", ".env.local"):
p = ct / fname
body = p.read_text() if p.exists() else ""
for k, v in keys.items():
body = upsert(body, k, v)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(body)
print(f"upserted Keycloak keys in {p}")
PY
if [[ "$NO_RESTART" == true ]]; then
echo "[ok] Keycloak OIDC vars merged on CT ${VMID} (no service restart)."
else
ssh "${SSH_OPTS[@]}" "root@${PROXMOX_HOST}" \
"pct exec ${VMID} -- systemctl restart ${SERVICE_NAME} && pct exec ${VMID} -- systemctl is-active ${SERVICE_NAME}"
echo "[ok] Keycloak OIDC vars merged on CT ${VMID}; ${SERVICE_NAME} restarted."
fi

View File

@@ -3,8 +3,9 @@
# then run RegisterGRUCompliantTokens to register all c* as GRU in UniversalAssetRegistry.
#
# Addresses are from docs/11-references/CONTRACT_ADDRESSES_REFERENCE.md and TOKENS_AND_NETWORKS_MINTABLE_TO_DEPLOYER.md.
# Usage: ./scripts/deployment/set-dotenv-c-tokens-and-register-gru.sh [--no-register]
# Usage: ./scripts/deployment/set-dotenv-c-tokens-and-register-gru.sh [--no-register] [--register-v2]
# --no-register Only update .env; do not run RegisterGRUCompliantTokens.
# --register-v2 After V1 registration, also register staged V2 cUSDT/cUSDC using RegisterGRUCompliantTokensV2.
#
# Note: RegisterGRUCompliantTokens requires (1) broadcast account has REGISTRAR_ROLE, and (2) the
# UniversalAssetRegistry *implementation* (not just proxy) exposes registerGRUCompliantAsset.
@@ -21,7 +22,11 @@ SMOM="$PROJECT_ROOT/smom-dbis-138"
ENV_FILE="$SMOM/.env"
RUN_REGISTER=1
for a in "$@"; do [[ "$a" == "--no-register" ]] && RUN_REGISTER=0; done
RUN_REGISTER_V2=0
for a in "$@"; do
[[ "$a" == "--no-register" ]] && RUN_REGISTER=0
[[ "$a" == "--register-v2" ]] && RUN_REGISTER_V2=1
done
if [[ ! -f "$ENV_FILE" ]]; then
echo "Missing $ENV_FILE. Create it first (e.g. copy from .env.example)." >&2
@@ -45,6 +50,10 @@ set_env_var "COMPLIANT_USDT" "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22"
set_env_var "COMPLIANT_USDC" "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
set_env_var "CUSDT_ADDRESS_138" "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22"
set_env_var "CUSDC_ADDRESS_138" "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
set_env_var "COMPLIANT_USDT_V2" "0x8d342d321DdEe97D0c5011DAF8ca0B59DA617D29"
set_env_var "COMPLIANT_USDC_V2" "0x1ac3F4942a71E86A9682D91837E1E71b7BACdF99"
set_env_var "CUSDT_V2_ADDRESS_138" "0x8d342d321DdEe97D0c5011DAF8ca0B59DA617D29"
set_env_var "CUSDC_V2_ADDRESS_138" "0x1ac3F4942a71E86A9682D91837E1E71b7BACdF99"
# cEURC (TOKENS_AND_NETWORKS_MINTABLE_TO_DEPLOYER)
set_env_var "CEURC_ADDRESS_138" "0x8085961F9cF02b4d800A3c6d386D31da4B34266a"
@@ -68,7 +77,7 @@ set_env_var "WETH10" "0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f"
set_env_var "LINK_TOKEN" "0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03"
set_env_var "CCIP_FEE_TOKEN" "0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03"
echo "Done. Set: COMPLIANT_USDT, COMPLIANT_USDC, all C*_ADDRESS_138 (cUSDT, cUSDC, cEURC, cEURT, cGBPC, cGBPT, cAUDC, cJPYC, cCHFC, cCADC, cXAUC, cXAUT), UNIVERSAL_ASSET_REGISTRY, WETH9, WETH10, LINK_TOKEN, CCIP_FEE_TOKEN."
echo "Done. Set: COMPLIANT_USDT, COMPLIANT_USDC, COMPLIANT_USDT_V2, COMPLIANT_USDC_V2, all C*_ADDRESS_138 (including staged V2 addresses), UNIVERSAL_ASSET_REGISTRY, WETH9, WETH10, LINK_TOKEN, CCIP_FEE_TOKEN."
echo "All c* on explorer.d-bis.org/tokens must be GRU-registered. See docs/04-configuration/EXPLORER_TOKENS_GRU_POLICY.md."
if [[ "$RUN_REGISTER" -eq 0 ]]; then
@@ -85,4 +94,15 @@ RPC="${RPC_URL_138:-http://192.168.11.211:8545}"
export RPC_URL_138="$RPC"
(cd "$SMOM" && forge script script/deploy/RegisterGRUCompliantTokens.s.sol \
--rpc-url "$RPC_URL_138" --broadcast --private-key "$PRIVATE_KEY" --with-gas-price 1000000000)
if [[ "$RUN_REGISTER_V2" -eq 1 ]]; then
echo ""
echo "=== Registering staged c* V2 inventory as GRU (RegisterGRUCompliantTokensV2) ==="
(cd "$SMOM" && forge script script/deploy/RegisterGRUCompliantTokensV2.s.sol \
--rpc-url "$RPC_URL_138" --broadcast --private-key "$PRIVATE_KEY" --with-gas-price 1000000000)
else
echo ""
echo "V2 note: COMPLIANT_USDT_V2 / COMPLIANT_USDC_V2 are staged in .env but not auto-registered."
echo "Use --register-v2 once downstream registry consumers are ready for version-aware symbols."
fi
echo "=== Done. ==="