- 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
298 lines
11 KiB
Bash
298 lines
11 KiB
Bash
#!/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'(?<!\d)\d+(?!\d)', text))
|
|
sys.exit(0 if chain_id in nums else 1)
|
|
" "$CHAIN_ID" <<<"$description"
|
|
then
|
|
ok "Official DODO SmartTrade docs publish chainId=${CHAIN_ID} support."
|
|
return 0
|
|
fi
|
|
fail "Official DODO SmartTrade docs do not publish chainId=${CHAIN_ID} support."
|
|
return 1
|
|
}
|
|
|
|
contract_list_support() {
|
|
local body
|
|
body="$(curl -sSL --max-time 30 "$CONTRACT_LIST_URL")" || {
|
|
warn "Failed to fetch DODO contract inventory."
|
|
return 1
|
|
}
|
|
if printf '%s' "$body" | jq -e --arg chain "$CHAIN_ID" '.data | has($chain)' >/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."
|