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
47 changes: 35 additions & 12 deletions bin/vechain-dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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}`)

Expand All @@ -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)`)
Expand Down Expand Up @@ -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 <command>
console.log(`Usage: vechain-dev <command> [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)
}
Expand Down
4 changes: 2 additions & 2 deletions compose/indexer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions lib/check.mjs
Original file line number Diff line number Diff line change
@@ -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/<project>.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'))
Comment on lines +13 to +16
} 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 }
}
25 changes: 25 additions & 0 deletions lib/register.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,28 @@ export interface RegisterAddressesInput {
* @returns the absolute path of the file written
*/
export function registerAddresses(input: RegisterAddressesInput): Promise<string>

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<ProjectDeploymentStatus>
2 changes: 2 additions & 0 deletions lib/register.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}$/

Expand Down
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.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",
Expand Down