734 lines
25 KiB
TypeScript
734 lines
25 KiB
TypeScript
import { storeJson } from './fileModels/storeJson'
|
|
import { i18n } from './i18n'
|
|
import { sdk } from './sdk'
|
|
import {
|
|
APP_USER,
|
|
BITCOIND_RPC_HOSTNAME,
|
|
BITCOIND_RPC_PORT,
|
|
CERTRELAY_ANCHOR_REFRESH,
|
|
CERTRELAY_BIN,
|
|
CERTRELAY_BIND,
|
|
CERTRELAY_CHAIN,
|
|
CERTRELAY_DATA_DIR,
|
|
CERTRELAY_DEFAULT_BOOTSTRAP,
|
|
CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE,
|
|
CERTRELAY_DEFAULT_SELF_URL,
|
|
CERTRELAY_DIR,
|
|
CERTRELAY_FABRIC_BIN,
|
|
CERTRELAY_FABRIC_DEST,
|
|
CERTRELAY_PORT,
|
|
CERTRELAY_REMOTE_IP_HEADER,
|
|
dataDir,
|
|
pgDataDir,
|
|
POSTGRES_PORT,
|
|
SPACED_CHAIN,
|
|
spacedRpcPort,
|
|
SUBSPACES_DATA_DIR,
|
|
SUBSPACES_DIR,
|
|
SUBSPACES_PROVER_BIN,
|
|
SUBSPACES_PROVER_DIR,
|
|
SUBSPACES_PROVER_PORT,
|
|
SUBSPACES_REGISTRY_BIN,
|
|
SUBSPACES_REGISTRY_DIR,
|
|
SUBSPACES_REGISTRY_PORT,
|
|
SUBSPACES_SUBS_BIN,
|
|
SUBSPACES_UI_PORT,
|
|
SUBSPACES_WALLET,
|
|
uiPort,
|
|
} from './utils'
|
|
|
|
export const main = sdk.setupMain(async ({ effects }) => {
|
|
console.info(i18n('Starting Spaces!'))
|
|
|
|
const store = await storeJson.read().const(effects)
|
|
if (
|
|
!store?.password ||
|
|
!store?.btcAuth ||
|
|
!store?.dbAuth ||
|
|
!store?.spacedAuth
|
|
) {
|
|
// taskSetPassword + taskBtcAuth + taskSeedDb + taskSeedSpacedAuth all seed
|
|
// these in init; if they aren't populated yet, init hasn't finished — let
|
|
// StartOS restart us.
|
|
throw new Error('Spaces store.json is not yet populated.')
|
|
}
|
|
|
|
const { password: APP_PASSWORD, btcAuth, dbAuth, spacedAuth } = store
|
|
const enableExplorer = store.enableExplorer === true
|
|
const enableSubspaces = store.enableSubspaces === true
|
|
// subs-prover is independently gated and defaults OFF (null/undefined => false).
|
|
const enableSubsProver = store.enableSubsProver === true
|
|
|
|
const spacedEnv = {
|
|
SPACED_CHAIN,
|
|
SPACED_DATA_DIR: dataDir,
|
|
// Listen on all interfaces so the spaced RPC can be exposed externally via
|
|
// the "Spaces API" StartOS interface. Internal clients (subs, certrelay,
|
|
// indexer, space-cli, health checks) still connect over loopback below.
|
|
SPACED_RPC_BIND: '0.0.0.0',
|
|
SPACED_RPC_PORT: String(spacedRpcPort),
|
|
SPACED_RPC_URL: `http://127.0.0.1:${spacedRpcPort}`,
|
|
SPACED_BLOCK_INDEX: 'true',
|
|
SPACED_BITCOIN_RPC_URL: `http://${BITCOIND_RPC_HOSTNAME}:${BITCOIND_RPC_PORT}`,
|
|
SPACED_BITCOIN_RPC_USER: btcAuth.username,
|
|
SPACED_BITCOIN_RPC_PASSWORD: btcAuth.password,
|
|
SPACED_RPC_USER: spacedAuth.username,
|
|
SPACED_RPC_PASSWORD: spacedAuth.password,
|
|
// legacy aliases for `bitcoin-cli` / shell helpers that read these names
|
|
BTC_RPC_HOST: BITCOIND_RPC_HOSTNAME,
|
|
BTC_RPC_PORT: String(BITCOIND_RPC_PORT),
|
|
BTC_RPC_USER: btcAuth.username,
|
|
BTC_RPC_PASSWORD: btcAuth.password,
|
|
APP_USER,
|
|
APP_PASSWORD,
|
|
DB_URL: `postgres://${dbAuth.username}:${dbAuth.password}@127.0.0.1:${POSTGRES_PORT}/${dbAuth.database}`,
|
|
}
|
|
|
|
// Certrelay config — user-tunable bits live in store.json (set via the
|
|
// "Configure Certrelay" action); the rest are fixed package defaults.
|
|
const certrelaySelfUrl =
|
|
store.certrelaySelfUrl ?? CERTRELAY_DEFAULT_SELF_URL
|
|
const certrelayBootstrap =
|
|
store.certrelayBootstrap ?? CERTRELAY_DEFAULT_BOOTSTRAP
|
|
const certrelayHealthcheckHandle =
|
|
store.certrelayHealthcheckHandle ?? CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE
|
|
|
|
const mounts = sdk.Mounts.of().mountVolume({
|
|
volumeId: 'main',
|
|
subpath: null,
|
|
mountpoint: dataDir,
|
|
readonly: false,
|
|
})
|
|
|
|
const spacedSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'spaces' },
|
|
mounts,
|
|
'spaced-sub',
|
|
)
|
|
|
|
const termSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'spaces' },
|
|
mounts,
|
|
'terminal-sub',
|
|
)
|
|
|
|
const postgresSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'postgres' },
|
|
mounts,
|
|
'postgres-sub',
|
|
)
|
|
|
|
const postgresEnv = {
|
|
POSTGRES_USER: dbAuth.username,
|
|
POSTGRES_PASSWORD: dbAuth.password,
|
|
POSTGRES_DB: dbAuth.database,
|
|
PGDATA: pgDataDir,
|
|
}
|
|
|
|
// TODO(prebuilt-indexer / prebuilt-explorer): the Go indexer and SvelteKit
|
|
// explorer used to be built from source in dedicated builder images
|
|
// (golang:1.23-alpine, node:20-alpine). Those images + their build chains
|
|
// were removed to shrink the .s9pk and cut first-enable latency. When the
|
|
// prebuilt images exist, re-add their SubContainers + env here, e.g.:
|
|
// const indexerSub = await sdk.SubContainer.of(
|
|
// effects, { imageId: '<prebuilt-indexer-image>' }, mounts, 'indexer-sub')
|
|
// const explorerSub = await sdk.SubContainer.of(
|
|
// effects, { imageId: '<prebuilt-explorer-image>' }, mounts, 'explorer-sub')
|
|
// const indexerEnv = { POSTGRES_URI, BITCOIN_NODE_URI/USER/PASSWORD,
|
|
// SPACES_NODE_URI, RPC_USER/PASSWORD, ACTIVATION_BLOCK_HEIGHT,
|
|
// FAST_SYNC_BLOCK_HEIGHT, UPDATE_DB_INTERVAL, MEMPOOL_CHUNK_SIZE }
|
|
// const explorerEnv = { DB_URL (?sslmode=disable), PUBLIC_BTC_NETWORK, PORT }
|
|
// Also add the image ids to startos/manifest/index.ts and re-add the daemon
|
|
// definitions in the explorer branch below. See git history for prior shapes.
|
|
|
|
const subspacesSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'subspaces' },
|
|
mounts,
|
|
'subspaces-sub',
|
|
)
|
|
|
|
// subs reads all of these from the environment (see subs/src/main.rs #[arg(env=...)]).
|
|
const subsEnv = {
|
|
SUBS_DATA_DIR: SUBSPACES_DATA_DIR,
|
|
SUBS_PORT: String(SUBSPACES_UI_PORT),
|
|
SUBS_WALLET: SUBSPACES_WALLET,
|
|
SUBS_SPACED_RPC_URL: `http://127.0.0.1:${spacedRpcPort}`,
|
|
SUBS_SPACED_RPC_USER: spacedAuth.username,
|
|
SUBS_SPACED_RPC_PASSWORD: spacedAuth.password,
|
|
SUBS_PROVER_ENDPOINT: `http://127.0.0.1:${SUBSPACES_PROVER_PORT}`,
|
|
SUBS_REGISTRY_ENDPOINT: `http://127.0.0.1:${SUBSPACES_REGISTRY_PORT}`,
|
|
HOME: SUBSPACES_DATA_DIR,
|
|
RUST_LOG: 'subs=info,subs_prover=info,registry_server=info',
|
|
}
|
|
|
|
const proverEnv = {
|
|
SUBS_PROVER_SERVER: '1',
|
|
SUBS_PROVER_PORT: String(SUBSPACES_PROVER_PORT),
|
|
HOME: SUBSPACES_PROVER_DIR,
|
|
RUST_LOG: 'subs_prover=info',
|
|
}
|
|
|
|
const registryEnv = {
|
|
REGISTRY_SERVER_PORT: String(SUBSPACES_REGISTRY_PORT),
|
|
HOME: SUBSPACES_REGISTRY_DIR,
|
|
RUST_LOG: 'registry_server=info',
|
|
}
|
|
|
|
const certrelaySub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'certrelay' },
|
|
mounts,
|
|
'certrelay-sub',
|
|
)
|
|
|
|
const certrelayEnv = {
|
|
CERTRELAY_CHAIN,
|
|
CERTRELAY_DATA_DIR,
|
|
CERTRELAY_BIND,
|
|
CERTRELAY_PORT: String(CERTRELAY_PORT),
|
|
CERTRELAY_SELF_URL: certrelaySelfUrl,
|
|
// Credentials come from the local spaced server (store.spacedAuth).
|
|
CERTRELAY_SPACED_RPC_URL: `http://${spacedAuth.username}:${spacedAuth.password}@127.0.0.1:${spacedRpcPort}`,
|
|
CERTRELAY_REMOTE_IP_HEADER,
|
|
CERTRELAY_BOOTSTRAP: certrelayBootstrap ? 'true' : 'false',
|
|
CERTRELAY_ANCHOR_REFRESH,
|
|
CERTRELAY_HEALTHCHECK_HANDLE: certrelayHealthcheckHandle,
|
|
HOME: CERTRELAY_DATA_DIR,
|
|
RUST_LOG: 'info',
|
|
}
|
|
|
|
const bashrc = [
|
|
'export PATH=/root/.cargo/bin:/data/bin:/usr/local/bin:/usr/bin:/bin',
|
|
"export PS1='spaces:\\w$ '",
|
|
`alias spaces='space-cli --chain ${SPACED_CHAIN} --rpc-user "$SPACED_RPC_USER" --rpc-password "$SPACED_RPC_PASSWORD" '`,
|
|
'cat <<EOF',
|
|
'',
|
|
'┌─ Spaces ─────────────────────────────────────────────────┐',
|
|
'│ spaced is managed by StartOS — do not run it manually. │',
|
|
'│ Use the `spaces` alias to call space-cli, e.g. │',
|
|
'│ spaces getserverinfo │',
|
|
'│ `fabric` resolves handles against the local certrelay. │',
|
|
'│ Docs: https://docs.spacesprotocol.org/ │',
|
|
'└──────────────────────────────────────────────────────────┘',
|
|
'',
|
|
'EOF',
|
|
].join('\n')
|
|
|
|
// Appends the always-on certrelay chain (setup oneshot + certrelay daemon) to
|
|
// any existing chain. certrelay serves cryptographic proofs binding handles to
|
|
// owner keys anchored to Bitcoin. The `fabric` CLI is copied onto /data/bin so
|
|
// the gotty terminal (which has /data/bin on PATH) can resolve handles.
|
|
const withCertrelay = (chain: any): any =>
|
|
chain
|
|
.addOneshot('certrelay-setup', {
|
|
subcontainer: certrelaySub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`set -eu; \
|
|
mkdir -p ${CERTRELAY_DATA_DIR} /data/bin; \
|
|
echo "certrelay-setup: installing fabric CLI to ${CERTRELAY_FABRIC_DEST}..."; \
|
|
cp -f ${CERTRELAY_FABRIC_BIN} ${CERTRELAY_FABRIC_DEST}; \
|
|
chmod +x ${CERTRELAY_FABRIC_DEST}; \
|
|
echo "certrelay-setup: done."`,
|
|
],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('certrelay', {
|
|
subcontainer: certrelaySub,
|
|
exec: {
|
|
command: [CERTRELAY_BIN],
|
|
env: certrelayEnv,
|
|
// /data is root-owned; the image's USER certrelay can't write it.
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Certrelay'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, CERTRELAY_PORT, {
|
|
successMessage: i18n('certrelay is ready'),
|
|
errorMessage: i18n('certrelay is not ready'),
|
|
}),
|
|
// Match the image's 120s start-period: anchor-refresh/checkpoint work
|
|
// on first start can delay the listener.
|
|
gracePeriod: 120_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['certrelay-setup', 'spaced'],
|
|
})
|
|
|
|
// Appends the Subspaces chain (fetch → build → wallet ensure → subs-prover →
|
|
// subs daemon) to any existing chain. `as any` because the chain's TS type
|
|
// depends on prior IDs and we can't easily express the union here.
|
|
const withSubspaces = (chain: any): any => {
|
|
let c = chain
|
|
.addOneshot('subspaces-dirs', {
|
|
subcontainer: subspacesSub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`set -eu; \
|
|
mkdir -p ${SUBSPACES_DATA_DIR} ${SUBSPACES_PROVER_DIR} ${SUBSPACES_REGISTRY_DIR}; \
|
|
echo "subspaces-dirs: ensured data/prover/registry dirs under ${SUBSPACES_DIR}."`,
|
|
],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('subs-registry', {
|
|
subcontainer: subspacesSub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`cd ${SUBSPACES_REGISTRY_DIR} && exec ${SUBSPACES_REGISTRY_BIN} --port ${SUBSPACES_REGISTRY_PORT}`,
|
|
],
|
|
env: registryEnv,
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Subspaces Registry'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, SUBSPACES_REGISTRY_PORT, {
|
|
successMessage: i18n('registry-server is ready'),
|
|
errorMessage: i18n('registry-server is not ready'),
|
|
}),
|
|
gracePeriod: 60_000,
|
|
},
|
|
requires: ['subspaces-dirs'],
|
|
})
|
|
.addDaemon('subs', {
|
|
subcontainer: subspacesSub,
|
|
exec: {
|
|
command: [SUBSPACES_SUBS_BIN],
|
|
env: subsEnv,
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Subspaces Web UI'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, SUBSPACES_UI_PORT, {
|
|
successMessage: i18n('subspaces UI is ready'),
|
|
errorMessage: i18n('subspaces UI is not ready'),
|
|
}),
|
|
gracePeriod: 60_000,
|
|
},
|
|
// Intentionally NOT contingent on subs-prover: the RISC Zero prover can
|
|
// take a while to become ready, and subs only needs it on-demand (proof
|
|
// generation), not at startup. subs reaches the prover via
|
|
// SUBS_PROVER_ENDPOINT once the prover is up.
|
|
requires: ['subspaces-dirs', 'spaced', 'subs-registry'],
|
|
})
|
|
|
|
// subs-prover is opt-in and DISABLED BY DEFAULT (toggle via the "Enable
|
|
// Subspaces Prover" action). Its interface (8888) stays registered either
|
|
// way (see interfaces.ts), but the daemon only starts when enabled. It's
|
|
// added LAST and depends on every other subspaces daemon; it runs lengthy
|
|
// timing tests on boot and NOTHING depends on it, so its slow startup never
|
|
// blocks anything else.
|
|
if (enableSubsProver) {
|
|
c = c.addDaemon('subs-prover', {
|
|
subcontainer: subspacesSub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`cd ${SUBSPACES_PROVER_DIR} && exec ${SUBSPACES_PROVER_BIN} --server --server-port ${SUBSPACES_PROVER_PORT}`,
|
|
],
|
|
env: proverEnv,
|
|
// Run as root: the horologger/subs image declares USER subs, but our
|
|
// /data volume + subdirs are root-owned, so the non-root user can't
|
|
// traverse/write them. Root sidesteps the ownership mismatch.
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Subspaces Prover'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, SUBSPACES_PROVER_PORT, {
|
|
successMessage: i18n('subs-prover is ready'),
|
|
errorMessage: i18n('subs-prover is not ready'),
|
|
}),
|
|
gracePeriod: 60_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['subspaces-dirs', 'subs-registry', 'subs'],
|
|
})
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
if (!enableExplorer) {
|
|
// Spaces-only mode: spaced + gotty terminal. No PostgreSQL, no indexer.
|
|
// User can flip the toggle via the Enable Embedded Explorer action;
|
|
// store.json.enableExplorer is read via .const() so the merge triggers
|
|
// an automatic service restart and the full chain takes over.
|
|
let chain: any = sdk.Daemons.of(effects)
|
|
.addOneshot('bashrc', {
|
|
subcontainer: termSub,
|
|
exec: {
|
|
command: ['bash', '-c', `cat > /root/.bashrc <<'SPACES_BASHRC_EOF'
|
|
${bashrc}
|
|
SPACES_BASHRC_EOF`],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('spaced', {
|
|
subcontainer: spacedSub,
|
|
exec: {
|
|
command: ['/root/.cargo/bin/spaced'],
|
|
env: spacedEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Spaced RPC'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, spacedRpcPort, {
|
|
successMessage: i18n('spaced RPC is ready'),
|
|
errorMessage: i18n('spaced RPC is not ready'),
|
|
}),
|
|
gracePeriod: 120_000,
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('web-terminal', {
|
|
subcontainer: termSub,
|
|
exec: {
|
|
command: [
|
|
'gotty',
|
|
'--port',
|
|
String(uiPort),
|
|
'-c',
|
|
`${APP_USER}:${APP_PASSWORD}`,
|
|
'--permit-write',
|
|
'--reconnect',
|
|
'/bin/bash',
|
|
],
|
|
env: spacedEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Web Interface'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
|
successMessage: i18n('The web terminal is ready'),
|
|
errorMessage: i18n('The web terminal is not ready'),
|
|
}),
|
|
},
|
|
requires: ['bashrc'],
|
|
})
|
|
.addHealthCheck('sync', {
|
|
ready: {
|
|
display: i18n('Spaced Sync'),
|
|
fn: async () => {
|
|
try {
|
|
const probe = await spacedSub.exec(
|
|
[
|
|
'/root/.cargo/bin/space-cli',
|
|
'--chain',
|
|
SPACED_CHAIN,
|
|
'--rpc-user',
|
|
spacedAuth.username,
|
|
'--rpc-password',
|
|
spacedAuth.password,
|
|
'--output-format',
|
|
'json',
|
|
'getserverinfo',
|
|
],
|
|
{},
|
|
)
|
|
const stderr = (probe.stderr ?? '').toString().trim()
|
|
const stdoutText = (probe.stdout ?? '').toString()
|
|
const stdoutTrimmed = stdoutText.trim()
|
|
if (probe.exitCode !== 0) {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n('space-cli exited ${code}: ${error}', {
|
|
code: String(probe.exitCode),
|
|
error: (stderr || stdoutTrimmed || '<no output>').slice(0, 200),
|
|
}),
|
|
}
|
|
}
|
|
let parsed: {
|
|
ready?: boolean
|
|
progress?: number
|
|
chain?: { blocks?: number; headers?: number }
|
|
}
|
|
try {
|
|
parsed = JSON.parse(stdoutText)
|
|
} catch {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n(
|
|
'getserverinfo non-JSON. stdout=${stdout} stderr=${stderr}',
|
|
{
|
|
stdout: (stdoutTrimmed || '<empty>').slice(0, 160),
|
|
stderr: (stderr || '<empty>').slice(0, 160),
|
|
},
|
|
),
|
|
}
|
|
}
|
|
const progress = Math.min(
|
|
100,
|
|
Math.max(0, Math.round((parsed.progress ?? 0) * 100)),
|
|
)
|
|
const blocks = parsed.chain?.blocks ?? 0
|
|
const headers = parsed.chain?.headers ?? 0
|
|
if (parsed.ready === true && progress >= 100) {
|
|
return {
|
|
result: 'success',
|
|
message: i18n(
|
|
'spaced is fully synced (blocks ${blocks} / headers ${headers}).',
|
|
{ blocks: String(blocks), headers: String(headers) },
|
|
),
|
|
}
|
|
}
|
|
return {
|
|
result: 'loading',
|
|
message: i18n(
|
|
'spaced is indexing: ${pct}% (blocks ${blocks} / headers ${headers}).',
|
|
{
|
|
pct: String(progress),
|
|
blocks: String(blocks),
|
|
headers: String(headers),
|
|
},
|
|
),
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n('Spaced Sync health check crashed: ${error}', {
|
|
error: (e as Error)?.message ?? String(e),
|
|
}),
|
|
}
|
|
}
|
|
},
|
|
gracePeriod: 30_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['spaced'],
|
|
})
|
|
chain = withCertrelay(chain)
|
|
if (enableSubspaces) chain = withSubspaces(chain)
|
|
return chain
|
|
}
|
|
|
|
// enableExplorer = true: full chain — spaced + terminal + embedded
|
|
// PostgreSQL + Go indexer + indexer-sync health check.
|
|
let chain: any = sdk.Daemons.of(effects)
|
|
.addOneshot('bashrc', {
|
|
subcontainer: termSub,
|
|
exec: {
|
|
command: ['bash', '-c', `cat > /root/.bashrc <<'SPACES_BASHRC_EOF'
|
|
${bashrc}
|
|
SPACES_BASHRC_EOF`],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addOneshot('postgres-chown', {
|
|
subcontainer: postgresSub,
|
|
exec: {
|
|
command: [
|
|
'bash',
|
|
'-c',
|
|
`mkdir -p ${pgDataDir} && chown -R postgres:postgres ${pgDataDir} && chmod 700 ${pgDataDir}`,
|
|
],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('postgres', {
|
|
subcontainer: postgresSub,
|
|
exec: {
|
|
command: [
|
|
'docker-entrypoint.sh',
|
|
'postgres',
|
|
'-c',
|
|
'listen_addresses=127.0.0.1',
|
|
],
|
|
env: postgresEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Database'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, POSTGRES_PORT, {
|
|
successMessage: i18n('postgres is ready'),
|
|
errorMessage: i18n('postgres is not ready'),
|
|
}),
|
|
gracePeriod: 60_000,
|
|
},
|
|
requires: ['postgres-chown'],
|
|
})
|
|
.addDaemon('spaced', {
|
|
subcontainer: spacedSub,
|
|
exec: {
|
|
command: ['/root/.cargo/bin/spaced'],
|
|
env: spacedEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Spaced RPC'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, spacedRpcPort, {
|
|
successMessage: i18n('spaced RPC is ready'),
|
|
errorMessage: i18n('spaced RPC is not ready'),
|
|
}),
|
|
gracePeriod: 120_000,
|
|
},
|
|
requires: [],
|
|
})
|
|
// TODO(prebuilt-indexer): the Go indexer build-from-source chain
|
|
// (indexer-fetch + indexer-build + indexer-cleanup-legacy +
|
|
// indexer-migrate + `indexer` daemon) was removed to shrink the .s9pk
|
|
// and cut first-enable latency. Re-add an `indexer` daemon here that runs
|
|
// the prebuilt `sync` binary from the future indexer image (image id +
|
|
// binary path TBD) against PostgreSQL + spaced. Env it needs is the old
|
|
// indexerEnv (POSTGRES_URI, BITCOIN_NODE_*, SPACES_NODE_URI, RPC_USER/
|
|
// PASSWORD, ACTIVATION_BLOCK_HEIGHT, FAST_SYNC_BLOCK_HEIGHT,
|
|
// UPDATE_DB_INTERVAL, MEMPOOL_CHUNK_SIZE). A goose migration step must
|
|
// run before it. See git history (build-from-source) for the prior shape.
|
|
//
|
|
// TODO(prebuilt-explorer): the SvelteKit explorer build chain
|
|
// (explorer-fetch + explorer-install + `explorer-ui` daemon) was removed
|
|
// for the same reason. Re-add an `explorer-ui` daemon that runs the
|
|
// prebuilt explorer server from the provided explorer image (image id +
|
|
// command TBD) on port 3000, reading DB_URL + PUBLIC_BTC_NETWORK.
|
|
.addDaemon('web-terminal', {
|
|
subcontainer: termSub,
|
|
exec: {
|
|
command: [
|
|
'gotty',
|
|
'--port',
|
|
String(uiPort),
|
|
'-c',
|
|
`${APP_USER}:${APP_PASSWORD}`,
|
|
'--permit-write',
|
|
'--reconnect',
|
|
'/bin/bash',
|
|
],
|
|
env: spacedEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Web Interface'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
|
successMessage: i18n('The web terminal is ready'),
|
|
errorMessage: i18n('The web terminal is not ready'),
|
|
}),
|
|
},
|
|
requires: ['bashrc'],
|
|
})
|
|
.addHealthCheck('sync', {
|
|
ready: {
|
|
display: i18n('Spaced Sync'),
|
|
fn: async () => {
|
|
try {
|
|
const probe = await spacedSub.exec(
|
|
[
|
|
'/root/.cargo/bin/space-cli',
|
|
'--chain',
|
|
SPACED_CHAIN,
|
|
'--rpc-user',
|
|
spacedAuth.username,
|
|
'--rpc-password',
|
|
spacedAuth.password,
|
|
'--output-format',
|
|
'json',
|
|
'getserverinfo',
|
|
],
|
|
{},
|
|
)
|
|
|
|
const stderr = (probe.stderr ?? '').toString().trim()
|
|
const stdoutText = (probe.stdout ?? '').toString()
|
|
const stdoutTrimmed = stdoutText.trim()
|
|
|
|
if (probe.exitCode !== 0) {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n('space-cli exited ${code}: ${error}', {
|
|
code: String(probe.exitCode),
|
|
error: (stderr || stdoutTrimmed || '<no output>').slice(0, 200),
|
|
}),
|
|
}
|
|
}
|
|
|
|
let parsed: {
|
|
ready?: boolean
|
|
progress?: number
|
|
chain?: { blocks?: number; headers?: number }
|
|
}
|
|
try {
|
|
parsed = JSON.parse(stdoutText)
|
|
} catch {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n(
|
|
'getserverinfo non-JSON. stdout=${stdout} stderr=${stderr}',
|
|
{
|
|
stdout: (stdoutTrimmed || '<empty>').slice(0, 160),
|
|
stderr: (stderr || '<empty>').slice(0, 160),
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
const progress = Math.min(
|
|
100,
|
|
Math.max(0, Math.round((parsed.progress ?? 0) * 100)),
|
|
)
|
|
const blocks = parsed.chain?.blocks ?? 0
|
|
const headers = parsed.chain?.headers ?? 0
|
|
|
|
if (parsed.ready === true && progress >= 100) {
|
|
return {
|
|
result: 'success',
|
|
message: i18n(
|
|
'spaced is fully synced (blocks ${blocks} / headers ${headers}).',
|
|
{ blocks: String(blocks), headers: String(headers) },
|
|
),
|
|
}
|
|
}
|
|
|
|
return {
|
|
result: 'loading',
|
|
message: i18n(
|
|
'spaced is indexing: ${pct}% (blocks ${blocks} / headers ${headers}).',
|
|
{
|
|
pct: String(progress),
|
|
blocks: String(blocks),
|
|
headers: String(headers),
|
|
},
|
|
),
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n(
|
|
'Spaced Sync health check crashed: ${error}',
|
|
{ error: (e as Error)?.message ?? String(e) },
|
|
),
|
|
}
|
|
}
|
|
},
|
|
gracePeriod: 30_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['spaced'],
|
|
})
|
|
// TODO(prebuilt-indexer): re-add the `indexer-sync` standalone health
|
|
// check once the prebuilt indexer daemon is wired back in. It queried
|
|
// MAX(blocks.height) (non-orphan) via psql and compared against spaced's
|
|
// tip from `space-cli getserverinfo`. See git history for the prior body.
|
|
chain = withCertrelay(chain)
|
|
if (enableSubspaces) chain = withSubspaces(chain)
|
|
return chain
|
|
})
|