diff --git a/toolbox/src/toolbox/L1/SelfHostedExplorer.tsx b/toolbox/src/toolbox/L1/SelfHostedExplorer.tsx new file mode 100644 index 00000000000..684d34cc960 --- /dev/null +++ b/toolbox/src/toolbox/L1/SelfHostedExplorer.tsx @@ -0,0 +1,426 @@ +"use client"; + +import { useState, useEffect, useMemo } from "react"; +import { Container } from "../../components/Container"; +import { Input } from "../../components/Input"; +import { getBlockchainInfo } from "../../coreViem/utils/glacier"; +import InputChainId from "../../components/InputChainId"; +import versions from "../../versions.json"; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import { Steps, Step } from "fumadocs-ui/components/steps"; +import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; +import { dockerInstallInstructions, type OS, nodeConfigBase64 } from "../Nodes/AvalanchegoDocker"; +import { useL1ByChainId } from "../../stores/l1ListStore"; + +const genCaddyfile = (domain: string) => ` +${domain} { + # Backend API routes + handle /api* { + reverse_proxy backend:4000 + } + + handle /socket* { + reverse_proxy backend:4000 + } + + handle /sitemap.xml { + reverse_proxy backend:4000 + } + + handle /auth* { + reverse_proxy backend:4000 + } + + handle /metrics { + reverse_proxy backend:4000 + } + + # Avago blockchain proxy + handle /ext/bc/* { + reverse_proxy avago:9650 + } + + # Shared files with directory browsing + handle /shared/* { + root * /var + file_server browse + } + + # Frontend (default catch-all) + handle { + reverse_proxy bc_frontend:3000 + } +} +` + +interface DockerComposeConfig { + domain: string; + subnetId: string; + blockchainId: string; + networkName: string; + networkShortName: string; + tokenName: string; + tokenSymbol: string; +} + +const genDockerCompose = (config: DockerComposeConfig) => ` +services: + redis-db: + image: 'redis:alpine' + container_name: redis-db + command: redis-server + db-init: + image: postgres:15 + entrypoint: + - sh + - -c + - | + chown -R 2000:2000 /var/lib/postgresql/data + volumes: + - postgres_data:/var/lib/postgresql/data + db: + depends_on: + db-init: + condition: service_completed_successfully + image: postgres:15 + shm_size: 256m + restart: always + container_name: 'db' + command: postgres -c 'max_connections=200' -c 'client_connection_check_interval=60000' + environment: + POSTGRES_PASSWORD: "" + POSTGRES_USER: "postgres" + POSTGRES_HOST_AUTH_METHOD: "trust" + ports: + - target: 5432 + published: 7432 + volumes: + - postgres_data:/var/lib/postgresql/data + backend: + depends_on: + - db + - redis-db + image: blockscout/blockscout:6.10.1 + pull_policy: always + restart: always + stop_grace_period: 5m + container_name: 'backend' + command: sh -c 'bin/blockscout eval \"Elixir.Explorer.ReleaseTasks.create_and_migrate()\" && bin/blockscout start' + environment: + ETHEREUM_JSONRPC_VARIANT: geth + ETHEREUM_JSONRPC_HTTP_URL: http://avago:9650/ext/bc/${config.blockchainId}/rpc + ETHEREUM_JSONRPC_TRACE_URL: http://avago:9650/ext/bc/${config.blockchainId}/rpc + DATABASE_URL: postgresql://postgres:ceWb1MeLBEeOIfk65gU8EjF8@db:5432/blockscout # TODO: default, please change + SECRET_KEY_BASE: 56NtB48ear7+wMSf0IQuWDAAazhpb31qyc7GiyspBP2vh7t5zlCsF5QDv76chXeN # TODO: default, please change + NETWORK: EVM + SUBNETWORK: MySubnet # TODO: what is this ? + PORT: 4000 + INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER: false + INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER: false + ECTO_USE_SSL: false + DISABLE_EXCHANGE_RATES: true + SUPPORTED_CHAINS: "[]" + TXS_STATS_DAYS_TO_COMPILE_AT_INIT: 10 + MICROSERVICE_SC_VERIFIER_ENABLED: false + MICROSERVICE_SC_VERIFIER_URL: http://sc-verifier:8050 + MICROSERVICE_SC_VERIFIER_TYPE: sc_verifier + MICROSERVICE_VISUALIZE_SOL2UML_ENABLED: false + MICROSERVICE_VISUALIZE_SOL2UML_URL: http://visualizer:8050 + MICROSERVICE_SIG_PROVIDER_ENABLED: false + MICROSERVICE_SIG_PROVIDER_URL: http://sig-provider:8050 + links: + - db:database + # volumes: + # - /etc/blockscout/conf/custom/images:/app/apps/block_scout_web/assets/static/images + bc_frontend: + depends_on: + - backend + - caddy + image: ghcr.io/blockscout/frontend:v1.37.4 + pull_policy: always + platform: linux/amd64 + restart: always + container_name: 'bc_frontend' + environment: + NEXT_PUBLIC_API_HOST: ${config.domain} + NEXT_PUBLIC_API_PROTOCOL: https + NEXT_PUBLIC_API_BASE_PATH: / + FAVICON_MASTER_URL: https://ash.center/img/ash-logo.svg # TODO: change to dynamic ? + NEXT_PUBLIC_NETWORK_NAME: ${config.networkName} + NEXT_PUBLIC_NETWORK_SHORT_NAME: ${config.networkShortName} + NEXT_PUBLIC_NETWORK_ID: 66666 # TODO: change to dynamic + NEXT_PUBLIC_NETWORK_RPC_URL: https://${config.domain}/ext/bc/${config.blockchainId}/rpc + NEXT_PUBLIC_NETWORK_CURRENCY_NAME: ${config.tokenName} + NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: ${config.tokenSymbol} + NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: 18 + NEXT_PUBLIC_APP_HOST: ${config.domain} + NEXT_PUBLIC_APP_PROTOCOL: https + NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs']" + NEXT_PUBLIC_IS_TESTNET: true + NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL: wss + NEXT_PUBLIC_API_SPEC_URL: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml + NEXT_PUBLIC_VISUALIZE_API_HOST: https://${config.domain} + NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: /visualizer-service + NEXT_PUBLIC_STATS_API_HOST: "" + NEXT_PUBLIC_STATS_API_BASE_PATH: /stats-service + caddy: + depends_on: + - backend + image: caddy:latest + container_name: caddy + restart: always + extra_hosts: + - 'host.docker.internal:host-gateway' + volumes: + - "./Caddyfile:/etc/caddy/Caddyfile" + - caddy_data:/data + - caddy_config:/config + ports: + - "80:80" + - "443:443" + avago: + image: avaplatform/subnet-evm:${versions['avaplatform/subnet-evm']} + container_name: avago + restart: always + ports: + - "127.0.0.1:9650:9650" + - "9651:9651" + volumes: + - ~/.avalanchego:/root/.avalanchego + environment: + AVAGO_PARTIAL_SYNC_PRIMARY_NETWORK: "true" + AVAGO_PUBLIC_IP_RESOLUTION_SERVICE: "opendns" + AVAGO_HTTP_HOST: "0.0.0.0" + AVAGO_TRACK_SUBNETS: "${config.subnetId}" + AVAGO_HTTP_ALLOWED_HOSTS: "*" + AVAGO_CHAIN_CONFIG_CONTENT: "${nodeConfigBase64(config.blockchainId, true, false)}" + logging: + driver: json-file + options: + max-size: "50m" + max-file: "3" + +volumes: + postgres_data: + caddy_data: + caddy_config: +` + +export default function BlockScout() { + const [chainId, setChainId] = useState(""); + const [subnetId, setSubnetId] = useState(""); + const [domain, setDomain] = useState(""); + const [networkName, setNetworkName] = useState(""); + const [networkShortName, setNetworkShortName] = useState(""); + const [tokenName, setTokenName] = useState(""); + const [tokenSymbol, setTokenSymbol] = useState(""); + const [subnetIdError, setSubnetIdError] = useState(null); + const [composeYaml, setComposeYaml] = useState(""); + const [caddyfile, setCaddyfile] = useState(""); + + const getL1Info = useL1ByChainId(chainId); + + useEffect(() => { + setSubnetIdError(null); + setSubnetId(""); + if (!chainId) return + + // Set defaults from L1 store if available + const l1Info = getL1Info(); + if (l1Info) { + setNetworkName(l1Info.name); + setNetworkShortName(l1Info.name.split(" ")[0]); // First word as short name + setTokenName(l1Info.coinName); + setTokenSymbol(l1Info.coinName); + } + + getBlockchainInfo(chainId).then((chainInfo) => { + setSubnetId(chainInfo.subnetId); + }).catch((error) => { + setSubnetIdError((error as Error).message); + }); + }, [chainId]); + + const domainError = useMemo(() => { + if (!domain) return null; + // Updated regex to handle both traditional domains and IP-based domains like 1.2.3.4.sslip.io + const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-\.]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$/; + if (!domainRegex.test(domain)) return "Please enter a valid domain name (e.g. example.com or 1.2.3.4.sslip.io)"; + return null; + }, [domain]); + + useEffect(() => { + let ready = !!domain && !!subnetId && !!networkName && !!networkShortName && !!tokenName && !!tokenSymbol && !domainError && !subnetIdError + + if (ready) { + setCaddyfile(genCaddyfile(domain)); + setComposeYaml(genDockerCompose({ + domain, + subnetId, + blockchainId: chainId, + networkName, + networkShortName, + tokenName, + tokenSymbol + })); + } else { + setCaddyfile(""); + setComposeYaml(""); + } + }, [domain, subnetId, chainId, networkName, networkShortName, tokenName, tokenSymbol, domainError, subnetIdError]); + + return ( + <> + + + +

Set up Instance

+

Set up a linux server with any cloud provider, like AWS, GCP, Azure, or Digital Ocean. 4 vCPUs, 8GB RAM, 40GB storage is enough to get you started. Choose more storage if the the Explorer is for a long-running testnet or mainnet L1.

+
+ +

Docker Installation

+

Make sure you have Docker installed on your system. You can use the following commands to install it:

+ + + {Object.keys(dockerInstallInstructions).map((os) => ( + + + + ))} + +
+ + +

Select L1

+

Enter the Avalanche Blockchain ID (not EVM chain ID) of the L1 you want to run a node for.

+ + + + +
+ + {subnetId && ( + <> + +

Domain

+

Enter your domain name or server's public IP address. For a free domain, use your server's public IP with .sslip.io (e.g. 1.2.3.4.sslip.io). Get your IP with 'curl checkip.amazonaws.com'.

+ +
+ + +

Network Details

+

Configure your network's public display information. These will be shown in the block explorer.

+ +
+ + + + + + + +
+
+ )} + + {composeYaml && (<> + +

Caddyfile

+

Create a file named Caddyfile and paste the following code:

+ +
+ +

Docker Compose

+

Create a file named compose.yml in the same directory as your Caddyfile and paste the following code:

+ +
+ + +

Start Your Explorer

+

Navigate to the directory containing your Caddyfile and compose.yml files and run these commands:

+ +
+
+

Start the services (detached mode):

+ +

The -d flag runs containers in the background so you can close your terminal.

+
+ +
+

Check if everything is running:

+ +

Shows the status of all containers. They should all show "Up" or "running".

+
+ +
+

View logs if something goes wrong:

+ +

Press Ctrl+C to stop watching logs. Replace "backend" with any service name to see its logs.

+
+ +
+

Stop everything and clean up:

+ +

The -v flag removes volumes (databases). Warning: This forces reindexing.

+
+ +

+ Services take 2-5 minutes to fully start up. Your BlockScout explorer will be available at https://{domain || "your-domain.com"}.

+ +

If containers keep restarting, check logs with docker logs [service-name]. Use docker compose restart [service-name] to restart individual services. +

+
+ + +
+ )} + + +
+ + +
+ + ); +}; diff --git a/toolbox/src/toolbox/Nodes/AvalanchegoDocker.tsx b/toolbox/src/toolbox/Nodes/AvalanchegoDocker.tsx index ce9a76a1820..dc9bac272e6 100644 --- a/toolbox/src/toolbox/Nodes/AvalanchegoDocker.tsx +++ b/toolbox/src/toolbox/Nodes/AvalanchegoDocker.tsx @@ -21,8 +21,9 @@ import { RadioGroup } from "../../components/RadioGroup"; import { Success } from "../../components/Success"; -const debugConfigBase64 = (chainId: string) => { - const debugConfig = { +export const nodeConfigBase64 = (chainId: string, debugEnabled: boolean, pruningEnabled: boolean) => { + const vmConfig = debugEnabled ? { + "pruning-enabled": pruningEnabled, "log-level": "debug", "warp-api-enabled": true, "eth-apis": [ @@ -42,21 +43,21 @@ const debugConfigBase64 = (chainId: string) => { "debug-file-tracer", "debug-handler" ] + } : { + "pruning-enabled": pruningEnabled, } // First encode the inner config object - const debugConfigEncoded = btoa(JSON.stringify(debugConfig)); + const vmConfigEncoded = btoa(JSON.stringify(vmConfig)); const configMap: Record = {} - configMap[chainId] = { Config: debugConfigEncoded, Upgrade: null } - - console.log('configMap', configMap); + configMap[chainId] = { Config: vmConfigEncoded, Upgrade: null } return btoa(JSON.stringify(configMap)) } -const generateDockerCommand = (subnets: string[], isRPC: boolean, networkID: number, debugChainId?: string) => { +const generateDockerCommand = (subnets: string[], isRPC: boolean, networkID: number, chainId: string, debugEnabled: boolean = false, pruningEnabled: boolean = false) => { const env: Record = { AVAGO_PARTIAL_SYNC_PRIMARY_NETWORK: "true", AVAGO_PUBLIC_IP_RESOLUTION_SERVICE: "opendns", @@ -80,9 +81,7 @@ const generateDockerCommand = (subnets: string[], isRPC: boolean, networkID: num env.AVAGO_HTTP_ALLOWED_HOSTS = "\"*\""; } - if (debugChainId) { - env.AVAGO_CHAIN_CONFIG_CONTENT = debugConfigBase64(debugChainId); - } + env.AVAGO_CHAIN_CONFIG_CONTENT = nodeConfigBase64(chainId, debugEnabled, pruningEnabled); const chunks = [ "docker run -it -d", @@ -135,7 +134,7 @@ ${domain}/ext/bc/${chainID}/rpc` } } -const dockerInstallInstructions: Record = { +export const dockerInstallInstructions: Record = { 'Ubuntu/Debian': `sudo apt-get update && \\ sudo apt-get install -y docker.io && \\ sudo usermod -aG docker $USER && \\ @@ -164,7 +163,7 @@ docker run -it --rm hello-world `, } as const; -type OS = keyof typeof dockerInstallInstructions; +export type OS = keyof typeof dockerInstallInstructions; export default function AvalanchegoDocker() { const [chainId, setChainId] = useState(""); @@ -174,20 +173,21 @@ export default function AvalanchegoDocker() { const [nodeRunningMode, setNodeRunningMode] = useState("server"); const [domain, setDomain] = useState(""); const [enableDebugTrace, setEnableDebugTrace] = useState(false); + const [pruningEnabled, setPruningEnabled] = useState(true); const [subnetIdError, setSubnetIdError] = useState(null); const [isAddChainModalOpen, setIsAddChainModalOpen] = useState(false); const [chainAddedToWallet, setChainAddedToWallet] = useState(null); - + const { avalancheNetworkID } = useWalletStore(); const { addL1 } = useL1ListStore()(); useEffect(() => { try { - setRpcCommand(generateDockerCommand([subnetId], isRPC, avalancheNetworkID, enableDebugTrace ? chainId : undefined)); + setRpcCommand(generateDockerCommand([subnetId], isRPC, avalancheNetworkID, chainId, enableDebugTrace, pruningEnabled)); } catch (error) { setRpcCommand((error as Error).message); } - }, [subnetId, isRPC, avalancheNetworkID, enableDebugTrace, chainId]); + }, [subnetId, isRPC, avalancheNetworkID, enableDebugTrace, chainId, pruningEnabled]); useEffect(() => { if (!isRPC) { @@ -217,6 +217,7 @@ export default function AvalanchegoDocker() { setNodeRunningMode("server"); setDomain(""); setEnableDebugTrace(false); + setPruningEnabled(true); setSubnetIdError(null); setIsAddChainModalOpen(false); }; @@ -315,6 +316,12 @@ export default function AvalanchegoDocker() { checked={enableDebugTrace} onChange={setEnableDebugTrace} />} + + {isRPC && setPruningEnabled(!checked)} + />} {nodeRunningMode === "server" && (

Port Configuration

diff --git a/toolbox/src/toolbox/ToolboxApp.tsx b/toolbox/src/toolbox/ToolboxApp.tsx index b73c4af997c..371431bffbc 100644 --- a/toolbox/src/toolbox/ToolboxApp.tsx +++ b/toolbox/src/toolbox/ToolboxApp.tsx @@ -55,6 +55,13 @@ const componentGroups: Record = { component: lazy(() => import('./L1/ConvertToL1')), fileNames: ["toolbox/src/toolbox/L1/ConvertToL1.tsx"], walletMode: "c-chain" + }, + { + id: "selfHostedExplorer", + label: "Self-Hosted Explorer", + component: lazy(() => import('./L1/SelfHostedExplorer')), + fileNames: ["toolbox/src/toolbox/Nodes/SelfHostedExplorer.tsx"], + walletMode: "testnet-mainnet", } ] }, @@ -357,7 +364,7 @@ const componentGroups: Record = { } ] }, - "Nodes Utils": { + "Node Utils": { components: [ { id: "rpcMethodsCheck",