#!/usr/bin/env bash # Safely probe DODO's hosted SmartTrade API support for Chain 138 without leaking secrets. # # What it checks: # 1. Official DODO docs swagger for published chain support. # 2. Official DODO contract inventory for published on-chain support. # 3. Hosted DODO SmartTrade quotes when a developer API key is available. # 4. Local/public Chain 138 token-aggregation quote behavior for the same pairs. # # Usage: # bash scripts/verify/check-dodo-api-chain138-route-support.sh # DODO_API_KEY=... bash scripts/verify/check-dodo-api-chain138-route-support.sh # BASE_URL=https://explorer.d-bis.org bash scripts/verify/check-dodo-api-chain138-route-support.sh # # Optional env: # DODO_API_KEY / DODO_SECRET_KEY / DODO_DEVELOPER_API_KEY # CHAIN_ID=138 # USER_ADDR=0x... # BASE_URL=https://explorer.d-bis.org # DODO_SLIPPAGE=0.03 # AMOUNTS_WEI="1000000000000000000 5000000000000000000 25000000000000000000" set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" source "$PROJECT_ROOT/scripts/lib/load-project-env.sh" >/dev/null 2>&1 || true DOCS_URL="${DODO_DOCS_URL:-https://docs.dodoex.io/en/developer/developers-portal/api/smart-trade/api}" CONTRACT_LIST_URL="${DODO_CONTRACT_LIST_URL:-https://api.dodoex.io/dodo-contract/list?version=v1,v2}" QUOTE_URL="${DODO_QUOTE_URL:-https://api.dodoex.io/route-service/developer/swap}" CHAIN_ID="${CHAIN_ID:-138}" BASE_URL="${BASE_URL:-https://explorer.d-bis.org}" BASE_URL="${BASE_URL%/}" DODO_SLIPPAGE="${DODO_SLIPPAGE:-0.03}" API_KEY="${DODO_API_KEY:-${DODO_SECRET_KEY:-${DODO_DEVELOPER_API_KEY:-}}}" WETH="${CHAIN138_WETH_ADDRESS:-0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2}" USDT="${OFFICIAL_USDT_ADDRESS:-0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1}" USDC="${OFFICIAL_USDC_ADDRESS:-0x71D6687F38b93CCad569Fa6352c876eea967201b}" CUSDT="${COMPLIANT_USDT_ADDRESS:-0x93E66202A11B1772E55407B32B44e5Cd8eda7f22}" CUSDC="${COMPLIANT_USDC_ADDRESS:-0xf22258f57794CC8E06237084b353Ab30fFfa640b}" AMOUNTS_WEI="${AMOUNTS_WEI:-1000000000000000000 5000000000000000000 25000000000000000000}" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT log() { printf '%s\n' "$*"; } ok() { printf '[OK] %s\n' "$*"; } warn() { printf '[WARN] %s\n' "$*"; } fail() { printf '[FAIL] %s\n' "$*"; } need_cmd() { command -v "$1" >/dev/null 2>&1 || { printf 'ERROR: missing required command: %s\n' "$1" >&2 exit 1 } } need_cmd curl need_cmd jq need_cmd python3 derive_user_addr() { if [[ -n "${USER_ADDR:-}" ]]; then printf '%s\n' "$USER_ADDR" return 0 fi if command -v cast >/dev/null 2>&1 && [[ -n "${PRIVATE_KEY:-}" ]]; then cast wallet address --private-key "$PRIVATE_KEY" 2>/dev/null || true return 0 fi printf '%s\n' "0x4A666F96fC8764181194447A7dFdb7d471b301C8" } USER_ADDR="$(derive_user_addr)" detect_token_aggregation_prefix() { local prefix response status body for prefix in "" "/token-aggregation"; do response="$(curl -sSL --max-time 20 -w $'\n%{http_code}' "${BASE_URL}${prefix}/api/v1/networks" 2>/dev/null)" || continue status="$(printf '%s' "$response" | tail -n 1)" body="$(printf '%s' "$response" | sed '$d')" if [[ "$status" != "200" ]]; then response="$(curl -sSL --max-time 20 -w $'\n%{http_code}' "${BASE_URL}${prefix}/api/v1/quote?chainId=${CHAIN_ID}&tokenIn=${CUSDT}&tokenOut=${CUSDC}&amountIn=1000000" 2>/dev/null)" || continue status="$(printf '%s' "$response" | tail -n 1)" body="$(printf '%s' "$response" | sed '$d')" fi if printf '%s' "$body" | jq -e 'type == "object" and (.networks | type == "array")' >/dev/null 2>&1; then printf '%s\n' "$prefix" return 0 fi if printf '%s' "$body" | jq -e 'type == "object" and (.amountOut != null or .executorAddress != null)' >/dev/null 2>&1; then printf '%s\n' "$prefix" return 0 fi done printf '%s\n' "" } extract_docs_swagger() { python3 -c " import re import sys import urllib.parse html = sys.stdin.read() m = re.search(r'href=\"data:text/plain;charset=utf-8,([^\"]+)\"', html) if not m: sys.exit(1) print(urllib.parse.unquote(m.group(1))) " < <(curl -sSL --max-time 30 "$DOCS_URL") } docs_chain_supported() { local swagger_json description if ! swagger_json="$(extract_docs_swagger 2>/dev/null)"; then warn "Could not extract SmartTrade swagger from official docs." return 1 fi description="$(printf '%s' "$swagger_json" | jq -r '.paths["/route-service/developer/swap"].get.parameters[] | select(.name == "chainId") | .description' 2>/dev/null || true)" if [[ -z "$description" || "$description" == "null" ]]; then warn "Swagger did not expose the published chain list." return 1 fi if python3 -c " import re import sys chain_id = sys.argv[1] text = sys.stdin.read() nums = set(re.findall(r'(?/dev/null 2>&1; then ok "Official DODO contract inventory includes chainId=${CHAIN_ID}." return 0 fi fail "Official DODO contract inventory does not include chainId=${CHAIN_ID}." return 1 } probe_dodo_quote() { local label="$1" local token_in="$2" local token_out="$3" local amount="$4" local body_file="$TMP_DIR/dodo-${label//[^a-zA-Z0-9_-]/_}-${amount}.json" local code status res_amount use_source msg_error if [[ -z "$API_KEY" ]]; then warn "Skipping hosted DODO quote for ${label} amount=${amount}: no DODO_API_KEY/DODO_SECRET_KEY set." return 2 fi code="$( curl -sS -G -o "$body_file" -w "%{http_code}" --max-time 30 "$QUOTE_URL" \ --data-urlencode "chainId=${CHAIN_ID}" \ --data-urlencode "fromAmount=${amount}" \ --data-urlencode "fromTokenAddress=${token_in}" \ --data-urlencode "toTokenAddress=${token_out}" \ --data-urlencode "apikey=${API_KEY}" \ --data-urlencode "slippage=${DODO_SLIPPAGE}" \ --data-urlencode "userAddr=${USER_ADDR}" )" || code="000" if [[ "$code" != "200" ]]; then if [[ -f "$body_file" ]]; then fail "Hosted DODO quote ${label} amount=${amount} returned HTTP ${code}: $(head -c 220 "$body_file")" else fail "Hosted DODO quote ${label} amount=${amount} failed with HTTP ${code}." fi return 1 fi status="$(jq -r '.status // empty' "$body_file" 2>/dev/null || true)" res_amount="$(jq -r '.data.resAmount // empty' "$body_file" 2>/dev/null || true)" use_source="$(jq -r '.data.useSource // empty' "$body_file" 2>/dev/null || true)" msg_error="$(jq -r '.data.msgError // empty' "$body_file" 2>/dev/null || true)" if [[ "$status" == "200" && -n "$res_amount" && "$res_amount" != "0" && "$res_amount" != "0.0" && -z "$msg_error" ]]; then ok "Hosted DODO quote ${label} amount=${amount} succeeded via ${use_source:-unknown} with resAmount=${res_amount}." return 0 fi fail "Hosted DODO quote ${label} amount=${amount} returned no executable route. status=${status:-n/a} source=${use_source:-n/a} msgError=${msg_error:-n/a} resAmount=${res_amount:-n/a}" return 1 } probe_local_quote() { local label="$1" local token_in="$2" local token_out="$3" local amount="$4" local prefix="$5" local body_file="$TMP_DIR/local-${label//[^a-zA-Z0-9_-]/_}-${amount}.json" local code error executor amount_out code="$( curl -sS -o "$body_file" -w "%{http_code}" --max-time 30 \ "${BASE_URL}${prefix}/api/v1/quote?chainId=${CHAIN_ID}&tokenIn=${token_in}&tokenOut=${token_out}&amountIn=${amount}" )" || code="000" if [[ "$code" != "200" ]]; then fail "Local token-aggregation quote ${label} amount=${amount} returned HTTP ${code}." return 1 fi if jq -e 'type == "object" and has("error")' "$body_file" >/dev/null 2>&1; then error="$(jq -r '.error' "$body_file" 2>/dev/null || true)" fail "Local token-aggregation quote ${label} amount=${amount} returned error: ${error:-unknown error}" return 1 fi executor="$(jq -r '.executorAddress // empty' "$body_file" 2>/dev/null || true)" amount_out="$(jq -r '.amountOut // empty' "$body_file" 2>/dev/null || true)" if [[ -n "$amount_out" && "$amount_out" != "0" && "$amount_out" != "0.0" ]]; then ok "Local token-aggregation quote ${label} amount=${amount} succeeded with amountOut=${amount_out} executor=${executor:-n/a}." return 0 fi fail "Local token-aggregation quote ${label} amount=${amount} returned no amountOut." return 1 } run_pair_suite() { local name="$1" local token_in="$2" local token_out="$3" local prefix="$4" local amount log "" log "Pair: ${name}" for amount in $AMOUNTS_WEI; do probe_dodo_quote "$name" "$token_in" "$token_out" "$amount" || true probe_local_quote "$name" "$token_in" "$token_out" "$amount" "$prefix" || true done } TA_PREFIX="$(detect_token_aggregation_prefix)" log "=== DODO SmartTrade / Chain 138 support probe ===" log "Docs URL: $DOCS_URL" log "Contract list: $CONTRACT_LIST_URL" log "Quote endpoint: $QUOTE_URL" log "Chain ID: $CHAIN_ID" log "Base URL: $BASE_URL" log "User address: $USER_ADDR" if [[ -n "$API_KEY" ]]; then ok "DODO API key present in environment (value intentionally not printed)." else warn "No DODO API key found in environment; hosted quote probes will be skipped." fi if [[ -n "$TA_PREFIX" ]]; then ok "Detected token-aggregation prefix: ${TA_PREFIX}/api/v1" else warn "Could not auto-detect token-aggregation prefix; defaulting to root /api/v1." fi log "" log "=== Published support checks ===" docs_chain_supported || true contract_list_support || true log "" log "=== Live quote probes ===" run_pair_suite "WETH->USDT" "$WETH" "$USDT" "$TA_PREFIX" run_pair_suite "WETH->USDC" "$WETH" "$USDC" "$TA_PREFIX" log "" log "=== Positive-control local pairs ===" probe_local_quote "cUSDT->USDT" "$CUSDT" "$USDT" "1000000" "$TA_PREFIX" || true probe_local_quote "cUSDT->cUSDC" "$CUSDT" "$CUSDC" "1000000" "$TA_PREFIX" || true log "" log "Notes:" log " - Hosted DODO SmartTrade probes are quote-only and never print the API key." log " - If official docs and contract inventory both omit chainId=${CHAIN_ID}, the hosted API should be treated as unsupported until DODO adds the chain." log " - Local token-aggregation probes show current Chain 138 route availability regardless of hosted DODO support."