refactor(archive): move historical contracts and adapters to archive directory
- Archived multiple non-EVM adapters (Algorand, Hedera, Tron, TON, Cosmos, Solana) and compliance contracts (IndyVerifier) to `archive/solidity/contracts/`. - Updated documentation to reflect the historical status of archived components. - Adjusted `foundry.toml` and `README.md` for clarity on historical dependencies and configurations. - Enhanced Makefile and package.json scripts for improved contract testing and building processes. - Removed obsolete contracts (AlltraCustomBridge, CommodityCCIPBridge, ISO4217WCCIPBridge, VaultBridgeAdapter) from the main directory. - Updated implementation reports to indicate archived status for various components.
This commit is contained in:
140
scripts/forge/report-contract-reachability.py
Executable file
140
scripts/forge/report-contract-reachability.py
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Report Solidity contracts that are not reachable from current tests or scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
EXCLUDED_PARTS = {"artifacts", "broadcast", "cache", "lib", "node_modules", "out"}
|
||||
IMPORT_RE = re.compile(r'^\s*import\s+(?:\{[^}]+\}\s+from\s+)?["\']([^"\']+)["\'];', re.M)
|
||||
|
||||
|
||||
def discover_sources(repo_root: Path) -> dict[str, Path]:
|
||||
return {
|
||||
path.relative_to(repo_root).as_posix(): path
|
||||
for path in repo_root.rglob("*.sol")
|
||||
if path.is_file() and not any(part in EXCLUDED_PARTS for part in path.parts)
|
||||
}
|
||||
|
||||
|
||||
def resolve_import(importer: Path, import_path: str, repo_root: Path) -> str | None:
|
||||
if import_path.startswith(("./", "../")):
|
||||
target = (importer.parent / import_path).resolve()
|
||||
try:
|
||||
return target.relative_to(repo_root).as_posix()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if import_path.startswith("@emoney/"):
|
||||
return f"contracts/emoney/{import_path[len('@emoney/'):]}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def build_graph(repo_root: Path, files: dict[str, Path]) -> tuple[dict[str, list[str]], dict[str, set[str]]]:
|
||||
imports: dict[str, list[str]] = {}
|
||||
inbound: dict[str, set[str]] = {rel: set() for rel in files}
|
||||
|
||||
for rel_path, file_path in files.items():
|
||||
text = file_path.read_text(errors="ignore")
|
||||
resolved_targets: list[str] = []
|
||||
for import_path in IMPORT_RE.findall(text):
|
||||
target = resolve_import(file_path, import_path, repo_root)
|
||||
if target and target in files:
|
||||
resolved_targets.append(target)
|
||||
inbound[target].add(rel_path)
|
||||
imports[rel_path] = resolved_targets
|
||||
|
||||
return imports, inbound
|
||||
|
||||
|
||||
def walk_reachable(imports: dict[str, list[str]], roots: list[str]) -> set[str]:
|
||||
seen: set[str] = set()
|
||||
stack = list(roots)
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
if current in seen:
|
||||
continue
|
||||
seen.add(current)
|
||||
stack.extend(imports.get(current, []))
|
||||
return seen
|
||||
|
||||
|
||||
def create_report(repo_root: Path) -> dict[str, object]:
|
||||
files = discover_sources(repo_root)
|
||||
imports, inbound = build_graph(repo_root, files)
|
||||
|
||||
contract_files = sorted(rel for rel in files if rel.startswith("contracts/"))
|
||||
root_files = sorted(rel for rel in files if rel.startswith(("test/", "script/")))
|
||||
reachable = walk_reachable(imports, root_files)
|
||||
|
||||
unreachable = [rel for rel in contract_files if rel not in reachable]
|
||||
no_inbound = [rel for rel in contract_files if not inbound[rel]]
|
||||
|
||||
unreachable_by_bucket = Counter(path.split("/")[1] for path in unreachable)
|
||||
no_inbound_by_bucket = Counter(path.split("/")[1] for path in no_inbound)
|
||||
|
||||
return {
|
||||
"repoRoot": repo_root.as_posix(),
|
||||
"summary": {
|
||||
"contractsTotal": len(contract_files),
|
||||
"rootsTotal": len(root_files),
|
||||
"contractsReachableFromTestsOrScripts": len(contract_files) - len(unreachable),
|
||||
"contractsUnreachableFromTestsOrScripts": len(unreachable),
|
||||
"contractsWithNoLocalInboundRefs": len(no_inbound),
|
||||
},
|
||||
"unreachableByBucket": dict(unreachable_by_bucket.most_common()),
|
||||
"noInboundByBucket": dict(no_inbound_by_bucket.most_common()),
|
||||
"unreachableContracts": unreachable,
|
||||
"contractsWithNoLocalInboundRefs": no_inbound,
|
||||
}
|
||||
|
||||
|
||||
def print_text(report: dict[str, object]) -> None:
|
||||
summary = report["summary"]
|
||||
print(f"repo root: {report['repoRoot']}")
|
||||
print(f"contracts total: {summary['contractsTotal']}")
|
||||
print(f"tests/scripts roots total: {summary['rootsTotal']}")
|
||||
print(f"contracts reachable from tests or scripts: {summary['contractsReachableFromTestsOrScripts']}")
|
||||
print(f"contracts unreachable from tests or scripts: {summary['contractsUnreachableFromTestsOrScripts']}")
|
||||
print(f"contracts with no local inbound refs: {summary['contractsWithNoLocalInboundRefs']}")
|
||||
print()
|
||||
print("Unreachable by top-level bucket:")
|
||||
for bucket, count in report["unreachableByBucket"].items():
|
||||
print(f" {bucket}: {count}")
|
||||
print()
|
||||
print("Archive candidates (unreachable from current tests/scripts):")
|
||||
for contract in report["unreachableContracts"]:
|
||||
print(f" {contract}")
|
||||
print()
|
||||
print("Note: unreachable does not prove safe deletion; it only means this repo's current Solidity tests/scripts do not import the file.")
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = parse_args(argv)
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
report = create_report(repo_root)
|
||||
|
||||
if args.json:
|
||||
json.dump(report, sys.stdout, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
|
||||
print_text(report)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
324
scripts/forge/scope.sh
Executable file
324
scripts/forge/scope.sh
Executable file
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
REPO_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||
|
||||
declare -A ROOT_SCRIPT_SCOPE_ALIASES=(
|
||||
["DeployAddressMapper.s.sol"]="utils"
|
||||
["DeployAddressMapperOtherChain.s.sol"]="utils"
|
||||
["DeployCCIPRouter.s.sol"]="ccip"
|
||||
["DeployCCIPSender.s.sol"]="ccip"
|
||||
["DeployCCIPSenderMainnet.s.sol"]="ccip"
|
||||
["DeployCCIPReceiver.s.sol"]="ccip"
|
||||
["DeployCCIPReceiverMainnet.s.sol"]="ccip"
|
||||
["DeployCCIPRelay.s.sol"]="relay"
|
||||
["DeployCCIPWETH9Bridge.s.sol"]="ccip"
|
||||
["DeployCCIPWETH10Bridge.s.sol"]="ccip"
|
||||
["DeployComplianceRegistry.s.sol"]="compliance"
|
||||
["DeployCompliantUSDC.s.sol"]="tokens"
|
||||
["DeployCompliantUSDT.s.sol"]="tokens"
|
||||
["DeployCWAssetReserveVerifier.s.sol"]="bridge/integration"
|
||||
["DeployCWReserveVerifier.s.sol"]="bridge/integration"
|
||||
["DeployFeeCollector.s.sol"]="utils"
|
||||
["DeployGenericStateChannelManager.s.sol"]="channels"
|
||||
["DeployMainnetTether.s.sol"]="tether"
|
||||
["DeployMirrorManager.s.sol"]="mirror"
|
||||
["DeployMulticall.s.sol"]="utils"
|
||||
["DeployMultiSig.s.sol"]="governance"
|
||||
["DeployOfficialUSDC138.s.sol"]="tokens"
|
||||
["DeployOfficialUSDT138.s.sol"]="tokens"
|
||||
["DeployOracle.s.sol"]="oracle"
|
||||
["DeployPaymentChannelManager.s.sol"]="channels"
|
||||
["DeployTokenRegistry.s.sol"]="utils"
|
||||
["DeployTransactionMirror.s.sol"]="mirror"
|
||||
["DeployWETH.s.sol"]="tokens"
|
||||
["DeployWETH10.s.sol"]="tokens"
|
||||
["DeployWETHWithCCIP.s.sol"]="full"
|
||||
)
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash scripts/forge/scope.sh list
|
||||
bash scripts/forge/scope.sh build [scope] [forge build args...]
|
||||
bash scripts/forge/scope.sh test [scope] [forge test args...]
|
||||
bash scripts/forge/scope.sh script [scope] <script-target> [forge script args...]
|
||||
bash scripts/forge/scope.sh orphans [--json]
|
||||
|
||||
Examples:
|
||||
bash scripts/forge/scope.sh build treasury
|
||||
bash scripts/forge/scope.sh test flash --match-path 'test/flash/*.t.sol'
|
||||
bash scripts/forge/scope.sh script bridge/trustless script/bridge/trustless/DeployTrustlessBridge.s.sol:DeployTrustlessBridge --rpc-url "$RPC_URL_138"
|
||||
FORGE_SCOPE=vault bash scripts/forge/scope.sh test --match-path 'test/vault/*.t.sol'
|
||||
|
||||
Notes:
|
||||
- Omit [scope] to use FORGE_SCOPE, otherwise default to 'full'.
|
||||
- 'full' preserves the historical repo-wide Forge behavior.
|
||||
- Any other scope is resolved relative to contracts/, for example:
|
||||
treasury -> contracts/treasury
|
||||
bridge/trustless -> contracts/bridge/trustless
|
||||
- If no explicit scope is given, the runner tries to infer one from
|
||||
script/test/build paths and common root-level deployment script names.
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
info() {
|
||||
echo "$*" >&2
|
||||
}
|
||||
|
||||
resolve_scope() {
|
||||
local raw="${1:-${FORGE_SCOPE:-full}}"
|
||||
raw="${raw#contracts/}"
|
||||
raw="${raw#/}"
|
||||
if [[ -z "$raw" ]]; then
|
||||
raw="full"
|
||||
fi
|
||||
printf '%s\n' "$raw"
|
||||
}
|
||||
|
||||
scope_label() {
|
||||
printf '%s\n' "${1//\//-}"
|
||||
}
|
||||
|
||||
scope_exists() {
|
||||
local scope="$1"
|
||||
[[ "$scope" == "full" || -d "$REPO_ROOT/contracts/$scope" ]]
|
||||
}
|
||||
|
||||
infer_scope_from_script_alias() {
|
||||
local raw="${1%%:*}"
|
||||
local base
|
||||
base=$(basename "$raw")
|
||||
if [[ -n "${ROOT_SCRIPT_SCOPE_ALIASES[$base]:-}" ]]; then
|
||||
printf '%s\n' "${ROOT_SCRIPT_SCOPE_ALIASES[$base]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
infer_scope_from_script_imports() {
|
||||
local raw="${1%%:*}"
|
||||
local script_path="$REPO_ROOT/${raw#./}"
|
||||
[[ -f "$script_path" ]] || return 1
|
||||
|
||||
python3 - "$REPO_ROOT" "$script_path" <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
repo_root = Path(sys.argv[1]).resolve()
|
||||
script_path = Path(sys.argv[2]).resolve()
|
||||
text = script_path.read_text(errors="ignore")
|
||||
imports = re.findall(r'^\s*import\s+(?:\{[^}]+\}\s+from\s+)?["\']([^"\']+)["\'];', text, re.M)
|
||||
|
||||
for imp in imports:
|
||||
if not imp.startswith(("./", "../")):
|
||||
continue
|
||||
candidate = (script_path.parent / imp).resolve()
|
||||
try:
|
||||
rel = candidate.relative_to(repo_root).as_posix()
|
||||
except ValueError:
|
||||
continue
|
||||
if not rel.startswith("contracts/"):
|
||||
continue
|
||||
scope = rel[len("contracts/"):]
|
||||
if candidate.is_file():
|
||||
scope = str(Path(scope).parent).replace("\\", "/")
|
||||
scope = scope.strip(".")
|
||||
while scope:
|
||||
if (repo_root / "contracts" / scope).is_dir():
|
||||
print(scope)
|
||||
raise SystemExit(0)
|
||||
scope = scope.rsplit("/", 1)[0] if "/" in scope else ""
|
||||
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
extract_scope_from_path() {
|
||||
local raw="${1%%:*}"
|
||||
raw="${raw#./}"
|
||||
|
||||
case "$raw" in
|
||||
contracts/*)
|
||||
raw="${raw#contracts/}"
|
||||
;;
|
||||
test/*)
|
||||
raw="${raw#test/}"
|
||||
;;
|
||||
script/*)
|
||||
raw="${raw#script/}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local candidate
|
||||
for candidate in "$raw" "${raw#deploy/}"; do
|
||||
[[ -n "$candidate" ]] || continue
|
||||
|
||||
if [[ -d "$REPO_ROOT/contracts/$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local probe="${candidate%/*}"
|
||||
if [[ "$probe" != "$candidate" ]]; then
|
||||
while [[ -n "$probe" ]]; do
|
||||
if [[ -d "$REPO_ROOT/contracts/$probe" ]]; then
|
||||
printf '%s\n' "$probe"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$probe" != *"/"* ]]; then
|
||||
break
|
||||
fi
|
||||
probe="${probe%/*}"
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$1" == script/* ]]; then
|
||||
infer_scope_from_script_alias "$1" && return 0
|
||||
infer_scope_from_script_imports "$1" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
infer_scope_from_args() {
|
||||
local arg inferred
|
||||
for arg in "$@"; do
|
||||
inferred=$(extract_scope_from_path "$arg" || true)
|
||||
if [[ -n "$inferred" ]]; then
|
||||
printf '%s\n' "$inferred"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
list_scopes() {
|
||||
(
|
||||
cd "$REPO_ROOT"
|
||||
echo "full"
|
||||
find contracts -mindepth 1 -maxdepth 2 -type d | sed 's#^contracts/##' | sort
|
||||
)
|
||||
}
|
||||
|
||||
prepare_scope_env() {
|
||||
local scope="$1"
|
||||
local command="$2"
|
||||
|
||||
if [[ "$scope" == "full" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local src_dir="contracts/$scope"
|
||||
[[ -d "$REPO_ROOT/$src_dir" ]] || die "unknown scope '$scope' (expected directory '$src_dir')"
|
||||
|
||||
local label
|
||||
label=$(scope_label "$scope")
|
||||
|
||||
export FOUNDRY_SRC="$src_dir"
|
||||
export FOUNDRY_OUT="out/scopes/$label"
|
||||
export FOUNDRY_CACHE_PATH="cache/scopes/$label"
|
||||
export FOUNDRY_SPARSE_MODE="${FOUNDRY_SPARSE_MODE:-true}"
|
||||
|
||||
if [[ "$command" == "test" ]]; then
|
||||
local test_dir="test/$scope"
|
||||
if [[ -d "$REPO_ROOT/$test_dir" ]]; then
|
||||
export FOUNDRY_TEST="$test_dir"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$command" == "script" ]]; then
|
||||
local script_dir="script/$scope"
|
||||
if [[ -d "$REPO_ROOT/$script_dir" ]]; then
|
||||
export FOUNDRY_SCRIPT="$script_dir"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
local command="${1:-}"
|
||||
[[ -n "$command" ]] || {
|
||||
usage
|
||||
exit 1
|
||||
}
|
||||
shift || true
|
||||
|
||||
case "$command" in
|
||||
help|-h|--help)
|
||||
usage
|
||||
;;
|
||||
list)
|
||||
list_scopes
|
||||
;;
|
||||
orphans)
|
||||
exec python3 scripts/forge/report-contract-reachability.py "$@"
|
||||
;;
|
||||
build|test|script)
|
||||
local scope=""
|
||||
if [[ $# -gt 0 && "$1" != --* ]]; then
|
||||
local maybe_scope
|
||||
maybe_scope=$(resolve_scope "$1")
|
||||
if scope_exists "$maybe_scope"; then
|
||||
scope="$maybe_scope"
|
||||
shift
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$scope" ]]; then
|
||||
if [[ -n "${FORGE_SCOPE:-}" ]]; then
|
||||
scope=$(resolve_scope)
|
||||
else
|
||||
scope=$(infer_scope_from_args "$@" || printf 'full\n')
|
||||
fi
|
||||
fi
|
||||
|
||||
prepare_scope_env "$scope" "$command"
|
||||
|
||||
if [[ "$scope" == "full" ]]; then
|
||||
info "Forge scope: full repo"
|
||||
else
|
||||
info "Forge scope: $scope"
|
||||
info " src=$FOUNDRY_SRC out=$FOUNDRY_OUT cache=$FOUNDRY_CACHE_PATH"
|
||||
[[ -n "${FOUNDRY_TEST:-}" ]] && info " test=$FOUNDRY_TEST"
|
||||
[[ -n "${FOUNDRY_SCRIPT:-}" ]] && info " script=$FOUNDRY_SCRIPT"
|
||||
fi
|
||||
|
||||
case "$command" in
|
||||
build)
|
||||
if [[ "$scope" == "full" ]]; then
|
||||
exec forge build "$@"
|
||||
fi
|
||||
exec forge build --skip test --skip script "$@"
|
||||
;;
|
||||
test)
|
||||
if [[ "$scope" != "full" && -z "${FOUNDRY_TEST:-}" ]]; then
|
||||
die "scope '$scope' has no matching test/<scope> directory"
|
||||
fi
|
||||
exec forge test "$@"
|
||||
;;
|
||||
script)
|
||||
[[ $# -gt 0 ]] || die "script command requires a script target, e.g. script/treasury/DeployTreasuryExecutor138.s.sol:DeployTreasuryExecutor138"
|
||||
exec forge script "$@"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
die "unknown command '$command'"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user