This commit is contained in:
@@ -92,9 +92,12 @@ mainnet-only.
|
|||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `reset-password` | Reset Web UI Password | Enabled | Any | Regenerates the web-UI password and restarts the terminal daemon |
|
| `reset-password` | Reset Web UI Password | Enabled | Any | Regenerates the web-UI password and restarts the terminal daemon |
|
||||||
| `show-credentials` | Show Web UI Credentials | Hidden | Any | Surfaces the current `admin` username + masked password (launched by the first-install task) |
|
| `show-credentials` | Show Web UI Credentials | Hidden | Any | Surfaces the current `admin` username + masked password (launched by the first-install task) |
|
||||||
|
| `show-password` | Show Web UI Password | Enabled | Any | Same as `show-credentials` but visible in the actions list, for routine re-display of the current admin credentials |
|
||||||
| `set-bitcoin-rpc` | Set up Bitcoin RPC | Enabled | Any | Re-invokes bitcoind's `generate-rpc-dependent` with the stored credentials. Safe to call repeatedly. |
|
| `set-bitcoin-rpc` | Set up Bitcoin RPC | Enabled | Any | Re-invokes bitcoind's `generate-rpc-dependent` with the stored credentials. Safe to call repeatedly. |
|
||||||
| `sync-status` | Sync Status | Enabled | Only running | Runs `space-cli getserverinfo` inside the daemon container and returns the JSON output |
|
| `sync-status` | Sync Status | Enabled | Only running | Runs `space-cli getserverinfo` inside the daemon container and returns the JSON output |
|
||||||
| `reset-spaced-state` | Reset Spaced State | Enabled | Any | Deletes `/data/mainnet/` so spaced resyncs its index from spaces' anchor. Preserves `store.json` (passwords + RPC credentials). Use when spaced crash-loops on a stale or corrupt index. |
|
| `reset-spaced-state` | Reset Spaced State | Enabled | Any | Deletes `/data/mainnet/` so spaced resyncs its index from spaces' anchor. Preserves `store.json` (passwords + RPC credentials). Use when spaced crash-loops on a stale or corrupt index. |
|
||||||
|
| `export-wallet` | Export Wallet | Enabled | Only running | Runs `space-cli exportwallet /data/mainnet/wallets_backup/default.json` and surfaces the resulting JSON as a masked/copyable result. The file is also persisted inside the volume at that path. |
|
||||||
|
| `import-wallet` | Import Wallet | Enabled | Only running | Accepts a pasted JSON payload (textarea), writes it to `/data/mainnet/wallets_backup/default.json` (rotating the existing file to `.bakNNN`), rotates `/data/mainnet/wallets/default` to `.bakNNN`, then runs `space-cli importwallet` + `loadwallet`. |
|
||||||
|
|
||||||
## Backups and Restore
|
## Backups and Restore
|
||||||
|
|
||||||
@@ -177,6 +180,7 @@ startos_managed_env_vars:
|
|||||||
- SPACED_BITCOIN_RPC_URL
|
- SPACED_BITCOIN_RPC_URL
|
||||||
- SPACED_BITCOIN_RPC_USER
|
- SPACED_BITCOIN_RPC_USER
|
||||||
- SPACED_BITCOIN_RPC_PASSWORD
|
- SPACED_BITCOIN_RPC_PASSWORD
|
||||||
|
- SPACED_RPC_COOKIE
|
||||||
- BTC_RPC_HOST
|
- BTC_RPC_HOST
|
||||||
- BTC_RPC_PORT
|
- BTC_RPC_PORT
|
||||||
- BTC_RPC_USER
|
- BTC_RPC_USER
|
||||||
@@ -186,7 +190,10 @@ startos_managed_env_vars:
|
|||||||
actions:
|
actions:
|
||||||
- reset-password
|
- reset-password
|
||||||
- show-credentials
|
- show-credentials
|
||||||
|
- show-password
|
||||||
- set-bitcoin-rpc
|
- set-bitcoin-rpc
|
||||||
- sync-status
|
- sync-status
|
||||||
- reset-spaced-state
|
- reset-spaced-state
|
||||||
|
- export-wallet
|
||||||
|
- import-wallet
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ spaces walletbalance default
|
|||||||
- **Reset Spaced State** — wipes `/data/mainnet/` so spaced resyncs its index
|
- **Reset Spaced State** — wipes `/data/mainnet/` so spaced resyncs its index
|
||||||
from spaces' anchor. Use this if spaced is crash-looping on a stale or
|
from spaces' anchor. Use this if spaced is crash-looping on a stale or
|
||||||
corrupt index. `store.json` (passwords + RPC credentials) is preserved.
|
corrupt index. `store.json` (passwords + RPC credentials) is preserved.
|
||||||
|
- **Export Wallet** — runs `space-cli exportwallet` on the `default` wallet,
|
||||||
|
writes the JSON to `/data/mainnet/wallets_backup/default.json` inside the
|
||||||
|
container, and surfaces the same JSON as a copyable result so you can save
|
||||||
|
it locally. Requires the service to be running.
|
||||||
|
- **Import Wallet** — paste a previously-exported `default.json` payload to
|
||||||
|
restore it. The action rotates any existing
|
||||||
|
`/data/mainnet/wallets_backup/default.json` and
|
||||||
|
`/data/mainnet/wallets/default` to `.bakNNN` suffixes so no prior state is
|
||||||
|
lost, then calls `space-cli importwallet` and `space-cli loadwallet`.
|
||||||
|
Requires the service to be running.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { dataDir, SPACED_CHAIN } from '../utils'
|
||||||
|
|
||||||
|
const BACKUP_DIR = `${dataDir}/${SPACED_CHAIN}/wallets_backup`
|
||||||
|
const BACKUP_PATH = `${BACKUP_DIR}/default.json`
|
||||||
|
|
||||||
|
export const exportWallet = sdk.Action.withoutInput(
|
||||||
|
// id
|
||||||
|
'export-wallet',
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
async ({ effects }) => ({
|
||||||
|
name: i18n('Export Wallet'),
|
||||||
|
description: i18n(
|
||||||
|
'Export the `default` spaces wallet to /data/mainnet/wallets_backup/default.json and surface the JSON below.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
allowedStatuses: 'only-running',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// run
|
||||||
|
async ({ effects }) => {
|
||||||
|
const mounts = sdk.Mounts.of().mountVolume({
|
||||||
|
volumeId: 'main',
|
||||||
|
subpath: null,
|
||||||
|
mountpoint: dataDir,
|
||||||
|
readonly: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cmd = [
|
||||||
|
`mkdir -p ${BACKUP_DIR}`,
|
||||||
|
`/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-cookie ${dataDir}/${SPACED_CHAIN}/.cookie exportwallet ${BACKUP_PATH}`,
|
||||||
|
].join(' && ')
|
||||||
|
|
||||||
|
const result = await sdk.SubContainer.withTemp(
|
||||||
|
effects,
|
||||||
|
{ imageId: 'spaces' },
|
||||||
|
mounts,
|
||||||
|
'spaces-export-wallet',
|
||||||
|
async (subc) => {
|
||||||
|
const exportRes = await subc.exec(['bash', '-c', cmd], { user: 'root' })
|
||||||
|
if (exportRes.exitCode !== 0) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
stderr: (exportRes.stderr ?? '').toString(),
|
||||||
|
exitCode: exportRes.exitCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const catRes = await subc.exec(['cat', BACKUP_PATH], { user: 'root' })
|
||||||
|
if (catRes.exitCode !== 0) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
stderr: (catRes.stderr ?? '').toString(),
|
||||||
|
exitCode: catRes.exitCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: true as const, body: (catRes.stdout ?? '').toString() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Failure'),
|
||||||
|
message: i18n('Could not export wallet: ${error}', {
|
||||||
|
error: result.stderr || `exit ${result.exitCode}`,
|
||||||
|
}),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Wallet exported.'),
|
||||||
|
message: i18n(
|
||||||
|
'A copy of this JSON is saved inside the container at /data/mainnet/wallets_backup/default.json.',
|
||||||
|
),
|
||||||
|
result: {
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Wallet JSON'),
|
||||||
|
description: null,
|
||||||
|
value: result.body.trim(),
|
||||||
|
masked: true,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { dataDir, SPACED_CHAIN } from '../utils'
|
||||||
|
|
||||||
|
const { InputSpec, Value } = sdk
|
||||||
|
|
||||||
|
const BACKUP_DIR = `${dataDir}/${SPACED_CHAIN}/wallets_backup`
|
||||||
|
const BACKUP_PATH = `${BACKUP_DIR}/default.json`
|
||||||
|
const WALLET_DIR = `${dataDir}/${SPACED_CHAIN}/wallets/default`
|
||||||
|
const COOKIE_PATH = `${dataDir}/${SPACED_CHAIN}/.cookie`
|
||||||
|
|
||||||
|
const inputSpec = InputSpec.of({
|
||||||
|
walletJson: Value.textarea({
|
||||||
|
name: i18n('Wallet JSON'),
|
||||||
|
description: i18n('Paste the JSON contents of a default.json wallet export.'),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: null,
|
||||||
|
required: true,
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: null,
|
||||||
|
placeholder: '{...}',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const importWallet = sdk.Action.withInput(
|
||||||
|
// id
|
||||||
|
'import-wallet',
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
async ({ effects }) => ({
|
||||||
|
name: i18n('Import Wallet'),
|
||||||
|
description: i18n(
|
||||||
|
'Restore a previously-exported default.json wallet into spaced.',
|
||||||
|
),
|
||||||
|
warning: i18n(
|
||||||
|
'Existing /data/mainnet/wallets_backup/default.json and /data/mainnet/wallets/default will be renamed with .bakNNN suffixes before the import. The active spaced daemon will load the imported wallet on success.',
|
||||||
|
),
|
||||||
|
allowedStatuses: 'only-running',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// input
|
||||||
|
inputSpec,
|
||||||
|
|
||||||
|
// prefill
|
||||||
|
async ({ effects }) => {},
|
||||||
|
|
||||||
|
// run
|
||||||
|
async ({ effects, input }) => {
|
||||||
|
// Validate JSON before touching disk.
|
||||||
|
try {
|
||||||
|
JSON.parse(input.walletJson)
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Failure'),
|
||||||
|
message: i18n('Could not parse wallet JSON: ${error}', {
|
||||||
|
error: (e as Error).message,
|
||||||
|
}),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64-encode so shell quoting can never break on user content.
|
||||||
|
const b64 = Buffer.from(input.walletJson, 'utf8').toString('base64')
|
||||||
|
|
||||||
|
const script = `set -e
|
||||||
|
mkdir -p '${BACKUP_DIR}'
|
||||||
|
|
||||||
|
# Rotate /data/mainnet/wallets_backup/default.json -> .bakNNN if exists.
|
||||||
|
BACKUP='${BACKUP_PATH}'
|
||||||
|
if [ -e "$BACKUP" ]; then
|
||||||
|
i=0
|
||||||
|
while [ -e "$(printf '%s.bak%03d' "$BACKUP" "$i")" ]; do
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
mv "$BACKUP" "$(printf '%s.bak%03d' "$BACKUP" "$i")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write fresh default.json from base64 payload.
|
||||||
|
printf '%s' '${b64}' | base64 -d > "$BACKUP"
|
||||||
|
|
||||||
|
# Rotate /data/mainnet/wallets/default folder -> .bakNNN if exists.
|
||||||
|
WDIR='${WALLET_DIR}'
|
||||||
|
if [ -d "$WDIR" ]; then
|
||||||
|
j=0
|
||||||
|
while [ -d "$(printf '%s.bak%03d' "$WDIR" "$j")" ]; do
|
||||||
|
j=$((j+1))
|
||||||
|
done
|
||||||
|
mv "$WDIR" "$(printf '%s.bak%03d' "$WDIR" "$j")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import + load via spaced RPC.
|
||||||
|
/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-cookie '${COOKIE_PATH}' importwallet "$BACKUP"
|
||||||
|
/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-cookie '${COOKIE_PATH}' loadwallet
|
||||||
|
`
|
||||||
|
|
||||||
|
const res = await sdk.SubContainer.withTemp(
|
||||||
|
effects,
|
||||||
|
{ imageId: 'spaces' },
|
||||||
|
sdk.Mounts.of().mountVolume({
|
||||||
|
volumeId: 'main',
|
||||||
|
subpath: null,
|
||||||
|
mountpoint: dataDir,
|
||||||
|
readonly: false,
|
||||||
|
}),
|
||||||
|
'spaces-import-wallet',
|
||||||
|
(subc) => subc.exec(['bash', '-c', script], { user: 'root' }),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.exitCode !== 0) {
|
||||||
|
const stderr = (res.stderr ?? '').toString()
|
||||||
|
const stdout = (res.stdout ?? '').toString()
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Failure'),
|
||||||
|
message: i18n('Could not import wallet: ${error}', {
|
||||||
|
error: stderr || stdout || `exit ${res.exitCode}`,
|
||||||
|
}),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Success'),
|
||||||
|
message: i18n('Wallet imported and loaded.'),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
import { sdk } from '../sdk'
|
import { sdk } from '../sdk'
|
||||||
|
import { exportWallet } from './exportWallet'
|
||||||
|
import { importWallet } from './importWallet'
|
||||||
import { resetPassword } from './resetPassword'
|
import { resetPassword } from './resetPassword'
|
||||||
import { resetSpacedState } from './resetSpacedState'
|
import { resetSpacedState } from './resetSpacedState'
|
||||||
import { setBitcoinRpc } from './setBitcoinRpc'
|
import { setBitcoinRpc } from './setBitcoinRpc'
|
||||||
import { showCredentials } from './showCredentials'
|
import { showCredentials } from './showCredentials'
|
||||||
|
import { showPassword } from './showPassword'
|
||||||
import { syncStatus } from './syncStatus'
|
import { syncStatus } from './syncStatus'
|
||||||
|
|
||||||
export const actions = sdk.Actions.of()
|
export const actions = sdk.Actions.of()
|
||||||
.addAction(resetPassword)
|
.addAction(resetPassword)
|
||||||
.addAction(showCredentials)
|
.addAction(showCredentials)
|
||||||
|
.addAction(showPassword)
|
||||||
.addAction(setBitcoinRpc)
|
.addAction(setBitcoinRpc)
|
||||||
.addAction(syncStatus)
|
.addAction(syncStatus)
|
||||||
.addAction(resetSpacedState)
|
.addAction(resetSpacedState)
|
||||||
|
.addAction(exportWallet)
|
||||||
|
.addAction(importWallet)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { storeJson } from '../fileModels/storeJson'
|
||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { APP_USER } from '../utils'
|
||||||
|
|
||||||
|
export const showPassword = sdk.Action.withoutInput(
|
||||||
|
// id
|
||||||
|
'show-password',
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
async ({ effects }) => ({
|
||||||
|
name: i18n('Show Web UI Password'),
|
||||||
|
description: i18n(
|
||||||
|
'Display the existing username and password for the Spaces web terminal.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
allowedStatuses: 'any',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// run
|
||||||
|
async ({ effects }) => {
|
||||||
|
const password = await storeJson.read((s) => s.password).once()
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Show Web UI Password'),
|
||||||
|
message: i18n(
|
||||||
|
'Use these credentials to log in to the Spaces web terminal.',
|
||||||
|
),
|
||||||
|
result: {
|
||||||
|
type: 'group',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Username'),
|
||||||
|
description: null,
|
||||||
|
value: APP_USER,
|
||||||
|
masked: false,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Password'),
|
||||||
|
description: null,
|
||||||
|
value: password ?? '',
|
||||||
|
masked: true,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -54,7 +54,7 @@ export const syncStatus = sdk.Action.withoutInput(
|
|||||||
return {
|
return {
|
||||||
version: '1',
|
version: '1',
|
||||||
title: i18n('Sync Status'),
|
title: i18n('Sync Status'),
|
||||||
message: stdout || i18n('spaced is fully synced.'),
|
message: stdout || i18n('Sync Status'),
|
||||||
result: {
|
result: {
|
||||||
type: 'single',
|
type: 'single',
|
||||||
name: 'getserverinfo',
|
name: 'getserverinfo',
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ const dict = {
|
|||||||
'Spaced Sync': 7,
|
'Spaced Sync': 7,
|
||||||
'spaced is querying Bitcoin and indexing — this can take a while on first run.':
|
'spaced is querying Bitcoin and indexing — this can take a while on first run.':
|
||||||
8,
|
8,
|
||||||
'spaced is fully synced.': 9,
|
'spaced is fully synced (blocks ${blocks} / headers ${headers}).': 9,
|
||||||
'spaced is indexing. Progress: ${pct}%': 10,
|
'spaced is indexing: ${pct}% (blocks ${blocks} / headers ${headers}).': 10,
|
||||||
'spaced RPC did not respond.': 11,
|
'getserverinfo non-JSON. stdout=${stdout} stderr=${stderr}': 11,
|
||||||
|
'Spaced Sync health check crashed: ${error}': 53,
|
||||||
|
'space-cli exited ${code}: ${error}': 54,
|
||||||
|
'Show Web UI Password': 55,
|
||||||
|
'Display the existing username and password for the Spaces web terminal.':
|
||||||
|
56,
|
||||||
|
|
||||||
// interfaces.ts
|
// interfaces.ts
|
||||||
'Web UI': 12,
|
'Web UI': 12,
|
||||||
@@ -53,6 +58,22 @@ const dict = {
|
|||||||
36,
|
36,
|
||||||
'Spaced state has been wiped. Start (or restart) the service to resync.': 37,
|
'Spaced state has been wiped. Start (or restart) the service to resync.': 37,
|
||||||
'Could not wipe spaced state: ${error}': 38,
|
'Could not wipe spaced state: ${error}': 38,
|
||||||
|
'Export Wallet': 39,
|
||||||
|
"Export the `default` spaces wallet to /data/mainnet/wallets_backup/default.json and surface the JSON below.":
|
||||||
|
40,
|
||||||
|
'Could not export wallet: ${error}': 41,
|
||||||
|
'Wallet exported.': 42,
|
||||||
|
'Wallet JSON': 43,
|
||||||
|
'A copy of this JSON is saved inside the container at /data/mainnet/wallets_backup/default.json.':
|
||||||
|
44,
|
||||||
|
'Import Wallet': 45,
|
||||||
|
'Restore a previously-exported default.json wallet into spaced.': 46,
|
||||||
|
'Existing /data/mainnet/wallets_backup/default.json and /data/mainnet/wallets/default will be renamed with .bakNNN suffixes before the import. The active spaced daemon will load the imported wallet on success.':
|
||||||
|
47,
|
||||||
|
'Paste the JSON contents of a default.json wallet export.': 48,
|
||||||
|
'Could not parse wallet JSON: ${error}': 49,
|
||||||
|
'Wallet imported and loaded.': 50,
|
||||||
|
'Could not import wallet: ${error}': 51,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+79
-34
@@ -33,6 +33,7 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
|||||||
SPACED_BITCOIN_RPC_URL: `http://${BITCOIND_RPC_HOSTNAME}:${BITCOIND_RPC_PORT}`,
|
SPACED_BITCOIN_RPC_URL: `http://${BITCOIND_RPC_HOSTNAME}:${BITCOIND_RPC_PORT}`,
|
||||||
SPACED_BITCOIN_RPC_USER: btcAuth.username,
|
SPACED_BITCOIN_RPC_USER: btcAuth.username,
|
||||||
SPACED_BITCOIN_RPC_PASSWORD: btcAuth.password,
|
SPACED_BITCOIN_RPC_PASSWORD: btcAuth.password,
|
||||||
|
SPACED_RPC_COOKIE: `/data/mainnet/.cookie`,
|
||||||
// legacy aliases for `bitcoin-cli` / shell helpers that read these names
|
// legacy aliases for `bitcoin-cli` / shell helpers that read these names
|
||||||
BTC_RPC_HOST: BITCOIND_RPC_HOSTNAME,
|
BTC_RPC_HOST: BITCOIND_RPC_HOSTNAME,
|
||||||
BTC_RPC_PORT: String(BITCOIND_RPC_PORT),
|
BTC_RPC_PORT: String(BITCOIND_RPC_PORT),
|
||||||
@@ -136,51 +137,95 @@ SPACES_BASHRC_EOF`],
|
|||||||
ready: {
|
ready: {
|
||||||
display: i18n('Spaced Sync'),
|
display: i18n('Spaced Sync'),
|
||||||
fn: async () => {
|
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 {
|
try {
|
||||||
const parsed = JSON.parse(stdout) as {
|
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 || '<no output>').slice(0, 200),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: {
|
||||||
ready?: boolean
|
ready?: boolean
|
||||||
progress?: number
|
progress?: number
|
||||||
chain?: { blocks?: number; headers?: number }
|
chain?: { blocks?: number; headers?: number }
|
||||||
}
|
}
|
||||||
if (parsed.ready === true) {
|
try {
|
||||||
|
parsed = JSON.parse(stdoutText)
|
||||||
|
} catch {
|
||||||
return {
|
return {
|
||||||
result: 'success',
|
result: 'failure',
|
||||||
message: i18n('spaced is fully synced.'),
|
message: i18n(
|
||||||
|
'getserverinfo non-JSON. stdout=${stdout} stderr=${stderr}',
|
||||||
|
{
|
||||||
|
stdout: (stdoutTrimmed || '<empty>').slice(0, 160),
|
||||||
|
stderr: (stderr || '<empty>').slice(0, 160),
|
||||||
|
},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
progress = Math.floor((parsed.progress ?? 0) * 100)
|
|
||||||
} catch {
|
|
||||||
// fall through to loading
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const progress = Math.min(
|
||||||
result: 'loading',
|
100,
|
||||||
message: i18n('spaced is indexing. Progress: ${pct}%', {
|
Math.max(0, Math.round((parsed.progress ?? 0) * 100)),
|
||||||
pct: String(progress),
|
)
|
||||||
}),
|
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: 300_000,
|
gracePeriod: 30_000,
|
||||||
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
||||||
},
|
},
|
||||||
requires: ['spaced'],
|
requires: ['spaced'],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user