- Update dbis_core, cross-chain-pmm-lps, explorer-monorepo, metamask-integration, pr-workspace/chains - Omit embedded publish git dirs and empty placeholders from index Made-with: Cursor
206 lines
7.9 KiB
Bash
Executable File
206 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Send WETH cross-chain via CCIP (Chain 138 → destination chain).
|
|
# Usage: ./scripts/bridge/run-send-cross-chain.sh <amount_eth> [recipient] [--dry-run]
|
|
# Env: CCIP_DEST_CHAIN_SELECTOR, GAS_PRICE, GAS_LIMIT, CONFIRM_ABOVE_ETH (prompt above this amount)
|
|
# Version: 2026-03-30
|
|
|
|
set -euo pipefail
|
|
[[ "${DEBUG:-0}" = "1" ]] && set -x
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
had_nounset=0
|
|
if [[ $- == *u* ]]; then
|
|
had_nounset=1
|
|
set +u
|
|
fi
|
|
source "${SCRIPT_DIR}/../lib/load-project-env.sh"
|
|
(( had_nounset )) && set -u
|
|
|
|
[[ -z "${PRIVATE_KEY:-}" ]] && { echo "PRIVATE_KEY required"; exit 1; }
|
|
[[ -z "${CCIPWETH9_BRIDGE_CHAIN138:-}" ]] && { echo "CCIPWETH9_BRIDGE_CHAIN138 required"; exit 1; }
|
|
command -v cast &>/dev/null || { echo "ERROR: cast not found (install Foundry)"; exit 1; }
|
|
|
|
DRY_RUN=false
|
|
ARGS=()
|
|
for a in "$@"; do
|
|
[[ "$a" = "--dry-run" ]] && DRY_RUN=true || ARGS+=("$a")
|
|
done
|
|
|
|
AMOUNT_ETH="${ARGS[0]:?Usage: $0 amount_eth [recipient] [--dry-run]}"
|
|
RECIPIENT="${ARGS[1]:-$(cast wallet address "$PRIVATE_KEY" 2>/dev/null)}"
|
|
SENDER_ADDR="$(cast wallet address "$PRIVATE_KEY" 2>/dev/null)"
|
|
|
|
DEST_SELECTOR="${CCIP_DEST_CHAIN_SELECTOR:-5009297550715157269}"
|
|
GAS_PRICE="${GAS_PRICE:-1000000000}"
|
|
GAS_LIMIT="${GAS_LIMIT:-}"
|
|
RPC="${RPC_URL_138:-$CHAIN138_RPC}"
|
|
[[ -z "$RPC" ]] && { echo "ERROR: RPC_URL_138 or CHAIN138_RPC required"; exit 1; }
|
|
BRIDGE="${CCIPWETH9_BRIDGE_CHAIN138}"
|
|
|
|
extract_first_address() {
|
|
echo "$1" | grep -oE '0x[a-fA-F0-9]{40}' | sed -n '1p'
|
|
}
|
|
|
|
raw_uint() {
|
|
echo "$1" | awk '{print $1}'
|
|
}
|
|
|
|
lower() {
|
|
echo "$1" | tr '[:upper:]' '[:lower:]'
|
|
}
|
|
|
|
MAINNET_SELECTOR_VALUE="${MAINNET_SELECTOR:-5009297550715157269}"
|
|
BSC_SELECTOR_VALUE="${BSC_SELECTOR:-11344663589394136015}"
|
|
AVALANCHE_SELECTOR_VALUE="${AVALANCHE_SELECTOR:-6433500567565415381}"
|
|
|
|
assert_supported_direct_first_hop() {
|
|
case "$DEST_SELECTOR" in
|
|
"$MAINNET_SELECTOR_VALUE"|"${BSC_SELECTOR_VALUE}"|"${AVALANCHE_SELECTOR_VALUE}")
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
if [[ "${ALLOW_UNSUPPORTED_DIRECT_FIRST_HOP:-0}" = "1" ]]; then
|
|
echo "WARNING: proceeding with unsupported direct first hop for selector $DEST_SELECTOR because ALLOW_UNSUPPORTED_DIRECT_FIRST_HOP=1"
|
|
return 0
|
|
fi
|
|
|
|
cat <<EOF
|
|
ERROR: selector $DEST_SELECTOR is not a proven direct first hop from the current Chain 138 router.
|
|
|
|
The supported direct first-hop lanes today are:
|
|
- Mainnet ($MAINNET_SELECTOR_VALUE)
|
|
- BSC ($BSC_SELECTOR_VALUE)
|
|
- Avalanche ($AVALANCHE_SELECTOR_VALUE)
|
|
|
|
For Gnosis, Cronos, Celo, Polygon, Arbitrum, Optimism, and Base, use the Mainnet hub:
|
|
1. bridge 138 -> Mainnet
|
|
2. then bridge Mainnet -> destination
|
|
|
|
Set ALLOW_UNSUPPORTED_DIRECT_FIRST_HOP=1 only if you are intentionally testing an unsupported path
|
|
and accept that the source send may succeed without destination delivery.
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
DEST_RAW="$(cast call "$BRIDGE" 'destinations(uint64)((uint64,address,bool))' "$DEST_SELECTOR" --rpc-url "$RPC" 2>/dev/null || echo "")"
|
|
DEST_ADDR="$(extract_first_address "$DEST_RAW")"
|
|
assert_supported_direct_first_hop
|
|
|
|
if [[ "$DEST_SELECTOR" == "$AVALANCHE_SELECTOR_VALUE" ]]; then
|
|
AVALANCHE_NATIVE_BRIDGE="${CCIPWETH9_BRIDGE_AVALANCHE:-}"
|
|
if [[ -n "$AVALANCHE_NATIVE_BRIDGE" ]] && [[ "$(lower "$DEST_ADDR")" == "$(lower "$AVALANCHE_NATIVE_BRIDGE")" ]] && [[ "${ALLOW_UNSUPPORTED_AVAX_NATIVE:-0}" != "1" ]]; then
|
|
cat <<EOF
|
|
ERROR: current Avalanche destination mapping points at the native AVAX WETH9 bridge ($DEST_ADDR).
|
|
|
|
That path is not live from the current Chain 138 router. On 2026-03-30, a live test message to the
|
|
native AVAX bridge remained unprocessed because the Chain 138 router emits MessageSent events but
|
|
the AVAX native bridge only accepts ccipReceive from its own trusted AVAX router.
|
|
|
|
Use the relay-backed AVAX receiver instead, or set ALLOW_UNSUPPORTED_AVAX_NATIVE=1 if you are
|
|
intentionally testing the unsupported native path.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "$DEST_ADDR" ]]; then
|
|
echo "Info: Avalanche send will use current mapped receiver $DEST_ADDR"
|
|
fi
|
|
fi
|
|
|
|
# Confirmation for large amounts
|
|
CONFIRM_ABOVE="${CONFIRM_ABOVE_ETH:-1}"
|
|
if [[ -n "$CONFIRM_ABOVE" ]] && awk -v a="$AMOUNT_ETH" -v b="$CONFIRM_ABOVE" 'BEGIN{exit !(a+0>=b+0)}' 2>/dev/null; then
|
|
read -p "Send $AMOUNT_ETH ETH to $RECIPIENT? [y/N] " r
|
|
[[ "${r,,}" != "y" ]] && [[ "${r,,}" != "yes" ]] && exit 0
|
|
fi
|
|
|
|
AMOUNT_WEI=$(cast --to-wei "$AMOUNT_ETH" ether)
|
|
ALLOWANCE_WETH_RAW="$(cast call "$WETH9" "allowance(address,address)(uint256)" "$SENDER_ADDR" "$BRIDGE" --rpc-url "$RPC" 2>/dev/null || echo "0")"
|
|
ALLOWANCE_WETH="$(raw_uint "$ALLOWANCE_WETH_RAW")"
|
|
if [[ "$ALLOWANCE_WETH" =~ ^[0-9]+$ ]] && (( ALLOWANCE_WETH < AMOUNT_WEI )); then
|
|
cat <<EOF
|
|
ERROR: insufficient WETH allowance for source bridge pull.
|
|
|
|
Current allowance:
|
|
token: $WETH9
|
|
owner: $SENDER_ADDR
|
|
spender: $BRIDGE
|
|
allowance_raw: $ALLOWANCE_WETH
|
|
amount_raw: $AMOUNT_WEI
|
|
|
|
Approve WETH to the Chain 138 bridge first, for example:
|
|
cast send "$WETH9" "approve(address,uint256)" "$BRIDGE" "$AMOUNT_WEI" --rpc-url "$RPC" --private-key "\$PRIVATE_KEY" --gas-price "$GAS_PRICE" --legacy
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
FEE_WEI=$(cast call "$BRIDGE" "calculateFee(uint64,uint256)" "$DEST_SELECTOR" "$AMOUNT_WEI" --rpc-url "$RPC" 2>/dev/null | cast --to-dec || echo "0")
|
|
FEE_TOKEN=$(cast call "$BRIDGE" "feeToken()(address)" --rpc-url "$RPC" 2>/dev/null || echo "0x0")
|
|
if [[ "$FEE_TOKEN" != "0x0000000000000000000000000000000000000000" ]] && [[ -n "$FEE_WEI" ]] && [[ "$FEE_WEI" != "0" ]]; then
|
|
ALLOWANCE_FEE_RAW="$(cast call "$FEE_TOKEN" "allowance(address,address)(uint256)" "$SENDER_ADDR" "$BRIDGE" --rpc-url "$RPC" 2>/dev/null || echo "0")"
|
|
ALLOWANCE_FEE="$(raw_uint "$ALLOWANCE_FEE_RAW")"
|
|
if [[ "$ALLOWANCE_FEE" =~ ^[0-9]+$ ]] && (( ALLOWANCE_FEE < FEE_WEI )); then
|
|
cat <<EOF
|
|
ERROR: insufficient fee-token allowance for bridge fee pull.
|
|
|
|
Current allowance:
|
|
token: $FEE_TOKEN
|
|
owner: $SENDER_ADDR
|
|
spender: $BRIDGE
|
|
allowance_raw: $ALLOWANCE_FEE
|
|
required_fee_raw: $FEE_WEI
|
|
EOF
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
DEST_RPC=""
|
|
DEST_WETH=""
|
|
case "$DEST_SELECTOR" in
|
|
"$MAINNET_SELECTOR_VALUE")
|
|
DEST_RPC="${ETHEREUM_MAINNET_RPC:-}"
|
|
DEST_WETH="${WETH9_MAINNET:-}"
|
|
;;
|
|
"$BSC_SELECTOR_VALUE")
|
|
DEST_RPC="${BSC_RPC_URL:-${BSC_MAINNET_RPC:-}}"
|
|
DEST_WETH="${WETH9_BSC:-}"
|
|
;;
|
|
"$AVALANCHE_SELECTOR_VALUE")
|
|
DEST_RPC="${AVALANCHE_RPC_URL:-${AVALANCHE_MAINNET_RPC:-}}"
|
|
DEST_WETH="${WETH9_AVALANCHE:-}"
|
|
;;
|
|
esac
|
|
|
|
if [[ -n "$DEST_ADDR" && -n "$DEST_RPC" && -n "$DEST_WETH" ]]; then
|
|
DEST_BALANCE_RAW="$(cast call "$DEST_WETH" "balanceOf(address)(uint256)" "$DEST_ADDR" --rpc-url "$DEST_RPC" 2>/dev/null || echo "0")"
|
|
DEST_BALANCE="$(raw_uint "$DEST_BALANCE_RAW")"
|
|
if [[ "$DEST_BALANCE" =~ ^[0-9]+$ ]] && (( DEST_BALANCE < AMOUNT_WEI )); then
|
|
SHORTFALL=$(( AMOUNT_WEI - DEST_BALANCE ))
|
|
cat <<EOF
|
|
ERROR: insufficient destination relay inventory for this direct first hop.
|
|
|
|
Destination receiver: $DEST_ADDR
|
|
Destination token: $DEST_WETH
|
|
Destination balance: $DEST_BALANCE
|
|
Requested amount: $AMOUNT_WEI
|
|
Shortfall: $SHORTFALL
|
|
|
|
Top up the destination relay inventory first, or reduce the send amount.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
V=""; [[ "$FEE_TOKEN" = "0x0000000000000000000000000000000000000000" ]] && [[ -n "$FEE_WEI" ]] && [[ "$FEE_WEI" != "0" ]] && V="--value $FEE_WEI"
|
|
|
|
GAS_OPT=""; [[ -n "$GAS_LIMIT" ]] && GAS_OPT="--gas-limit $GAS_LIMIT"
|
|
|
|
if [[ "$DRY_RUN" = true ]]; then
|
|
echo "DRY-RUN: cast send $BRIDGE sendCrossChain($DEST_SELECTOR,$RECIPIENT,$AMOUNT_WEI) --gas-price $GAS_PRICE --legacy $V $GAS_OPT"
|
|
cast call "$BRIDGE" "sendCrossChain(uint64,address,uint256)" "$DEST_SELECTOR" "$RECIPIENT" "$AMOUNT_WEI" --rpc-url "$RPC" --private-key "$PRIVATE_KEY" $V 2>/dev/null && echo "Simulation: OK" || echo "Simulation: (check params)"
|
|
exit 0
|
|
fi
|
|
|
|
# Real execution: broadcasts sendCrossChain transaction (no --dry-run)
|
|
cast send "$BRIDGE" "sendCrossChain(uint64,address,uint256)" "$DEST_SELECTOR" "$RECIPIENT" "$AMOUNT_WEI" --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --gas-price "$GAS_PRICE" --legacy $V $GAS_OPT
|