chore(repo): sync operator workspace (config, scripts, docs, multi-chain)
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:
5
scripts/deployment/continue-cwusdc-ei-matrix-wallets.sh
Executable file
5
scripts/deployment/continue-cwusdc-ei-matrix-wallets.sh
Executable 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 "$@"
|
||||
0
scripts/deployment/continue-mint-cwusdc-ei-matrix-wallets.sh
Normal file → Executable file
0
scripts/deployment/continue-mint-cwusdc-ei-matrix-wallets.sh
Normal file → Executable file
391
scripts/deployment/deploy-configure-cw-multitoken-l2-receivers.sh
Executable file
391
scripts/deployment/deploy-configure-cw-multitoken-l2-receivers.sh
Executable 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"
|
||||
89
scripts/deployment/deploy-engine-x-single-sided-dodo-cwusdc-vault-mainnet.sh
Executable file
89
scripts/deployment/deploy-engine-x-single-sided-dodo-cwusdc-vault-mainnet.sh
Executable 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}"
|
||||
@@ -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)"
|
||||
|
||||
172
scripts/deployment/execute-coffee-money-gas-topups.mjs
Normal file
172
scripts/deployment/execute-coffee-money-gas-topups.mjs
Normal 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);
|
||||
63
scripts/deployment/fix-dev-vm-srv-projects-ownership.sh
Executable file
63
scripts/deployment/fix-dev-vm-srv-projects-ownership.sh
Executable 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."
|
||||
@@ -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
35
scripts/deployment/mint-cwusdc-ei-matrix-wallets.sh
Normal file → Executable 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
|
||||
|
||||
30
scripts/deployment/mirror-github-fork-to-gitea.sh
Executable file
30
scripts/deployment/mirror-github-fork-to-gitea.sh
Executable 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
15
scripts/deployment/pipeline-ei-matrix-mint-cwusdc.sh
Normal file → Executable 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 ""
|
||||
|
||||
104
scripts/deployment/pipeline-ei-matrix-remediate-cwusdc-from-audit.sh
Executable file
104
scripts/deployment/pipeline-ei-matrix-remediate-cwusdc-from-audit.sh
Executable 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"
|
||||
325
scripts/deployment/plan-coffee-money-gas-topups.mjs
Normal file
325
scripts/deployment/plan-coffee-money-gas-topups.mjs
Normal 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);
|
||||
73
scripts/deployment/probe-dev-vm-ssh.sh
Executable file
73
scripts/deployment/probe-dev-vm-ssh.sh
Executable 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."
|
||||
@@ -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"
|
||||
|
||||
74
scripts/deployment/run-ei-matrix-cwusdc-targeted-chunked.sh
Executable file
74
scripts/deployment/run-ei-matrix-cwusdc-targeted-chunked.sh
Executable 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
|
||||
169
scripts/deployment/run-engine-x-univ2-public-indexed-loop.sh
Executable file
169
scripts/deployment/run-engine-x-univ2-public-indexed-loop.sh
Executable 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
|
||||
@@ -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}"
|
||||
|
||||
32
scripts/deployment/send-cwusdc-ei-matrix-multicall-batches.sh
Executable file
32
scripts/deployment/send-cwusdc-ei-matrix-multicall-batches.sh
Executable 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" "$@"
|
||||
297
scripts/deployment/send-cwusdc-ei-matrix-targeted.sh
Executable file
297
scripts/deployment/send-cwusdc-ei-matrix-targeted.sh
Executable 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
|
||||
355
scripts/deployment/send-cwusdc-ei-matrix-wallets.sh
Executable file
355
scripts/deployment/send-cwusdc-ei-matrix-wallets.sh
Executable 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
|
||||
148
scripts/deployment/solana-transfer-native.py
Executable file
148
scripts/deployment/solana-transfer-native.py
Executable 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()
|
||||
127
scripts/deployment/sync-gov-portals-ct-7804-from-git.sh
Executable file
127
scripts/deployment/sync-gov-portals-ct-7804-from-git.sh
Executable 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."
|
||||
154
scripts/deployment/sync-local-projects-to-dev-vm.sh
Executable file
154
scripts/deployment/sync-local-projects-to-dev-vm.sh
Executable 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."
|
||||
Reference in New Issue
Block a user