imp/exp/show/cookie
Build Service / BuildPackage (push) Has been cancelled

This commit is contained in:
2026-05-14 18:27:20 -04:00
parent 172e3f63ac
commit 2cdc4a5f34
9 changed files with 409 additions and 38 deletions
+92
View File
@@ -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,
},
}
},
)
+133
View File
@@ -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,
}
},
)
+6
View File
@@ -1,13 +1,19 @@
import { sdk } from '../sdk'
import { exportWallet } from './exportWallet'
import { importWallet } from './importWallet'
import { resetPassword } from './resetPassword'
import { resetSpacedState } from './resetSpacedState'
import { setBitcoinRpc } from './setBitcoinRpc'
import { showCredentials } from './showCredentials'
import { showPassword } from './showPassword'
import { syncStatus } from './syncStatus'
export const actions = sdk.Actions.of()
.addAction(resetPassword)
.addAction(showCredentials)
.addAction(showPassword)
.addAction(setBitcoinRpc)
.addAction(syncStatus)
.addAction(resetSpacedState)
.addAction(exportWallet)
.addAction(importWallet)
+57
View File
@@ -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,
},
],
},
}
},
)
+1 -1
View File
@@ -54,7 +54,7 @@ export const syncStatus = sdk.Action.withoutInput(
return {
version: '1',
title: i18n('Sync Status'),
message: stdout || i18n('spaced is fully synced.'),
message: stdout || i18n('Sync Status'),
result: {
type: 'single',
name: 'getserverinfo',