Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,18 @@ sv2-ui/

## Docker Images Used

- `stratumv2/translator_sv2:main` - Translator Proxy
- `stratumv2/jd_client_sv2:main` - JD Client
`sv2-ui` selects the JDC and Translator Proxy images from the Bitcoin Core compatibility table.

## Bitcoin Core Compatibility

| Bitcoin Core | JDC image | Translator Proxy image | Status | Reference |
|--------------|-----------|------------------------|--------|-----------|
| 30.2 | `stratumv2/jd_client_sv2:v0.3.5` | `stratumv2/translator_sv2:v0.3.5` | Supported | [`release/v0.1.4`](https://github.com/stratum-mining/sv2-ui/tree/release/v0.1.4) |
| 31.0 | `stratumv2/jd_client_sv2:main` | `stratumv2/translator_sv2:main` | Supported using development images | Release branches pin matching sv2-apps release tags |

## Compatibility Notes

- [Monitoring API compatibility](docs/monitoring-api-compatibility.md) - How `sv2-ui` should handle monitoring API changes across supported JDC and Translator Proxy image sets.

## Ports

Expand Down
61 changes: 61 additions & 0 deletions docs/monitoring-api-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Monitoring API Compatibility

`sv2-ui` consumes monitoring APIs exposed by `sv2-apps` containers, currently JD Client (JDC) and Translator Proxy (tProxy).

These APIs may still be considered unstable on the `sv2-apps` side. Because of that, `sv2-ui` must not assume that every supported `sv2-apps` image exposes the same raw monitoring schema.

This matters especially because `sv2-ui` may intentionally run older JDC/tProxy images when users are running older Bitcoin Core versions. For example, a user on Bitcoin Core 30.2 may require an older JDC image, while a user on Bitcoin Core 31.0 may require a newer one. Both setups may need to be supported by the same `sv2-ui` release.

## Compatibility Profiles

`sv2-ui` should select `sv2-apps` image versions through runtime compatibility profiles.

Each profile should describe:

- supported Bitcoin Core version/range
- JDC image tag
- Translator Proxy image tag
- expected monitoring API contract for each app
- optional monitoring features available for that image set

## Handling API Changes

When `sv2-apps` changes a monitoring API, `sv2-ui` should classify the change before updating the dashboard.

### Backward-Compatible Additions

Examples:

- new optional field
- new endpoint
- new optional detail beside an existing field

Action in `sv2-ui`:

- no change if unused
- optional UI enhancement if useful
- field-presence check is usually enough

### Breaking Changes

Examples:

- field removed
- field renamed
- field type changed
- field meaning or unit changed
- endpoint removed or response shape changed

Action in `sv2-ui`:

- update the compatibility profile for the affected image
- add or update backend normalization if the frontend needs a stable shape
- add fixture tests using payloads from the affected image versions

## Frontend Rule

The React frontend should avoid depending on version-specific raw JDC/tProxy response shapes when multiple supported images differ.

If a raw API difference affects data already shown by the dashboard, normalize it in the `sv2-ui` backend or shared monitoring layer first.

If a future image exposes new monitoring data that older images do not have, treat it as an optional feature and render it only for compatible profiles.
74 changes: 74 additions & 0 deletions server/src/compatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { SetupData } from './types.js';

export type BitcoinCoreVersion = '30.2' | '31.0';

export type CompatibilityProfileId =
| 'bitcoin-core-30.2'
| 'bitcoin-core-31.0';

export interface CompatibilityProfile {
id: CompatibilityProfileId;
bitcoinCoreVersion: BitcoinCoreVersion;
status: 'supported';
images: {
jdc: string;
translator: string;
};
monitoringApi: {
jdc: string;
translator: string;
};
}

export const COMPATIBILITY_PROFILES: Record<BitcoinCoreVersion, CompatibilityProfile> = {
'30.2': {
id: 'bitcoin-core-30.2',
bitcoinCoreVersion: '30.2',
status: 'supported',
images: {
jdc: 'stratumv2/jd_client_sv2:v0.3.5',
translator: 'stratumv2/translator_sv2:v0.3.5',
},
monitoringApi: {
jdc: '/api/v1',
translator: '/api/v1',
},
},
'31.0': {
id: 'bitcoin-core-31.0',
bitcoinCoreVersion: '31.0',
status: 'supported',
images: {
// Development uses the latest sv2-apps images. Release branches must
// replace these with the matching published sv2-apps release tags.
jdc: 'stratumv2/jd_client_sv2:main',
translator: 'stratumv2/translator_sv2:main',
},
monitoringApi: {
jdc: '/api/v1',
translator: '/api/v1',
},
},
} as const;

export function isSupportedBitcoinCoreVersion(
version: string | null | undefined
): version is BitcoinCoreVersion {
return version === '30.2' || version === '31.0';
}

export function getCompatibilityProfileForBitcoinCore(
version: string | null | undefined
): CompatibilityProfile {
if (!isSupportedBitcoinCoreVersion(version)) {
throw new Error(
'Unsupported or missing Bitcoin Core version. Select Bitcoin Core 30.2 or 31.0 before starting the stack.'
);
}

return COMPATIBILITY_PROFILES[version];
}

export function getCompatibilityProfileForSetup(data: SetupData): CompatibilityProfile {
return getCompatibilityProfileForBitcoinCore(data.bitcoin?.core_version);
}
4 changes: 3 additions & 1 deletion server/src/config-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ function positiveInteger(value: number | undefined, fallback: number): number {
}

export function normalizeSetupData(data: SetupData): SetupData {
if (!data.translator) return data;
if (!data.translator) {
return data;
}

return {
...data,
Expand Down
27 changes: 16 additions & 11 deletions server/src/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Docker from 'dockerode';
import os from 'os';
import type { SetupData, ContainerStatus, HealthStatus } from './types.js';
import type { ContainerLogLine, LogContainerRole, LogOutputStream } from './logs/types.js';
import { getCompatibilityProfileForSetup } from './compatibility.js';

/**
* Expand ~ to home directory in a path.
Expand Down Expand Up @@ -145,8 +146,6 @@ const NETWORK_NAME = 'sv2-network';
const CONFIG_VOLUME = 'sv2-config';
const TRANSLATOR_CONTAINER = 'sv2-translator';
const JDC_CONTAINER = 'sv2-jdc';
const TRANSLATOR_IMAGE = 'stratumv2/translator_sv2:main';
const JDC_IMAGE = 'stratumv2/jd_client_sv2:main';
const DOCKER_LOG_HEADER_SIZE = 8;

/**
Expand Down Expand Up @@ -385,15 +384,15 @@ async function getContainerStatus(name: string): Promise<ContainerStatus | null>
* - In Docker: uses shared volume (sv2-config) for config
* - In dev: bind-mounts config file from host filesystem
*/
async function startTranslator(configPath: string): Promise<void> {
async function startTranslator(configPath: string, image: string): Promise<void> {
await removeContainer(TRANSLATOR_CONTAINER);

const binds = isRunningInDocker
? [`${CONFIG_VOLUME}:/config:ro`]
: [`${configPath}:/config/translator.toml:ro`];

const container = await docker.createContainer({
Image: TRANSLATOR_IMAGE,
Image: image,
name: TRANSLATOR_CONTAINER,
Entrypoint: ['/app/translator_sv2'],
Cmd: ['-c', '/config/translator.toml'],
Expand Down Expand Up @@ -425,7 +424,8 @@ async function startTranslator(configPath: string): Promise<void> {
async function startJdc(
configPath: string,
bitcoinSocketPath: string,
network: string
network: string,
image: string
): Promise<void> {
await removeContainer(JDC_CONTAINER);

Expand All @@ -447,7 +447,7 @@ async function startJdc(
];

const container = await docker.createContainer({
Image: JDC_IMAGE,
Image: image,
name: JDC_CONTAINER,
Entrypoint: ['/app/jd_client_sv2'],
Cmd: ['-c', '/config/jdc.toml'],
Expand Down Expand Up @@ -486,22 +486,27 @@ export async function startStack(
// Connect sv2-ui to the network so it can proxy API requests
await connectSv2UiToNetwork();

// Pull latest images from Docker Hub
await pullImage(TRANSLATOR_IMAGE);
const profile = getCompatibilityProfileForSetup(data);
console.log(
`Using compatibility profile ${profile.id} for Bitcoin Core ${profile.bitcoinCoreVersion}`
);

// Pull profile-selected images from Docker Hub
await pullImage(profile.images.translator);
if (data.mode === 'jd') {
await pullImage(JDC_IMAGE);
await pullImage(profile.images.jdc);
}

// Start JDC first if in JD mode (Translator connects to JDC)
if (data.mode === 'jd' && data.bitcoin) {
const socketPath = expandHomePath(data.bitcoin.socket_path);
await startJdc(`${configDir}/jdc.toml`, socketPath, data.bitcoin.network);
await startJdc(`${configDir}/jdc.toml`, socketPath, data.bitcoin.network, profile.images.jdc);
console.log('Waiting for JDC to initialize...');
await new Promise(resolve => setTimeout(resolve, 3000));
}

// Start Translator
await startTranslator(`${configDir}/translator.toml`);
await startTranslator(`${configDir}/translator.toml`, profile.images.translator);
}

/**
Expand Down
28 changes: 28 additions & 0 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { fileURLToPath } from 'url';

import type { SetupData, StatusResponse, SetupResponse } from './types.js';
import { generateTranslatorConfig, generateJdcConfig, normalizeSetupData } from './config-generator.js';
import { isSupportedBitcoinCoreVersion } from './compatibility.js';
import {
startStack,
stopStack,
Expand Down Expand Up @@ -70,6 +71,18 @@ async function saveState(data: SetupData): Promise<void> {
}, null, 2));
}

function getBitcoinCoreVersionError(data: SetupData): string | null {
if (data.mode !== 'jd') {
return null;
}

if (!isSupportedBitcoinCoreVersion(data.bitcoin?.core_version)) {
return 'Select a supported Bitcoin Core version: 30.2 or 31.0';
}

return null;
}

/**
* GET /api/health - Health check
*/
Expand Down Expand Up @@ -219,6 +232,11 @@ app.put('/api/config', async (req, res) => {
return res.status(400).json({ success: false, error: 'JD mode requires JDC and Bitcoin configuration' });
}

const bitcoinCoreVersionError = getBitcoinCoreVersionError(newData);
if (bitcoinCoreVersionError) {
return res.status(400).json({ success: false, error: bitcoinCoreVersionError });
}

await ensureDockerAvailable();

await fs.mkdir(CONFIG_DIR, { recursive: true });
Expand Down Expand Up @@ -343,6 +361,11 @@ app.post('/api/setup', async (req, res) => {
return res.status(400).json({ success: false, error: 'JD mode requires JDC and Bitcoin configuration' });
}

const bitcoinCoreVersionError = getBitcoinCoreVersionError(data);
if (bitcoinCoreVersionError) {
return res.status(400).json({ success: false, error: bitcoinCoreVersionError });
}

await ensureDockerAvailable();

// Generate config files
Expand Down Expand Up @@ -429,6 +452,11 @@ app.post('/api/restart', async (_req, res) => {
return res.status(400).json({ success: false, error: 'Not configured' });
}

const bitcoinCoreVersionError = getBitcoinCoreVersionError(state.data);
if (bitcoinCoreVersionError) {
return res.status(400).json({ success: false, error: bitcoinCoreVersionError });
}

await stopStack();
await startStack(state.data, CONFIG_DIR);

Expand Down
Loading
Loading