chore(repo): sync operator workspace (config, scripts, docs, multi-chain)
Some checks failed
Deploy to Phoenix / validate (push) Failing after 1s
Deploy to Phoenix / deploy (push) Has been skipped
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Has been skipped
Deploy to Phoenix / cloudflare (push) Has been skipped

Add optional Cosmos/Engine-X/act-runner templates, CWUSDC/EI-matrix tooling,
non-EVM route planner in multi-chain-execution (tests passing), token list and
extraction updates, and documentation (MetaMask matrix, GRU/CWUSDC packets).

Ignore institutional evidence tarballs/sha256 under reports/status.

Validated with: bash scripts/verify/run-all-validation.sh --skip-genesis

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-11 16:25:08 -07:00
parent a5f7400955
commit 4ebf2d7902
292 changed files with 21574 additions and 1146 deletions

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Resume mainnet cWUSDC EI matrix transfers from ei-matrix-cwusdc-send-last-idx.txt + 1.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$SCRIPT_DIR/send-cwusdc-ei-matrix-wallets.sh" --resume-next "$@"

View File

View File

@@ -0,0 +1,391 @@
#!/usr/bin/env bash
# Deploy/configure CWMultiTokenBridgeL2 receivers for active public cW chains.
# Defaults to dry-run. Use --apply to broadcast.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SMOM_ROOT="$PROJECT_ROOT/smom-dbis-138"
ENV_FILE="$SMOM_ROOT/.env"
REPORT_DIR="$PROJECT_ROOT/reports/status"
APPLY=false
FULL_FAMILY=false
CHAIN_FILTER=""
TS="$(date -u +%Y%m%dT%H%M%SZ)"
MANIFEST="$REPORT_DIR/cw-multitoken-l2-remediation-${TS}.jsonl"
usage() {
cat <<USAGE
Usage: $0 [--apply] [--full-family] [--chain <chainId>]
Without --apply this prints actions only. With --apply it broadcasts deployments
and configuration transactions, then updates smom-dbis-138/.env CW_BRIDGE_*.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--apply) APPLY=true ;;
--full-family) FULL_FAMILY=true ;;
--chain) CHAIN_FILTER="${2:-}"; shift ;;
--chain=*) CHAIN_FILTER="${1#*=}" ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;;
esac
shift
done
# shellcheck disable=SC1091
source "$PROJECT_ROOT/scripts/lib/load-project-env.sh" >/dev/null 2>&1 || true
[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY is required" >&2; exit 1; }
command -v cast >/dev/null 2>&1 || { echo "cast is required" >&2; exit 1; }
command -v forge >/dev/null 2>&1 || { echo "forge is required" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2; exit 1; }
mkdir -p "$REPORT_DIR"
: > "$MANIFEST"
DEPLOYER="${DEPLOYER_ADDRESS:-$(cast wallet address "$PRIVATE_KEY")}"
L1_BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}"
RPC_138="${RPC_URL_138:-${CHAIN138_RPC:-${CHAIN138_RPC_URL:-}}}"
[[ -n "$L1_BRIDGE" && "$L1_BRIDGE" != "0x0000000000000000000000000000000000000000" ]] || { echo "CW_L1_BRIDGE_CHAIN138 is required" >&2; exit 1; }
[[ -n "$RPC_138" ]] || { echo "RPC_URL_138/CHAIN138_RPC is required" >&2; exit 1; }
MINTER_ROLE="$(cast keccak "MINTER_ROLE")"
BURNER_ROLE="$(cast keccak "BURNER_ROLE")"
declare -A CHAIN_NAME=(
[10]="Optimism"
[25]="Cronos"
[56]="BSC"
[100]="Gnosis"
[137]="Polygon"
[8453]="Base"
[42161]="Arbitrum"
[42220]="Celo"
)
declare -A CHAIN_SUFFIX=(
[10]="OPTIMISM"
[25]="CRONOS"
[56]="BSC"
[100]="GNOSIS"
[137]="POLYGON"
[8453]="BASE"
[42161]="ARBITRUM"
[42220]="CELO"
)
declare -A CHAIN_SELECTOR=(
[10]="3734403246176062136"
[25]="1456215246176062136"
[56]="11344663589394136015"
[100]="465200170687744372"
[137]="4051577828743386545"
[8453]="15971525489660198786"
[42161]="4949039107694359620"
[42220]="1346049177634351622"
)
set_env_value() {
local file="$1" key="$2" value="$3"
if grep -qE "^${key}=" "$file"; then
sed -i "s|^${key}=.*|${key}=${value}|" "$file"
else
printf '\n%s=%s\n' "$key" "$value" >> "$file"
fi
}
var_value() {
local key="$1"
printf '%s' "${!key:-}"
}
first_env() {
local key value
for key in "$@"; do
value="$(var_value "$key")"
if [[ -n "$value" ]]; then
printf '%s\n' "$value"
return 0
fi
done
return 1
}
rpc_for_chain() {
local suffix="$1"
case "$suffix" in
OPTIMISM) first_env OPTIMISM_RPC_URL OPTIMISM_MAINNET_RPC ;;
CRONOS) first_env CRONOS_RPC_URL CRONOS_RPC CRONOS_MAINNET_RPC ;;
BSC) first_env BSC_RPC_URL BSC_MAINNET_RPC ;;
GNOSIS) first_env GNOSIS_RPC_URL GNOSIS_MAINNET_RPC GNOSIS_RPC ;;
POLYGON) first_env POLYGON_RPC_URL POLYGON_MAINNET_RPC ;;
BASE) first_env BASE_RPC_URL BASE_MAINNET_RPC ;;
ARBITRUM) first_env ARBITRUM_RPC_URL ARBITRUM_MAINNET_RPC ;;
CELO) first_env CELO_RPC_URL CELO_MAINNET_RPC CELO_RPC ;;
esac
}
old_bridge_for_suffix() {
local suffix="$1" key="CW_BRIDGE_${suffix}"
var_value "$key"
}
old_bridge_call() {
local bridge="$1" rpc="$2" sig="$3"
[[ -n "$bridge" && -n "$rpc" ]] || return 1
cast call "$bridge" "$sig" --rpc-url "$rpc" 2>/dev/null | awk 'NF{print $1; exit}'
}
send_router_for_chain() {
local suffix="$1" old_bridge="$2" rpc="$3" value
value="$(first_env "CCIP_${suffix}_ROUTER" "CCIP_ROUTER_${suffix}" 2>/dev/null || true)"
if [[ -n "$value" ]]; then printf '%s\n' "$value"; return; fi
old_bridge_call "$old_bridge" "$rpc" "ccipRouter()(address)"
}
receive_router_for_chain() {
local suffix="$1" send_router="$2" value
value="$(first_env "CCIP_RELAY_ROUTER_${suffix}_CW" "CCIP_RELAY_ROUTER_${suffix}" 2>/dev/null || true)"
if [[ -n "$value" ]]; then printf '%s\n' "$value"; return; fi
printf '%s\n' "$send_router"
}
fee_token_for_chain() {
local suffix="$1" old_bridge="$2" rpc="$3" value
value="$(first_env "CCIP_${suffix}_LINK_TOKEN" "LINK_TOKEN_${suffix}" "LINK_${suffix}" 2>/dev/null || true)"
if [[ -n "$value" ]]; then printf '%s\n' "$value"; return; fi
old_bridge_call "$old_bridge" "$rpc" "feeToken()(address)" || printf '0x0000000000000000000000000000000000000000\n'
}
json_log() {
python3 - "$MANIFEST" "$@" <<'PY'
import json, sys
path = sys.argv[1]
items = dict(arg.split("=", 1) for arg in sys.argv[2:])
with open(path, "a", encoding="utf-8") as fh:
fh.write(json.dumps(items, sort_keys=True) + "\n")
PY
}
token_rows_for_chain() {
local chain_id="$1"
jq -r --argjson cid "$chain_id" --argjson full "$($FULL_FAMILY && echo true || echo false)" '
.pairs[]
| select(.fromChainId == 138 and .toChainId == $cid)
| .tokens[]
| select((.key == "Compliant_USDT_cW") or (.key == "Compliant_USDC_cW") or ($full and (.key | endswith("_cW"))))
| select(.addressFrom and .addressTo and .addressTo != "0x0000000000000000000000000000000000000000")
| [.key, .addressFrom, .addressTo] | @tsv
' "$PROJECT_ROOT/config/token-mapping-multichain.json"
}
send_tx() {
local rpc="$1"; shift
local base_cmd=(cast send --rpc-url "$rpc" --private-key "$PRIVATE_KEY" --gas-limit 500000 "$@")
if ! $APPLY; then
printf '[dry-run] '
printf '%q ' "${base_cmd[@]/$PRIVATE_KEY/\$PRIVATE_KEY}"
printf '\n'
return 0
fi
local attempt mode out rc
for mode in legacy eip1559; do
local cmd=("${base_cmd[@]}")
if [[ "$mode" == "legacy" ]]; then
cmd=(cast send --rpc-url "$rpc" --private-key "$PRIVATE_KEY" --legacy --gas-limit 500000 "$@")
fi
for attempt in 1 2 3; do
set +e
out="$("${cmd[@]}" 2>&1)"
rc=$?
set -e
printf '%s\n' "$out"
if [[ "$rc" -eq 0 ]]; then
sleep 3
return 0
fi
if [[ "$mode" == "legacy" && ( "$out" == *"Invalid params"* || "$out" == *"legacy"* || "$out" == *"transaction type"* ) ]]; then
echo " legacy tx rejected; retrying with EIP-1559 tx params"
break
fi
if [[ "$out" == *"max fee per gas less than block base fee"* ]]; then
local gas_price
gas_price="$(cast gas-price --rpc-url "$rpc" 2>/dev/null || echo 25000000)"
gas_price="$((gas_price * 2))"
echo " gas price below base fee; retrying with --gas-price $gas_price"
cmd=(cast send --rpc-url "$rpc" --private-key "$PRIVATE_KEY" --gas-limit 500000 --gas-price "$gas_price" "$@")
sleep $((attempt * 3))
continue
fi
if [[ "$out" == *"replacement transaction underpriced"* || "$out" == *"nonce too low"* || "$out" == *"invalid nonce"* || "$out" == *"invalid sequence"* || "$out" == *"already known"* ]]; then
echo " retryable tx error (attempt $attempt); waiting before retry"
sleep $((attempt * 10))
continue
fi
return "$rc"
done
done
return "$rc"
}
forge_create_l2() {
local rpc="$1" chain_id="$2" send_router="$3" receive_router="$4" fee_token="$5"
local mode out rc
for mode in legacy eip1559; do
set +e
if [[ "$mode" == "legacy" ]]; then
out="$(
cd "$SMOM_ROOT" &&
forge create contracts/bridge/CWMultiTokenBridgeL2.sol:CWMultiTokenBridgeL2 \
--rpc-url "$rpc" --chain-id "$chain_id" --broadcast --private-key "$PRIVATE_KEY" --legacy \
--constructor-args "$send_router" "$receive_router" "$fee_token" 2>&1
)"
else
out="$(
cd "$SMOM_ROOT" &&
forge create contracts/bridge/CWMultiTokenBridgeL2.sol:CWMultiTokenBridgeL2 \
--rpc-url "$rpc" --chain-id "$chain_id" --broadcast --private-key "$PRIVATE_KEY" \
--constructor-args "$send_router" "$receive_router" "$fee_token" 2>&1
)"
fi
rc=$?
set -e
printf '%s\n' "$out"
if [[ "$rc" -eq 0 ]]; then
return 0
fi
if [[ "$mode" == "legacy" && ( "$out" == *"Invalid params"* || "$out" == *"legacy"* || "$out" == *"transaction type"* ) ]]; then
echo " legacy deploy rejected; retrying with EIP-1559 tx params"
continue
fi
return "$rc"
done
return "$rc"
}
ensure_token_pair() {
local rpc="$1" bridge="$2" key="$3" canonical="$4" mirrored="$5"
local current
current="$(cast call "$bridge" "canonicalToMirrored(address)(address)" "$canonical" --rpc-url "$rpc" 2>/dev/null || true)"
if [[ "${current,,}" == "${mirrored,,}" ]]; then
echo " pair ok $key"
return
fi
echo " configure pair $key"
send_tx "$rpc" "$bridge" "configureTokenPair(address,address)" "$canonical" "$mirrored"
}
ensure_l2_destination() {
local rpc="$1" bridge="$2"
local current
current="$(cast call "$bridge" "destinations(uint64)((address,bool))" 138 --rpc-url "$rpc" 2>/dev/null || true)"
if [[ "${current,,}" == *"${L1_BRIDGE,,}"* && "${current,,}" == *"true"* ]]; then
echo " L2 destination 138 ok"
return
fi
echo " configure L2 destination 138"
send_tx "$rpc" "$bridge" "configureDestination(uint64,address,bool)" 138 "$L1_BRIDGE" true
}
ensure_l1_destination() {
local selector="$1" key="$2" canonical="$3" bridge="$4"
local current
current="$(cast call "$L1_BRIDGE" "destinations(address,uint64)((address,bool))" "$canonical" "$selector" --rpc-url "$RPC_138" 2>/dev/null || true)"
if [[ "${current,,}" == *"${bridge,,}"* && "${current,,}" == *"true"* ]]; then
echo " L1 destination ok $key"
return
fi
echo " configure L1 destination $key selector=$selector"
send_tx "$RPC_138" "$L1_BRIDGE" "configureDestination(address,uint64,address,bool)" "$canonical" "$selector" "$bridge" true
}
ensure_role() {
local rpc="$1" token="$2" role_name="$3" role="$4" holder="$5"
local current
current="$(cast call "$token" "hasRole(bytes32,address)(bool)" "$role" "$holder" --rpc-url "$rpc" 2>/dev/null || echo false)"
if [[ "$current" == "true" ]]; then
echo " role ok $role_name $token"
return
fi
echo " grant $role_name on $token"
send_tx "$rpc" "$token" "grantRole(bytes32,address)" "$role" "$holder"
}
deploy_l2() {
local chain_id="$1" suffix="$2" rpc="$3" send_router="$4" receive_router="$5" fee_token="$6"
echo " deploying CWMultiTokenBridgeL2 send=$send_router receive=$receive_router fee=$fee_token"
if ! $APPLY; then
echo " [dry-run] forge script DeployCWMultiTokenBridgeL2"
printf '0x000000000000000000000000000000000000%04x\n' "$chain_id"
return
fi
local out addr
out="$(forge_create_l2 "$rpc" "$chain_id" "$send_router" "$receive_router" "$fee_token")"
printf '%s\n' "$out"
addr="$(printf '%s\n' "$out" | sed -nE 's/.*Deployed to:[[:space:]]*(0x[a-fA-F0-9]{40}).*/\1/p; s/.*CWMultiTokenBridgeL2:[[:space:]]*(0x[a-fA-F0-9]{40}).*/\1/p' | tail -1)"
[[ -n "$addr" ]] || { echo "Could not parse deployed bridge address for $suffix" >&2; return 1; }
printf '%s\n' "$addr"
}
is_cw_multitoken_l2() {
local rpc="$1" bridge="$2"
[[ -n "$bridge" ]] || return 1
cast call "$bridge" "sendRouter()(address)" --rpc-url "$rpc" >/dev/null 2>&1 &&
cast call "$bridge" "receiveRouter()(address)" --rpc-url "$rpc" >/dev/null 2>&1 &&
cast call "$bridge" "canonicalToMirrored(address)(address)" "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22" --rpc-url "$rpc" >/dev/null 2>&1
}
echo "CWMultiToken L2 remediation"
echo "Apply: $APPLY"
echo "Full family: $FULL_FAMILY"
echo "Deployer: $DEPLOYER"
echo "Manifest: $MANIFEST"
echo
for chain_id in 10 25 56 100 137 8453 42161 42220; do
[[ -n "$CHAIN_FILTER" && "$CHAIN_FILTER" != "$chain_id" ]] && continue
suffix="${CHAIN_SUFFIX[$chain_id]}"
rpc="$(rpc_for_chain "$suffix" || true)"
old_bridge="$(old_bridge_for_suffix "$suffix")"
selector="${CHAIN_SELECTOR[$chain_id]}"
echo "=== $chain_id ${CHAIN_NAME[$chain_id]} ($suffix) ==="
if [[ -z "$rpc" ]]; then
echo " skip: missing RPC"
json_log chainId="$chain_id" suffix="$suffix" status="skipped" reason="missing_rpc"
continue
fi
send_router="$(send_router_for_chain "$suffix" "$old_bridge" "$rpc" || true)"
receive_router="$(receive_router_for_chain "$suffix" "$send_router")"
fee_token="$(fee_token_for_chain "$suffix" "$old_bridge" "$rpc")"
if [[ -z "$send_router" || -z "$receive_router" || -z "$fee_token" ]]; then
echo " skip: missing router or fee token"
json_log chainId="$chain_id" suffix="$suffix" status="skipped" reason="missing_router_or_fee"
continue
fi
current_bridge="$(old_bridge_for_suffix "$suffix")"
if is_cw_multitoken_l2 "$rpc" "$current_bridge"; then
bridge="$current_bridge"
echo " using existing CWMultiTokenBridgeL2: $bridge"
else
bridge="$(deploy_l2 "$chain_id" "$suffix" "$rpc" "$send_router" "$receive_router" "$fee_token" | tail -1)"
echo " new bridge: $bridge"
fi
if $APPLY; then
set_env_value "$ENV_FILE" "CW_BRIDGE_${suffix}" "$bridge"
fi
ensure_l2_destination "$rpc" "$bridge"
while IFS=$'\t' read -r key canonical mirrored; do
ensure_token_pair "$rpc" "$bridge" "$key" "$canonical" "$mirrored"
ensure_l1_destination "$selector" "$key" "$canonical" "$bridge"
ensure_role "$rpc" "$mirrored" "MINTER_ROLE" "$MINTER_ROLE" "$bridge"
ensure_role "$rpc" "$mirrored" "BURNER_ROLE" "$BURNER_ROLE" "$bridge"
done < <(token_rows_for_chain "$chain_id")
json_log chainId="$chain_id" suffix="$suffix" status="configured" bridge="$bridge" sendRouter="$send_router" receiveRouter="$receive_router" feeToken="$fee_token"
done
echo
echo "Done. Manifest: $MANIFEST"
echo "Next: pnpm cw:bridge-e2e-readiness && pnpm cw:full-readiness"

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/load-project-env.sh
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh"
: "${ETHEREUM_MAINNET_RPC:?ETHEREUM_MAINNET_RPC is required}"
CWUSDC="${CWUSDC_MAINNET:-0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a}"
QUOTE_TOKEN="${ENGINE_X_SINGLE_SIDED_DODO_QUOTE_TOKEN:-${WETH9_MAINNET:-0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2}}"
DODO_INTEGRATION="${ENGINE_X_SINGLE_SIDED_DODO_INTEGRATION:-${DODO_PMM_INTEGRATION_MAINNET:-${CHAIN_1_DODO_PMM_INTEGRATION:-}}}"
VERIFY="${VERIFY:-1}"
EXECUTE="${EXECUTE:-0}"
OWNER="${ENGINE_X_SINGLE_SIDED_DODO_OWNER:-${DEPLOYER_ADDRESS:-}}"
if [[ -n "${PRIVATE_KEY:-}" ]]; then
OWNER="$(cast wallet address --private-key "${PRIVATE_KEY}")"
fi
if [[ -z "${OWNER}" ]]; then
echo "Set PRIVATE_KEY, DEPLOYER_ADDRESS, or ENGINE_X_SINGLE_SIDED_DODO_OWNER" >&2
exit 1
fi
if [[ -z "${DODO_INTEGRATION}" ]]; then
echo "Set DODO_PMM_INTEGRATION_MAINNET or ENGINE_X_SINGLE_SIDED_DODO_INTEGRATION" >&2
exit 1
fi
if [[ "${EXECUTE}" == "1" && -z "${PRIVATE_KEY:-}" ]]; then
echo "PRIVATE_KEY is required when EXECUTE=1" >&2
exit 1
fi
VERIFY_ARGS=()
if [[ "${VERIFY}" == "1" ]]; then
VERIFY_ARGS+=(--verify)
fi
CREATE_CMD_EXEC=(
forge create
--broadcast
--rpc-url "${ETHEREUM_MAINNET_RPC}"
--private-key "${PRIVATE_KEY:-}"
"${VERIFY_ARGS[@]}"
contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol:DBISEngineXSingleSidedDodoCwusdcVault
--constructor-args
"${CWUSDC}" "${QUOTE_TOKEN}" "${DODO_INTEGRATION}" "${OWNER}"
)
cat <<EOF
Engine X single-sided DODO cWUSDC wrapper deployment plan
mode EXECUTE: ${EXECUTE}
owner: ${OWNER}
cWUSDC: ${CWUSDC}
quote token: ${QUOTE_TOKEN}
DODO integration: ${DODO_INTEGRATION}
Boundary:
This wrapper may hold cWUSDC-only inventory as Engine X accounted support
inventory. It does not make cWUSDC-only inventory executable DODO liquidity.
Promotion to DODO requires nonzero cWUSDC and nonzero quote-token inventory,
a configured cWUSDC/quote DODO pool, and passing querySellBase/querySellQuote
canary guards.
EOF
if [[ "${EXECUTE}" != "1" ]]; then
cat <<EOF
Dry-run only. Review command:
cd smom-dbis-138
forge create --broadcast --rpc-url "\$ETHEREUM_MAINNET_RPC" --private-key "\$PRIVATE_KEY" ${VERIFY_ARGS[*]} contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol:DBISEngineXSingleSidedDodoCwusdcVault --constructor-args "${CWUSDC}" "${QUOTE_TOKEN}" "${DODO_INTEGRATION}" "${OWNER}"
EOF
exit 0
fi
pushd "${PROJECT_ROOT}/smom-dbis-138" >/dev/null
DEPLOY_OUT="$("${CREATE_CMD_EXEC[@]}")"
popd >/dev/null
printf '%s\n' "${DEPLOY_OUT}"
VAULT="$(printf '%s\n' "${DEPLOY_OUT}" | grep -oE 'Deployed to: 0x[a-fA-F0-9]{40}' | awk '{print $3}' | tail -1)"
if [[ -z "${VAULT}" ]]; then
echo "Could not parse deployed single-sided DODO cWUSDC wrapper address" >&2
exit 1
fi
echo "DBIS_ENGINE_X_SINGLE_SIDED_DODO_CWUSDC_VAULT=${VAULT}"

View File

@@ -3,7 +3,8 @@
# Usage: ./scripts/deployment/deploy-sankofa-studio-lxc.sh [--dry-run] [--skip-create]
# --dry-run Print commands only.
# --skip-create Use existing container 7805 (only install Docker / compose / deploy app).
# Env: PROXMOX_HOST, NODE, VMID, HOSTNAME, IP_SANKOFA_STUDIO, REPO_URL or REPO_PATH, ENV_FILE.
# Env: PROXMOX_HOST (defaults from VMID), NODE, VMID, HOSTNAME, IP_SANKOFA_STUDIO, REPO_URL or REPO_PATH, ENV_FILE.
# DEPLOY_PCT_ON_LOCAL_PVE=1 — only on a Proxmox node: run pct locally (no SSH).
# See: docs/03-deployment/SANKOFA_STUDIO_DEPLOYMENT.md
set -euo pipefail
@@ -45,6 +46,11 @@ for a in "$@"; do
[[ "$a" == "--skip-create" ]] && SKIP_CREATE=true
done
PROXMOX_MONOREPO_ROOT="$PROXMOX_ROOT"
# shellcheck disable=SC1091
source "$PROXMOX_ROOT/scripts/lib/require-proxmox-ssh-for-pct.sh"
require_proxmox_ssh_for_pct || exit 1
run_cmd() {
if [[ -n "$PROXMOX_HOST" ]]; then
ssh $SSH_OPTS root@"$PROXMOX_HOST" "$@"
@@ -72,17 +78,6 @@ echo "URL: https://studio.sankofa.nexus → http://${IP}:8000"
echo "IP: $IP | Memory: ${MEMORY_MB}MB | Cores: $CORES | Disk: ${DISK_GB}G"
echo ""
# pct runs only on Proxmox hosts; from another machine set PROXMOX_HOST to SSH there
if ! $DRY_RUN && [[ -z "${PROXMOX_HOST:-}" ]] && ! command -v pct &>/dev/null; then
echo "ERROR: 'pct' not found. This script must run on a Proxmox host or with PROXMOX_HOST set."
echo ""
echo "From your current machine, run:"
echo " PROXMOX_HOST=192.168.11.11 REPO_URL='https://gitea.d-bis.org/d-bis/FusionAI-Creator.git' $0"
echo ""
echo "Or SSH to the Proxmox host and run the script there (with REPO_URL set)."
exit 1
fi
if ! $SKIP_CREATE; then
if $DRY_RUN; then
echo "[DRY-RUN] Would create LXC $VMID with hostname=$HOSTNAME, ip=$IP/24 (Docker + FusionAI Creator)"

View File

@@ -0,0 +1,172 @@
#!/usr/bin/env node
/**
* Execute the coffee-money gas top-up packet generated by
* scripts/deployment/plan-coffee-money-gas-topups.mjs.
*
* Default mode is dry-run. Pass --execute to approve exact source-token allowance
* and submit the bounded LiFi bridge transactions.
*/
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { Contract, JsonRpcProvider, Wallet } from "ethers";
const repoRoot = resolve(new URL("../..", import.meta.url).pathname);
const planPath = resolve(repoRoot, "reports/status/coffee-money-gas-topup-plan-latest.json");
const outJson = resolve(repoRoot, "reports/status/coffee-money-gas-topup-execution-latest.json");
const outMd = resolve(repoRoot, "reports/status/coffee-money-gas-topup-execution-latest.md");
const execute = process.argv.includes("--execute");
const confirmations = Number(process.env.COFFEE_MONEY_CONFIRMATIONS || "1");
const rpcUrl = process.env.ETHEREUM_MAINNET_RPC || process.env.RPC_URL_1 || process.env.RPC_URL_MAINNET || "https://ethereum.publicnode.com";
const privateKey = process.env.PRIVATE_KEY;
const erc20Abi = [
"function allowance(address owner,address spender) view returns (uint256)",
"function approve(address spender,uint256 amount) returns (bool)",
"function balanceOf(address owner) view returns (uint256)",
];
const plan = JSON.parse(readFileSync(planPath, "utf8"));
if (execute && !privateKey) {
console.error("PRIVATE_KEY is required for --execute");
process.exit(1);
}
if (!plan.readiness?.sourceTokenSufficient && !plan.readiness?.sourceUsdcSufficient) {
console.error("Plan source token balance is not sufficient. Re-run the planner and inspect readiness.");
process.exit(1);
}
if (!plan.readiness?.mainnetEthGasSufficientForQuotedBridgeTxs || !plan.readiness?.allQuotesOk) {
console.error("Plan is not ready for execution. Re-run the planner and inspect readiness.");
process.exit(1);
}
const provider = new JsonRpcProvider(rpcUrl, 1);
const wallet = execute ? new Wallet(privateKey, provider) : null;
const operator = execute ? await wallet.getAddress() : plan.deployer;
if (operator.toLowerCase() !== String(plan.deployer).toLowerCase()) {
console.error(`PRIVATE_KEY resolves to ${operator}, expected ${plan.deployer}`);
process.exit(1);
}
const sourceTokenAddress = plan.source?.tokenAddress;
const sourceTokenSymbol = plan.source?.token || "source token";
if (!sourceTokenAddress) {
console.error("Plan is missing source.tokenAddress. Re-run the planner with the updated planner script.");
process.exit(1);
}
const token = new Contract(sourceTokenAddress, erc20Abi, execute ? wallet : provider);
const execution = {
generatedAt: new Date().toISOString(),
mode: execute ? "execute_broadcast" : "dry_run_no_broadcast",
deployer: plan.deployer,
plan: "reports/status/coffee-money-gas-topup-plan-latest.json",
approvals: [],
transactions: [],
totals: plan.totals,
};
for (const allowancePlan of plan.allowances ?? []) {
const spender = allowancePlan.spender;
const required = BigInt(allowancePlan.requiredRaw);
const current = await token.allowance(plan.deployer, spender);
const approvalRecord = {
spender,
requiredRaw: required.toString(),
currentRaw: current.toString(),
action: current >= required ? "skip_allowance_sufficient" : "approve_exact_required",
txHash: null,
status: "pending",
};
if (execute && current < required) {
const tx = await token.approve(spender, required, { gasLimit: 100_000n });
approvalRecord.txHash = tx.hash;
const receipt = await tx.wait(confirmations);
approvalRecord.status = receipt?.status === 1 ? "confirmed" : "failed";
approvalRecord.blockNumber = receipt?.blockNumber ?? null;
if (receipt?.status !== 1) {
execution.approvals.push(approvalRecord);
throw new Error(`Approval failed: ${tx.hash}`);
}
} else {
approvalRecord.status = execute ? "confirmed" : "dry_run";
}
execution.approvals.push(approvalRecord);
}
for (const row of plan.rows) {
const request = row.transactionRequest;
const record = {
chainId: row.chainId,
nativeSymbol: row.nativeSymbol,
spend: row.spend,
spendUSDC: row.spendUSDC,
expectedOutNative: row.toAmountNative,
tool: row.tool,
to: request?.to ?? null,
value: request?.value ?? null,
gasLimit: request?.gasLimit ?? null,
gasPrice: request?.gasPrice ?? null,
txHash: null,
status: execute ? "pending" : "dry_run",
};
if (execute) {
const tx = await wallet.sendTransaction({
to: request.to,
data: request.data,
value: BigInt(request.value || "0"),
gasLimit: BigInt(request.gasLimit),
gasPrice: BigInt(request.gasPrice),
chainId: 1,
});
record.txHash = tx.hash;
const receipt = await tx.wait(confirmations);
record.status = receipt?.status === 1 ? "confirmed" : "failed";
record.blockNumber = receipt?.blockNumber ?? null;
if (receipt?.status !== 1) {
execution.transactions.push(record);
throw new Error(`Bridge tx failed: ${tx.hash}`);
}
}
execution.transactions.push(record);
}
function table(headers, rows) {
return [
`| ${headers.join(" | ")} |`,
`| ${headers.map(() => "---").join(" | ")} |`,
...rows.map((row) => `| ${row.map((cell) => String(cell ?? "").replace(/\|/g, "\\|")).join(" | ")} |`),
].join("\n");
}
const md = [
"# Coffee-Money Gas Top-Up Execution",
"",
`- Generated: \`${execution.generatedAt}\``,
`- Mode: \`${execution.mode}\``,
`- Deployer: \`${execution.deployer}\``,
`- Planned spend: \`${execution.totals.spend || execution.totals.spendUSDC} ${sourceTokenSymbol}\``,
`- Mainnet gas estimate from plan: \`${execution.totals.mainnetGasCostETH} ETH\``,
"",
"## Approvals",
"",
table(
["Spender", "Required raw", "Action", "Status", "Tx"],
execution.approvals.map((row) => [row.spender, row.requiredRaw, row.action, row.status, row.txHash]),
),
"",
"## Bridge Transactions",
"",
table(
["Destination", "Spend", "Expected out", "Tool", "Status", "Tx"],
execution.transactions.map((row) => [row.chainId, `${row.spend || row.spendUSDC} ${sourceTokenSymbol}`, `${row.expectedOutNative} ${row.nativeSymbol}`, row.tool, row.status, row.txHash]),
),
].join("\n");
mkdirSync(resolve(repoRoot, "reports/status"), { recursive: true });
writeFileSync(outJson, `${JSON.stringify(execution, null, 2)}\n`);
writeFileSync(outMd, `${md}\n`);
console.log(outJson);

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Recursively chown /srv/projects inside Dev CT 5700 to the primary dev user.
# Use when rsync --delete-remote fails with Permission denied (root-owned files on VM).
#
# Requires: SSH as root to the Proxmox node that hosts VMID 5700 (default r630-04).
#
# Usage:
# ./scripts/deployment/fix-dev-vm-srv-projects-ownership.sh --dry-run
# ./scripts/deployment/fix-dev-vm-srv-projects-ownership.sh
#
# Env:
# DEV_VM_CTID — LXC ID (default 5700)
# DEV_VM_USER — owning user inside CT (default dev1)
# DEV_VM_PVE_HOST — override Proxmox node IP/hostname (default: get_host_for_vmid + R630_04 fallback)
# Do not use generic PROXMOX_HOST here; it may point at the wrong node.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
export PROJECT_ROOT
# shellcheck source=/dev/null
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" 2>/dev/null || true
DRY_RUN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--help|-h)
sed -n '1,22p' "$0" | tail -n +2
exit 0
;;
*)
echo "ERROR: unknown argument: $1 (try --help)" >&2
exit 1
;;
esac
done
CTID="${DEV_VM_CTID:-5700}"
OWNER="${DEV_VM_USER:-dev1}"
if [[ -n "${DEV_VM_PVE_HOST:-}" ]]; then
NODE="$DEV_VM_PVE_HOST"
else
NODE="$(get_host_for_vmid "$CTID" 2>/dev/null || true)"
fi
NODE="${NODE:-${PROXMOX_HOST_R630_04:-192.168.11.14}}"
REMOTE_CMD="pct exec $CTID -- chown -R ${OWNER}:${OWNER} /srv/projects"
echo "=== Fix Dev VM /srv/projects ownership ==="
echo "Node: root@${NODE}"
echo "CT: $CTID"
echo "Owner: $OWNER"
echo ""
if [[ "$DRY_RUN" == "1" ]]; then
echo "DRY-RUN: ssh root@${NODE} \"$REMOTE_CMD\""
exit 0
fi
ssh -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new "root@${NODE}" "$REMOTE_CMD"
echo "Done."

View File

@@ -6,6 +6,8 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/load-project-env.sh
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/mev-protection.sh
source "${PROJECT_ROOT}/scripts/lib/mev-protection.sh"
: "${ETHEREUM_MAINNET_RPC:?ETHEREUM_MAINNET_RPC is required}"
@@ -103,8 +105,24 @@ available_usdc = vault_usdc if include_lender == "1" else max(vault_usdc - lende
amount = min(pool_cw, pool_usdc, vault_cw, available_usdc)
if override_amount:
amount = int(override_amount)
def units(raw: int) -> str:
return f"{Decimal(raw) / Decimal(10**6):f}"
def emit(name, value):
print(f"{name}='{value}'")
emit("POOL_CWUSDC_UNITS", units(pool_cw))
emit("POOL_USDC_UNITS", units(pool_usdc))
emit("LENDER_USDC_UNITS", units(lender_usdc))
emit("VAULT_CWUSDC_UNITS", units(vault_cw))
emit("VAULT_USDC_UNITS", units(vault_usdc))
emit("POOL_USDC_AVAILABLE_FOR_MIGRATION_RAW", available_usdc)
emit("POOL_USDC_AVAILABLE_FOR_MIGRATION_UNITS", units(available_usdc))
if amount <= 0:
raise SystemExit("no balanced vault liquidity is available to migrate")
emit("NO_MIGRATION", "1")
emit("NO_MIGRATION_REASON", "no balanced vault liquidity is available to migrate")
raise SystemExit(0)
if amount > vault_cw:
raise SystemExit("migration amount exceeds vault cWUSDC balance")
if amount > vault_usdc:
@@ -117,28 +135,40 @@ token0, token1 = addrs
amount0 = amount if token0 == cwusdc.lower() else amount
amount1 = amount if token1 == usdc.lower() else amount
def units(raw: int) -> str:
return f"{Decimal(raw) / Decimal(10**6):f}"
def emit(name, value):
print(f"{name}='{value}'")
emit("NO_MIGRATION", "0")
emit("TOKEN0", token0)
emit("TOKEN1", token1)
emit("AMOUNT0_RAW", amount0)
emit("AMOUNT1_RAW", amount1)
emit("MIGRATE_RAW", amount)
emit("MIGRATE_UNITS", units(amount))
emit("POOL_CWUSDC_UNITS", units(pool_cw))
emit("POOL_USDC_UNITS", units(pool_usdc))
emit("LENDER_USDC_UNITS", units(lender_usdc))
emit("VAULT_CWUSDC_UNITS", units(vault_cw))
emit("VAULT_USDC_UNITS", units(vault_usdc))
emit("POOL_USDC_AVAILABLE_FOR_MIGRATION_RAW", available_usdc)
emit("POOL_USDC_AVAILABLE_FOR_MIGRATION_UNITS", units(available_usdc))
PY
)"
if [[ "${NO_MIGRATION}" == "1" ]]; then
cat <<EOF
Engine X indexed-liquidity migration plan
mode: ${EXECUTE}
vault: ${VAULT}
vault owner: ${OWNER}
signer/recipient: ${SIGNER}
Engine X virtual vault state
accounted pool: ${POOL_CWUSDC_UNITS} cWUSDC / ${POOL_USDC_UNITS} USDC
lender bucket: ${LENDER_USDC_UNITS} USDC
actual token balances: ${VAULT_CWUSDC_UNITS} cWUSDC / ${VAULT_USDC_UNITS} USDC
USDC available without lender bucket: ${POOL_USDC_AVAILABLE_FOR_MIGRATION_UNITS}
No migration commands emitted:
${NO_MIGRATION_REASON}
Boundary
The previous tiny Engine X public-liquidity attempt appears already swept or spent.
Rerun only after an upgraded vault is seeded with balanced cWUSDC and official Mainnet USDC.
EOF
exit 0
fi
POOL="$(cast call "${FACTORY}" 'getPool(address,address,uint24)(address)' "${TOKEN0}" "${TOKEN1}" "${FEE}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | grep -oE '0x[a-fA-F0-9]{40}' | head -1)"
SIMULATED_POOL=""
if [[ "${POOL}" == "0x0000000000000000000000000000000000000000" ]]; then
@@ -216,21 +246,19 @@ EOF
exit 0
fi
cast send "${VAULT}" 'withdrawPoolLiquidity(address,uint256,uint256)' "${SIGNER}" "${MIGRATE_RAW}" "${MIGRATE_RAW}" \
--rpc-url "${ETHEREUM_MAINNET_RPC}" --private-key "${PRIVATE_KEY}"
mev_require_private_for_action "engine-x-univ3-indexed-lp-migration"
cast send "${POSITION_MANAGER}" 'createAndInitializePoolIfNecessary(address,address,uint24,uint160)' \
"${TOKEN0}" "${TOKEN1}" "${FEE}" "${SQRT_PRICE_X96}" \
--rpc-url "${ETHEREUM_MAINNET_RPC}" --private-key "${PRIVATE_KEY}"
mev_cast_send "${VAULT}" 'withdrawPoolLiquidity(address,uint256,uint256)' "${SIGNER}" "${MIGRATE_RAW}" "${MIGRATE_RAW}"
cast send "${TOKEN0}" 'approve(address,uint256)' "${POSITION_MANAGER}" "${AMOUNT0_RAW}" \
--rpc-url "${ETHEREUM_MAINNET_RPC}" --private-key "${PRIVATE_KEY}"
cast send "${TOKEN1}" 'approve(address,uint256)' "${POSITION_MANAGER}" "${AMOUNT1_RAW}" \
--rpc-url "${ETHEREUM_MAINNET_RPC}" --private-key "${PRIVATE_KEY}"
mev_cast_send "${POSITION_MANAGER}" 'createAndInitializePoolIfNecessary(address,address,uint24,uint160)' \
"${TOKEN0}" "${TOKEN1}" "${FEE}" "${SQRT_PRICE_X96}"
cast send "${POSITION_MANAGER}" 'mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))' \
mev_cast_send "${TOKEN0}" 'approve(address,uint256)' "${POSITION_MANAGER}" "${AMOUNT0_RAW}"
mev_cast_send "${TOKEN1}" 'approve(address,uint256)' "${POSITION_MANAGER}" "${AMOUNT1_RAW}"
mev_cast_send "${POSITION_MANAGER}" 'mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))' \
"(${TOKEN0},${TOKEN1},${FEE},${TICK_LOWER},${TICK_UPPER},${AMOUNT0_RAW},${AMOUNT1_RAW},0,0,${SIGNER},${DEADLINE})" \
--rpc-url "${ETHEREUM_MAINNET_RPC}" --private-key "${PRIVATE_KEY}" -vv
-vv
NEW_POOL="$(cast call "${FACTORY}" 'getPool(address,address,uint24)(address)' "${TOKEN0}" "${TOKEN1}" "${FEE}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | grep -oE '0x[a-fA-F0-9]{40}' | head -1)"
echo "Post-migration UniV3 pool: ${NEW_POOL}"

35
scripts/deployment/mint-cwusdc-ei-matrix-wallets.sh Normal file → Executable file
View File

@@ -305,19 +305,28 @@ if ! $DRY_RUN && [[ "${EI_MATRIX_SKIP_GAS_CHECK:-}" != "1" ]]; then
fi
echo ""
echo "Sample (first 3, last 3):"
_s_idx=$OFFSET
while IFS= read -r s_addr && IFS= read -r s_raw <&3; do
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
_s_idx=$((_s_idx + 1))
done < <(head -3 "$ADDR_TMP") 3< <(head -3 "$AMOUNTS_TMP")
_s_idx=$((OFFSET + WALLET_COUNT - 3))
while IFS= read -r s_addr && IFS= read -r s_raw <&3; do
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
_s_idx=$((_s_idx + 1))
done < <(tail -3 "$ADDR_TMP") 3< <(tail -3 "$AMOUNTS_TMP")
echo "Sample mints:"
if [[ "$WALLET_COUNT" -le 6 ]]; then
_s_idx=$OFFSET
while IFS=$'\t' read -r s_addr s_raw; do
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
_s_idx=$((_s_idx + 1))
done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP")
else
_s_idx=$OFFSET
while IFS=$'\t' read -r s_addr s_raw; do
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
_s_idx=$((_s_idx + 1))
done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP" | head -3)
_s_idx=$((OFFSET + WALLET_COUNT - 3))
while IFS=$'\t' read -r s_addr s_raw; do
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
_s_idx=$((_s_idx + 1))
done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP" | tail -3)
fi
echo ""
sent=0

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Mirror a GitHub fork to Gitea (push --mirror). Run from operator LAN when Gitea is reachable.
#
# Required:
# GITEA_REMOTE — e.g. https://gitea.d-bis.org/ORG/DefiLlama-Adapters.git
# Optional:
# GITHUB_FORK — default https://github.com/Defi-Oracle-Meta-Blockchain/DefiLlama-Adapters.git
#
# Usage:
# export GITEA_REMOTE='https://USER:TOKEN@gitea.d-bis.org/d-bis/DefiLlama-Adapters.git'
# ./scripts/deployment/mirror-github-fork-to-gitea.sh
#
# Or dry-run (fetch only):
# DRY_RUN=1 ./scripts/deployment/mirror-github-fork-to-gitea.sh
set -euo pipefail
GITHUB_FORK="${GITHUB_FORK:-https://github.com/Defi-Oracle-Meta-Blockchain/DefiLlama-Adapters.git}"
if [[ -z "${GITEA_REMOTE:-}" ]]; then
echo "Set GITEA_REMOTE to your Gitea repo URL (with credentials if needed)." >&2
exit 1
fi
TMP="${TMPDIR:-/tmp}/mirror-defillama-$$"
cleanup() { rm -rf "$TMP"; }
trap cleanup EXIT
git clone --mirror "$GITHUB_FORK" "$TMP"
if [[ "${DRY_RUN:-}" == "1" ]]; then
echo "DRY_RUN=1: mirror clone OK; skip push to GITEA_REMOTE"
exit 0
fi
git -C "$TMP" push --mirror "$GITEA_REMOTE"
echo "Mirror push completed."

15
scripts/deployment/pipeline-ei-matrix-mint-cwusdc.sh Normal file → Executable file
View File

@@ -49,17 +49,18 @@ if [[ "$CID" != "1" ]]; then
exit 1
fi
ROLE=$(cast keccak "MINTER_ROLE()")
# AccessControl MINTER_ROLE is keccak256 of the string "MINTER_ROLE" for OZ — use cast keccak
ROLE=$(cast keccak "MINTER_ROLE")
if HR=$(cast call "$CWUSDC" "hasRole(bytes32,address)(bool)" "$ROLE" "$FROM" --rpc-url "$RPC" 2>/dev/null); then
if [[ "${HR,,}" != *true* ]]; then
echo "[WARN] hasRole(MINTER_ROLE) returned false for signer — mints will likely revert." >&2
else
if [[ "${SKIP_EI_MATRIX_MINT_PREFLIGHT:-}" == "1" ]]; then
echo "Preflight: skipped (SKIP_EI_MATRIX_MINT_PREFLIGHT=1)"
elif HR=$(cast call "$CWUSDC" "hasRole(bytes32,address)(bool)" "$ROLE" "$FROM" --rpc-url "$RPC" 2>/dev/null); then
HR_TR=$(echo "$HR" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
if [[ "$HR_TR" == *true* || "$HR_TR" == *0x0000000000000000000000000000000000000000000000000000000000000001* ]]; then
echo "Preflight: MINTER_ROLE on cWUSDC for signer — OK"
else
echo "[WARN] hasRole(MINTER_ROLE) not true for signer (got: $HR) — mints may revert if minter is elsewhere." >&2
fi
else
echo "[WARN] Could not call hasRole (ABI may differ) — continuing." >&2
echo "[WARN] Could not call hasRole — continuing (token ABI may differ)." >&2
fi
echo ""

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Run full-grid readiness audit (optional), then remediate mainnet cWUSDC gaps.
#
# Modes:
# Default: fixed --send-raw to each gap index (send-cwusdc-ei-matrix-targeted.sh).
# --multicall: per-wallet deficit TSV + Multicall3 batches (cheapest; see send-cwusdc-ei-matrix-multicall-batches.sh).
#
# Usage:
# ./scripts/deployment/pipeline-ei-matrix-remediate-cwusdc-from-audit.sh --dry-run --send-raw 5000000
# ./scripts/deployment/pipeline-ei-matrix-remediate-cwusdc-from-audit.sh --dry-run --multicall
# ./scripts/deployment/pipeline-ei-matrix-remediate-cwusdc-from-audit.sh --execute --multicall
# SKIP_EI_MATRIX_REMEDIATE_AUDIT=1 ./scripts/deployment/pipeline-ei-matrix-remediate-cwusdc-from-audit.sh --multicall --execute
#
# Env:
# SKIP_EI_MATRIX_REMEDIATE_AUDIT=1
# EI_MATRIX_REMEDIATE_MULTICALL=1 Same as --multicall
# EI_MATRIX_REMEDIATE_TOPUP_TSV=path Default: reports/status/ei-matrix-cwusdc-topup-amounts.tsv
# Rebuild TSV after audit: scripts/verify/build-ei-matrix-cwusdc-topup-tsv-from-audit-json.sh
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
DRY=(false)
SEND_RAW=""
INDICES_OVERRIDE=""
USE_MULTICALL=false
PASS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY=(true); PASS+=(--dry-run); shift ;;
--send-raw) SEND_RAW="${2:?}"; shift 2 ;;
--indices-file) INDICES_OVERRIDE="${2:?}"; shift 2 ;;
--multicall) USE_MULTICALL=true; shift ;;
--) shift; PASS+=("$@"); break ;;
*) PASS+=("$1"); shift ;;
esac
done
if [[ "${EI_MATRIX_REMEDIATE_MULTICALL:-}" == "1" ]]; then
USE_MULTICALL=true
fi
AUDIT_SH="$PROJECT_ROOT/scripts/verify/run-ei-matrix-full-readiness-audit.sh"
TARGET_SH="$PROJECT_ROOT/scripts/deployment/send-cwusdc-ei-matrix-targeted.sh"
MULTICALL_SH="$PROJECT_ROOT/scripts/deployment/send-cwusdc-ei-matrix-multicall-batches.sh"
BUILD_TSV_SH="$PROJECT_ROOT/scripts/verify/build-ei-matrix-cwusdc-topup-tsv-from-audit-json.sh"
if [[ "$USE_MULTICALL" == true ]]; then
if [[ -n "$SEND_RAW" ]]; then
echo "Do not combine --multicall with --send-raw (multicall uses per-row TSV amounts)." >&2
exit 1
fi
else
if [[ -z "$SEND_RAW" ]]; then
echo "Required: --send-raw R, or use --multicall with a top-up TSV." >&2
exit 1
fi
fi
if [[ "${SKIP_EI_MATRIX_REMEDIATE_AUDIT:-}" != "1" ]]; then
echo "→ Step 1: full readiness audit (refreshes gap files; exit 1 means gaps below policy — OK)"
_audit_rc=0
"$AUDIT_SH" || _audit_rc=$?
if [[ "$_audit_rc" -gt 1 ]]; then
echo "Audit failed with exit $_audit_rc (RPC/config?)." >&2
exit "$_audit_rc"
fi
if [[ "$USE_MULTICALL" == true ]]; then
echo "→ Step 1b: rebuild top-up TSV from ei-matrix-readiness-audit-latest.json"
"$BUILD_TSV_SH"
fi
else
echo "→ Step 1: skipped (SKIP_EI_MATRIX_REMEDIATE_AUDIT=1)"
fi
if [[ "$USE_MULTICALL" == true ]]; then
TSV="${EI_MATRIX_REMEDIATE_TOPUP_TSV:-$PROJECT_ROOT/reports/status/ei-matrix-cwusdc-topup-amounts.tsv}"
if [[ ! -f "$TSV" ]]; then
echo "Missing top-up TSV: $TSV — run $BUILD_TSV_SH or audit without SKIP." >&2
exit 1
fi
N=$(wc -l <"$TSV" | tr -d ' ')
echo "→ Step 2: Multicall3 batches ($N rows) $TSV"
if $DRY; then
exec "$MULTICALL_SH" --dry-run --tsv "$TSV"
else
exec "$MULTICALL_SH" --execute --tsv "$TSV"
fi
fi
GAPS="${INDICES_OVERRIDE:-${EI_MATRIX_AUDIT_GAPS_MAINNET:-${PROJECT_ROOT}/reports/status/ei-matrix-readiness-gaps-mainnet-indices.txt}}"
if [[ ! -f "$GAPS" ]]; then
echo "Missing gap file: $GAPS" >&2
exit 1
fi
N=$(grep -cE '^[0-9]+$' "$GAPS" 2>/dev/null || echo 0)
if [[ "$N" -eq 0 ]]; then
echo "No mainnet gap indices in $GAPS — nothing to remediate."
exit 0
fi
echo "→ Step 2: targeted send to $N indices ($GAPS) --send-raw $SEND_RAW"
exec "$TARGET_SH" "${PASS[@]}" --send-raw "$SEND_RAW" --indices-file "$GAPS"

View File

@@ -0,0 +1,325 @@
#!/usr/bin/env node
/**
* Read-only coffee-money gas top-up planner.
*
* Quotes a Mainnet source token -> destination native gas via LiFi for the chains that
* currently block token-aggregation stability work. It does not approve,
* bridge, sign, or broadcast transactions.
*/
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
const repoRoot = resolve(new URL("../..", import.meta.url).pathname);
const fundingPlanPath = resolve(repoRoot, "reports/status/token-aggregation-liquidity-gap-funding-plan-latest.json");
const jsonOut = resolve(repoRoot, "reports/status/coffee-money-gas-topup-plan-latest.json");
const mdOut = resolve(repoRoot, "reports/status/coffee-money-gas-topup-plan-latest.md");
const deployer = process.env.DEPLOYER_ADDRESS || "0x4A666F96fC8764181194447A7dFdb7d471b301C8";
const sourceChain = 1;
const sourceToken = process.env.COFFEE_MONEY_SOURCE_SYMBOL || "USDC";
const sourceTokenAddress = process.env.COFFEE_MONEY_SOURCE_TOKEN_ADDRESS || "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const sourceDecimals = Number(process.env.COFFEE_MONEY_SOURCE_DECIMALS || "6");
const safetyMultiplierBps = BigInt(process.env.COFFEE_MONEY_TOPUP_SAFETY_BPS || "15000");
const minSpendRaw = BigInt(process.env.COFFEE_MONEY_MIN_SPEND_USDC_RAW || "1000000");
const maxSpendRaw = BigInt(process.env.COFFEE_MONEY_MAX_SPEND_USDC_RAW || "4000000");
const targetChains = new Set(
(process.env.COFFEE_MONEY_TARGET_CHAINS || "10,56,137,42161")
.split(",")
.map((value) => Number(value.trim()))
.filter(Boolean),
);
const nativeTokenAddress = "0x0000000000000000000000000000000000000000";
const nativeTokenOverrides = {
// LiFi models CELO as the canonical CELO token on Celo, not the zero address.
42220: "0x471EcE3750Da237f93B8E339c536989b8978a438",
};
const ethereumRpc = process.env.ETHEREUM_MAINNET_RPC || process.env.RPC_URL_1 || "https://ethereum.publicnode.com";
const fundingPlan = JSON.parse(readFileSync(fundingPlanPath, "utf8"));
function parseUnitsDecimal(value, decimals = 18) {
const [whole, frac = ""] = String(value).split(".");
const padded = `${frac}${"0".repeat(decimals)}`.slice(0, decimals);
return BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(padded || "0");
}
function decimalUnits(raw, decimals) {
const scale = 10n ** BigInt(decimals);
const whole = raw / scale;
const frac = raw % scale;
const fracText = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
return fracText ? `${whole}.${fracText}` : whole.toString();
}
function padAddress(address) {
return String(address).replace(/^0x/i, "").padStart(64, "0");
}
async function rpcCall(rpcUrl, method, params) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 12_000);
try {
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", method, params, id: 1 }),
signal: controller.signal,
});
const json = await response.json();
if (json.error) return { ok: false, error: json.error.message || JSON.stringify(json.error) };
return { ok: true, result: json.result };
} catch (error) {
return { ok: false, error: error.message };
} finally {
clearTimeout(timeout);
}
}
function bigintFromHex(hex) {
if (!hex || hex === "0x") return 0n;
return BigInt(hex);
}
async function erc20Allowance(token, owner, spender) {
if (!spender) return { ok: false, raw: "0", units: "0", error: "missing_spender" };
const selector = "0xdd62ed3e";
const data = `${selector}${padAddress(owner)}${padAddress(spender)}`;
const result = await rpcCall(ethereumRpc, "eth_call", [{ to: token, data }, "latest"]);
const raw = result.ok ? bigintFromHex(result.result) : 0n;
return {
ok: result.ok,
raw: raw.toString(),
units: decimalUnits(raw, sourceDecimals),
error: result.ok ? null : result.error,
};
}
function spendForShortfall(shortfallNative, quote) {
const shortfallRaw = parseUnitsDecimal(shortfallNative, 18);
const toAmountRaw = BigInt(quote.estimate?.toAmount || "0");
const fromAmountRaw = BigInt(quote.estimate?.fromAmount || "0");
if (toAmountRaw === 0n || fromAmountRaw === 0n) return maxSpendRaw;
const desiredOutRaw = (shortfallRaw * safetyMultiplierBps + 9_999n) / 10_000n;
const spend = (desiredOutRaw * fromAmountRaw + toAmountRaw - 1n) / toAmountRaw;
if (spend < minSpendRaw) return minSpendRaw;
if (spend > maxSpendRaw) return maxSpendRaw;
return spend;
}
async function lifiQuote(toChain, fromAmountRaw) {
const url = new URL("https://li.quest/v1/quote");
url.searchParams.set("fromChain", String(sourceChain));
url.searchParams.set("toChain", String(toChain));
url.searchParams.set("fromToken", sourceTokenAddress);
url.searchParams.set("toToken", nativeTokenOverrides[toChain] || nativeTokenAddress);
url.searchParams.set("fromAmount", String(fromAmountRaw));
url.searchParams.set("fromAddress", deployer);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 20_000);
try {
const response = await fetch(url, { signal: controller.signal });
const text = await response.text();
let data = null;
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}
return {
ok: response.ok,
statusCode: response.status,
url: url.toString(),
data,
error: response.ok ? null : text.slice(0, 500),
};
} catch (error) {
return { ok: false, statusCode: 0, url: url.toString(), data: null, error: error.message };
} finally {
clearTimeout(timeout);
}
}
const shortfalls = fundingPlan.chainGasBudgetRows.filter((row) => (
targetChains.has(Number(row.chainId)) && row.status === "chain_gas_budget_shortfall"
));
const sourceInventory = fundingPlan.ethereumSourceInventory?.tokens?.find((token) => (
token.symbol === sourceToken || String(token.address).toLowerCase() === String(sourceTokenAddress).toLowerCase()
));
const sourceBalanceRaw = BigInt(sourceInventory?.balanceRaw || "0");
const rows = [];
for (const shortfall of shortfalls) {
const oneDollarQuote = await lifiQuote(shortfall.chainId, minSpendRaw);
let spendRaw = minSpendRaw;
let finalQuote = oneDollarQuote;
if (oneDollarQuote.ok) {
spendRaw = spendForShortfall(shortfall.shortfallNative, oneDollarQuote.data);
if (spendRaw !== minSpendRaw) finalQuote = await lifiQuote(shortfall.chainId, spendRaw);
}
const estimate = finalQuote.data?.estimate ?? {};
const tx = finalQuote.data?.transactionRequest ?? null;
rows.push({
chainId: shortfall.chainId,
symbols: shortfall.symbols,
nativeSymbol: shortfall.nativeSymbol,
shortfallNative: shortfall.shortfallNative,
spendRaw: spendRaw.toString(),
spend: decimalUnits(spendRaw, sourceDecimals),
spendUSDC: sourceToken === "USDC" ? decimalUnits(spendRaw, sourceDecimals) : null,
quoteOk: finalQuote.ok,
quoteStatusCode: finalQuote.statusCode,
tool: finalQuote.data?.tool ?? estimate.tool ?? null,
toAmountRaw: estimate.toAmount ?? null,
toAmountMinRaw: estimate.toAmountMin ?? null,
toAmountNative: estimate.toAmount ? decimalUnits(BigInt(estimate.toAmount), 18) : null,
fromAmountUSD: estimate.fromAmountUSD ?? null,
toAmountUSD: estimate.toAmountUSD ?? null,
mainnetGasCostRaw: estimate.gasCosts?.[0]?.amount ?? null,
mainnetGasCostETH: estimate.gasCosts?.[0]?.amount ? decimalUnits(BigInt(estimate.gasCosts[0].amount), 18) : null,
approvalAddress: estimate.approvalAddress ?? null,
transactionTo: tx?.to ?? null,
transactionValue: tx?.value ?? null,
transactionGasLimit: tx?.gasLimit ?? null,
transactionDataPresent: Boolean(tx?.data),
transactionRequest: tx ? {
chainId: tx.chainId,
to: tx.to,
value: tx.value,
gasLimit: tx.gasLimit,
gasPrice: tx.gasPrice,
data: tx.data,
} : null,
quoteUrl: finalQuote.url,
blocker: finalQuote.ok ? null : finalQuote.error,
});
}
const allowanceBySpender = {};
for (const spender of [...new Set(rows.map((row) => row.approvalAddress).filter(Boolean))]) {
const allowance = await erc20Allowance(sourceTokenAddress, deployer, spender);
const requiredRaw = rows
.filter((row) => row.approvalAddress === spender)
.reduce((sum, row) => sum + BigInt(row.spendRaw || "0"), 0n);
allowanceBySpender[spender] = {
spender,
requiredRaw: requiredRaw.toString(),
required: decimalUnits(requiredRaw, sourceDecimals),
requiredUSDC: sourceToken === "USDC" ? decimalUnits(requiredRaw, sourceDecimals) : null,
allowanceRaw: allowance.raw,
allowance: allowance.units,
allowanceUSDC: sourceToken === "USDC" ? allowance.units : null,
sufficient: BigInt(allowance.raw || "0") >= requiredRaw,
error: allowance.error,
};
}
const totalSpendRaw = rows.reduce((sum, row) => sum + BigInt(row.spendRaw || "0"), 0n);
const totalMainnetGasRaw = rows.reduce((sum, row) => sum + BigInt(row.mainnetGasCostRaw || "0"), 0n);
const mainnetEth = fundingPlan.ethereumSourceInventory?.native?.balanceRaw ? BigInt(fundingPlan.ethereumSourceInventory.native.balanceRaw) : 0n;
const payload = {
generatedAt: new Date().toISOString(),
mode: "read_only_no_broadcast",
deployer,
source: {
chainId: sourceChain,
token: sourceToken,
tokenAddress: sourceTokenAddress,
decimals: sourceDecimals,
balanceRaw: sourceBalanceRaw.toString(),
balance: decimalUnits(sourceBalanceRaw, sourceDecimals),
balanceUSDC: sourceToken === "USDC" ? decimalUnits(sourceBalanceRaw, sourceDecimals) : null,
},
policy: {
safetyMultiplierBps: Number(safetyMultiplierBps),
minSpend: decimalUnits(minSpendRaw, sourceDecimals),
maxSpend: decimalUnits(maxSpendRaw, sourceDecimals),
unit: sourceToken,
minSpendUSDC: sourceToken === "USDC" ? decimalUnits(minSpendRaw, sourceDecimals) : null,
maxSpendUSDC: sourceToken === "USDC" ? decimalUnits(maxSpendRaw, sourceDecimals) : null,
},
totals: {
spendRaw: totalSpendRaw.toString(),
spend: decimalUnits(totalSpendRaw, sourceDecimals),
spendUSDC: sourceToken === "USDC" ? decimalUnits(totalSpendRaw, sourceDecimals) : null,
sourceBalanceAfterRaw: sourceBalanceRaw > totalSpendRaw ? (sourceBalanceRaw - totalSpendRaw).toString() : "0",
sourceBalanceAfter: decimalUnits(sourceBalanceRaw > totalSpendRaw ? sourceBalanceRaw - totalSpendRaw : 0n, sourceDecimals),
sourceBalanceAfterUSDC: sourceToken === "USDC" ? decimalUnits(sourceBalanceRaw > totalSpendRaw ? sourceBalanceRaw - totalSpendRaw : 0n, sourceDecimals) : null,
mainnetGasCostRaw: totalMainnetGasRaw.toString(),
mainnetGasCostETH: decimalUnits(totalMainnetGasRaw, 18),
mainnetEthBalanceRaw: mainnetEth.toString(),
mainnetEthBalance: decimalUnits(mainnetEth, 18),
mainnetEthAfterBridgeGasRaw: mainnetEth > totalMainnetGasRaw ? (mainnetEth - totalMainnetGasRaw).toString() : "0",
mainnetEthAfterBridgeGas: decimalUnits(mainnetEth > totalMainnetGasRaw ? mainnetEth - totalMainnetGasRaw : 0n, 18),
},
readiness: {
sourceTokenSufficient: sourceBalanceRaw >= totalSpendRaw,
sourceUsdcSufficient: sourceToken === "USDC" ? sourceBalanceRaw >= totalSpendRaw : null,
mainnetEthGasSufficientForQuotedBridgeTxs: mainnetEth >= totalMainnetGasRaw,
allQuotesOk: rows.every((row) => row.quoteOk),
sourceAllowancesSufficient: Object.values(allowanceBySpender).every((row) => row.sufficient),
usdcAllowancesSufficient: sourceToken === "USDC" ? Object.values(allowanceBySpender).every((row) => row.sufficient) : null,
broadcastReady: false,
broadcastBoundary: "Quotes include transaction data, but this planner intentionally does not sign, approve, or broadcast.",
},
allowances: Object.values(allowanceBySpender),
rows,
};
function table(headers, tableRows) {
return [
`| ${headers.join(" | ")} |`,
`| ${headers.map(() => "---").join(" | ")} |`,
...tableRows.map((row) => `| ${row.map((cell) => String(cell ?? "").replace(/\|/g, "\\|")).join(" | ")} |`),
].join("\n");
}
const md = [
"# Coffee-Money Gas Top-Up Plan",
"",
`- Generated: \`${payload.generatedAt}\``,
`- Mode: \`${payload.mode}\``,
`- Deployer: \`${deployer}\``,
`- Source: \`${payload.source.balance} ${sourceToken}\` on Ethereum Mainnet`,
`- Planned spend: \`${payload.totals.spend} ${sourceToken}\``,
`- Mainnet bridge gas estimate: \`${payload.totals.mainnetGasCostETH} ETH\``,
`- Mainnet ETH after quoted bridge gas: \`${payload.totals.mainnetEthAfterBridgeGas} ETH\``,
"",
table(
["Check", "Value"],
Object.entries(payload.readiness).map(([key, value]) => [key, value]),
),
"",
`## ${sourceToken} Allowances`,
"",
table(
["Spender", "Required", "Allowance", "Sufficient"],
payload.allowances.map((row) => [row.spender, `${row.required} ${sourceToken}`, `${row.allowance} ${sourceToken}`, row.sufficient]),
),
"",
"## Top-Up Quotes",
"",
table(
["Chain", "Symbols", "Need", "Spend", "Tool", "Out", "Mainnet gas", "Tx data"],
rows.map((row) => [
row.chainId,
row.symbols,
`${row.shortfallNative} ${row.nativeSymbol}`,
`${row.spend} ${sourceToken}`,
row.tool,
row.toAmountNative ? `${row.toAmountNative} ${row.nativeSymbol}` : "quote_failed",
row.mainnetGasCostETH ? `${row.mainnetGasCostETH} ETH` : "",
row.transactionDataPresent,
]),
),
"",
"## Execution Boundary",
"",
"This artifact proves route availability and bounded spend only. Before broadcast, each row still needs allowance/approval handling, final quote refresh, private-key signing, and tx submission.",
].join("\n");
mkdirSync(resolve(repoRoot, "reports/status"), { recursive: true });
writeFileSync(jsonOut, `${JSON.stringify(payload, null, 2)}\n`);
writeFileSync(mdOut, `${md}\n`);
console.log(jsonOut);

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Probe Dev VM SSH: LAN IP vs Cloudflare FQDN (tunnel + Access).
# See: docs/04-configuration/DEV_VM_SSH_REMOTE_ACCESS.md
#
# Usage:
# ./scripts/deployment/probe-dev-vm-ssh.sh
# DEV_VM_USER=dev1 DEV_VM_FQDN=ssh.dev.d-bis.org ./scripts/deployment/probe-dev-vm-ssh.sh
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
USER_NAME="${DEV_VM_USER:-dev1}"
IP="${IP_DEV_VM:-192.168.11.59}"
FQDN="${DEV_VM_FQDN:-ssh.dev.d-bis.org}"
echo "=== Dev VM SSH probes (user=$USER_NAME) ==="
echo ""
echo "1) LAN: BatchMode SSH to $IP"
if ssh -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new "${USER_NAME}@${IP}" true 2>/dev/null; then
echo " OK ${USER_NAME}@${IP}"
else
echo " FAIL ${USER_NAME}@${IP} (no route, firewall, or key not accepted)"
fi
echo ""
echo "2) DNS: $FQDN"
if command -v dig >/dev/null 2>&1; then
dig +short "$FQDN" A 2>/dev/null | head -3 | sed 's/^/ A: /' || true
dig +short "$FQDN" AAAA 2>/dev/null | head -2 | sed 's/^/ AAAA: /' || true
else
echo " (dig not installed; skip)"
fi
echo ""
echo "3) Plain SSH to $FQDN:22 (usually FAILS behind Cloudflare — tunnel expects cloudflared client)"
set +e
out=$(ssh -4 -o BatchMode=yes -o ConnectTimeout=12 -o StrictHostKeyChecking=accept-new "${USER_NAME}@${FQDN}" true 2>&1)
code=$?
set -e
if [[ "$code" -eq 0 ]]; then
echo " OK (unexpected for CF tunnel host — you may be using port-forward / direct)"
else
echo " FAIL (expected for tunnel hostname): $out"
fi
echo ""
echo "4) FQDN via cloudflared access ssh (needs cloudflared on PATH + Access policy / service token)"
PATH="$HOME/bin:$PATH"
if command -v cloudflared >/dev/null 2>&1; then
set +e
out=$(ssh -o BatchMode=yes -o ConnectTimeout=25 \
-o ProxyCommand="cloudflared access ssh --hostname %h" \
-o StrictHostKeyChecking=accept-new \
"${USER_NAME}@${FQDN}" true 2>&1)
code=$?
set -e
if [[ "$code" -eq 0 ]]; then
echo " OK ProxyCommand → ${USER_NAME}@${FQDN}"
else
echo " FAIL: $out"
fi
else
echo " SKIP: cloudflared not in PATH"
echo " Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/"
echo " Then set CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET if using service tokens (see DEV_VM_SSH_REMOTE_ACCESS.md)."
fi
echo ""
echo "Done."

View File

@@ -6,6 +6,8 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/load-project-env.sh
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/mev-protection.sh
source "${PROJECT_ROOT}/scripts/lib/mev-protection.sh"
: "${ETHEREUM_MAINNET_RPC:?ETHEREUM_MAINNET_RPC is required}"
: "${PRIVATE_KEY:?PRIVATE_KEY is required}"
@@ -131,24 +133,21 @@ EOF
exit 0
fi
mev_require_private_for_action "mainnet-cwusdc-usdc-univ2-canary-repair"
DEADLINE="$(( $(date +%s) + 1800 ))"
cast send "${USDC}" 'approve(address,uint256)(bool)' "${ROUTER}" "${QUOTE_IN_RAW}" \
--private-key "${PRIVATE_KEY}" --rpc-url "${ETHEREUM_MAINNET_RPC}"
mev_cast_send "${USDC}" 'approve(address,uint256)(bool)' "${ROUTER}" "${QUOTE_IN_RAW}"
cast send "${ROUTER}" 'swapExactTokensForTokens(uint256,uint256,address[],address,uint256)' \
mev_cast_send "${ROUTER}" 'swapExactTokensForTokens(uint256,uint256,address[],address,uint256)' \
"${QUOTE_IN_RAW}" "${MIN_BASE_OUT_RAW}" "[${USDC},${CWUSDC}]" "${SIGNER}" "${DEADLINE}" \
--private-key "${PRIVATE_KEY}" --rpc-url "${ETHEREUM_MAINNET_RPC}"
if [[ "${BALANCED_ADD_RAW}" != "0" ]]; then
cast send "${CWUSDC}" 'approve(address,uint256)(bool)' "${ROUTER}" "${BALANCED_ADD_RAW}" \
--private-key "${PRIVATE_KEY}" --rpc-url "${ETHEREUM_MAINNET_RPC}"
cast send "${USDC}" 'approve(address,uint256)(bool)' "${ROUTER}" "${BALANCED_ADD_RAW}" \
--private-key "${PRIVATE_KEY}" --rpc-url "${ETHEREUM_MAINNET_RPC}"
cast send "${ROUTER}" 'addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)' \
mev_cast_send "${CWUSDC}" 'approve(address,uint256)(bool)' "${ROUTER}" "${BALANCED_ADD_RAW}"
mev_cast_send "${USDC}" 'approve(address,uint256)(bool)' "${ROUTER}" "${BALANCED_ADD_RAW}"
mev_cast_send "${ROUTER}" 'addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)' \
"${CWUSDC}" "${USDC}" "${BALANCED_ADD_RAW}" "${BALANCED_ADD_RAW}" \
"${MIN_BALANCED_ADD_RAW}" "${MIN_BALANCED_ADD_RAW}" "${SIGNER}" "${DEADLINE}" \
--private-key "${PRIVATE_KEY}" --rpc-url "${ETHEREUM_MAINNET_RPC}"
fi
bash "${PROJECT_ROOT}/scripts/verify/snapshot-mainnet-cwusdc-usdc-preflight.sh"

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Chunked continuation of send-cwusdc-ei-matrix-targeted.sh using the full topup TSV.
# Uses ei-matrix-cwusdc-targeted-last-idx.txt: next index is last+1 (TSV line last+2).
# Chooses chunk size from current ETH and gas price (never exceeds --max-chunk).
# Stops when TSV exhausted, chunk size < 1, or the targeted script exits non-zero.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
MAX_CHUNK="${EI_MATRIX_TARGETED_MAX_CHUNK:-500}"
TSV="${EI_MATRIX_TARGETED_TSV:-${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-topup-amounts.tsv}"
LAST_FILE="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-targeted-last-idx.txt"
LOG="${EI_MATRIX_TARGETED_CHUNK_LOG:-${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-targeted-chunked.log}"
# shellcheck disable=SC1091
source "$PROJECT_ROOT/scripts/lib/load-project-env.sh"
[[ -f "$TSV" ]] || { echo "Missing TSV: $TSV" >&2; exit 1; }
[[ -f "$LAST_FILE" ]] || { echo "Missing progress file: $LAST_FILE" >&2; exit 1; }
command -v cast &>/dev/null || { echo "cast required" >&2; exit 1; }
RPC="${ETHEREUM_MAINNET_RPC:-${RPC_URL_1:-}}"
[[ -n "$RPC" ]] || { echo "ETHEREUM_MAINNET_RPC or RPC_URL_1 required" >&2; exit 1; }
[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required" >&2; exit 1; }
D=$(cast wallet address --private-key "$PRIVATE_KEY")
TOTAL_LINES=$(wc -l <"$TSV" | tr -d ' ')
exec >>"$LOG" 2>&1
echo "======== $(date -Is) chunked targeted cWUSDC start signer=$D max_chunk=$MAX_CHUNK tsv=$TSV"
while true; do
LAST=$(tr -d '[:space:]' <"$LAST_FILE" || echo "-1")
START=$((LAST + 2))
REM=$((TOTAL_LINES - START + 1))
if [[ "$REM" -le 0 ]]; then
echo "======== $(date -Is) COMPLETE: all indices done (last_idx=$LAST lines=$TOTAL_LINES)"
exit 0
fi
ETH_WEI=$(cast balance "$D" --rpc-url "$RPC" | awk '{print $1}')
GP=$(cast gas-price --rpc-url "$RPC" | awk '{print $1}' | head -1)
CHUNK=$(python3 -c "
eth = int('${ETH_WEI}')
gp = int('${GP}')
rem = int('${REM}')
mx = int('${MAX_CHUNK}')
# ~72k gas per ERC-20 transfer + 20% headroom on fee
cost = max(1, 72000 * gp * 12 // 10)
# spend at most 85% of ETH on this chunk
n = eth * 85 // 100 // cost
chunk = max(0, min(n, rem, mx))
print(chunk)
")
if [[ "$CHUNK" -lt 1 ]]; then
echo "======== $(date -Is) STOP: not enough ETH for one transfer (rem=$REM wei=$ETH_WEI gp=$GP). Top up and re-run."
exit 2
fi
echo "-------- $(date -Is) last=$LAST start_line=$START chunk=$CHUNK rem=$REM eth_wei=$ETH_WEI gp=$GP"
# Avoid tail|head under pipefail (SIGPIPE → exit 141).
_end=$((START + CHUNK - 1))
sed -n "${START},${_end}p" "$TSV" >"${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-chunk.tsv"
awk '{print $1}' "${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-chunk.tsv" >"${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-chunk.idx"
EI_MATRIX_SKIP_GAS_CHECK=1 \
"$SCRIPT_DIR/send-cwusdc-ei-matrix-targeted.sh" \
--amounts-tsv "${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-chunk.tsv" \
--indices-file "${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-chunk.idx"
done

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/load-project-env.sh
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/mev-protection.sh
source "${PROJECT_ROOT}/scripts/lib/mev-protection.sh"
: "${ETHEREUM_MAINNET_RPC:?ETHEREUM_MAINNET_RPC is required}"
CWUSDC="${CWUSDC_MAINNET:-0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a}"
USDC="${USDC_MAINNET:-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48}"
PAIR="${MAINNET_CWUSDC_USDC_UNIV2_PAIR:-0xC28706F899266b36BC43cc072b3a921BDf2C48D9}"
ROUTER="${CHAIN_1_UNISWAP_V2_ROUTER:-0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D}"
TARGET_USDC_OUT_RAW="${TARGET_USDC_OUT_RAW:-10000}"
SLIPPAGE_BPS="${SLIPPAGE_BPS:-100}"
DEADLINE_SECONDS="${DEADLINE_SECONDS:-900}"
EXECUTE="${EXECUTE:-0}"
STAMP="${ENGINE_X_UNIV2_LOOP_STAMP:-$(date -u +%Y%m%dT%H%M%SZ)}"
OUT_JSON="${OUT_JSON:-reports/status/engine-x-univ2-public-indexed-loop-${STAMP}.json}"
LATEST_JSON="${LATEST_JSON:-reports/status/engine-x-univ2-public-indexed-loop-latest.json}"
if [[ -n "${PRIVATE_KEY:-}" ]]; then
SIGNER="$(cast wallet address --private-key "${PRIVATE_KEY}")"
else
SIGNER="${DEPLOYER_ADDRESS:-}"
fi
if [[ -z "${SIGNER}" ]]; then
echo "Set PRIVATE_KEY or DEPLOYER_ADDRESS" >&2
exit 1
fi
if [[ "${EXECUTE}" == "1" && -z "${PRIVATE_KEY:-}" ]]; then
echo "PRIVATE_KEY is required when EXECUTE=1" >&2
exit 1
fi
TOKEN0="$(cast call "${PAIR}" 'token0()(address)' --rpc-url "${ETHEREUM_MAINNET_RPC}" | grep -oE '0x[a-fA-F0-9]{40}' | head -1)"
TOKEN1="$(cast call "${PAIR}" 'token1()(address)' --rpc-url "${ETHEREUM_MAINNET_RPC}" | grep -oE '0x[a-fA-F0-9]{40}' | head -1)"
if [[ "${TOKEN0,,}" != "${CWUSDC,,}" || "${TOKEN1,,}" != "${USDC,,}" ]]; then
echo "Configured pair is not token0=cWUSDC/token1=USDC: ${PAIR}" >&2
exit 1
fi
RESERVES_BEFORE="$(cast call "${PAIR}" 'getReserves()(uint112,uint112,uint32)' --rpc-url "${ETHEREUM_MAINNET_RPC}" | tr '\n' ' ')"
CW_BAL_BEFORE="$(cast call "${CWUSDC}" 'balanceOf(address)(uint256)' "${SIGNER}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | awk '{print $1}')"
USDC_BAL_BEFORE="$(cast call "${USDC}" 'balanceOf(address)(uint256)' "${SIGNER}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | awk '{print $1}')"
CW_ALLOW_BEFORE="$(cast call "${CWUSDC}" 'allowance(address,address)(uint256)' "${SIGNER}" "${ROUTER}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | awk '{print $1}')"
USDC_ALLOW_BEFORE="$(cast call "${USDC}" 'allowance(address,address)(uint256)' "${SIGNER}" "${ROUTER}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | awk '{print $1}')"
ETH_BEFORE="$(cast balance "${SIGNER}" --rpc-url "${ETHEREUM_MAINNET_RPC}")"
GAS_PRICE_WEI="$(cast gas-price --rpc-url "${ETHEREUM_MAINNET_RPC}")"
BLOCK_BEFORE="$(cast block-number --rpc-url "${ETHEREUM_MAINNET_RPC}")"
CW_IN_RAW="$(cast call --json "${ROUTER}" 'getAmountsIn(uint256,address[])(uint256[])' "${TARGET_USDC_OUT_RAW}" "[${CWUSDC},${USDC}]" --rpc-url "${ETHEREUM_MAINNET_RPC}" | jq -r '.[0][0]')"
ROUNDTRIP_CW_OUT_RAW="$(cast call --json "${ROUTER}" 'getAmountsOut(uint256,address[])(uint256[])' "${TARGET_USDC_OUT_RAW}" "[${USDC},${CWUSDC}]" --rpc-url "${ETHEREUM_MAINNET_RPC}" | jq -r '.[0][-1]')"
MAX_CW_IN_RAW="$(( CW_IN_RAW * (10000 + SLIPPAGE_BPS) / 10000 + 1 ))"
MIN_CW_BACK_RAW="$(( ROUNDTRIP_CW_OUT_RAW * (10000 - SLIPPAGE_BPS) / 10000 ))"
if (( CW_BAL_BEFORE < MAX_CW_IN_RAW )); then
echo "Insufficient cWUSDC: need ${MAX_CW_IN_RAW}, have ${CW_BAL_BEFORE}" >&2
exit 1
fi
cat <<EOF
Engine X UniV2 public indexed loop plan
mode: ${EXECUTE}
pair: ${PAIR}
router: ${ROUTER}
signer: ${SIGNER}
target USDC out raw: ${TARGET_USDC_OUT_RAW}
cWUSDC max input raw: ${MAX_CW_IN_RAW}
expected cWUSDC input raw: ${CW_IN_RAW}
expected cWUSDC back raw: ${ROUNDTRIP_CW_OUT_RAW}
min cWUSDC back raw: ${MIN_CW_BACK_RAW}
reserves before: ${RESERVES_BEFORE}
EOF
mkdir -p "$(dirname "${OUT_JSON}")"
python3 - "${OUT_JSON}" "${LATEST_JSON}" \
"${EXECUTE}" "${STAMP}" "${BLOCK_BEFORE}" "${GAS_PRICE_WEI}" "${SIGNER}" "${PAIR}" "${ROUTER}" \
"${CWUSDC}" "${USDC}" "${TARGET_USDC_OUT_RAW}" "${CW_IN_RAW}" "${MAX_CW_IN_RAW}" \
"${ROUNDTRIP_CW_OUT_RAW}" "${MIN_CW_BACK_RAW}" "${RESERVES_BEFORE}" "${CW_BAL_BEFORE}" "${USDC_BAL_BEFORE}" \
"${CW_ALLOW_BEFORE}" "${USDC_ALLOW_BEFORE}" "${ETH_BEFORE}" <<'PY'
import json
from pathlib import Path
import sys
(
out_json, latest_json, execute, stamp, block, gas_price, signer, pair, router, cw, usdc,
target, cw_in, max_cw_in, cw_back, min_cw_back, reserves, cw_bal, usdc_bal, cw_allow,
usdc_allow, eth,
) = sys.argv[1:]
payload = {
"schema": "engine-x-univ2-public-indexed-loop/v1",
"executed": execute == "1",
"stamp": stamp,
"blockBefore": block,
"gasPriceWei": gas_price,
"signer": signer,
"pair": pair,
"router": router,
"tokens": {"cwusdc": cw, "usdc": usdc},
"targetUsdcOutRaw": target,
"expectedCwusdcInputRaw": cw_in,
"maxCwusdcInputRaw": max_cw_in,
"expectedCwusdcBackRaw": cw_back,
"minCwusdcBackRaw": min_cw_back,
"reservesBefore": reserves,
"balancesBefore": {"ethWei": eth, "cwusdcRaw": cw_bal, "usdcRaw": usdc_bal},
"allowancesBefore": {"cwusdcRaw": cw_allow, "usdcRaw": usdc_allow},
"transactions": {},
}
Path(out_json).write_text(json.dumps(payload, indent=2) + "\n")
Path(latest_json).write_text(json.dumps(payload, indent=2) + "\n")
PY
if [[ "${EXECUTE}" != "1" ]]; then
cat <<EOF
Dry-run only. To broadcast this exact public indexed loop:
EXECUTE=1 TARGET_USDC_OUT_RAW=${TARGET_USDC_OUT_RAW} SLIPPAGE_BPS=${SLIPPAGE_BPS} \\
bash scripts/deployment/run-engine-x-univ2-public-indexed-loop.sh
EOF
exit 0
fi
mev_require_private_for_action "engine-x-univ2-public-indexed-loop"
DEADLINE="$(( $(date +%s) + DEADLINE_SECONDS ))"
CW_APPROVE_TX=""
USDC_APPROVE_TX=""
FORWARD_TX=""
REVERSE_TX=""
if (( CW_ALLOW_BEFORE < MAX_CW_IN_RAW )); then
CW_APPROVE_TX="$(mev_cast_send "${CWUSDC}" 'approve(address,uint256)(bool)' "${ROUTER}" "${MAX_CW_IN_RAW}" --json | jq -r '.transactionHash')"
fi
FORWARD_TX="$(mev_cast_send "${ROUTER}" 'swapTokensForExactTokens(uint256,uint256,address[],address,uint256)' "${TARGET_USDC_OUT_RAW}" "${MAX_CW_IN_RAW}" "[${CWUSDC},${USDC}]" "${SIGNER}" "${DEADLINE}" --json | jq -r '.transactionHash')"
USDC_ALLOW_AFTER_FORWARD="$(cast call "${USDC}" 'allowance(address,address)(uint256)' "${SIGNER}" "${ROUTER}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | awk '{print $1}')"
if (( USDC_ALLOW_AFTER_FORWARD < TARGET_USDC_OUT_RAW )); then
USDC_APPROVE_TX="$(mev_cast_send "${USDC}" 'approve(address,uint256)(bool)' "${ROUTER}" "${TARGET_USDC_OUT_RAW}" --json | jq -r '.transactionHash')"
fi
REVERSE_TX="$(mev_cast_send "${ROUTER}" 'swapExactTokensForTokens(uint256,uint256,address[],address,uint256)' "${TARGET_USDC_OUT_RAW}" "${MIN_CW_BACK_RAW}" "[${USDC},${CWUSDC}]" "${SIGNER}" "${DEADLINE}" --json | jq -r '.transactionHash')"
RESERVES_AFTER="$(cast call "${PAIR}" 'getReserves()(uint112,uint112,uint32)' --rpc-url "${ETHEREUM_MAINNET_RPC}" | tr '\n' ' ')"
CW_BAL_AFTER="$(cast call "${CWUSDC}" 'balanceOf(address)(uint256)' "${SIGNER}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | awk '{print $1}')"
USDC_BAL_AFTER="$(cast call "${USDC}" 'balanceOf(address)(uint256)' "${SIGNER}" --rpc-url "${ETHEREUM_MAINNET_RPC}" | awk '{print $1}')"
ETH_AFTER="$(cast balance "${SIGNER}" --rpc-url "${ETHEREUM_MAINNET_RPC}")"
python3 - "${OUT_JSON}" "${LATEST_JSON}" "${CW_APPROVE_TX}" "${FORWARD_TX}" "${USDC_APPROVE_TX}" "${REVERSE_TX}" "${RESERVES_AFTER}" "${CW_BAL_AFTER}" "${USDC_BAL_AFTER}" "${ETH_AFTER}" <<'PY'
import json
from pathlib import Path
import sys
out_json, latest_json, cw_approve, forward, usdc_approve, reverse, reserves, cw_bal, usdc_bal, eth = sys.argv[1:]
payload = json.loads(Path(out_json).read_text())
payload["executed"] = True
payload["transactions"] = {
"cwusdcApprove": cw_approve or None,
"forwardCwusdcToUsdc": forward,
"usdcApprove": usdc_approve or None,
"reverseUsdcToCwusdc": reverse,
}
payload["reservesAfter"] = reserves
payload["balancesAfter"] = {"ethWei": eth, "cwusdcRaw": cw_bal, "usdcRaw": usdc_bal}
Path(out_json).write_text(json.dumps(payload, indent=2) + "\n")
Path(latest_json).write_text(json.dumps(payload, indent=2) + "\n")
print(json.dumps(payload["transactions"], indent=2))
PY

View File

@@ -6,6 +6,8 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/load-project-env.sh
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh"
# shellcheck source=/home/intlc/projects/proxmox/scripts/lib/mev-protection.sh
source "${PROJECT_ROOT}/scripts/lib/mev-protection.sh"
: "${ETHEREUM_MAINNET_RPC:?ETHEREUM_MAINNET_RPC is required}"
@@ -107,7 +109,8 @@ EOF
exit 0
fi
cast send "${TOKEN_IN}" 'approve(address,uint256)' "${ROUTER}" "${AMOUNT_IN_RAW}" --rpc-url "${ETHEREUM_MAINNET_RPC}" --private-key "${PRIVATE_KEY}"
cast send "${ROUTER}" 'exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))(uint256)' \
mev_require_private_for_action "engine-x-univ3-public-swap-proof"
mev_cast_send "${TOKEN_IN}" 'approve(address,uint256)' "${ROUTER}" "${AMOUNT_IN_RAW}"
mev_cast_send "${ROUTER}" 'exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))(uint256)' \
"(${TOKEN_IN},${TOKEN_OUT},${FEE},${SIGNER},${DEADLINE},${AMOUNT_IN_RAW},${MIN_OUT_RAW},0)" \
--rpc-url "${ETHEREUM_MAINNET_RPC}" --private-key "${PRIVATE_KEY}"

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Fast/cheap EI matrix mainnet cWUSDC distribution via Multicall3 aggregate3 + transferFrom.
# One approve(Multicall3, budget) then one on-chain tx per chunk (default 200 transfers).
#
# Requires: cast, PRIVATE_KEY, ETHEREUM_MAINNET_RPC; token must allow transferFrom.
#
# Usage:
# ./scripts/deployment/send-cwusdc-ei-matrix-multicall-batches.sh --dry-run \\
# --tsv reports/status/ei-matrix-cwusdc-topup-amounts.tsv
# ./scripts/deployment/send-cwusdc-ei-matrix-multicall-batches.sh --execute \\
# --tsv reports/status/ei-matrix-cwusdc-topup-amounts.tsv
#
# Env: EI_MATRIX_MC_CHUNK (default 200), MULTICALL3_MAINNET, CWUSDC_MAINNET, DEPLOYER_ADDRESS (dry-run only)
#
# Core: scripts/lib/ei_matrix_multicall3_cwusdc_batch.py
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
# shellcheck disable=SC1091
source "$PROJECT_ROOT/scripts/lib/load-project-env.sh"
[[ "${1:-}" == "--" ]] && shift
LOCK_FILE="${PROJECT_ROOT}/reports/status/ei-matrix-multicall3-send.lock"
mkdir -p "$(dirname "$LOCK_FILE")"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "Another send-cwusdc-ei-matrix-multicall-batches.sh is running (lock: $LOCK_FILE)." >&2
exit 1
fi
exec python3 "$PROJECT_ROOT/scripts/lib/ei_matrix_multicall3_cwusdc_batch.py" "$@"

View File

@@ -0,0 +1,297 @@
#!/usr/bin/env bash
# Targeted mainnet cWUSDC transfers to a subset of EI matrix wallets by linear index.
# Intended for triage: feed indices from run-ei-matrix-full-readiness-audit.sh gap files.
#
# Usage:
# ./scripts/deployment/send-cwusdc-ei-matrix-targeted.sh [--dry-run] --send-raw R \
# [--indices-file PATH] [--amounts-tsv PATH] [--resume-next] [--quiet-dry-run] [--legacy]
#
# --indices-file Newline-separated linear indices (default: reports/status/ei-matrix-readiness-gaps-mainnet-indices.txt).
# Empty lines and # comments ignored.
# --send-raw R Amount (raw, 6 decimals) per wallet when not using --amounts-tsv.
# --amounts-tsv F Tab-separated: linearIndex <TAB> amountRaw (must cover every index in indices file).
# --resume-next Continue from reports/status/ei-matrix-cwusdc-targeted-last-idx.txt + 1
# (skips indices at or below last completed).
#
# Env: same as send-cwusdc-ei-matrix-wallets.sh (PRIVATE_KEY, ETHEREUM_MAINNET_RPC, CWUSDC_MAINNET, …)
# Lock: reports/status/ei-matrix-cwusdc-targeted-send.lock
# Progress: reports/status/ei-matrix-cwusdc-targeted-last-idx.txt
# Failures: reports/status/ei-matrix-cwusdc-targeted-failures.log
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
DRY_RUN=false
QUIET_DRY_RUN=false
CAST_LEGACY=false
RESUME_NEXT=false
INDICES_FILE="${EI_MATRIX_TARGETED_INDICES_FILE:-${PROJECT_ROOT}/reports/status/ei-matrix-readiness-gaps-mainnet-indices.txt}"
AMOUNTS_TSV=""
SEND_RAW=""
LAST_IDX_FILE="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-targeted-last-idx.txt"
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--quiet-dry-run) QUIET_DRY_RUN=true; shift ;;
--legacy) CAST_LEGACY=true; shift ;;
--resume-next) RESUME_NEXT=true; shift ;;
--indices-file) INDICES_FILE="${2:?}"; shift 2 ;;
--amounts-tsv) AMOUNTS_TSV="${2:?}"; shift 2 ;;
--send-raw) SEND_RAW="${2:?}"; shift 2 ;;
*) echo "Unknown arg: $1" >&2; exit 1 ;;
esac
done
if [[ -z "$SEND_RAW" && -z "$AMOUNTS_TSV" ]]; then
echo "Set --send-raw R or --amounts-tsv PATH." >&2
exit 1
fi
if [[ -n "$SEND_RAW" && -n "$AMOUNTS_TSV" ]]; then
echo "Use only one of --send-raw or --amounts-tsv." >&2
exit 1
fi
# shellcheck disable=SC1091
source "$PROJECT_ROOT/scripts/lib/load-project-env.sh"
CWUSDC="${CWUSDC_MAINNET:-0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a}"
PUBLIC_ETH_RPC="${ETHEREUM_MAINNET_PUBLIC_RPC:-https://ethereum-rpc.publicnode.com}"
RPC="${ETHEREUM_MAINNET_RPC:-${RPC_URL_1:-${ETH_MAINNET_RPC_URL:-$PUBLIC_ETH_RPC}}}"
BALANCE_RPC="${EI_MATRIX_BALANCE_RPC:-$RPC}"
GRID="$PROJECT_ROOT/config/pmm-soak-wallet-grid.json"
[[ -f "$INDICES_FILE" ]] || { echo "Missing indices file: $INDICES_FILE" >&2; exit 1; }
[[ -f "$GRID" ]] || { echo "Missing $GRID" >&2; exit 1; }
command -v cast &>/dev/null || { echo "cast required" >&2; exit 1; }
command -v jq &>/dev/null || { echo "jq required" >&2; exit 1; }
[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY not set" >&2; exit 1; }
LOCK_FILE="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-targeted-send.lock"
mkdir -p "${PROJECT_ROOT}/reports/status"
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
echo "Another send-cwusdc-ei-matrix-targeted.sh is already running (lock: $LOCK_FILE)." >&2
exit 1
fi
FROM_ADDR=$(cast wallet address --private-key "$PRIVATE_KEY")
CHAIN_ID=$(cast chain-id --rpc-url "$RPC" 2>/dev/null | tr -d '[:space:]' || true)
[[ -n "$CHAIN_ID" ]] || CHAIN_ID="1"
if [[ "$CHAIN_ID" != "1" ]]; then
echo "[WARN] chain-id=$CHAIN_ID (expected 1)." >&2
fi
pending_nonce() {
local resp hex
resp=$(curl -sS -X POST "$RPC" -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionCount\",\"params\":[\"${FROM_ADDR}\",\"pending\"],\"id\":1}" 2>/dev/null) || return 1
hex=$(echo "$resp" | jq -r '.result // empty')
[[ -n "$hex" ]] || return 1
cast to-dec "$hex"
}
token_decimals() {
cast call "$CWUSDC" 'decimals()(uint8)' --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}'
}
token_balance_raw() {
cast call "$CWUSDC" "balanceOf(address)(uint256)" "$FROM_ADDR" --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}'
}
ERR_LOG="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-targeted-failures.log"
# Sorted unique indices from file (one index per line; # comments; whitespace tolerated)
IND_TMP=$(mktemp)
grep -v '^[[:space:]]*$' "$INDICES_FILE" | sed 's/#.*//' \
| awk '{ gsub(/[[:space:]]/, "", $0); if ($0 ~ /^[0-9]+$/) print $0 }' \
| sort -n -u >"$IND_TMP" || true
if [[ ! -s "$IND_TMP" ]]; then
echo "No numeric indices in $INDICES_FILE — nothing to do."
rm -f "$IND_TMP"
exit 0
fi
if $RESUME_NEXT; then
[[ -f "$LAST_IDX_FILE" ]] || { echo "Missing $LAST_IDX_FILE for --resume-next" >&2; rm -f "$IND_TMP"; exit 1; }
_last="$(tr -d '[:space:]' < "$LAST_IDX_FILE" || echo "")"
[[ -n "$_last" ]] || { echo "Empty $LAST_IDX_FILE" >&2; rm -f "$IND_TMP"; exit 1; }
_filtered=$(mktemp)
while read -r line; do
[[ "$line" =~ ^[0-9]+$ ]] || continue
if [[ "$line" -gt "$_last" ]]; then
echo "$line"
fi
done <"$IND_TMP" >"$_filtered"
mv "$_filtered" "$IND_TMP"
echo "Resume-next: last completed idx=$_last; remaining indices: $(wc -l <"$IND_TMP" | tr -d ' ')"
fi
N_IND=$(wc -l <"$IND_TMP" | tr -d ' ')
if [[ "$N_IND" -eq 0 ]]; then
echo "No indices to process after filters."
rm -f "$IND_TMP"
exit 0
fi
PAIR_TMP=$(mktemp)
cleanup() {
[[ -f "$IND_TMP" ]] && rm -f "$IND_TMP"
[[ -f "$PAIR_TMP" ]] && rm -f "$PAIR_TMP"
}
trap cleanup EXIT
if [[ -n "$AMOUNTS_TSV" ]]; then
[[ -f "$AMOUNTS_TSV" ]] || { echo "Missing amounts TSV: $AMOUNTS_TSV" >&2; exit 1; }
# Build map idx->amount in Python for validation + join
python3 - "$IND_TMP" "$AMOUNTS_TSV" "$PAIR_TMP" <<'PY'
import sys
from pathlib import Path
ind_path = Path(sys.argv[1])
amt_path = Path(sys.argv[2])
out_path = Path(sys.argv[3])
wanted = [int(x) for x in ind_path.read_text().split() if x.strip().isdigit()]
wanted_set = set(wanted)
amap: dict[int, int] = {}
for line in amt_path.read_text().splitlines():
line = line.split("#", 1)[0].strip()
if not line:
continue
parts = line.split("\t")
if len(parts) < 2:
parts = line.split()
if len(parts) < 2:
print(f"Bad amounts line: {line!r}", file=sys.stderr)
sys.exit(1)
idx = int(parts[0].strip())
raw = int(parts[1].strip())
amap[idx] = raw
missing = sorted(wanted_set - set(amap))
if missing:
print(f"Amounts TSV missing {len(missing)} indices (first 20): {missing[:20]}", file=sys.stderr)
sys.exit(1)
lines = []
for idx in sorted(wanted_set):
lines.append(f"{idx}\t{amap[idx]}")
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
PY
# PAIR_TMP now: idx \t raw per line sorted by idx
BUDGET_RAW=$(awk '{s+=$2} END {print s}' "$PAIR_TMP")
else
while read -r idx; do
echo -e "${idx}\t${SEND_RAW}"
done <"$IND_TMP" | sort -n -t $'\t' -k1,1 >"$PAIR_TMP"
BUDGET_RAW=$(python3 -c "print(int('$SEND_RAW') * int('$N_IND'))")
fi
ADDR_AMT_TMP=$(mktemp)
cleanup() {
[[ -f "$IND_TMP" ]] && rm -f "$IND_TMP"
[[ -f "$PAIR_TMP" ]] && rm -f "$PAIR_TMP"
[[ -f "$ADDR_AMT_TMP" ]] && rm -f "$ADDR_AMT_TMP"
}
trap cleanup EXIT
while IFS=$'\t' read -r idx raw_amt; do
addr=$(jq -r --argjson i "$idx" '.wallets[$i].address // empty' "$GRID")
[[ -n "$addr" && "$addr" != null ]] || { echo "No address for index $idx in grid" >&2; exit 1; }
echo -e "$addr\t$raw_amt\t$idx"
done <"$PAIR_TMP" >"$ADDR_AMT_TMP"
DECIMALS=$(token_decimals || echo "6")
GAS_EST="${EI_MATRIX_SEND_GAS_EST:-70000}"
HEADROOM_BPS="${EI_MATRIX_GAS_HEADROOM_BPS:-10500}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "EI matrix cWUSDC targeted transfer (mainnet)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "RPC: $RPC"
echo "Token: $CWUSDC"
echo "Signer: $FROM_ADDR"
echo "Indices: $N_IND (from $INDICES_FILE)"
echo "Budget: $BUDGET_RAW raw total"
echo "Dry-run: $DRY_RUN"
echo ""
ETH_WEI=$(cast balance "$FROM_ADDR" --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}' || echo "0")
TOKEN_BAL=$(token_balance_raw || echo "0")
echo "Signer ETH: $ETH_WEI wei"
echo "Signer cWUSDC (raw): $TOKEN_BAL"
if ! $DRY_RUN && [[ "${EI_MATRIX_SKIP_BALANCE_CHECK:-}" != "1" ]]; then
if ! python3 -c "import sys; sys.exit(0 if int('$TOKEN_BAL') >= int('$BUDGET_RAW') else 1)"; then
echo "Insufficient cWUSDC for budget $BUDGET_RAW raw." >&2
exit 1
fi
fi
if ! $DRY_RUN && [[ "${EI_MATRIX_SKIP_GAS_CHECK:-}" != "1" ]]; then
GAS_PRICE_WEI=$(cast gas-price --rpc-url "$RPC" 2>/dev/null | awk '{print $1}' | head -1)
[[ -n "$GAS_PRICE_WEI" ]] || GAS_PRICE_WEI=0
MIN_WEI=$(python3 -c "c=int('$N_IND'); g=int('$GAS_EST'); p=int('$GAS_PRICE_WEI'); b=int('$HEADROOM_BPS'); print(c*g*p*b//10000)")
if ! python3 -c "import sys; sys.exit(0 if int('$ETH_WEI') >= int('$MIN_WEI') else 1)"; then
echo "Insufficient ETH for gas (need ≈ $MIN_WEI wei)." >&2
exit 1
fi
fi
matrix_try_transfer() {
local addr="$1" raw_amt="$2" idx="$3"
local dec human out tx attempt=1
dec="${DECIMALS:-6}"
[[ "$raw_amt" != "0" ]] || { echo "[skip] idx=$idx zero"; return 0; }
human=$(python3 -c "d=int('$dec'); a=int('$raw_amt'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$raw_amt")
if $DRY_RUN; then
if ! $QUIET_DRY_RUN; then
echo "[dry-run] idx=$idx $addr raw=$raw_amt (~$human)"
fi
return 0
fi
local cast_extra=()
$CAST_LEGACY && cast_extra+=(--legacy)
while [[ "$attempt" -le 2 ]]; do
if out=$(cast send "$CWUSDC" "transfer(address,uint256)" "$addr" "$raw_amt" \
--rpc-url "$RPC" --private-key "$PRIVATE_KEY" \
--nonce "$NONCE" "${cast_extra[@]}" 2>&1); then
tx=$(echo "$out" | tail -n1)
echo "[ok] idx=$idx nonce=$NONCE $addr raw=$raw_amt tx=$tx"
sent=$((sent + 1))
NONCE=$((NONCE + 1))
echo "$idx" >"$LAST_IDX_FILE"
return 0
fi
if [[ "$attempt" -eq 1 ]] && echo "$out" | grep -qi 'nonce too low'; then
NONCE=$(pending_nonce) || true
attempt=$((attempt + 1))
continue
fi
echo "[fail] idx=$idx $out" | tee -a "$ERR_LOG" >&2
failed=$((failed + 1))
NONCE=$(pending_nonce) || true
return 0
done
}
sent=0
failed=0
NONCE=$(pending_nonce) || { echo "Could not read nonce" >&2; exit 1; }
echo "Starting nonce: $NONCE"
echo ""
while IFS=$'\t' read -r addr raw_amt idx; do
matrix_try_transfer "$addr" "$raw_amt" "$idx"
done <"$ADDR_AMT_TMP"
if $DRY_RUN; then
echo "Dry-run complete ($N_IND wallets)."
else
echo "Done. sent=$sent failed=$failed"
fi

View File

@@ -0,0 +1,355 @@
#!/usr/bin/env bash
# Transfer Ethereum mainnet cWUSDC from PRIVATE_KEY holder to each address in
# config/pmm-soak-wallet-grid.json (EI matrix slice). Uses ERC-20 transfer(address,uint256).
#
# Modes (exactly one):
# --send-raw R Same raw units sent to every wallet in the slice.
# --total-send-raw B Total sent across the slice, split with ±spread then renormalized to B.
#
# Usage:
# ./scripts/deployment/send-cwusdc-ei-matrix-wallets.sh [--dry-run] [--limit N] [--offset N|--resume-next]
# (--send-raw R | --total-send-raw B [--spread-pct S])
#
# --quiet-dry-run With --dry-run, suppress per-wallet lines.
# --legacy Pass --legacy to cast send.
#
# Env: ETHEREUM_MAINNET_RPC, CWUSDC_MAINNET, PRIVATE_KEY,
# EI_MATRIX_SEND_GAS_EST (default 70000), EI_MATRIX_GAS_HEADROOM_BPS (default 10500),
# EI_MATRIX_SKIP_GAS_CHECK=1, EI_MATRIX_SKIP_BALANCE_CHECK=1 (operator risk).
#
# Progress: reports/status/ei-matrix-cwusdc-send-last-idx.txt
# Failures: reports/status/ei-matrix-cwusdc-send-failures.log
# Lock: reports/status/ei-matrix-cwusdc-send.lock
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
DRY_RUN=false
LIMIT=""
OFFSET="0"
OFFSET_EXPLICIT=false
RESUME_NEXT=false
SPREAD_PCT="${EI_MATRIX_SPREAD_PCT:-15}"
CAST_LEGACY=false
QUIET_DRY_RUN=false
SEND_RAW=""
TOTAL_SEND_RAW=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--quiet-dry-run) QUIET_DRY_RUN=true; shift ;;
--limit) LIMIT="${2:?}"; shift 2 ;;
--resume-next) RESUME_NEXT=true; shift ;;
--offset) OFFSET="${2:?}"; OFFSET_EXPLICIT=true; shift 2 ;;
--spread-pct) SPREAD_PCT="${2:?}"; shift 2 ;;
--send-raw) SEND_RAW="${2:?}"; shift 2 ;;
--total-send-raw) TOTAL_SEND_RAW="${2:?}"; shift 2 ;;
--legacy) CAST_LEGACY=true; shift ;;
*) echo "Unknown arg: $1" >&2; exit 1 ;;
esac
done
LAST_IDX_FILE="${EI_MATRIX_CWUSDC_SEND_LAST_IDX_FILE:-${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-send-last-idx.txt}"
if $RESUME_NEXT && $OFFSET_EXPLICIT; then
echo "Use only one of --offset or --resume-next." >&2
exit 1
fi
if $RESUME_NEXT; then
[[ -f "$LAST_IDX_FILE" ]] || { echo "Missing last-index file for --resume-next: $LAST_IDX_FILE" >&2; exit 1; }
_last="$(tr -d '[:space:]' < "$LAST_IDX_FILE" || echo "")"
[[ -n "$_last" ]] || { echo "Empty $LAST_IDX_FILE" >&2; exit 1; }
OFFSET=$((_last + 1))
echo "Resume-next (send): last completed idx=$_last → offset=$OFFSET"
fi
if [[ -n "$SEND_RAW" && -n "$TOTAL_SEND_RAW" ]]; then
echo "Use only one of --send-raw or --total-send-raw." >&2
exit 1
fi
if [[ -z "$SEND_RAW" && -z "$TOTAL_SEND_RAW" ]]; then
echo "Set --send-raw or --total-send-raw." >&2
exit 1
fi
# shellcheck disable=SC1091
source "$PROJECT_ROOT/scripts/lib/load-project-env.sh"
CWUSDC="${CWUSDC_MAINNET:-0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a}"
PUBLIC_ETH_RPC="${ETHEREUM_MAINNET_PUBLIC_RPC:-https://ethereum-rpc.publicnode.com}"
RPC="${ETHEREUM_MAINNET_RPC:-${RPC_URL_1:-${ETH_MAINNET_RPC_URL:-$PUBLIC_ETH_RPC}}}"
BALANCE_RPC="${EI_MATRIX_BALANCE_RPC:-$RPC}"
LOCK_FILE="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-send.lock"
MANIFEST_DIR="${PROJECT_ROOT}/reports/status"
mkdir -p "$MANIFEST_DIR"
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
echo "Another send-cwusdc-ei-matrix-wallets.sh is already running (lock: $LOCK_FILE)." >&2
exit 1
fi
GRID="$PROJECT_ROOT/config/pmm-soak-wallet-grid.json"
[[ -f "$GRID" ]] || { echo "Missing $GRID" >&2; exit 1; }
command -v cast &>/dev/null || { echo "cast required" >&2; exit 1; }
command -v jq &>/dev/null || { echo "jq required" >&2; exit 1; }
[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY not set" >&2; exit 1; }
FROM_ADDR=$(cast wallet address --private-key "$PRIVATE_KEY")
CHAIN_ID=$(cast chain-id --rpc-url "$RPC" 2>/dev/null | tr -d '[:space:]' || true)
[[ -n "$CHAIN_ID" ]] || CHAIN_ID="1"
if [[ "$CHAIN_ID" != "1" ]]; then
echo "[WARN] chain-id=$CHAIN_ID (expected 1)." >&2
fi
pending_nonce() {
local resp hex
resp=$(curl -sS -X POST "$RPC" -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionCount\",\"params\":[\"${FROM_ADDR}\",\"pending\"],\"id\":1}" 2>/dev/null) || return 1
hex=$(echo "$resp" | jq -r '.result // empty')
[[ -n "$hex" ]] || return 1
cast to-dec "$hex"
}
token_decimals() {
cast call "$CWUSDC" 'decimals()(uint8)' --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}'
}
token_balance_raw() {
cast call "$CWUSDC" "balanceOf(address)(uint256)" "$FROM_ADDR" --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}'
}
generate_spread_amounts_raw() {
local count="$1" budget="$2" spread="$3"
python3 - "$count" "$budget" "$spread" <<'PY'
import random
import sys
n = int(sys.argv[1])
budget = int(sys.argv[2])
spread = float(sys.argv[3])
if n <= 0:
sys.exit("count must be positive")
if budget < 0:
sys.exit("budget must be non-negative")
if spread < 0 or spread > 100:
sys.exit("spread-pct must be in [0, 100]")
base = 10000
low_w = max(1, (100 * base - int(spread * base)) // 100)
high_w = (100 * base + int(spread * base)) // 100
w = [random.randint(low_w, high_w) for _ in range(n)]
s = sum(w)
raw = [(budget * wi) // s for wi in w]
rem = budget - sum(raw)
for i in range(rem):
raw[i % n] += 1
for x in raw:
print(x)
PY
}
stream_addresses() {
if [[ -n "${LIMIT:-}" ]]; then
jq -r --argjson o "$OFFSET" --argjson l "$LIMIT" '.wallets[$o:$o+$l][] | .address' "$GRID"
else
jq -r --argjson o "$OFFSET" '.wallets[$o:][] | .address' "$GRID"
fi
}
ERR_LOG="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-send-failures.log"
LAST_IDX="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-send-last-idx.txt"
matrix_try_transfer() {
local addr="$1" raw_amt="$2" idx="$3"
local dec human out tx attempt=1
dec="${DECIMALS:-6}"
if [[ "$raw_amt" == "0" ]]; then
echo "[skip] idx=$idx $addr zero raw"
return 0
fi
human=$(python3 -c "d=int('$dec'); a=int('$raw_amt'); print(f'{a / (10**d):.{min(d,8)}f}')" 2>/dev/null || echo "$raw_amt")
if $DRY_RUN; then
if ! $QUIET_DRY_RUN; then
echo "[dry-run] idx=$idx $addr raw=$raw_amt (~$human)"
fi
return 0
fi
local cast_extra=()
$CAST_LEGACY && cast_extra+=(--legacy)
while [[ "$attempt" -le 2 ]]; do
if out=$(cast send "$CWUSDC" "transfer(address,uint256)" "$addr" "$raw_amt" \
--rpc-url "$RPC" --private-key "$PRIVATE_KEY" \
--nonce "$NONCE" "${cast_extra[@]}" 2>&1); then
tx=$(echo "$out" | tail -n1)
echo "[ok] idx=$idx nonce=$NONCE $addr raw=$raw_amt (~$human) tx=$tx"
sent=$((sent + 1))
NONCE=$((NONCE + 1))
echo "$idx" > "$LAST_IDX"
return 0
fi
if [[ "$attempt" -eq 1 ]] && echo "$out" | grep -qi 'nonce too low'; then
NONCE=$(pending_nonce) || true
echo "[retry] idx=$idx nonce refreshed to $NONCE (nonce too low)" >&2
attempt=$((attempt + 1))
continue
fi
echo "[fail] idx=$idx nonce=$NONCE $addr $out" | tee -a "$ERR_LOG" >&2
failed=$((failed + 1))
NONCE=$(pending_nonce) || true
return 0
done
}
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "EI matrix cWUSDC transfer (mainnet)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "RPC: $RPC"
echo "Token: $CWUSDC"
echo "Signer: $FROM_ADDR"
echo "Grid: $GRID"
echo "Dry-run: $DRY_RUN Quiet: $QUIET_DRY_RUN"
echo "Offset: $OFFSET Limit: ${LIMIT:-all}"
if [[ -n "$SEND_RAW" ]]; then
echo "Mode: fixed --send-raw $SEND_RAW per wallet"
else
echo "Mode: --total-send-raw $TOTAL_SEND_RAW spread: ±${SPREAD_PCT}% normalized"
fi
echo ""
DECIMALS=$(token_decimals || echo "6")
ADDR_TMP=$(mktemp)
AMOUNTS_TMP=$(mktemp)
cleanup_tmp() {
[[ -f "$ADDR_TMP" ]] && rm -f "$ADDR_TMP"
[[ -f "$AMOUNTS_TMP" ]] && rm -f "$AMOUNTS_TMP"
}
trap cleanup_tmp EXIT
stream_addresses > "$ADDR_TMP"
WALLET_COUNT=$(wc -l < "$ADDR_TMP" | tr -d '[:space:]')
if [[ -z "$WALLET_COUNT" || "$WALLET_COUNT" -eq 0 ]]; then
echo "No wallets in range (offset=$OFFSET limit=${LIMIT:-all})." >&2
exit 1
fi
if [[ -n "$SEND_RAW" ]]; then
awk -v r="$SEND_RAW" '{print r}' "$ADDR_TMP" > "$AMOUNTS_TMP"
BUDGET_RAW=$((SEND_RAW * WALLET_COUNT))
else
BUDGET_RAW="$TOTAL_SEND_RAW"
if [[ "$BUDGET_RAW" -le 0 ]]; then
echo "total-send-raw must be positive." >&2
exit 1
fi
generate_spread_amounts_raw "$WALLET_COUNT" "$BUDGET_RAW" "$SPREAD_PCT" > "$AMOUNTS_TMP"
fi
SUM_CHECK=$(awk '{s+=$1} END {print s}' "$AMOUNTS_TMP")
if [[ "$SUM_CHECK" != "$BUDGET_RAW" ]]; then
echo "INTERNAL: amount sum $SUM_CHECK != budget $BUDGET_RAW" >&2
exit 1
fi
AMOUNTS_SHA256=$(sha256sum "$AMOUNTS_TMP" | awk '{print $1}')
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
MANIFEST="$MANIFEST_DIR/ei-matrix-cwusdc-send-manifest-${TS//:/-}.json"
cat >"$MANIFEST" <<EOF
{
"version": 1,
"kind": "ei-matrix-cwusdc-transfer",
"timestamp": "$TS",
"chainId": 1,
"token": "$CWUSDC",
"signer": "$FROM_ADDR",
"offset": $OFFSET,
"limit": ${LIMIT:-null},
"walletCount": $WALLET_COUNT,
"budgetRaw": "$BUDGET_RAW",
"fixedSendRaw": ${SEND_RAW:-null},
"spreadPct": ${SPREAD_PCT},
"amountsSha256": "$AMOUNTS_SHA256",
"manifestPath": "$MANIFEST"
}
EOF
echo "Manifest: $MANIFEST"
echo "Amounts SHA256: $AMOUNTS_SHA256"
echo ""
GAS_EST="${EI_MATRIX_SEND_GAS_EST:-70000}"
HEADROOM_BPS="${EI_MATRIX_GAS_HEADROOM_BPS:-10500}"
ETH_WEI=$(cast balance "$FROM_ADDR" --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}' || echo "0")
ETH_HUMAN=$(python3 -c "print(f'{int(\"$ETH_WEI\") / 1e18:.6f}')" 2>/dev/null || echo "?")
echo "Signer ETH (gas): ${ETH_WEI} wei (~$ETH_HUMAN ETH)"
TOKEN_BAL=$(token_balance_raw || echo "0")
echo "Signer cWUSDC balance (raw): $TOKEN_BAL (need >= $BUDGET_RAW for this slice)"
if ! $DRY_RUN && [[ "${EI_MATRIX_SKIP_BALANCE_CHECK:-}" != "1" ]]; then
if python3 -c "import sys; sys.exit(0 if int('$TOKEN_BAL') >= int('$BUDGET_RAW') else 1)"; then
echo "Token preflight OK: balance covers budget $BUDGET_RAW raw."
else
echo "Insufficient cWUSDC: have $TOKEN_BAL raw, need $BUDGET_RAW raw." >&2
echo "Set EI_MATRIX_SKIP_BALANCE_CHECK=1 to override (operator risk)." >&2
exit 1
fi
fi
if ! $DRY_RUN && [[ "${EI_MATRIX_SKIP_GAS_CHECK:-}" != "1" ]]; then
GAS_PRICE_WEI=$(cast gas-price --rpc-url "$RPC" 2>/dev/null | awk '{print $1}' | head -1)
[[ -n "$GAS_PRICE_WEI" ]] || GAS_PRICE_WEI=0
MIN_WEI=$(python3 -c "c=int('$WALLET_COUNT'); g=int('$GAS_EST'); p=int('$GAS_PRICE_WEI'); b=int('$HEADROOM_BPS'); print(c*g*p*b//10000)")
if python3 -c "import sys; sys.exit(0 if int('$ETH_WEI') >= int('$MIN_WEI') else 1)"; then
echo "Gas preflight OK: est ${GAS_EST} gas/tx × $WALLET_COUNT × gasPrice $GAS_PRICE_WEI × (${HEADROOM_BPS}/10000) ≈ $MIN_WEI wei."
else
echo "Insufficient ETH for gas preflight. Need ≈ $MIN_WEI wei." >&2
echo "Set EI_MATRIX_SKIP_GAS_CHECK=1 to override (operator risk)." >&2
exit 1
fi
fi
echo ""
echo "Sample transfers:"
if [[ "$WALLET_COUNT" -le 6 ]]; then
_s_idx=$OFFSET
while IFS=$'\t' read -r s_addr s_raw; do
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
_s_idx=$((_s_idx + 1))
done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP")
else
_s_idx=$OFFSET
while IFS=$'\t' read -r s_addr s_raw; do
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
_s_idx=$((_s_idx + 1))
done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP" | head -3)
_s_idx=$((OFFSET + WALLET_COUNT - 3))
while IFS=$'\t' read -r s_addr s_raw; do
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
_s_idx=$((_s_idx + 1))
done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP" | tail -3)
fi
echo ""
sent=0
failed=0
idx=$OFFSET
NONCE=$(pending_nonce) || { echo "Could not read pending nonce" >&2; exit 1; }
echo "Starting nonce (pending): $NONCE"
echo ""
while IFS=$'\t' read -r addr raw_amt; do
matrix_try_transfer "$addr" "$raw_amt" "$idx"
idx=$((idx + 1))
done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP")
if $DRY_RUN; then
echo "Dry-run complete. Indices covered: $OFFSET..$((idx - 1))."
else
echo "Done. Transfer txs attempted: sent=$sent failed=$failed"
fi

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Transfer native SOL (lamports) on Solana mainnet-beta (or any RPC you pass).
Loads ``SOLANA_RPC_URL`` and ``SOLANA_KEYPAIR_PATH`` from the environment when
set (after ``source scripts/lib/load-project-env.sh``). Submits via
``solana_jsonrpc.send_transaction_wire`` (``scripts/lib/solana_jsonrpc.py``) so
RPCs that return only a signature string for ``sendTransaction`` do not hit
``solana-py``'s ``SendTransactionResp`` parser (which can panic on ``missing field 'data'``).
Install (venv recommended)::
pip install -r scripts/lib/requirements-solana-ops.txt
"""
from __future__ import annotations
import argparse
import base64
import json
import os
import sys
from pathlib import Path
# ``scripts/lib`` is not a Python package; load ``solana_jsonrpc`` by path.
_LIB = Path(__file__).resolve().parents[1] / "lib"
if str(_LIB) not in sys.path:
sys.path.insert(0, str(_LIB))
import solana_jsonrpc # noqa: E402
def _load_keypair(path: Path):
with path.open() as f:
raw = json.load(f)
if not isinstance(raw, list) or len(raw) != 64:
raise SystemExit("keypair JSON must be a 64-element byte array (Solana CLI format)")
from solders.keypair import Keypair
return Keypair.from_bytes(bytes(raw))
def main() -> None:
p = argparse.ArgumentParser(description="Send native SOL via JSON-RPC (robust sendTransaction parsing).")
p.add_argument("--to", required=True, help="Destination base58 pubkey")
p.add_argument(
"--lamports",
type=int,
help="Amount to send (excludes fee; payer pays fee separately). Omit with --sweep-all",
)
p.add_argument(
"--sweep-all",
action="store_true",
help="Send entire balance minus 5000 lamports legacy fee reserve",
)
p.add_argument("--fee-lamports", type=int, default=5000, help="Reserved for fee when using --sweep-all")
p.add_argument("--rpc-url", default=os.environ.get("SOLANA_RPC_URL", "").strip())
p.add_argument(
"--keypair",
type=Path,
default=Path(os.environ.get("SOLANA_KEYPAIR_PATH", "").strip() or "."),
help="Solana CLI JSON keypair path (default: SOLANA_KEYPAIR_PATH)",
)
p.add_argument("--skip-preflight", action="store_true")
p.add_argument(
"--dry-run",
action="store_true",
help="Print base64 wire and exit without sending",
)
p.add_argument(
"--no-wait",
action="store_true",
help="Do not poll getSignatureStatuses after send (default: wait up to 90s)",
)
args = p.parse_args()
try:
from solders.hash import Hash
from solders.pubkey import Pubkey
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction
except ImportError:
print(
"Missing dependency: install with\n"
" pip install -r scripts/lib/requirements-solana-ops.txt",
file=sys.stderr,
)
raise SystemExit(2) from None
if not args.rpc_url:
print("Set SOLANA_RPC_URL or pass --rpc-url", file=sys.stderr)
raise SystemExit(2)
if not args.keypair.is_file():
print(f"Keypair not found: {args.keypair}", file=sys.stderr)
raise SystemExit(2)
kp = _load_keypair(args.keypair)
dest = Pubkey.from_string(args.to)
src = kp.pubkey()
if args.dry_run:
if args.sweep_all:
raise SystemExit("--dry-run requires explicit --lamports (no balance query)")
if args.lamports is None:
raise SystemExit("Pass --lamports N with --dry-run")
send_lamports = args.lamports
else:
bal = solana_jsonrpc.get_balance_lamports(args.rpc_url, str(src))
if args.sweep_all:
send_lamports = bal - args.fee_lamports
if send_lamports <= 0:
raise SystemExit("Nothing to sweep after fee reserve")
elif args.lamports is not None:
send_lamports = args.lamports
if send_lamports <= 0:
raise SystemExit("--lamports must be positive")
if bal < send_lamports + args.fee_lamports:
raise SystemExit(
f"Insufficient balance: have {bal} lamports, need {send_lamports + args.fee_lamports}"
)
else:
raise SystemExit("Pass --lamports N or --sweep-all")
bh_str = solana_jsonrpc.get_latest_blockhash(args.rpc_url)
bh = Hash.from_string(bh_str)
ix = transfer(TransferParams(from_pubkey=src, to_pubkey=dest, lamports=send_lamports))
tx = Transaction.new_signed_with_payer([ix], src, [kp], bh)
wire = bytes(tx)
if args.dry_run:
print("blockhash", bh_str)
print("wire_b64", base64.b64encode(wire).decode("ascii"))
return
sig = solana_jsonrpc.send_transaction_wire(
args.rpc_url,
wire,
skip_preflight=args.skip_preflight,
preflight_commitment="confirmed",
)
print(sig)
if not args.no_wait:
st = solana_jsonrpc.wait_until_signature_confirmed(args.rpc_url, sig)
print("confirmationStatus", st.get("confirmationStatus"), "slot", st.get("slot"), file=sys.stderr)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# Sync Gov Portals monorepo from Gitea to CT 7804 (gov-portals-dev), install deps,
# build DBIS + ICCC (and OMNL/XOM when they define a "build" script), restart systemd units.
#
# CT 7804 typically runs on r630-04 (192.168.11.14); tarball deploys omit .git, so
# in-container "git pull" is not enough — this script refreshes a local clone then
# streams the tree into the container.
#
# Usage (from proxmox repo root):
# export GITEA_TOKEN=... # or ensure it is in .env (see .env.master.example)
# bash scripts/deployment/sync-gov-portals-ct-7804-from-git.sh
#
# Options:
# --skip-fetch Use GOV_PORTALS_SOURCE as-is (no git fetch; no token required)
# --dry-run Print steps only
#
# Env:
# GOV_PORTALS_SOURCE Default: /home/intlc/projects/gov-portals-monorepo
# GOV_PORTALS_REPO_URL Default: https://gitea.d-bis.org/Gov_Web_Portals/gov-portals-monorepo.git
# GOV_PORTALS_REF Default: main
# PROXMOX_HOST / DBIS_PORTAL_PROXMOX_HOST / PROXMOX_HOST_GOV_PORTALS Default: 192.168.11.14
# VMID_GOV_PORTALS Default: 7804
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck disable=SC1090
source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true
# shellcheck disable=SC1090
[ -f "$PROJECT_ROOT/.env" ] && set +u && source "$PROJECT_ROOT/.env" 2>/dev/null || true && set -u
GOV_PORTALS_SOURCE="${GOV_PORTALS_SOURCE:-/home/intlc/projects/gov-portals-monorepo}"
GOV_PORTALS_REPO_URL="${GOV_PORTALS_REPO_URL:-https://gitea.d-bis.org/Gov_Web_Portals/gov-portals-monorepo.git}"
GOV_PORTALS_REF="${GOV_PORTALS_REF:-main}"
VMID_GOV_PORTALS="${VMID_GOV_PORTALS:-7804}"
PROXMOX_HOST="${DBIS_PORTAL_PROXMOX_HOST:-${PROXMOX_HOST_GOV_PORTALS:-192.168.11.14}}"
SKIP_FETCH=false
DRY_RUN=false
for arg in "$@"; do
[[ "$arg" == "--skip-fetch" ]] && SKIP_FETCH=true
[[ "$arg" == "--dry-run" ]] && DRY_RUN=true
done
die() { echo "ERROR: $*" >&2; exit 1; }
log() { echo "[$(date +%H:%M:%S)] $*"; }
[[ -d "$GOV_PORTALS_SOURCE" ]] || die "GOV_PORTALS_SOURCE is not a directory: $GOV_PORTALS_SOURCE"
git_auth_args=()
if [[ -n "${GITEA_TOKEN:-}" ]]; then
git_auth_args=(-c "http.extraHeader=Authorization: token ${GITEA_TOKEN}")
fi
if [[ "$SKIP_FETCH" != "true" ]]; then
[[ -d "$GOV_PORTALS_SOURCE/.git" ]] || die "Not a git clone: $GOV_PORTALS_SOURCE (use --skip-fetch to rsync only)"
if [[ ${#git_auth_args[@]} -eq 0 ]]; then
die "GITEA_TOKEN is unset. Add it to $PROJECT_ROOT/.env or run: export GITEA_TOKEN=... (Or use --skip-fetch.)"
fi
if [[ "$DRY_RUN" == "true" ]]; then
log "DRY: would git fetch $GOV_PORTALS_REF and submodule update in $GOV_PORTALS_SOURCE"
else
log "Fetching $GOV_PORTALS_REF and updating submodules in $GOV_PORTALS_SOURCE"
git -C "$GOV_PORTALS_SOURCE" "${git_auth_args[@]}" fetch origin
git -C "$GOV_PORTALS_SOURCE" reset --hard "origin/$GOV_PORTALS_REF"
git -C "$GOV_PORTALS_SOURCE" "${git_auth_args[@]}" submodule update --init --recursive --force
log "Monorepo HEAD: $(git -C "$GOV_PORTALS_SOURCE" log -1 --oneline)"
if [[ -e "$GOV_PORTALS_SOURCE/DBIS/.git" ]]; then
log "DBIS HEAD: $(git -C "$GOV_PORTALS_SOURCE/DBIS" log -1 --oneline)"
fi
fi
else
log "Skipping git fetch (--skip-fetch)"
fi
SYNC_ID="gov-portals-ct-${VMID_GOV_PORTALS}-$(date +%s)"
REMOTE_SYNC="/tmp/$SYNC_ID"
if [[ "$DRY_RUN" == "true" ]]; then
log "DRY: would rsync to root@$PROXMOX_HOST:$REMOTE_SYNC/ and tar into CT $VMID_GOV_PORTALS"
exit 0
fi
log "Rsync to $PROXMOX_HOST:$REMOTE_SYNC/"
rsync -az --delete \
--exclude 'node_modules' --exclude '.next' --exclude '.git' \
--exclude '*/node_modules' --exclude '*/.next' --exclude '*/.git' \
"$GOV_PORTALS_SOURCE/" "root@$PROXMOX_HOST:$REMOTE_SYNC/"
run_pve() {
ssh -o ConnectTimeout=20 -o StrictHostKeyChecking=accept-new "root@$PROXMOX_HOST" "$@"
}
VMID="$VMID_GOV_PORTALS"
run_pve "pct exec $VMID -- mkdir -p /srv/gov-portals /tmp/gov-env-7804"
for portal in DBIS ICCC; do
for f in .env .env.local .env.production; do
run_pve "pct exec $VMID -- bash -c '[ -f /srv/gov-portals/${portal}/${f} ] && cp -a /srv/gov-portals/${portal}/${f} /tmp/gov-env-7804/${portal}_${f} || true'"
done
done
run_pve "pct exec $VMID -- bash -c 'if [ -d /srv/gov-portals ]; then find /srv/gov-portals -mindepth 1 -maxdepth 1 -exec rm -rf {} +; else mkdir -p /srv/gov-portals; fi'"
run_pve "bash -c 'cd $REMOTE_SYNC && tar cf - . | pct exec $VMID -- tar xf - -C /srv/gov-portals'"
for portal in DBIS ICCC; do
for f in .env .env.local .env.production; do
run_pve "pct exec $VMID -- bash -c '[ -f /tmp/gov-env-7804/${portal}_${f} ] && [ ! -f /srv/gov-portals/${portal}/${f} ] && cp -a /tmp/gov-env-7804/${portal}_${f} /srv/gov-portals/${portal}/${f} || true'"
done
done
run_pve "pct exec $VMID -- bash -lc 'export PATH=/usr/local/bin:/usr/bin:/bin:\$PATH; cd /srv/gov-portals && (pnpm install --frozen-lockfile || pnpm install)'"
run_pve "pct exec $VMID -- bash -lc 'export PATH=/usr/local/bin:/usr/bin:/bin:\$PATH; cd /srv/gov-portals/DBIS && pnpm run build && systemctl restart gov-portal-DBIS'"
run_pve "pct exec $VMID -- bash -lc 'export PATH=/usr/local/bin:/usr/bin:/bin:\$PATH; cd /srv/gov-portals/ICCC && pnpm run build && systemctl restart gov-portal-ICCC'"
run_pve "pct exec $VMID -- bash -lc 'export PATH=/usr/local/bin:/usr/bin:/bin:\$PATH; for p in OMNL XOM; do d=/srv/gov-portals/\$p; if [ -f \"\$d/package.json\" ] && grep -qF \"\\\"build\\\"\" \"\$d/package.json\" 2>/dev/null; then (cd \"\$d\" && pnpm run build && systemctl restart gov-portal-\$p) || true; fi; done'"
run_pve "pct exec $VMID -- bash -lc 'systemctl is-active gov-portal-DBIS gov-portal-ICCC gov-portal-OMNL gov-portal-XOM || true; printf DBIS:; curl -s -o /dev/null -w %{http_code} http://127.0.0.1:3001/; echo; printf ICCC:; curl -s -o /dev/null -w %{http_code} http://127.0.0.1:3002/; echo'"
run_pve "rm -rf $REMOTE_SYNC"
log "Removed $PROXMOX_HOST:$REMOTE_SYNC"
log "Done. CT $VMID_GOV_PORTALS on $PROXMOX_HOST updated."

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# One-way sync: local workstation project tree → Dev VM (5700) /srv/projects
# for coordinated development over SSH (Cursor Remote-SSH, shared tree).
#
# Prerequisites:
# - CT 5700 exists, ssh dev1@IP_DEV_VM works, /srv/projects is writable (see setup-dev-vm-users-and-gitea.sh).
# - Run from a machine that has your local clone (default: ~/projects).
#
# Usage:
# ./scripts/deployment/sync-local-projects-to-dev-vm.sh --dry-run
# ./scripts/deployment/sync-local-projects-to-dev-vm.sh
# ./scripts/deployment/sync-local-projects-to-dev-vm.sh --delete-remote # mirror: remove remote files absent locally
# RSYNC_RSH='ssh -o ProxyCommand="cloudflared access ssh --hostname ssh.dev.d-bis.org"' \
# DEV_VM_HOST=ssh.dev.d-bis.org ./scripts/deployment/sync-local-projects-to-dev-vm.sh
#
# Env:
# SOURCE — local directory (default: ~/projects)
# DEV_VM_USER — SSH user on dev VM (default: dev1)
# DEV_VM_HOST — override IP/hostname (default: IP_DEV_VM from config)
# DEV_VM_PROJECTS — remote path (default: /srv/projects)
# RSYNC_EXTRA_OPTS — extra rsync args (quoted string)
# RSYNC_RSH — passed to rsync as SSH transport (e.g. cloudflared ProxyCommand)
#
# If --delete-remote fails with rsync code 23 (Permission denied on delete), run:
# ./scripts/deployment/fix-dev-vm-srv-projects-ownership.sh
# See: docs/04-configuration/DEV_VM_WORKSTATION_MIGRATION_RUNBOOK.md
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
SOURCE="${SOURCE:-${HOME}/projects}"
DEV_VM_USER="${DEV_VM_USER:-dev1}"
DEV_VM_HOST="${DEV_VM_HOST:-${IP_DEV_VM:-192.168.11.59}}"
DEV_VM_PROJECTS="${DEV_VM_PROJECTS:-/srv/projects}"
RSYNC_EXTRA_OPTS="${RSYNC_EXTRA_OPTS:-}"
DRY_RUN=()
DELETE_REMOTE=0
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=(-n); shift ;;
--delete-remote) DELETE_REMOTE=1; shift ;;
--help|-h)
sed -n '1,35p' "$0" | tail -n +2
exit 0
;;
*)
echo "ERROR: unknown argument: $1 (try --help)" >&2
exit 1
;;
esac
done
if [[ ! -d "$SOURCE" ]]; then
echo "ERROR: SOURCE is not a directory: $SOURCE" >&2
exit 1
fi
REMOTE="${DEV_VM_USER}@${DEV_VM_HOST}:${DEV_VM_PROJECTS}/"
RSYNC_DELETE=()
if [[ "$DELETE_REMOTE" == "1" ]]; then
RSYNC_DELETE=(--delete-delay)
echo "WARNING: --delete-remote — files on Dev VM under DEST not present locally (after excludes) will be REMOVED."
echo ""
else
echo "Safe mode: no remote delete (omit files only on VM). Use --delete-remote to mirror (destructive)."
echo ""
fi
echo "=== Sync local projects → Dev VM ==="
echo "SOURCE: $SOURCE"
echo "DEST: $REMOTE"
echo "Mode: ${DRY_RUN[*]:-live}"
if [[ -n "${RSYNC_RSH:-}" ]]; then
echo "RSYNC_RSH: set (custom SSH / cloudflared)"
fi
echo ""
echo "NOTE: Review secrets after sync — chmod 600 remote .env; share only with trusted dev users."
echo ""
# Heavy / reproducible artifacts (keep .git; omit bulky caches)
RSYNC_EXCLUDES=(
--exclude=node_modules
--exclude=__pycache__
--exclude=.pnpm-store
--exclude=.pnpm
--exclude=venv
--exclude=.venv
--exclude=.venv-*
--exclude=dist
--exclude=build
--exclude=.next
--exclude=out
--exclude=.turbo
--exclude=.cache
--exclude=.parcel-cache
--exclude=coverage
--exclude=.pytest_cache
--exclude=.mypy_cache
--exclude=.ruff_cache
--exclude=forge-cache
--exclude=artifacts
--exclude=broadcast
--exclude=tmp
--exclude=.tmp
--exclude=.codex-artifacts
)
# Optional: skip known multi-GB trees (re-clone or sync later with --no-skip-large)
if [[ "${SKIP_LARGE_LOCAL_TREES:-1}" == "1" ]]; then
RSYNC_EXCLUDES+=(
--exclude=MEV_Bot
--exclude=the-order
)
echo "SKIP_LARGE_LOCAL_TREES=1: excluding MEV_Bot, the-order (set SKIP_LARGE_LOCAL_TREES=0 to include)."
echo ""
fi
if [[ -n "${RSYNC_RSH:-}" ]]; then
export RSYNC_RSH
else
unset RSYNC_RSH 2>/dev/null || true
fi
set -x
# shellcheck disable=SC2086
set +e
rsync -av "${DRY_RUN[@]}" "${RSYNC_DELETE[@]}" --omit-dir-times \
"${RSYNC_EXCLUDES[@]}" \
${RSYNC_EXTRA_OPTS} \
"${SOURCE}/" "${REMOTE}"
rsync_ec=$?
set -e
set +x
if [[ "$rsync_ec" -ne 0 ]]; then
echo "" >&2
echo "ERROR: rsync exited with code $rsync_ec." >&2
if [[ "$rsync_ec" -eq 23 && "$DELETE_REMOTE" == "1" ]]; then
echo " Common cause: root-owned files on the VM block deletes. From repo root (Proxmox root SSH to the node that runs CT 5700):" >&2
echo " ./scripts/deployment/fix-dev-vm-srv-projects-ownership.sh" >&2
fi
exit "$rsync_ec"
fi
echo ""
echo "Done. Next: SSH to dev VM, open /srv/projects/proxmox in Cursor (Remote-SSH), run pnpm/npm install where needed."