diff --git a/blueprints/reef-bootnode/docker-compose.yml b/blueprints/reef-bootnode/docker-compose.yml
new file mode 100644
index 000000000..85c9b2718
--- /dev/null
+++ b/blueprints/reef-bootnode/docker-compose.yml
@@ -0,0 +1,51 @@
+version: "3.8"
+
+services:
+ reef-bootnode:
+ image: anukulpandey/reef-chain-node:latest
+ restart: unless-stopped
+ working_dir: /workspace
+
+ volumes:
+ - ./output:/output
+
+ expose:
+ - "${PORT}"
+ - "${P2P_PORT}"
+
+ environment:
+ V1_ADDR: ${V1_ADDR}
+ V1_SEED: ${V1_SEED}
+
+ V2_ADDR: ${V2_ADDR}
+ V2_SEED: ${V2_SEED}
+
+ V3_ADDR: ${V3_ADDR}
+ V3_SEED: ${V3_SEED}
+
+ PORT: ${PORT}
+ P2P_PORT: ${P2P_PORT}
+
+ entrypoint: /bin/sh
+ command: -c "
+ set -e &&
+
+ echo '📦 Installing dependencies...' &&
+ apt-get update &&
+ apt-get install -y python3 wget ca-certificates &&
+
+ echo '📥 Downloading bootstrap scripts...' &&
+ mkdir -p /workspace/bootstrap/scripts &&
+ wget -O /workspace/bootstrap/scripts/run.sh \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-reef-chain-scripts/52c3e30d06bba10936aea2db9740eafd76b3ac40/bootstrap/scripts/run.sh &&
+ wget -O /workspace/bootstrap/scripts/update-spec.py \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-reef-chain-scripts/52c3e30d06bba10936aea2db9740eafd76b3ac40/bootstrap/scripts/update-spec.py &&
+ wget -O /workspace/bootstrap/scripts/update-spec.sh \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-reef-chain-scripts/52c3e30d06bba10936aea2db9740eafd76b3ac40/bootstrap/scripts/update-spec.sh &&
+
+ echo '🔧 Making scripts executable...' &&
+ chmod +x /workspace/bootstrap/scripts/run.sh &&
+ chmod +x /workspace/bootstrap/scripts/update-spec.sh &&
+
+ echo '🚀 Running spec generator + bootnode:' &&
+ exec /workspace/bootstrap/scripts/run.sh"
diff --git a/blueprints/reef-bootnode/meta.json b/blueprints/reef-bootnode/meta.json
new file mode 100644
index 000000000..0684d4fa1
--- /dev/null
+++ b/blueprints/reef-bootnode/meta.json
@@ -0,0 +1,18 @@
+{
+ "id": "reef-bootnode",
+ "name": "Reef Chain - Bootnode Validator",
+ "version": "1",
+ "description": "Runs bootnode for a custom Reef Chain cluster",
+ "links": {
+ "github": "https://github.com/reef-chain/chain-upgrade",
+ "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node",
+ "docs": "https://docs.reef.io/"
+ },
+ "logo": "reef.svg",
+ "tags": [
+ "reef-chain",
+ "validator",
+ "self-hosted",
+ "evm"
+ ]
+}
diff --git a/blueprints/reef-bootnode/reef.svg b/blueprints/reef-bootnode/reef.svg
new file mode 100644
index 000000000..598d7930a
--- /dev/null
+++ b/blueprints/reef-bootnode/reef.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/blueprints/reef-bootnode/template.toml b/blueprints/reef-bootnode/template.toml
new file mode 100644
index 000000000..9907faa74
--- /dev/null
+++ b/blueprints/reef-bootnode/template.toml
@@ -0,0 +1,37 @@
+[variables]
+V1_ADDR = ""
+V1_SEED = ""
+
+V2_ADDR = ""
+V2_SEED = ""
+
+V3_ADDR = ""
+V3_SEED = ""
+
+PORT="8000"
+P2P_PORT="30335"
+
+[[config.domains]]
+serviceName = "reef-bootnode"
+port = 8000
+host = "${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-bootnode"
+port = 8000
+host = "spec.${domain}"
+path = "/"
+
+[config.env]
+V1_ADDR = ""
+V1_SEED = ""
+
+V2_ADDR = ""
+V2_SEED = ""
+
+V3_ADDR = ""
+V3_SEED = ""
+
+PORT="8000"
+P2P_PORT="30335"
diff --git a/blueprints/reef-dev-cluster/docker-compose.yml b/blueprints/reef-dev-cluster/docker-compose.yml
new file mode 100644
index 000000000..49b726af8
--- /dev/null
+++ b/blueprints/reef-dev-cluster/docker-compose.yml
@@ -0,0 +1,129 @@
+services:
+ reef-dev-cluster:
+ image: anukulpandey/reef-chain-node:latest
+ restart: unless-stopped
+ working_dir: /workspace
+
+ environment:
+ v1sec: "${v1sec:-}"
+ v2sec: "${v2sec:-}"
+ v3sec: "${v3sec:-}"
+ faucetsec: "${faucetsec:-}"
+ DEFAULT_AMOUNT: "${DEFAULT_AMOUNT:-2000}"
+ MAX_AMOUNT: "${MAX_AMOUNT:-2000}"
+
+ volumes:
+ - reef-dev-cluster-state:/workspace/state
+ - reef-dev-cluster-download:/workspace/download
+
+ expose:
+ - "30335"
+ - "30333"
+ - "30334"
+ - "30336"
+ - "8001"
+ - "9944"
+ - "9945"
+ - "8080"
+
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - bash -lc 'exec 3<>/dev/tcp/127.0.0.1/9944'
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ start_period: 300s
+
+ entrypoint: /bin/sh
+
+ command:
+ - -c
+ - |
+ set -e
+
+ export DEBIAN_FRONTEND=noninteractive
+
+ echo '📦 Installing required tools...'
+
+ apt-get update
+
+ apt-get install -y \
+ wget \
+ curl \
+ python3 \
+ ca-certificates \
+ gnupg \
+ nginx
+
+ mkdir -p \
+ /etc/apt/keyrings \
+ /workspace \
+ /workspace/state \
+ /workspace/download
+
+ echo '🔑 Configuring NodeSource repository...'
+
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
+ | gpg --batch --yes --dearmor -o /etc/apt/keyrings/nodesource.gpg
+
+ echo 'deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main' \
+ > /etc/apt/sources.list.d/nodesource.list
+
+ apt-get update
+
+ apt-get install -y nodejs
+
+ echo '📥 Downloading cluster bootstrap...'
+
+ wget -O /workspace/run-cluster.sh \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-templates/d8a82420dad013a3eb7e5d8afc0b9f2700a0045a/blueprints/reef-dev-cluster/run-cluster.sh
+
+ echo '🔧 Making script executable...'
+
+ chmod +x /workspace/run-cluster.sh
+
+ echo '🚀 Starting self-contained network...'
+
+ exec env TEMPLATE_ASSET_REF=d8a82420dad013a3eb7e5d8afc0b9f2700a0045a \
+ /workspace/run-cluster.sh
+
+ reef-dev-cluster-eth-rpc:
+ image: anukulpandey/reef-chain-eth-rpc:latest
+ restart: unless-stopped
+ working_dir: /workspace
+
+ depends_on:
+ reef-dev-cluster:
+ condition: service_healthy
+
+ expose:
+ - "8545"
+
+ environment:
+ NODE_RPC_URL: ws://reef-dev-cluster:9945
+ PORT: "8545"
+
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - bash -lc 'exec 3<>/dev/tcp/127.0.0.1/8545'
+ interval: 10s
+ timeout: 5s
+ retries: 12
+ start_period: 20s
+
+ entrypoint: /bin/sh
+
+ command:
+ - -c
+ - |
+ exec eth-rpc \
+ --node-rpc-url "$$NODE_RPC_URL" \
+ --rpc-port "$$PORT" \
+ --rpc-external \
+ --rpc-cors all
+
+volumes:
+ reef-dev-cluster-state:
+ reef-dev-cluster-download:
diff --git a/blueprints/reef-dev-cluster/faucet/package.json b/blueprints/reef-dev-cluster/faucet/package.json
new file mode 100644
index 000000000..fd45ff3d7
--- /dev/null
+++ b/blueprints/reef-dev-cluster/faucet/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "reef-dev-cluster-faucet",
+ "version": "1.0.0",
+ "private": true,
+ "description": "HTTP faucet for a self-contained Reef dev cluster",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "@polkadot/api": "^16.4.8"
+ }
+}
diff --git a/blueprints/reef-dev-cluster/faucet/server.js b/blueprints/reef-dev-cluster/faucet/server.js
new file mode 100644
index 000000000..15d1ce32b
--- /dev/null
+++ b/blueprints/reef-dev-cluster/faucet/server.js
@@ -0,0 +1,696 @@
+#!/usr/bin/env node
+
+const http = require("node:http");
+const { ApiPromise, Keyring, WsProvider } = require("@polkadot/api");
+
+const config = {
+ wsEndpoint: process.env.WS_ENDPOINT || "ws://127.0.0.1:9944",
+ evmRpcUrl: process.env.EVM_RPC_URL || "",
+ faucetSeed: process.env.FAUCET_SEED || "",
+ port: Number.parseInt(process.env.PORT || "8080", 10),
+ defaultAmount: String(process.env.DEFAULT_AMOUNT || "2000"),
+ maxAmount: String(process.env.MAX_AMOUNT || "2000"),
+ serviceName: process.env.SERVICE_NAME || "reef-faucet",
+};
+
+if (!config.faucetSeed) {
+ throw new Error("FAUCET_SEED is required");
+}
+
+let contextPromise = null;
+let requestQueue = Promise.resolve();
+
+function normalizeEvmAddress(value) {
+ const normalized = String(value || "").trim();
+ if (!/^0x[0-9a-fA-F]{40}$/.test(normalized)) {
+ throw new Error(`Invalid EVM address: ${value}`);
+ }
+ return normalized;
+}
+
+function parseUnits(value, decimals) {
+ const input = String(value).trim();
+ if (!/^\d+(\.\d+)?$/.test(input)) {
+ throw new Error(`Invalid decimal amount: ${value}`);
+ }
+
+ const [whole, fraction = ""] = input.split(".");
+ if (fraction.length > decimals) {
+ throw new Error(`Too many decimal places in ${value}`);
+ }
+
+ const wholeUnits = BigInt(whole) * 10n ** BigInt(decimals);
+ const fractionUnits = fraction ? BigInt(fraction.padEnd(decimals, "0")) : 0n;
+ return wholeUnits + fractionUnits;
+}
+
+function formatUnits(value, decimals) {
+ const negative = value < 0n;
+ const abs = negative ? -value : value;
+ const base = 10n ** BigInt(decimals);
+ const whole = abs / base;
+ const fraction = abs % base;
+
+ if (fraction === 0n) {
+ return `${negative ? "-" : ""}${whole.toString()}`;
+ }
+
+ return `${negative ? "-" : ""}${whole.toString()}.${fraction
+ .toString()
+ .padStart(decimals, "0")
+ .replace(/0+$/, "")}`;
+}
+
+function fallbackAccountFromEvm(evmAddress) {
+ return `0x${evmAddress.slice(2).toLowerCase()}${"ee".repeat(12)}`;
+}
+
+function decodeDispatchError(api, dispatchError) {
+ if (dispatchError && dispatchError.isModule) {
+ const decoded = api.registry.findMetaError(dispatchError.asModule);
+ return `${decoded.section}.${decoded.name}: ${decoded.docs.join(" ")}`;
+ }
+
+ return dispatchError && dispatchError.toString ? dispatchError.toString() : String(dispatchError);
+}
+
+async function signAndWait(api, tx, signer) {
+ return new Promise(async (resolve, reject) => {
+ let unsubscribe = null;
+
+ try {
+ unsubscribe = await tx.signAndSend(signer, ({ dispatchError, events, status, txHash }) => {
+ if (dispatchError) {
+ if (unsubscribe) {
+ unsubscribe();
+ }
+ reject(new Error(decodeDispatchError(api, dispatchError)));
+ return;
+ }
+
+ if (status.isFinalized) {
+ if (unsubscribe) {
+ unsubscribe();
+ }
+ resolve({
+ txHash: txHash.toHex(),
+ finalizedBlock: status.asFinalized.toHex(),
+ eventCount: events.length,
+ });
+ }
+ });
+ } catch (error) {
+ if (unsubscribe) {
+ unsubscribe();
+ }
+ reject(error);
+ }
+ });
+}
+
+async function maybeClaimDefaultAccount(api, signer) {
+ const storage = api.query && api.query.evmAccounts && api.query.evmAccounts.evmAddresses;
+ const extrinsic = api.tx && api.tx.evmAccounts && api.tx.evmAccounts.claimDefaultAccount;
+
+ if (!storage || !extrinsic) {
+ return null;
+ }
+
+ const current = await storage(signer.address);
+ if (!current.isEmpty) {
+ return current.toString();
+ }
+
+ await signAndWait(api, extrinsic(), signer);
+ return (await storage(signer.address)).toString();
+}
+
+function buildReviveTransfer(api, signerAddress, targetEvm, amountUnits) {
+ const reviveTransfer = api.tx && api.tx.revive && api.tx.revive.transfer;
+ if (!reviveTransfer) {
+ return null;
+ }
+
+ const metaArgs = reviveTransfer.meta.toJSON().args || [];
+ const names = metaArgs.map((arg) => String((arg && arg.name) || "").toLowerCase());
+ const types = metaArgs.map((arg) => JSON.stringify((arg && arg.type) || "").toLowerCase());
+ const haystack = `${names.join(",")}|${types.join(",")}`;
+
+ if (metaArgs.length === 2 && haystack.includes("h160")) {
+ return {
+ strategy: "revive.transfer(H160, Balance)",
+ tx: reviveTransfer(targetEvm, amountUnits),
+ };
+ }
+
+ if (metaArgs.length === 3 && haystack.includes("accountid") && haystack.includes("h160")) {
+ return {
+ strategy: "revive.transfer(AccountId, H160, Balance)",
+ tx: reviveTransfer(signerAddress, targetEvm, amountUnits),
+ };
+ }
+
+ return {
+ strategy: "revive.transfer(fallback call signature)",
+ tx: reviveTransfer(targetEvm, amountUnits),
+ };
+}
+
+async function getContext() {
+ if (!contextPromise) {
+ contextPromise = (async () => {
+ const provider = new WsProvider(config.wsEndpoint);
+ const api = await ApiPromise.create({ provider });
+ const keyring = new Keyring({ type: "sr25519" });
+ const sender = keyring.addFromUri(config.faucetSeed);
+ const nativeDecimals = api.registry.chainDecimals[0] || 12;
+ const tokenSymbol = api.registry.chainTokens[0] || "REEF";
+
+ return { api, nativeDecimals, provider, sender, tokenSymbol };
+ })();
+ }
+
+ return contextPromise;
+}
+
+function jsonResponse(response, statusCode, body) {
+ response.writeHead(statusCode, { "Content-Type": "application/json" });
+ response.end(JSON.stringify(body));
+}
+
+function htmlResponse(response, statusCode, body) {
+ response.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" });
+ response.end(body);
+}
+
+function escapeHtml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function wantsHtml(request) {
+ const accept = String(request.headers.accept || "");
+ return accept.includes("text/html");
+}
+
+function buildInfoPayload() {
+ return {
+ service: config.serviceName,
+ status: "ok",
+ defaults: {
+ defaultAmount: config.defaultAmount,
+ maxAmount: config.maxAmount,
+ },
+ upstream: {
+ wsEndpoint: config.wsEndpoint,
+ evmRpcUrl: config.evmRpcUrl || null,
+ },
+ endpoints: {
+ health: {
+ method: "GET",
+ path: "/health",
+ },
+ drip: {
+ method: "POST",
+ path: "/drip",
+ body: {
+ to: "0x0000000000000000000000000000000000000000",
+ amount: config.defaultAmount,
+ },
+ },
+ },
+ };
+}
+
+function buildFrontendHtml() {
+ const bootPayload = JSON.stringify(buildInfoPayload());
+ const title = escapeHtml(config.serviceName);
+ const defaultAmount = escapeHtml(config.defaultAmount);
+ const maxAmount = escapeHtml(config.maxAmount);
+ const wsEndpoint = escapeHtml(config.wsEndpoint);
+ const evmRpcUrl = escapeHtml(config.evmRpcUrl || "Not configured");
+
+ return `
+
+
+
+
+ ${title}
+
+
+
+
+
+
+
Reef Faucet
+
${title}
+
Send test REEF to any EVM address without reaching for curl first.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+}
+
+async function parseRequestBody(request) {
+ const chunks = [];
+ for await (const chunk of request) {
+ chunks.push(chunk);
+ }
+
+ if (chunks.length === 0) {
+ return {};
+ }
+
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
+}
+
+async function executeDrip(targetEvm, amountText) {
+ const { api, nativeDecimals, sender, tokenSymbol } = await getContext();
+ const amountUnits = parseUnits(amountText, nativeDecimals);
+ const maxUnits = parseUnits(config.maxAmount, nativeDecimals);
+
+ if (amountUnits <= 0n) {
+ throw new Error("Amount must be greater than zero");
+ }
+
+ if (amountUnits > maxUnits) {
+ throw new Error(`Amount exceeds MAX_AMOUNT (${config.maxAmount})`);
+ }
+
+ await maybeClaimDefaultAccount(api, sender);
+
+ const senderBefore = (await api.query.system.account(sender.address)).data.free.toBigInt();
+
+ let strategy = "";
+ let result = null;
+
+ try {
+ const revive = buildReviveTransfer(api, sender.address, targetEvm, amountUnits);
+ if (!revive) {
+ throw new Error("revive.transfer unavailable");
+ }
+ strategy = revive.strategy;
+ result = await signAndWait(api, revive.tx, sender);
+ } catch (error) {
+ strategy = "balances.transferAllowDeath(AccountId32 fallback)";
+ result = await signAndWait(
+ api,
+ api.tx.balances.transferAllowDeath(fallbackAccountFromEvm(targetEvm), amountUnits),
+ sender
+ );
+ }
+
+ const senderAfter = (await api.query.system.account(sender.address)).data.free.toBigInt();
+
+ return {
+ amount: amountText,
+ strategy,
+ tokenSymbol,
+ txHash: result.txHash,
+ finalizedBlock: result.finalizedBlock,
+ eventCount: result.eventCount,
+ sender: sender.address,
+ senderNativeBefore: formatUnits(senderBefore, nativeDecimals),
+ senderNativeAfter: formatUnits(senderAfter, nativeDecimals),
+ };
+}
+
+function enqueue(task) {
+ const current = requestQueue.then(task, task);
+ requestQueue = current.catch(() => undefined);
+ return current;
+}
+
+const server = http.createServer(async (request, response) => {
+ try {
+ if (request.method === "GET" && request.url === "/favicon.ico") {
+ response.writeHead(204);
+ response.end();
+ return;
+ }
+
+ if (request.method === "GET" && request.url === "/") {
+ if (wantsHtml(request)) {
+ htmlResponse(response, 200, buildFrontendHtml());
+ } else {
+ jsonResponse(response, 200, buildInfoPayload());
+ }
+ return;
+ }
+
+ if (request.method === "GET" && request.url === "/health") {
+ jsonResponse(response, 200, { status: "ok" });
+ return;
+ }
+
+ if (request.method === "POST" && request.url === "/drip") {
+ const payload = await parseRequestBody(request);
+ const targetEvm = normalizeEvmAddress(payload.to);
+ const amount = payload.amount ? String(payload.amount) : config.defaultAmount;
+ const result = await enqueue(() => executeDrip(targetEvm, amount));
+ jsonResponse(response, 200, result);
+ return;
+ }
+
+ jsonResponse(response, 404, { error: "Not found" });
+ } catch (error) {
+ jsonResponse(response, 400, { error: error.message });
+ }
+});
+
+server.listen(config.port, "0.0.0.0", () => {
+ console.log(`reef dev-cluster faucet listening on ${config.port}`);
+});
diff --git a/blueprints/reef-dev-cluster/meta.json b/blueprints/reef-dev-cluster/meta.json
new file mode 100644
index 000000000..05687870b
--- /dev/null
+++ b/blueprints/reef-dev-cluster/meta.json
@@ -0,0 +1,19 @@
+{
+ "id": "reef-dev-cluster",
+ "name": "Reef Chain - Dev Cluster",
+ "version": "1",
+ "description": "Self-contained Reef dev cluster with bootnode, validators, spec server, substrate RPC, EVM RPC, and faucet",
+ "links": {
+ "github": "https://github.com/reef-chain/chain-upgrade",
+ "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node",
+ "docs": "https://docs.reef.io/"
+ },
+ "logo": "reef.svg",
+ "tags": [
+ "reef-chain",
+ "dev-cluster",
+ "self-hosted",
+ "evm",
+ "faucet"
+ ]
+}
diff --git a/blueprints/reef-dev-cluster/reef.svg b/blueprints/reef-dev-cluster/reef.svg
new file mode 100644
index 000000000..598d7930a
--- /dev/null
+++ b/blueprints/reef-dev-cluster/reef.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/blueprints/reef-dev-cluster/run-cluster.sh b/blueprints/reef-dev-cluster/run-cluster.sh
new file mode 100755
index 000000000..4dca6dc68
--- /dev/null
+++ b/blueprints/reef-dev-cluster/run-cluster.sh
@@ -0,0 +1,549 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+NODE_BIN="${NODE_BIN:-reef-node}"
+CHAIN_TEMPLATE="${CHAIN_TEMPLATE:-testnet-new}"
+WORK_DIR="${WORK_DIR:-/tmp/reef-dev-cluster}"
+STATE_DIR="${STATE_DIR:-/workspace/state}"
+SEED_DIR="${SEED_DIR:-$STATE_DIR/seeds}"
+NODE_KEY_DIR="${NODE_KEY_DIR:-$STATE_DIR/node-keys}"
+OUTPUT_DIR="${OUTPUT_DIR:-/workspace/download}"
+PLAIN_SPEC="${PLAIN_SPEC:-$WORK_DIR/local-chain-spec.json}"
+UPDATED_SPEC="${UPDATED_SPEC:-$WORK_DIR/local-chain-spec-updated.json}"
+SPEC_FILE="${SPEC_FILE:-$OUTPUT_DIR/local-chain-spec-raw.json}"
+SPEC_HTTP_PORT="${SPEC_HTTP_PORT:-8001}"
+BOOTNODE_P2P_PORT="${BOOTNODE_P2P_PORT:-30335}"
+VALIDATOR1_P2P_PORT="${VALIDATOR1_P2P_PORT:-30333}"
+VALIDATOR2_P2P_PORT="${VALIDATOR2_P2P_PORT:-30334}"
+VALIDATOR3_P2P_PORT="${VALIDATOR3_P2P_PORT:-30336}"
+RPC_NODE_P2P_PORT="${RPC_NODE_P2P_PORT:-30337}"
+RPC_NODE_WS_PORT="${RPC_NODE_WS_PORT:-9944}"
+RPC_NODE_PUBLIC_WS_PORT="${RPC_NODE_PUBLIC_WS_PORT:-9945}"
+ETH_RPC_PORT="${ETH_RPC_PORT:-8545}"
+FAUCET_PORT="${FAUCET_PORT:-8080}"
+BOOTNODE_PROMETHEUS_PORT="${BOOTNODE_PROMETHEUS_PORT:-9615}"
+VALIDATOR1_PROMETHEUS_PORT="${VALIDATOR1_PROMETHEUS_PORT:-9616}"
+VALIDATOR2_PROMETHEUS_PORT="${VALIDATOR2_PROMETHEUS_PORT:-9617}"
+VALIDATOR3_PROMETHEUS_PORT="${VALIDATOR3_PROMETHEUS_PORT:-9618}"
+RPC_NODE_PROMETHEUS_PORT="${RPC_NODE_PROMETHEUS_PORT:-9619}"
+DEFAULT_AMOUNT="${DEFAULT_AMOUNT:-2000}"
+MAX_AMOUNT="${MAX_AMOUNT:-2000}"
+TEMPLATE_ASSET_REF="${TEMPLATE_ASSET_REF:-reef-chain}"
+TEMPLATE_ASSET_BASE_URL="${TEMPLATE_ASSET_BASE_URL:-https://raw.githubusercontent.com/anukulpandey/dokploy-templates/${TEMPLATE_ASSET_REF}/blueprints/reef-dev-cluster}"
+FAUCET_DIR="${FAUCET_DIR:-/workspace/faucet}"
+BOOTNODE_PEER_ID_FILE="$OUTPUT_DIR/bootnode_peer_id.txt"
+BOOTNODE_COMPAT_FILE="$OUTPUT_DIR/bootnode_node_key.txt"
+CLUSTER_INFO_FILE="$OUTPUT_DIR/cluster-info.json"
+
+PIDS=()
+TAIL_PID=""
+
+cleanup() {
+ kill "$TAIL_PID" 2>/dev/null || true
+ if [ "${#PIDS[@]}" -gt 0 ]; then
+ kill "${PIDS[@]}" 2>/dev/null || true
+ fi
+}
+
+trap cleanup EXIT
+
+trim_whitespace() {
+ printf '%s' "$1" | tr -d '[:space:]'
+}
+
+canonicalize_seed() {
+ local name="$1"
+ local value
+ value=$(trim_whitespace "${2:-}")
+
+ if [ -z "$value" ]; then
+ printf '\n'
+ return 0
+ fi
+
+ if [[ "$value" =~ ^0x[0-9A-Fa-f]{64}$ ]]; then
+ printf '0x%s\n' "$(printf '%s' "${value#0x}" | tr '[:upper:]' '[:lower:]')"
+ return 0
+ fi
+
+ if [[ "$value" =~ ^[0-9A-Fa-f]{64}$ ]]; then
+ printf '0x%s\n' "$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
+ return 0
+ fi
+
+ echo "Invalid seed for $name. Expected 64 hex bytes with optional 0x prefix." >&2
+ exit 1
+}
+
+write_secret_file() {
+ local file="$1"
+ local value="$2"
+
+ mkdir -p "$(dirname "$file")"
+ printf '%s\n' "$value" > "$file"
+ chmod 600 "$file"
+}
+
+generate_seed() {
+ printf '0x%s\n' "$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n' | tr '[:upper:]' '[:lower:]')"
+}
+
+resolve_seed() {
+ local env_name="$1"
+ local file_name="$2"
+ local file="$SEED_DIR/$file_name"
+ local raw_value normalized source
+
+ raw_value=$(trim_whitespace "${!env_name:-}")
+ if [ -n "$raw_value" ]; then
+ normalized=$(canonicalize_seed "$env_name" "$raw_value")
+ source="env"
+ write_secret_file "$file" "$normalized"
+ elif [ -f "$file" ]; then
+ normalized=$(canonicalize_seed "$env_name" "$(cat "$file")")
+ source="persisted"
+ write_secret_file "$file" "$normalized"
+ else
+ normalized=$(generate_seed)
+ source="generated"
+ write_secret_file "$file" "$normalized"
+ fi
+
+ printf '%s|%s\n' "$normalized" "$source"
+}
+
+resolve_optional_seed() {
+ local env_name="$1"
+ local file_name="$2"
+ local file="$SEED_DIR/$file_name"
+ local raw_value normalized source
+
+ raw_value=$(trim_whitespace "${!env_name:-}")
+ if [ -n "$raw_value" ]; then
+ normalized=$(canonicalize_seed "$env_name" "$raw_value")
+ source="env"
+ write_secret_file "$file" "$normalized"
+ elif [ -f "$file" ]; then
+ normalized=$(canonicalize_seed "$env_name" "$(cat "$file")")
+ source="persisted"
+ write_secret_file "$file" "$normalized"
+ else
+ normalized=""
+ source="shared-v1"
+ fi
+
+ printf '%s|%s\n' "$normalized" "$source"
+}
+
+read_seed_result() {
+ local var_prefix="$1"
+ local result="$2"
+ local seed="${result%%|*}"
+ local source="${result##*|}"
+
+ printf -v "${var_prefix}_SEED" '%s' "$seed"
+ printf -v "${var_prefix}_SOURCE" '%s' "$source"
+}
+
+ensure_node_key_file() {
+ local name="$1"
+ local file="$NODE_KEY_DIR/${name}.key"
+
+ if [ ! -s "$file" ]; then
+ mkdir -p "$NODE_KEY_DIR"
+ "$NODE_BIN" key generate-node-key --chain local > "$file"
+ chmod 600 "$file"
+ fi
+
+ printf '%s\n' "$file"
+}
+
+derive_address() {
+ local suri="$1"
+ local scheme="${2:-Sr25519}"
+
+ "$NODE_BIN" key inspect --scheme "$scheme" "$suri" --output-type json \
+ | grep -o '"ss58Address": "[^"]*"' \
+ | cut -d'"' -f4
+}
+
+insert_keys() {
+ local base_path="$1"
+ local seed="$2"
+
+ "$NODE_BIN" key insert --base-path "$base_path" --chain "$SPEC_FILE" --scheme Sr25519 --suri "$seed//babe" --key-type babe
+ "$NODE_BIN" key insert --base-path "$base_path" --chain "$SPEC_FILE" --scheme Ed25519 --suri "$seed//grandpa" --key-type gran
+ "$NODE_BIN" key insert --base-path "$base_path" --chain "$SPEC_FILE" --scheme Sr25519 --suri "$seed//im_online" --key-type imon
+ "$NODE_BIN" key insert --base-path "$base_path" --chain "$SPEC_FILE" --scheme Sr25519 --suri "$seed//authority_discovery" --key-type audi
+}
+
+start_logged() {
+ local log_file="$1"
+ shift
+
+ "$@" >"$log_file" 2>&1 &
+ PIDS+=("$!")
+}
+
+write_ws_proxy_config() {
+ cat > /tmp/rpc-nginx.conf </dev/null || true
+
+echo "Resolved validator addresses:"
+echo " - Validator1: $V1_ADDR ($V1_SOURCE)"
+echo " - Validator2: $V2_ADDR ($V2_SOURCE)"
+echo " - Validator3: $V3_ADDR ($V3_SOURCE)"
+echo " - Faucet: $FAUCET_ADDR ($FAUCET_SOURCE)"
+echo "Private state directory: $STATE_DIR"
+
+echo "Downloading local faucet sources..."
+wget -q -O "$FAUCET_DIR/package.json" \
+ "${TEMPLATE_ASSET_BASE_URL}/faucet/package.json"
+wget -q -O "$FAUCET_DIR/server.js" \
+ "${TEMPLATE_ASSET_BASE_URL}/faucet/server.js"
+
+echo "Installing faucet dependencies..."
+(
+ cd "$FAUCET_DIR"
+ npm install --omit=dev --no-fund --no-audit
+)
+
+echo "Generating chain spec..."
+"$NODE_BIN" build-spec --chain "$CHAIN_TEMPLATE" --disable-default-bootnode > "$PLAIN_SPEC"
+
+python3 - "$PLAIN_SPEC" "$UPDATED_SPEC" \
+ "$V1_ADDR" "$V1_BABE" "$V1_GRAN" "$V1_IMON" "$V1_AUDI" \
+ "$V2_ADDR" "$V2_BABE" "$V2_GRAN" "$V2_IMON" "$V2_AUDI" \
+ "$V3_ADDR" "$V3_BABE" "$V3_GRAN" "$V3_IMON" "$V3_AUDI" \
+ "$FAUCET_ADDR" "$V1_ADDR" <<'PY'
+import json
+import sys
+
+input_file = sys.argv[1]
+output_file = sys.argv[2]
+
+v1_addr, v1_babe, v1_gran, v1_imon, v1_audi = sys.argv[3:8]
+v2_addr, v2_babe, v2_gran, v2_imon, v2_audi = sys.argv[8:13]
+v3_addr, v3_babe, v3_gran, v3_imon, v3_audi = sys.argv[13:18]
+faucet_addr = sys.argv[18]
+default_faucet_addr = sys.argv[19]
+
+AMOUNT = 100000000000000000000000000
+STAKE = 1000000000000000000000000
+
+with open(input_file, "r", encoding="utf-8") as handle:
+ spec = json.load(handle)
+
+balances = spec["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"]
+
+def upsert_balance(address, amount):
+ for entry in balances:
+ if entry[0] == address:
+ entry[1] = max(int(entry[1]), amount)
+ return
+ balances.append([address, amount])
+
+for address in (v1_addr, v2_addr, v3_addr):
+ upsert_balance(address, AMOUNT)
+
+if faucet_addr != default_faucet_addr:
+ upsert_balance(faucet_addr, AMOUNT)
+
+spec["genesis"]["runtimeGenesis"]["patch"]["session"]["keys"] = [
+ [
+ v1_addr,
+ v1_addr,
+ {
+ "authority_discovery": v1_audi,
+ "babe": v1_babe,
+ "grandpa": v1_gran,
+ "im_online": v1_imon,
+ },
+ ],
+ [
+ v2_addr,
+ v2_addr,
+ {
+ "authority_discovery": v2_audi,
+ "babe": v2_babe,
+ "grandpa": v2_gran,
+ "im_online": v2_imon,
+ },
+ ],
+ [
+ v3_addr,
+ v3_addr,
+ {
+ "authority_discovery": v3_audi,
+ "babe": v3_babe,
+ "grandpa": v3_gran,
+ "im_online": v3_imon,
+ },
+ ],
+]
+
+spec["genesis"]["runtimeGenesis"]["patch"]["staking"]["invulnerables"] = [
+ v1_addr,
+ v2_addr,
+ v3_addr,
+]
+
+spec["genesis"]["runtimeGenesis"]["patch"]["staking"]["stakers"] = [
+ [v1_addr, v1_addr, STAKE, "Validator"],
+ [v2_addr, v2_addr, STAKE, "Validator"],
+ [v3_addr, v3_addr, STAKE, "Validator"],
+]
+
+with open(output_file, "w", encoding="utf-8") as handle:
+ json.dump(spec, handle, indent=2)
+PY
+
+"$NODE_BIN" build-spec \
+ --chain "$UPDATED_SPEC" \
+ --disable-default-bootnode \
+ --raw > "$SPEC_FILE"
+
+BOOTNODE_NODE_KEY_FILE=$(ensure_node_key_file bootnode)
+V1_NODE_KEY_FILE=$(ensure_node_key_file validator1)
+V2_NODE_KEY_FILE=$(ensure_node_key_file validator2)
+V3_NODE_KEY_FILE=$(ensure_node_key_file validator3)
+RPC_NODE_KEY_FILE=$(ensure_node_key_file rpc-node)
+
+BOOTNODE_PEER_ID=$("$NODE_BIN" key inspect-node-key --file "$BOOTNODE_NODE_KEY_FILE" 2>/dev/null | tail -n1 | tr -d '\r')
+if [ -z "$BOOTNODE_PEER_ID" ]; then
+ echo "Failed to derive bootnode peer id" >&2
+ exit 1
+fi
+
+printf '%s\n' "$BOOTNODE_PEER_ID" > "$BOOTNODE_PEER_ID_FILE"
+printf '%s\n' "$BOOTNODE_PEER_ID" > "$BOOTNODE_COMPAT_FILE"
+write_cluster_info
+
+echo "Bootnode peer ID: $BOOTNODE_PEER_ID"
+
+insert_keys /tmp/validator1 "$V1_SEED"
+insert_keys /tmp/validator2 "$V2_SEED"
+insert_keys /tmp/validator3 "$V3_SEED"
+
+python3 -m http.server "$SPEC_HTTP_PORT" --bind 0.0.0.0 --directory "$OUTPUT_DIR" >/tmp/spec-server.log 2>&1 &
+PIDS+=("$!")
+
+BOOTNODE_MULTIADDR="/ip4/127.0.0.1/tcp/${BOOTNODE_P2P_PORT}/p2p/${BOOTNODE_PEER_ID}"
+
+write_ws_proxy_config
+
+start_logged /tmp/bootnode.log \
+ "$NODE_BIN" \
+ --base-path /tmp/bootnode \
+ --chain "$SPEC_FILE" \
+ --port "$BOOTNODE_P2P_PORT" \
+ --prometheus-port "$BOOTNODE_PROMETHEUS_PORT" \
+ --rpc-port 0 \
+ --node-key-file "$BOOTNODE_NODE_KEY_FILE" \
+ --name Bootnode \
+ --no-telemetry
+
+start_logged /tmp/validator1.log \
+ "$NODE_BIN" \
+ --base-path /tmp/validator1 \
+ --chain "$SPEC_FILE" \
+ --port "$VALIDATOR1_P2P_PORT" \
+ --prometheus-port "$VALIDATOR1_PROMETHEUS_PORT" \
+ --rpc-port 0 \
+ --node-key-file "$V1_NODE_KEY_FILE" \
+ --bootnodes "$BOOTNODE_MULTIADDR" \
+ --validator \
+ --name Validator1 \
+ --no-telemetry
+
+start_logged /tmp/validator2.log \
+ "$NODE_BIN" \
+ --base-path /tmp/validator2 \
+ --chain "$SPEC_FILE" \
+ --port "$VALIDATOR2_P2P_PORT" \
+ --prometheus-port "$VALIDATOR2_PROMETHEUS_PORT" \
+ --rpc-port 0 \
+ --node-key-file "$V2_NODE_KEY_FILE" \
+ --bootnodes "$BOOTNODE_MULTIADDR" \
+ --validator \
+ --name Validator2 \
+ --no-telemetry
+
+start_logged /tmp/validator3.log \
+ "$NODE_BIN" \
+ --base-path /tmp/validator3 \
+ --chain "$SPEC_FILE" \
+ --port "$VALIDATOR3_P2P_PORT" \
+ --prometheus-port "$VALIDATOR3_PROMETHEUS_PORT" \
+ --rpc-port 0 \
+ --node-key-file "$V3_NODE_KEY_FILE" \
+ --bootnodes "$BOOTNODE_MULTIADDR" \
+ --validator \
+ --name Validator3 \
+ --no-telemetry
+
+start_logged /tmp/rpc-node.log \
+ "$NODE_BIN" \
+ --base-path /tmp/rpc-node \
+ --chain "$SPEC_FILE" \
+ --port "$RPC_NODE_P2P_PORT" \
+ --prometheus-port "$RPC_NODE_PROMETHEUS_PORT" \
+ --node-key-file "$RPC_NODE_KEY_FILE" \
+ --bootnodes "$BOOTNODE_MULTIADDR" \
+ --rpc-external \
+ --rpc-port "$RPC_NODE_WS_PORT" \
+ --rpc-cors all \
+ --rpc-methods Unsafe \
+ --rpc-max-connections 10000 \
+ --pruning archive \
+ --name rpc-node \
+ --no-telemetry
+
+start_logged /tmp/ws-proxy.log \
+ nginx \
+ -c /tmp/rpc-nginx.conf \
+ -g 'daemon off;'
+
+start_logged /tmp/faucet.log \
+ env \
+ SERVICE_NAME="reef-dev-cluster-faucet" \
+ PORT="$FAUCET_PORT" \
+ WS_ENDPOINT="ws://127.0.0.1:${RPC_NODE_WS_PORT}" \
+ EVM_RPC_URL="http://127.0.0.1:${ETH_RPC_PORT}" \
+ FAUCET_SEED="$FAUCET_SEED" \
+ DEFAULT_AMOUNT="$DEFAULT_AMOUNT" \
+ MAX_AMOUNT="$MAX_AMOUNT" \
+ npm --prefix "$FAUCET_DIR" start
+
+tail -F /tmp/spec-server.log /tmp/bootnode.log /tmp/validator1.log /tmp/validator2.log /tmp/validator3.log /tmp/rpc-node.log /tmp/ws-proxy.log /tmp/faucet.log &
+TAIL_PID=$!
+
+wait -n "${PIDS[@]}"
+STATUS=$?
+exit "$STATUS"
diff --git a/blueprints/reef-dev-cluster/template.toml b/blueprints/reef-dev-cluster/template.toml
new file mode 100644
index 000000000..05345c2bd
--- /dev/null
+++ b/blueprints/reef-dev-cluster/template.toml
@@ -0,0 +1,51 @@
+[variables]
+# Optional advanced overrides. Leave blank to auto-generate and persist validator identities.
+v1sec = ""
+v2sec = ""
+v3sec = ""
+
+# Optional dedicated faucet seed. Leave blank to reuse validator 1.
+faucetsec = ""
+DEFAULT_AMOUNT = "2000"
+MAX_AMOUNT = "2000"
+
+[[config.domains]]
+serviceName = "reef-dev-cluster"
+port = 8001
+host = "${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-dev-cluster"
+port = 8001
+host = "spec.${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-dev-cluster"
+port = 9945
+host = "ws.${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-dev-cluster-eth-rpc"
+port = 8545
+host = "eth.${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-dev-cluster"
+port = 8080
+host = "faucet.${domain}"
+path = "/"
+
+[config.env]
+# Optional advanced overrides. Leave blank to auto-generate and persist validator identities.
+v1sec = ""
+v2sec = ""
+v3sec = ""
+
+# Optional dedicated faucet seed. Leave blank to reuse validator 1.
+faucetsec = ""
+DEFAULT_AMOUNT = "2000"
+MAX_AMOUNT = "2000"
diff --git a/blueprints/reef-eth-rpc/docker-compose.yml b/blueprints/reef-eth-rpc/docker-compose.yml
new file mode 100644
index 000000000..57ae2b8be
--- /dev/null
+++ b/blueprints/reef-eth-rpc/docker-compose.yml
@@ -0,0 +1,16 @@
+version: "3.8"
+
+services:
+ reef-eth-rpc:
+ image: anukulpandey/reef-chain-eth-rpc:latest
+ restart: unless-stopped
+ working_dir: /workspace
+ volumes:
+ - ./output:/output
+ environment:
+ - PORT=${PORT}
+ - NODE_RPC_URL=${NODE_RPC_URL}
+ expose:
+ - "${PORT}"
+ entrypoint: /bin/sh
+ command: -c "eth-rpc --node-rpc-url $$NODE_RPC_URL --rpc-port $$PORT --rpc-external --rpc-cors all"
diff --git a/blueprints/reef-eth-rpc/meta.json b/blueprints/reef-eth-rpc/meta.json
new file mode 100644
index 000000000..86e1b3034
--- /dev/null
+++ b/blueprints/reef-eth-rpc/meta.json
@@ -0,0 +1,18 @@
+{
+ "id": "reef-eth-rpc",
+ "name": "Reef Chain - ETH RPC",
+ "version": "1",
+ "description": "EVM layer rpc for a custom Reef Chain cluster",
+ "links": {
+ "github": "https://github.com/reef-chain/chain-upgrade",
+ "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-eth-rpc",
+ "docs": "https://docs.reef.io/"
+ },
+ "logo": "reef.svg",
+ "tags": [
+ "reef-chain",
+ "eth-rpc",
+ "self-hosted",
+ "keys"
+ ]
+}
diff --git a/blueprints/reef-eth-rpc/reef.svg b/blueprints/reef-eth-rpc/reef.svg
new file mode 100644
index 000000000..598d7930a
--- /dev/null
+++ b/blueprints/reef-eth-rpc/reef.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/blueprints/reef-eth-rpc/template.toml b/blueprints/reef-eth-rpc/template.toml
new file mode 100644
index 000000000..c611c4d1b
--- /dev/null
+++ b/blueprints/reef-eth-rpc/template.toml
@@ -0,0 +1,19 @@
+[variables]
+PORT = "8545"
+NODE_RPC_URL = "ws://ws.reef.host"
+
+[[config.domains]]
+serviceName = "reef-eth-rpc"
+port = 8545
+host = "${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-eth-rpc"
+port = 8545
+host = "eth.${domain}"
+path = "/"
+
+[config.env]
+PORT = "8545"
+NODE_RPC_URL = "ws://ws.reef.host"
diff --git a/blueprints/reef-faucet/docker-compose.yml b/blueprints/reef-faucet/docker-compose.yml
new file mode 100644
index 000000000..fb12738d9
--- /dev/null
+++ b/blueprints/reef-faucet/docker-compose.yml
@@ -0,0 +1,49 @@
+version: "3.8"
+
+services:
+ reef-faucet:
+ image: anukulpandey/reef-chain-node:latest
+ restart: unless-stopped
+ working_dir: /workspace
+
+ expose:
+ - "${PORT}"
+
+ environment:
+ SERVICE_NAME: reef-faucet
+ WS_ENDPOINT: ${WS_ENDPOINT}
+ EVM_RPC_URL: ${EVM_RPC_URL}
+ FAUCET_SEED: ${FAUCET_SEED}
+ PORT: ${PORT}
+ DEFAULT_AMOUNT: ${DEFAULT_AMOUNT}
+ MAX_AMOUNT: ${MAX_AMOUNT}
+
+ volumes:
+ - ./output:/output
+
+ entrypoint: /bin/sh
+ command: -c "
+ set -e &&
+
+ echo '📦 Installing dependencies...' &&
+ apt-get update &&
+ apt-get install -y wget curl ca-certificates gnupg &&
+ mkdir -p /etc/apt/keyrings &&
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg &&
+ echo 'deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main' > /etc/apt/sources.list.d/nodesource.list &&
+ apt-get update &&
+ apt-get install -y nodejs &&
+
+ echo '📥 Downloading faucet sources...' &&
+ mkdir -p /workspace/faucet &&
+ wget -O /workspace/faucet/package.json \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-templates/d8a82420dad013a3eb7e5d8afc0b9f2700a0045a/blueprints/reef-faucet/faucet/package.json &&
+ wget -O /workspace/faucet/server.js \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-templates/d8a82420dad013a3eb7e5d8afc0b9f2700a0045a/blueprints/reef-faucet/faucet/server.js &&
+
+ echo '📦 Installing faucet npm dependencies...' &&
+ cd /workspace/faucet &&
+ npm install --omit=dev &&
+
+ echo '🚰 Starting Reef faucet on port $PORT' &&
+ exec npm start"
diff --git a/blueprints/reef-faucet/faucet/package.json b/blueprints/reef-faucet/faucet/package.json
new file mode 100644
index 000000000..b2da90457
--- /dev/null
+++ b/blueprints/reef-faucet/faucet/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "reef-faucet",
+ "version": "1.0.0",
+ "private": true,
+ "description": "HTTP faucet for Reef deployments",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "@polkadot/api": "^16.4.8"
+ }
+}
diff --git a/blueprints/reef-faucet/faucet/server.js b/blueprints/reef-faucet/faucet/server.js
new file mode 100644
index 000000000..379ffc0da
--- /dev/null
+++ b/blueprints/reef-faucet/faucet/server.js
@@ -0,0 +1,696 @@
+#!/usr/bin/env node
+
+const http = require("node:http");
+const { ApiPromise, Keyring, WsProvider } = require("@polkadot/api");
+
+const config = {
+ wsEndpoint: process.env.WS_ENDPOINT || "ws://127.0.0.1:9944",
+ evmRpcUrl: process.env.EVM_RPC_URL || "",
+ faucetSeed: process.env.FAUCET_SEED || "",
+ port: Number.parseInt(process.env.PORT || "8080", 10),
+ defaultAmount: String(process.env.DEFAULT_AMOUNT || "2000"),
+ maxAmount: String(process.env.MAX_AMOUNT || "2000"),
+ serviceName: process.env.SERVICE_NAME || "reef-faucet",
+};
+
+if (!config.faucetSeed) {
+ throw new Error("FAUCET_SEED is required");
+}
+
+let contextPromise = null;
+let requestQueue = Promise.resolve();
+
+function normalizeEvmAddress(value) {
+ const normalized = String(value || "").trim();
+ if (!/^0x[0-9a-fA-F]{40}$/.test(normalized)) {
+ throw new Error(`Invalid EVM address: ${value}`);
+ }
+ return normalized;
+}
+
+function parseUnits(value, decimals) {
+ const input = String(value).trim();
+ if (!/^\d+(\.\d+)?$/.test(input)) {
+ throw new Error(`Invalid decimal amount: ${value}`);
+ }
+
+ const [whole, fraction = ""] = input.split(".");
+ if (fraction.length > decimals) {
+ throw new Error(`Too many decimal places in ${value}`);
+ }
+
+ const wholeUnits = BigInt(whole) * 10n ** BigInt(decimals);
+ const fractionUnits = fraction ? BigInt(fraction.padEnd(decimals, "0")) : 0n;
+ return wholeUnits + fractionUnits;
+}
+
+function formatUnits(value, decimals) {
+ const negative = value < 0n;
+ const abs = negative ? -value : value;
+ const base = 10n ** BigInt(decimals);
+ const whole = abs / base;
+ const fraction = abs % base;
+
+ if (fraction === 0n) {
+ return `${negative ? "-" : ""}${whole.toString()}`;
+ }
+
+ return `${negative ? "-" : ""}${whole.toString()}.${fraction
+ .toString()
+ .padStart(decimals, "0")
+ .replace(/0+$/, "")}`;
+}
+
+function fallbackAccountFromEvm(evmAddress) {
+ return `0x${evmAddress.slice(2).toLowerCase()}${"ee".repeat(12)}`;
+}
+
+function decodeDispatchError(api, dispatchError) {
+ if (dispatchError && dispatchError.isModule) {
+ const decoded = api.registry.findMetaError(dispatchError.asModule);
+ return `${decoded.section}.${decoded.name}: ${decoded.docs.join(" ")}`;
+ }
+
+ return dispatchError && dispatchError.toString ? dispatchError.toString() : String(dispatchError);
+}
+
+async function signAndWait(api, tx, signer) {
+ return new Promise(async (resolve, reject) => {
+ let unsubscribe = null;
+
+ try {
+ unsubscribe = await tx.signAndSend(signer, ({ dispatchError, events, status, txHash }) => {
+ if (dispatchError) {
+ if (unsubscribe) {
+ unsubscribe();
+ }
+ reject(new Error(decodeDispatchError(api, dispatchError)));
+ return;
+ }
+
+ if (status.isFinalized) {
+ if (unsubscribe) {
+ unsubscribe();
+ }
+ resolve({
+ txHash: txHash.toHex(),
+ finalizedBlock: status.asFinalized.toHex(),
+ eventCount: events.length,
+ });
+ }
+ });
+ } catch (error) {
+ if (unsubscribe) {
+ unsubscribe();
+ }
+ reject(error);
+ }
+ });
+}
+
+async function maybeClaimDefaultAccount(api, signer) {
+ const storage = api.query && api.query.evmAccounts && api.query.evmAccounts.evmAddresses;
+ const extrinsic = api.tx && api.tx.evmAccounts && api.tx.evmAccounts.claimDefaultAccount;
+
+ if (!storage || !extrinsic) {
+ return null;
+ }
+
+ const current = await storage(signer.address);
+ if (!current.isEmpty) {
+ return current.toString();
+ }
+
+ await signAndWait(api, extrinsic(), signer);
+ return (await storage(signer.address)).toString();
+}
+
+function buildReviveTransfer(api, signerAddress, targetEvm, amountUnits) {
+ const reviveTransfer = api.tx && api.tx.revive && api.tx.revive.transfer;
+ if (!reviveTransfer) {
+ return null;
+ }
+
+ const metaArgs = reviveTransfer.meta.toJSON().args || [];
+ const names = metaArgs.map((arg) => String((arg && arg.name) || "").toLowerCase());
+ const types = metaArgs.map((arg) => JSON.stringify((arg && arg.type) || "").toLowerCase());
+ const haystack = `${names.join(",")}|${types.join(",")}`;
+
+ if (metaArgs.length === 2 && haystack.includes("h160")) {
+ return {
+ strategy: "revive.transfer(H160, Balance)",
+ tx: reviveTransfer(targetEvm, amountUnits),
+ };
+ }
+
+ if (metaArgs.length === 3 && haystack.includes("accountid") && haystack.includes("h160")) {
+ return {
+ strategy: "revive.transfer(AccountId, H160, Balance)",
+ tx: reviveTransfer(signerAddress, targetEvm, amountUnits),
+ };
+ }
+
+ return {
+ strategy: "revive.transfer(fallback call signature)",
+ tx: reviveTransfer(targetEvm, amountUnits),
+ };
+}
+
+async function getContext() {
+ if (!contextPromise) {
+ contextPromise = (async () => {
+ const provider = new WsProvider(config.wsEndpoint);
+ const api = await ApiPromise.create({ provider });
+ const keyring = new Keyring({ type: "sr25519" });
+ const sender = keyring.addFromUri(config.faucetSeed);
+ const nativeDecimals = api.registry.chainDecimals[0] || 12;
+ const tokenSymbol = api.registry.chainTokens[0] || "REEF";
+
+ return { api, nativeDecimals, provider, sender, tokenSymbol };
+ })();
+ }
+
+ return contextPromise;
+}
+
+function jsonResponse(response, statusCode, body) {
+ response.writeHead(statusCode, { "Content-Type": "application/json" });
+ response.end(JSON.stringify(body));
+}
+
+function htmlResponse(response, statusCode, body) {
+ response.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" });
+ response.end(body);
+}
+
+function escapeHtml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function wantsHtml(request) {
+ const accept = String(request.headers.accept || "");
+ return accept.includes("text/html");
+}
+
+function buildInfoPayload() {
+ return {
+ service: config.serviceName,
+ status: "ok",
+ defaults: {
+ defaultAmount: config.defaultAmount,
+ maxAmount: config.maxAmount,
+ },
+ upstream: {
+ wsEndpoint: config.wsEndpoint,
+ evmRpcUrl: config.evmRpcUrl || null,
+ },
+ endpoints: {
+ health: {
+ method: "GET",
+ path: "/health",
+ },
+ drip: {
+ method: "POST",
+ path: "/drip",
+ body: {
+ to: "0x0000000000000000000000000000000000000000",
+ amount: config.defaultAmount,
+ },
+ },
+ },
+ };
+}
+
+function buildFrontendHtml() {
+ const bootPayload = JSON.stringify(buildInfoPayload());
+ const title = escapeHtml(config.serviceName);
+ const defaultAmount = escapeHtml(config.defaultAmount);
+ const maxAmount = escapeHtml(config.maxAmount);
+ const wsEndpoint = escapeHtml(config.wsEndpoint);
+ const evmRpcUrl = escapeHtml(config.evmRpcUrl || "Not configured");
+
+ return `
+
+
+
+
+ ${title}
+
+
+
+
+
+
+
Reef Faucet
+
${title}
+
Send test REEF to any EVM address without reaching for curl first.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+}
+
+async function parseRequestBody(request) {
+ const chunks = [];
+ for await (const chunk of request) {
+ chunks.push(chunk);
+ }
+
+ if (chunks.length === 0) {
+ return {};
+ }
+
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
+}
+
+async function executeDrip(targetEvm, amountText) {
+ const { api, nativeDecimals, sender, tokenSymbol } = await getContext();
+ const amountUnits = parseUnits(amountText, nativeDecimals);
+ const maxUnits = parseUnits(config.maxAmount, nativeDecimals);
+
+ if (amountUnits <= 0n) {
+ throw new Error("Amount must be greater than zero");
+ }
+
+ if (amountUnits > maxUnits) {
+ throw new Error(`Amount exceeds MAX_AMOUNT (${config.maxAmount})`);
+ }
+
+ await maybeClaimDefaultAccount(api, sender);
+
+ const senderBefore = (await api.query.system.account(sender.address)).data.free.toBigInt();
+
+ let strategy = "";
+ let result = null;
+
+ try {
+ const revive = buildReviveTransfer(api, sender.address, targetEvm, amountUnits);
+ if (!revive) {
+ throw new Error("revive.transfer unavailable");
+ }
+ strategy = revive.strategy;
+ result = await signAndWait(api, revive.tx, sender);
+ } catch (error) {
+ strategy = "balances.transferAllowDeath(AccountId32 fallback)";
+ result = await signAndWait(
+ api,
+ api.tx.balances.transferAllowDeath(fallbackAccountFromEvm(targetEvm), amountUnits),
+ sender
+ );
+ }
+
+ const senderAfter = (await api.query.system.account(sender.address)).data.free.toBigInt();
+
+ return {
+ amount: amountText,
+ strategy,
+ tokenSymbol,
+ txHash: result.txHash,
+ finalizedBlock: result.finalizedBlock,
+ eventCount: result.eventCount,
+ sender: sender.address,
+ senderNativeBefore: formatUnits(senderBefore, nativeDecimals),
+ senderNativeAfter: formatUnits(senderAfter, nativeDecimals),
+ };
+}
+
+function enqueue(task) {
+ const current = requestQueue.then(task, task);
+ requestQueue = current.catch(() => undefined);
+ return current;
+}
+
+const server = http.createServer(async (request, response) => {
+ try {
+ if (request.method === "GET" && request.url === "/favicon.ico") {
+ response.writeHead(204);
+ response.end();
+ return;
+ }
+
+ if (request.method === "GET" && request.url === "/") {
+ if (wantsHtml(request)) {
+ htmlResponse(response, 200, buildFrontendHtml());
+ } else {
+ jsonResponse(response, 200, buildInfoPayload());
+ }
+ return;
+ }
+
+ if (request.method === "GET" && request.url === "/health") {
+ jsonResponse(response, 200, { status: "ok" });
+ return;
+ }
+
+ if (request.method === "POST" && request.url === "/drip") {
+ const payload = await parseRequestBody(request);
+ const targetEvm = normalizeEvmAddress(payload.to);
+ const amount = payload.amount ? String(payload.amount) : config.defaultAmount;
+ const result = await enqueue(() => executeDrip(targetEvm, amount));
+ jsonResponse(response, 200, result);
+ return;
+ }
+
+ jsonResponse(response, 404, { error: "Not found" });
+ } catch (error) {
+ jsonResponse(response, 400, { error: error.message });
+ }
+});
+
+server.listen(config.port, "0.0.0.0", () => {
+ console.log(`${config.serviceName} listening on ${config.port}`);
+});
diff --git a/blueprints/reef-faucet/meta.json b/blueprints/reef-faucet/meta.json
new file mode 100644
index 000000000..eeba0bc95
--- /dev/null
+++ b/blueprints/reef-faucet/meta.json
@@ -0,0 +1,18 @@
+{
+ "id": "reef-faucet",
+ "name": "Reef Chain - Faucet",
+ "version": "1",
+ "description": "HTTP faucet for sending native Reef funds to EVM addresses on a custom Reef Chain cluster",
+ "links": {
+ "github": "https://github.com/anukulpandey/dokploy-reef-chain-scripts",
+ "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node",
+ "docs": "https://docs.reef.io/"
+ },
+ "logo": "reef.svg",
+ "tags": [
+ "reef-chain",
+ "faucet",
+ "self-hosted",
+ "evm"
+ ]
+}
diff --git a/blueprints/reef-faucet/reef.svg b/blueprints/reef-faucet/reef.svg
new file mode 100644
index 000000000..598d7930a
--- /dev/null
+++ b/blueprints/reef-faucet/reef.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/blueprints/reef-faucet/template.toml b/blueprints/reef-faucet/template.toml
new file mode 100644
index 000000000..1b27dbee8
--- /dev/null
+++ b/blueprints/reef-faucet/template.toml
@@ -0,0 +1,27 @@
+[variables]
+PORT = "8080"
+WS_ENDPOINT = "ws://ws.reef.host"
+EVM_RPC_URL = "http://eth.reef.host"
+FAUCET_SEED = ""
+DEFAULT_AMOUNT = "2000"
+MAX_AMOUNT = "2000"
+
+[[config.domains]]
+serviceName = "reef-faucet"
+port = 8080
+host = "${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-faucet"
+port = 8080
+host = "faucet.${domain}"
+path = "/"
+
+[config.env]
+PORT = "8080"
+WS_ENDPOINT = "ws://ws.reef.host"
+EVM_RPC_URL = "http://eth.reef.host"
+FAUCET_SEED = ""
+DEFAULT_AMOUNT = "2000"
+MAX_AMOUNT = "2000"
diff --git a/blueprints/reef-keygen/docker-compose.yml b/blueprints/reef-keygen/docker-compose.yml
new file mode 100644
index 000000000..b6d332ce2
--- /dev/null
+++ b/blueprints/reef-keygen/docker-compose.yml
@@ -0,0 +1,41 @@
+version: "3.8"
+
+services:
+ reef-keygen:
+ image: anukulpandey/reef-chain-node:latest
+ restart: unless-stopped
+ working_dir: /workspace
+ volumes:
+ - ./output:/output
+ expose:
+ - "${PORT}"
+ entrypoint: /bin/sh
+ command: -c "
+ set -e &&
+
+ echo '📦 Installing python3...' &&
+ apt-get update &&
+ apt-get install -y python3 &&
+
+ mkdir -p /output &&
+ chmod -R 777 /output &&
+
+ echo '=====================================' &&
+ echo '🔑 Generating Validator Keys' &&
+ echo '=====================================' &&
+
+ reef-node key generate --scheme Sr25519 --output-type json > /output/validator1.json &&
+ reef-node key generate-node-key --chain local > /output/v1_node_key.txt &&
+
+ reef-node key generate --scheme Sr25519 --output-type json > /output/validator2.json &&
+ reef-node key generate-node-key --chain local > /output/v2_node_key.txt &&
+
+ reef-node key generate --scheme Sr25519 --output-type json > /output/validator3.json &&
+ reef-node key generate-node-key --chain local > /output/v3_node_key.txt &&
+
+ echo '✅ Keys generated:' &&
+ ls -lah /output &&
+
+ echo '🌐 Starting HTTP server on port ${PORT}' &&
+ cd /output &&
+ python3 -m http.server ${PORT} --bind 0.0.0.0"
diff --git a/blueprints/reef-keygen/meta.json b/blueprints/reef-keygen/meta.json
new file mode 100644
index 000000000..9f1d7a4bd
--- /dev/null
+++ b/blueprints/reef-keygen/meta.json
@@ -0,0 +1,18 @@
+{
+ "id": "reef-keygen",
+ "name": "Reef Chain - Keys Generator",
+ "version": "1",
+ "description": "Generates 3 Validators keys for a custom Reef Chain cluster",
+ "links": {
+ "github": "https://github.com/reef-chain/chain-upgrade",
+ "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node",
+ "docs": "https://docs.reef.io/"
+ },
+ "logo": "reef.svg",
+ "tags": [
+ "reef-chain",
+ "keys generator",
+ "self-hosted",
+ "keys"
+ ]
+}
diff --git a/blueprints/reef-keygen/reef.svg b/blueprints/reef-keygen/reef.svg
new file mode 100644
index 000000000..598d7930a
--- /dev/null
+++ b/blueprints/reef-keygen/reef.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/blueprints/reef-keygen/template.toml b/blueprints/reef-keygen/template.toml
new file mode 100644
index 000000000..b8cde3e76
--- /dev/null
+++ b/blueprints/reef-keygen/template.toml
@@ -0,0 +1,11 @@
+[variables]
+PORT = "48765"
+
+[[config.domains]]
+serviceName = "reef-keygen"
+port = 48765
+host = "${domain}"
+path = "/"
+
+[config.env]
+PORT = "48765"
\ No newline at end of file
diff --git a/blueprints/reef-rpc/docker-compose.yml b/blueprints/reef-rpc/docker-compose.yml
new file mode 100644
index 000000000..2ea58ecb3
--- /dev/null
+++ b/blueprints/reef-rpc/docker-compose.yml
@@ -0,0 +1,66 @@
+version: "3.8"
+
+services:
+ reef-rpc:
+ image: anukulpandey/reef-chain-node:latest
+ restart: unless-stopped
+ working_dir: /workspace
+
+ expose:
+ - "${P2P_PORT}"
+ - "${RPC_PORT}"
+
+ environment:
+ SPEC_URL: ${SPEC_URL}
+ BOOTNODE_NODE_KEY: ${BOOTNODE_NODE_KEY}
+ BOOTNODE_IP: ${BOOTNODE_IP}
+ P2P_PORT: ${P2P_PORT}
+ RPC_PORT: ${RPC_PORT}
+
+ volumes:
+ - ./output:/output
+
+ entrypoint: /bin/sh
+ command:
+ - -c
+ - |
+ set -e
+
+ echo '📦 Installing dependencies...'
+ apt-get update
+ apt-get install -y wget ca-certificates
+
+ RPC_BASE_PATH=/tmp/rpc-node
+ NODE_KEY_FILE=/tmp/rpc_node_key.txt
+ CHAIN_FILE=/tmp/local-chain-spec-raw.json
+ PEER_ID_FILE=/tmp/bootnode_peer_id.txt
+
+ echo '📥 Downloading chain spec and bootnode peer ID...'
+ wget -q -O "$$CHAIN_FILE" "$$SPEC_URL"
+ wget -q -O "$$PEER_ID_FILE" "$$BOOTNODE_NODE_KEY"
+
+ BOOTNODE_PEER_ID=$$(tr -d '\r\n' < "$$PEER_ID_FILE")
+ if [ -z "$$BOOTNODE_PEER_ID" ]; then
+ echo 'Bootnode peer ID file is empty' >&2
+ exit 1
+ fi
+
+ BOOTNODE_MULTIADDR="/ip4/$$BOOTNODE_IP/tcp/30335/p2p/$$BOOTNODE_PEER_ID"
+
+ mkdir -p "$$RPC_BASE_PATH"
+ chmod 700 "$$RPC_BASE_PATH"
+
+ reef-node key generate-node-key --chain local > "$$NODE_KEY_FILE"
+ chmod 600 "$$NODE_KEY_FILE"
+
+ echo '🚀 Starting RPC node...'
+ exec reef-node \
+ --base-path "$$RPC_BASE_PATH" \
+ --chain "$$CHAIN_FILE" \
+ --port "$$P2P_PORT" \
+ --node-key-file "$$NODE_KEY_FILE" \
+ --bootnodes "$$BOOTNODE_MULTIADDR" \
+ --experimental-rpc-endpoint "listen-addr=0.0.0.0:$$RPC_PORT,cors=all,methods=unsafe,max-connections=10000" \
+ --pruning archive \
+ --name rpc-node \
+ --no-telemetry
diff --git a/blueprints/reef-rpc/meta.json b/blueprints/reef-rpc/meta.json
new file mode 100644
index 000000000..4d07db746
--- /dev/null
+++ b/blueprints/reef-rpc/meta.json
@@ -0,0 +1,18 @@
+{
+ "id": "reef-rpc",
+ "name": "Reef Chain - RPC Node",
+ "version": "1",
+ "description": "Runs RPC which connects to bootnode for a custom Reef Chain cluster",
+ "links": {
+ "github": "https://github.com/reef-chain/chain-upgrade",
+ "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node",
+ "docs": "https://docs.reef.io/"
+ },
+ "logo": "reef.svg",
+ "tags": [
+ "reef-chain",
+ "validator",
+ "self-hosted",
+ "reef-validator"
+ ]
+}
diff --git a/blueprints/reef-rpc/reef.svg b/blueprints/reef-rpc/reef.svg
new file mode 100644
index 000000000..598d7930a
--- /dev/null
+++ b/blueprints/reef-rpc/reef.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/blueprints/reef-rpc/template.toml b/blueprints/reef-rpc/template.toml
new file mode 100644
index 000000000..c146b6a27
--- /dev/null
+++ b/blueprints/reef-rpc/template.toml
@@ -0,0 +1,29 @@
+[variables]
+SPEC_URL = "http://spec.reef.host/local-chain-spec-raw.json"
+BOOTNODE_NODE_KEY="http://spec.reef.host/bootnode_node_key.txt"
+
+BOOTNODE_IP="72.60.35.83"
+
+P2P_PORT="30339"
+RPC_PORT="9994"
+
+[[config.domains]]
+serviceName = "reef-rpc"
+port = 9994
+host = "${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-rpc"
+port = 9994
+host = "ws.${domain}"
+path = "/"
+
+[config.env]
+SPEC_URL = "http://spec.reef.host/local-chain-spec-raw.json"
+BOOTNODE_NODE_KEY="http://spec.reef.host/bootnode_node_key.txt"
+
+BOOTNODE_IP="72.60.35.83"
+
+P2P_PORT="30339"
+RPC_PORT="9994"
diff --git a/blueprints/reef-spec-gen/docker-compose.yml b/blueprints/reef-spec-gen/docker-compose.yml
new file mode 100644
index 000000000..8f7baa274
--- /dev/null
+++ b/blueprints/reef-spec-gen/docker-compose.yml
@@ -0,0 +1,38 @@
+version: "3.8"
+
+services:
+ reef-spec-gen:
+ image: anukulpandey/reef-chain-node:latest
+ restart: unless-stopped
+ working_dir: /workspace
+
+ volumes:
+ - ./output:/output
+
+ expose:
+ - "8000"
+
+ entrypoint: /bin/sh
+ command: -c "
+ set -e &&
+ apt-get update &&
+ apt-get install -y python3 python3-pip wget ca-certificates &&
+
+ echo '📥 Downloading API + scripts...' &&
+ mkdir -p /workspace/scripts &&
+
+ wget -O /workspace/scripts/update-spec.py \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-reef-chain-scripts/52c3e30d06bba10936aea2db9740eafd76b3ac40/spec-generator/scripts/update-spec.py &&
+
+ wget -O /workspace/scripts/update-spec.sh \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-reef-chain-scripts/52c3e30d06bba10936aea2db9740eafd76b3ac40/spec-generator/scripts/update-spec.sh &&
+
+ wget -O /workspace/api.py \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-reef-chain-scripts/52c3e30d06bba10936aea2db9740eafd76b3ac40/spec-generator/api/api.py &&
+
+ chmod +x /workspace/scripts/update-spec.sh &&
+
+ pip3 install flask==3.1.0 &&
+
+ echo '🚀 Starting Spec Generator API on port 8000...' &&
+ python3 /workspace/api.py"
diff --git a/blueprints/reef-spec-gen/meta.json b/blueprints/reef-spec-gen/meta.json
new file mode 100644
index 000000000..d6eed9e09
--- /dev/null
+++ b/blueprints/reef-spec-gen/meta.json
@@ -0,0 +1,18 @@
+{
+ "id": "reef-spec-gen",
+ "name": "Reef Chain - Custom Spec Generator",
+ "version": "1",
+ "description": "Generates spec file for a custom Reef Chain cluster",
+ "links": {
+ "github": "https://github.com/reef-chain/chain-upgrade",
+ "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node",
+ "docs": "https://docs.reef.io/"
+ },
+ "logo": "reef.svg",
+ "tags": [
+ "reef-chain",
+ "rpc",
+ "self-hosted",
+ "evm"
+ ]
+}
diff --git a/blueprints/reef-spec-gen/reef.svg b/blueprints/reef-spec-gen/reef.svg
new file mode 100644
index 000000000..598d7930a
--- /dev/null
+++ b/blueprints/reef-spec-gen/reef.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/blueprints/reef-spec-gen/template.toml b/blueprints/reef-spec-gen/template.toml
new file mode 100644
index 000000000..c0412908f
--- /dev/null
+++ b/blueprints/reef-spec-gen/template.toml
@@ -0,0 +1,15 @@
+[variables]
+
+[[config.domains]]
+serviceName = "reef-spec-gen"
+port = 8000
+host = "${domain}"
+path = "/"
+
+[[config.domains]]
+serviceName = "reef-spec-gen"
+port = 8000
+host = "spec.${domain}"
+path = "/"
+
+[config.env]
diff --git a/blueprints/reef-validator/docker-compose.yml b/blueprints/reef-validator/docker-compose.yml
new file mode 100644
index 000000000..1caaa4784
--- /dev/null
+++ b/blueprints/reef-validator/docker-compose.yml
@@ -0,0 +1,41 @@
+version: "3.8"
+
+services:
+ reef-validator:
+ image: anukulpandey/reef-chain-node:latest
+ restart: unless-stopped
+ working_dir: /workspace
+
+ expose:
+ - "${P2P_PORT}"
+
+ environment:
+ SPEC_URL: ${SPEC_URL}
+ BOOTNODE_NODE_KEY: ${BOOTNODE_NODE_KEY}
+ BOOTNODE_IP: ${BOOTNODE_IP}
+ V1SEED: ${V1SEED}
+ V1ADDR: ${V1ADDR}
+ P2P_PORT: ${P2P_PORT}
+ VALIDATOR_NAME: ${VALIDATOR_NAME}
+
+ volumes:
+ - ./output:/output
+
+ entrypoint: /bin/sh
+ command: -c "
+ set -e &&
+
+ echo '📦 Installing dependencies...' &&
+ apt-get update &&
+ apt-get install -y wget ca-certificates &&
+
+ echo '📥 Downloading start-validator.sh...' &&
+ mkdir -p /workspace/validator/scripts &&
+ wget -O /workspace/validator/scripts/start-validator.sh \
+ https://raw.githubusercontent.com/anukulpandey/dokploy-reef-chain-scripts/52c3e30d06bba10936aea2db9740eafd76b3ac40/validator/scripts/start-validator.sh &&
+
+ echo '🔧 Making script executable...' &&
+ chmod +x /workspace/validator/scripts/start-validator.sh &&
+
+ echo '🚀 Starting validator $VALIDATOR_NAME' &&
+ exec /workspace/validator/scripts/start-validator.sh"
diff --git a/blueprints/reef-validator/meta.json b/blueprints/reef-validator/meta.json
new file mode 100644
index 000000000..45f8d9a6e
--- /dev/null
+++ b/blueprints/reef-validator/meta.json
@@ -0,0 +1,18 @@
+{
+ "id": "reef-validator",
+ "name": "Reef Chain - Validator",
+ "version": "1",
+ "description": "Runs Validator which connects to bootnode for a custom Reef Chain cluster",
+ "links": {
+ "github": "https://github.com/reef-chain/chain-upgrade",
+ "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node",
+ "docs": "https://docs.reef.io/"
+ },
+ "logo": "reef.svg",
+ "tags": [
+ "reef-chain",
+ "validator",
+ "self-hosted",
+ "reef-validator"
+ ]
+}
diff --git a/blueprints/reef-validator/reef.svg b/blueprints/reef-validator/reef.svg
new file mode 100644
index 000000000..598d7930a
--- /dev/null
+++ b/blueprints/reef-validator/reef.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/blueprints/reef-validator/template.toml b/blueprints/reef-validator/template.toml
new file mode 100644
index 000000000..c5e06fcf9
--- /dev/null
+++ b/blueprints/reef-validator/template.toml
@@ -0,0 +1,25 @@
+[variables]
+V1ADDR = "5GH14oJ4A3VDLoC6nXutsTwS4dMwebDipNdSugD5zFnnWKp5"
+V1SEED = ""
+
+SPEC_URL = "http://spec.reef.host/local-chain-spec-raw.json"
+BOOTNODE_NODE_KEY="http://spec.reef.host/bootnode_node_key.txt"
+
+BOOTNODE_IP="72.60.35.83"
+
+VALIDATOR_NAME="Validator-1"
+
+P2P_PORT="30333"
+
+[config.env]
+V1ADDR = "5GH14oJ4A3VDLoC6nXutsTwS4dMwebDipNdSugD5zFnnWKp5"
+V1SEED = ""
+
+SPEC_URL = "http://spec.reef.host/local-chain-spec-raw.json"
+BOOTNODE_NODE_KEY="http://spec.reef.host/bootnode_node_key.txt"
+
+BOOTNODE_IP="72.60.35.83"
+
+VALIDATOR_NAME="Validator-1"
+
+P2P_PORT="30333"