From 0f020360b5b2235787dc75a746f71a987f5a701a Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Wed, 20 May 2026 15:41:44 +0200 Subject: [PATCH] feat: gate deploy on registration + on-chain code; bump indexer to 6.31.5 vechain-dev up now skips the project's deploy command when the project is already registered in ~/.vechain-dev/config/.json AND every registered address still has code on thor-solo. Pass --redeploy to force. Also bumps vechain-indexer and api images to 6.31.5. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/vechain-dev.mjs | 47 +++++++++++++++++++++++++++++++++----------- compose/indexer.yaml | 4 ++-- lib/check.mjs | 32 ++++++++++++++++++++++++++++++ lib/register.d.ts | 25 +++++++++++++++++++++++ lib/register.mjs | 2 ++ package.json | 2 +- 6 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 lib/check.mjs diff --git a/bin/vechain-dev.mjs b/bin/vechain-dev.mjs index cc3a6ac..728cc7b 100755 --- a/bin/vechain-dev.mjs +++ b/bin/vechain-dev.mjs @@ -13,6 +13,7 @@ import { } from '../lib/docker.mjs' import { readAll, writeEnv } from '../lib/addressBook.mjs' import { waitForThor } from '../lib/thor.mjs' +import { isProjectDeployed } from '../lib/check.mjs' import { detail, error, info, step, warn } from '../lib/log.mjs' import { home } from '../lib/paths.mjs' @@ -32,7 +33,7 @@ async function shellExec(cmd, { exec = false } = {}) { }) } -async function up() { +async function up({ force = false } = {}) { const cfg = await loadConfig() step(`project: ${cfg.project}`) @@ -50,13 +51,25 @@ async function up() { ['block-explorer', 'vechain-indexer-api', 'vechain-indexer', 'mongo-setup', 'mongo-node1'], ) - step(`running deploy: ${cfg.deploy}`) - await shellExec(cfg.deploy) + const status = force ? { deployed: false, reason: 'forced' } : await isProjectDeployed(cfg.project) + if (status.deployed) { + step(`contracts already deployed for '${cfg.project}' — skipping deploy (pass --redeploy to force)`) + } else { + if (status.reason === 'missing-code') { + detail(`registered address ${status.address} has no code on-chain — redeploying`) + } else if (status.reason === 'not-registered') { + detail(`no registration found for '${cfg.project}' — deploying`) + } else if (status.reason === 'forced') { + detail('--redeploy: forcing deploy') + } + step(`running deploy: ${cfg.deploy}`) + await shellExec(cfg.deploy) + } step('merging address book') const projects = await readAll() if (!projects.find((p) => p.project === cfg.project)) { - warn(`deploy did not register addresses for project '${cfg.project}' — did you call registerAddresses?`) + warn(`no registration for project '${cfg.project}' — did the deploy step call registerAddresses?`) } const summary = await writeEnv(projects) detail(`${projects.length} project(s), ${summary.profileCount} profile(s), ${summary.addressCount} address var(s)`) @@ -132,18 +145,28 @@ async function status() { } } -const commands = { up, down, reset, sync, status } -const cmd = process.argv[2] +const argv = process.argv.slice(2) +const cmd = argv[0] +const flags = new Set(argv.slice(1).filter((a) => a.startsWith('--'))) + +const commands = { + up: () => up({ force: flags.has('--redeploy') }), + down, + reset, + sync, + status, +} if (!cmd || cmd === '--help' || cmd === '-h') { - console.log(`Usage: vechain-dev + console.log(`Usage: vechain-dev [flags] Commands: - up ensure shared infra, run deploy, sync env, restart indexer/explorer, exec dev - down stop the stack (thor state preserved; mongo is ephemeral) - reset tear down all shared infra, volumes, and ~/.vechain-dev/ - sync re-merge address book and recreate indexer/explorer - status show registered projects and service health + up [--redeploy] ensure shared infra, run deploy if needed, sync env, restart indexer/explorer, exec dev + --redeploy forces the project's deploy command even if the contracts are already on-chain + down stop the stack (thor state preserved; mongo is ephemeral) + reset tear down all shared infra, volumes, and ~/.vechain-dev/ + sync re-merge address book and recreate indexer/explorer + status show registered projects and service health `) process.exit(cmd ? 0 : 1) } diff --git a/compose/indexer.yaml b/compose/indexer.yaml index 28da80a..7d8b135 100644 --- a/compose/indexer.yaml +++ b/compose/indexer.yaml @@ -43,7 +43,7 @@ services: - vechain-thor vechain-indexer: - image: ${VECHAIN_DEV_INDEXER_IMAGE:-ghcr.io/vechain/vechain-indexer/indexer:6.31.4} + image: ${VECHAIN_DEV_INDEXER_IMAGE:-ghcr.io/vechain/vechain-indexer/indexer:6.31.5} container_name: vechain-indexer depends_on: mongo-setup: @@ -61,7 +61,7 @@ services: - vechain-thor vechain-indexer-api: - image: ${VECHAIN_DEV_INDEXER_API_IMAGE:-ghcr.io/vechain/vechain-indexer/api:6.31.4} + image: ${VECHAIN_DEV_INDEXER_API_IMAGE:-ghcr.io/vechain/vechain-indexer/api:6.31.5} container_name: vechain-indexer-api depends_on: mongo-setup: diff --git a/lib/check.mjs b/lib/check.mjs new file mode 100644 index 0000000..be83676 --- /dev/null +++ b/lib/check.mjs @@ -0,0 +1,32 @@ +import { readFile } from 'node:fs/promises' +import { projectConfigFile } from './paths.mjs' +import { hasCode } from './thor.mjs' + +const DEFAULT_RPC = 'http://localhost:8669' + +// Check whether a project's contracts are still deployed on the current chain. +// Truth source is ~/.vechain-dev/config/.json — wiped by +// `vechain-dev reset`, so its absence means the chain has been reset and the +// project must redeploy. Per-project files (e.g. b3tr's packages/config/local.ts) +// are deliberately NOT consulted: they live outside the shared state and go +// stale across resets. +export async function isProjectDeployed(project, { rpcUrl = DEFAULT_RPC } = {}) { + let registration + try { + registration = JSON.parse(await readFile(projectConfigFile(project), 'utf8')) + } catch (err) { + if (err.code === 'ENOENT') return { deployed: false, reason: 'not-registered' } + throw err + } + const addresses = Object.values(registration.addresses || {}) + if (!addresses.length) return { deployed: false, reason: 'not-registered' } + // De-dupe: registrations commonly expose the same address under multiple + // env-var aliases (e.g. STARGATE_CONTRACT + STARGATE_DELEGATION_CONTRACT). + const unique = [...new Set(addresses.map((a) => a.toLowerCase()))] + for (const addr of unique) { + if (!(await hasCode(addr, rpcUrl))) { + return { deployed: false, reason: 'missing-code', address: addr } + } + } + return { deployed: true } +} diff --git a/lib/register.d.ts b/lib/register.d.ts index ff80b3c..06a07e9 100644 --- a/lib/register.d.ts +++ b/lib/register.d.ts @@ -15,3 +15,28 @@ export interface RegisterAddressesInput { * @returns the absolute path of the file written */ export function registerAddresses(input: RegisterAddressesInput): Promise + +export interface IsProjectDeployedOptions { + /** Thor RPC URL. Defaults to http://localhost:8669. */ + rpcUrl?: string +} + +export type ProjectDeploymentStatus = + | { deployed: true } + | { deployed: false; reason: 'not-registered' } + | { deployed: false; reason: 'missing-code'; address: string } + +/** + * Checks whether a previously-registered project still has all of its + * contracts on-chain. Returns `{ deployed: true }` only if every address in + * the project's registration file has code on the current chain. + * + * Use this from a project's deploy script (or rely on `vechain-dev up`, + * which calls this for you) instead of doing a per-project `getCode` check + * against an in-repo config file — those files are not part of the shared + * state and go stale across `vechain-dev reset`. + */ +export function isProjectDeployed( + project: string, + options?: IsProjectDeployedOptions, +): Promise diff --git a/lib/register.mjs b/lib/register.mjs index cd879fb..68ae063 100644 --- a/lib/register.mjs +++ b/lib/register.mjs @@ -2,6 +2,8 @@ import { mkdir, rename, writeFile } from 'node:fs/promises' import { dirname } from 'node:path' import { projectConfigDir, projectConfigFile } from './paths.mjs' +export { isProjectDeployed } from './check.mjs' + const PROJECT_NAME = /^[a-z][a-z0-9-]*$/ const ADDRESS = /^0x[0-9a-fA-F]{40}$/ diff --git a/package.json b/package.json index 8075e27..9a84a14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vechain/dev-stack", - "version": "0.1.10", + "version": "0.1.11", "description": "Shared local dev environment for VeChain projects: thor-solo + indexer + block-explorer, with per-project address registration.", "license": "MIT", "author": "VeChain",