Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ Shared infra (one set per machine, kept up across project switches):

Per-project state:

- `vechain-dev.config.mjs` at the consumer project's root (declares `project`, `profiles`, `deploy`, `dev`, optional `overlay`)
- `vechain-dev.config.mjs` at the consumer project's root (declares `project`, optional `services`, `profiles`, `deploy`, `dev`, optional `overlay`)
- After deploy, the consumer calls `registerAddresses(...)` which writes `~/.vechain-dev/config/<project>.json`
- The CLI merges all registered projects' addresses + profiles and writes env files into `~/.vechain-dev/generated/` which the indexer and explorer containers env-file-mount

The point: each consumer deploys its own contracts to thor-solo and registers their addresses; the indexer/explorer see the **union** of every project's addresses + Spring profiles.

Per-consumer opt-out: `services` (default `['thor', 'indexer', 'explorer']`) lets a consumer disable parts of the stack they don't need — e.g. a frontend or backend that talks to thor directly via `THOR_NODE_URL` can declare `services: ['thor']` and the CLI will skip mongo/indexer/explorer entirely. `'thor'` is required; `deploy` + `profiles` are only required when `'indexer'` or `'explorer'` is in the list (since they're the only services that consume the merged address book).

## Repository layout

```
Expand All @@ -45,7 +47,7 @@ genesis/solo.default.json Default genesis used by thor-solo + indexer
Two things consumers depend on. Any change here is a breaking change for every downstream project.

1. **`registerAddresses({ project, profiles, addresses })`** — exported from package main. Signature defined in `lib/register.d.ts`. Validates and atomically writes `~/.vechain-dev/config/<project>.json`.
2. **`vechain-dev` CLI** — commands `up`, `down`, `reset`, `sync`, `status`. The `up` flow is load-config → ensure network → start thor+mongo → run consumer `deploy` → merge address book → recreate indexer+explorer → exec consumer `dev` (the dev process becomes the foreground; signals are forwarded).
2. **`vechain-dev` CLI** — commands `up`, `down`, `reset`, `sync`, `status`. The `up` flow is load-config → ensure network → start thor+mongo → run consumer `deploy` → merge address book → recreate indexer+explorer → exec consumer `dev` (the dev process becomes the foreground; signals are forwarded). When `services` opts out of `indexer`/`explorer`, the address-book merge and the deploy-then-verify cycle are skipped (the deploy command is still run if declared, but its registration isn't required). Thor-only consumers exit immediately after `ensureThor()`.
Comment on lines 49 to +50

## Conventions to respect

Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default {
profiles: ['safe', 'accounts', 'transactions'],
deploy: 'yarn contracts:deploy:solo',
// optional:
// services: ['thor', 'indexer', 'explorer'], // default — see "Selecting services"
// overlay: 'docker/overlay.yaml',
}
```
Expand All @@ -43,6 +44,33 @@ await registerAddresses({

This writes `~/.vechain-dev/config/my-project.json`.

### Selecting services

`services` controls which parts of the stack `vechain-dev up` brings up. The
default is the full set:

```js
services: ['thor', 'indexer', 'explorer']
```

- `'thor'` — required; the thor-solo node on `:8669`.
- `'indexer'` — mongo + vechain-indexer + vechain-indexer-api on `:8089`.
- `'explorer'` — block-explorer on `:8088`.

A client app that only needs the chain can opt out of the rest:

```js
export default {
project: 'my-client-app',
services: ['thor'],
}
```

When neither `'indexer'` nor `'explorer'` is selected, `deploy` and `profiles`
become optional (there's no address book to write). `vechain-dev down` and
`vechain-dev clean` still tear down the full stack so leftover containers from a
previous config are cleaned up.

## Commands

Project lifecycle — requires `vechain-dev.config.mjs`:
Expand Down
97 changes: 73 additions & 24 deletions bin/vechain-dev.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process'
import { rm } from 'node:fs/promises'
import { loadConfig } from '../lib/config.mjs'
import { loadConfig, needsAddressBook } from '../lib/config.mjs'
import {
composeDown,
composeLogs,
Expand Down Expand Up @@ -31,6 +31,28 @@ const INFRA_SERVICES = [
const INDEXER_SERVICES = ['mongo-node1', 'mongo-setup', 'vechain-indexer', 'vechain-indexer-api']
const INDEXER_LOG_SERVICES = ['vechain-indexer', 'vechain-indexer-api']

const SERVICE_FILE = {
thor: 'base.yaml',
indexer: 'indexer.yaml',
explorer: 'explorer.yaml',
}

const SERVICE_CONTAINERS = {
indexer: INDEXER_SERVICES,
explorer: ['block-explorer'],
}

function planFor(services) {
const files = services.map((s) => SERVICE_FILE[s])
const infraServices = services.flatMap((s) => SERVICE_CONTAINERS[s] ?? [])
return {
files,
infraServices,
wantsIndexer: services.includes('indexer'),
wantsExplorer: services.includes('explorer'),
}
}

async function shellExec(cmd, { exec = false } = {}) {
return new Promise((resolve, reject) => {
const shell = process.env.SHELL || '/bin/bash'
Expand Down Expand Up @@ -110,50 +132,72 @@ async function verifyDeployed(cfg) {
)
}

function printEndpoints() {
info('shared stack ready')
info(' thor-solo → http://localhost:8669')
info(' indexer-api → http://localhost:8089')
info(' block-explorer → http://localhost:8088')
async function waitForInfra({ indexer = true, explorer = true } = {}) {
if (indexer) {
step('waiting for mongo + indexer-api to be ready')
await waitHealthy('mongo-node1')
await waitForIndexerApi()
}
if (explorer) await waitHealthy('block-explorer')
}

async function waitForInfra({ explorer = true } = {}) {
step('waiting for mongo + indexer-api to be ready')
await waitHealthy('mongo-node1')
await waitForIndexerApi()
if (explorer) await waitHealthy('block-explorer')
function printEndpointsFor({ wantsIndexer, wantsExplorer }) {
info('shared stack ready')
info(' thor-solo → http://localhost:8669')
if (wantsIndexer) info(' indexer-api → http://localhost:8089')
if (wantsExplorer) info(' block-explorer → http://localhost:8088')
}

async function up({ force = false, skip = false } = {}) {
const cfg = await loadConfig()
const plan = planFor(cfg.services)
step(`project: ${cfg.project}`)
step(`services: ${cfg.services.join(', ')}`)

await ensureThor()

step('clearing ephemeral services (mongo + indexer + explorer)')
await composeRm(SHARED_FILES, INFRA_SERVICES)
if (plan.infraServices.length === 0) {
printEndpointsFor(plan)
return
}

await runDeployIfNeeded(cfg, { force, skip })
step('clearing ephemeral services')
await composeRm(plan.files, plan.infraServices)

await mergeAddressBook(cfg)
step('starting mongo + indexer + explorer (fresh state)')
await composeUp(SHARED_FILES, INFRA_SERVICES)
await waitForInfra()
if (needsAddressBook(cfg.services)) {
await runDeployIfNeeded(cfg, { force, skip })
await mergeAddressBook(cfg)
} else if (!skip && cfg.deploy) {
step(`running deploy: ${cfg.deploy}`)
await shellExec(cfg.deploy)
}

printEndpoints()
step('starting infra (fresh state)')
await composeUp(plan.files, plan.infraServices)
await waitForInfra({ indexer: plan.wantsIndexer, explorer: plan.wantsExplorer })

printEndpointsFor(plan)
}

async function deploy() {
const cfg = await loadConfig()
if (!cfg.deploy) {
throw new Error(`no 'deploy' command in vechain-dev.config.mjs — nothing to run`)
}
const plan = planFor(cfg.services)
step(`project: ${cfg.project}`)
await waitForThor()
step(`running deploy: ${cfg.deploy}`)
await shellExec(cfg.deploy)
await verifyDeployed(cfg)
await mergeAddressBook(cfg)
step('recreating indexer')
await composeRecreate(SHARED_FILES, INDEXER_LOG_SERVICES)
await waitForInfra({ explorer: false })
if (needsAddressBook(cfg.services)) {
await verifyDeployed(cfg)
await mergeAddressBook(cfg)
}
if (plan.wantsIndexer) {
step('recreating indexer')
await composeRecreate(plan.files, INDEXER_LOG_SERVICES)
await waitForInfra({ indexer: true, explorer: false })
}
info('deploy complete')
}

Expand Down Expand Up @@ -263,6 +307,7 @@ Project lifecycle (requires vechain-dev.config.mjs):
up [--redeploy] [--skip-deploy]
Ensure shared infra and run deploy if needed. Exits when infra is ready —
start your frontend in a separate terminal (e.g. yarn frontend:dev).
Only the services listed in cfg.services are started (default: all).
--redeploy force the deploy command even if contracts are already on-chain
--skip-deploy bring infra up without running the deploy command

Expand All @@ -271,6 +316,10 @@ Project lifecycle (requires vechain-dev.config.mjs):
Always runs — no on-chain check. Use when you've changed contracts but
the rest of the stack is already up.

Config services (vechain-dev.config.mjs):
services: ['thor', 'indexer', 'explorer'] // default — opt out by omitting entries
// 'thor' is required; 'deploy' + 'profiles' are required when 'indexer' or 'explorer' is enabled

down
Stop the full stack (thor state preserved; mongo is ephemeral).

Expand Down
2 changes: 1 addition & 1 deletion compose/base.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
thor-solo:
image: ${VECHAIN_DEV_THOR_IMAGE:-ghcr.io/vechain/thor:latest}
image: ${VECHAIN_DEV_THOR_IMAGE:-vechain/thor:v2.4.3}
container_name: thor-solo
Comment on lines 1 to 4
hostname: thor-solo
command:
Expand Down
2 changes: 1 addition & 1 deletion compose/explorer.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
block-explorer:
image: ${VECHAIN_DEV_EXPLORER_IMAGE:-ghcr.io/vechain/block-explorer:2.41.0}
image: ${VECHAIN_DEV_EXPLORER_IMAGE:-vechain/block-explorer:2.42}
container_name: block-explorer
Comment on lines 1 to 4
hostname: block-explorer
env_file:
Expand Down
33 changes: 30 additions & 3 deletions lib/config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { access } from 'node:fs/promises'
import { pathToFileURL } from 'node:url'
import { join, resolve } from 'node:path'
import { resolve } from 'node:path'

const CONFIG_FILE = 'vechain-dev.config.mjs'

export const ALL_SERVICES = ['thor', 'indexer', 'explorer']

export function needsAddressBook(services) {
return services.includes('indexer') || services.includes('explorer')
}

export async function loadConfig(cwd = process.cwd()) {
const path = resolve(cwd, CONFIG_FILE)
try {
Expand All @@ -23,6 +29,27 @@ export async function loadConfig(cwd = process.cwd()) {
const cfg = mod.default
if (!cfg || typeof cfg !== 'object') throw new Error(`${CONFIG_FILE} must default-export an object`)
if (!cfg.project) throw new Error(`${CONFIG_FILE}: 'project' required`)
if (!cfg.deploy) throw new Error(`${CONFIG_FILE}: 'deploy' command required`)
return cfg

const services = cfg.services ?? ALL_SERVICES
if (!Array.isArray(services) || services.length === 0) {
throw new Error(`${CONFIG_FILE}: 'services' must be a non-empty array`)
}
if (!services.includes('thor')) {
throw new Error(`${CONFIG_FILE}: 'services' must include 'thor'`)
}
for (const s of services) {
if (!ALL_SERVICES.includes(s)) {
throw new Error(
`${CONFIG_FILE}: unknown service '${s}' — must be one of ${ALL_SERVICES.join(', ')}`,
)
}
}

if (needsAddressBook(services) && !cfg.deploy) {
throw new Error(
`${CONFIG_FILE}: 'deploy' command required when 'services' includes 'indexer' or 'explorer'`,
)
}

return { ...cfg, services: [...new Set(services)] }
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vechain/dev-stack",
"version": "0.2.0",
"version": "0.3.0",
"description": "Shared local dev environment for VeChain projects: thor-solo + indexer + block-explorer, with per-project address registration.",
"license": "MIT",
"author": "VeChain",
Expand Down