import { storeJson } from './fileModels/storeJson' import { i18n } from './i18n' import { sdk } from './sdk' import { APP_USER, BITCOIND_RPC_HOSTNAME, BITCOIND_RPC_PORT, dataDir, SPACED_CHAIN, spacedRpcPort, 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) { // taskSetPassword + taskBtcAuth both 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 } = store const spacedEnv = { SPACED_CHAIN, SPACED_DATA_DIR: dataDir, SPACED_RPC_BIND: '127.0.0.1', 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_COOKIE: `/data/mainnet/.cookie`, // 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, } 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 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-cookie ${dataDir}/${SPACED_CHAIN}/.cookie '`, '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-cookie', `${dataDir}/${SPACED_CHAIN}/.cookie`, '--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 || '').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 || '').slice(0, 160), stderr: (stderr || '').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'], }) })