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, // 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 () => { const probe = await spacedSub.exec( [ 'bash', '-c', `/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-cookie ${dataDir}/${SPACED_CHAIN}/.cookie getserverinfo`, ], {}, ) if (probe.exitCode !== 0) { return { result: 'starting', message: i18n( 'spaced is querying Bitcoin and indexing — this can take a while on first run.', ), } } const stdout = (probe.stdout ?? '').toString() let progress = 0 try { const parsed = JSON.parse(stdout) as { ready?: boolean progress?: number chain?: { blocks?: number; headers?: number } } if (parsed.ready === true) { return { result: 'success', message: i18n('spaced is fully synced.'), } } progress = Math.floor((parsed.progress ?? 0) * 100) } catch { // fall through to loading } return { result: 'loading', message: i18n('spaced is indexing. Progress: ${pct}%', { pct: String(progress), }), } }, gracePeriod: 300_000, }, requires: ['spaced'], }) })