Merge pull request 'feat: surface stack versions in app footer' (#81) from feature/expose-stack-versions into master
Reviewed-on: https://codeberg.org/johba/harb/pulls/81
This commit is contained in:
commit
30ed4aa072
13 changed files with 480 additions and 49 deletions
|
|
@ -23,4 +23,10 @@ export { KRAIKEN_ABI, STAKE_ABI, ABIS } from './abis.js';
|
||||||
// Backward compatible aliases
|
// Backward compatible aliases
|
||||||
export { KRAIKEN_ABI as KraikenAbi, STAKE_ABI as StakeAbi } from './abis.js';
|
export { KRAIKEN_ABI as KraikenAbi, STAKE_ABI as StakeAbi } from './abis.js';
|
||||||
|
|
||||||
export { KRAIKEN_LIB_VERSION, COMPATIBLE_CONTRACT_VERSIONS, isCompatibleVersion, getVersionMismatchError } from './version.js';
|
export {
|
||||||
|
KRAIKEN_LIB_VERSION,
|
||||||
|
COMPATIBLE_CONTRACT_VERSIONS,
|
||||||
|
STACK_META_ID,
|
||||||
|
isCompatibleVersion,
|
||||||
|
getVersionMismatchError,
|
||||||
|
} from './version.js';
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@
|
||||||
*/
|
*/
|
||||||
export const KRAIKEN_LIB_VERSION = 1;
|
export const KRAIKEN_LIB_VERSION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton ID used for stack metadata rows across services.
|
||||||
|
*/
|
||||||
|
export const STACK_META_ID = 'stack-meta';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of Kraiken contract versions this library is compatible with.
|
* List of Kraiken contract versions this library is compatible with.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ type Meta {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
stackMeta(id: String!): stackMeta
|
||||||
|
stackMetas(where: stackMetaFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): stackMetaPage!
|
||||||
stats(id: String!): stats
|
stats(id: String!): stats
|
||||||
statss(where: statsFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): statsPage!
|
statss(where: statsFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): statsPage!
|
||||||
positions(id: String!): positions
|
positions(id: String!): positions
|
||||||
|
|
@ -24,6 +26,69 @@ type Query {
|
||||||
_meta: Meta
|
_meta: Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type stackMeta {
|
||||||
|
id: String!
|
||||||
|
contractVersion: Int!
|
||||||
|
ponderVersion: String!
|
||||||
|
kraikenLibVersion: Int!
|
||||||
|
updatedAt: BigInt!
|
||||||
|
}
|
||||||
|
|
||||||
|
type stackMetaPage {
|
||||||
|
items: [stackMeta!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
totalCount: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
input stackMetaFilter {
|
||||||
|
AND: [stackMetaFilter]
|
||||||
|
OR: [stackMetaFilter]
|
||||||
|
id: String
|
||||||
|
id_not: String
|
||||||
|
id_in: [String]
|
||||||
|
id_not_in: [String]
|
||||||
|
id_contains: String
|
||||||
|
id_not_contains: String
|
||||||
|
id_starts_with: String
|
||||||
|
id_ends_with: String
|
||||||
|
id_not_starts_with: String
|
||||||
|
id_not_ends_with: String
|
||||||
|
contractVersion: Int
|
||||||
|
contractVersion_not: Int
|
||||||
|
contractVersion_in: [Int]
|
||||||
|
contractVersion_not_in: [Int]
|
||||||
|
contractVersion_gt: Int
|
||||||
|
contractVersion_lt: Int
|
||||||
|
contractVersion_gte: Int
|
||||||
|
contractVersion_lte: Int
|
||||||
|
ponderVersion: String
|
||||||
|
ponderVersion_not: String
|
||||||
|
ponderVersion_in: [String]
|
||||||
|
ponderVersion_not_in: [String]
|
||||||
|
ponderVersion_contains: String
|
||||||
|
ponderVersion_not_contains: String
|
||||||
|
ponderVersion_starts_with: String
|
||||||
|
ponderVersion_ends_with: String
|
||||||
|
ponderVersion_not_starts_with: String
|
||||||
|
ponderVersion_not_ends_with: String
|
||||||
|
kraikenLibVersion: Int
|
||||||
|
kraikenLibVersion_not: Int
|
||||||
|
kraikenLibVersion_in: [Int]
|
||||||
|
kraikenLibVersion_not_in: [Int]
|
||||||
|
kraikenLibVersion_gt: Int
|
||||||
|
kraikenLibVersion_lt: Int
|
||||||
|
kraikenLibVersion_gte: Int
|
||||||
|
kraikenLibVersion_lte: Int
|
||||||
|
updatedAt: BigInt
|
||||||
|
updatedAt_not: BigInt
|
||||||
|
updatedAt_in: [BigInt]
|
||||||
|
updatedAt_not_in: [BigInt]
|
||||||
|
updatedAt_gt: BigInt
|
||||||
|
updatedAt_lt: BigInt
|
||||||
|
updatedAt_gte: BigInt
|
||||||
|
updatedAt_lte: BigInt
|
||||||
|
}
|
||||||
|
|
||||||
type stats {
|
type stats {
|
||||||
id: String!
|
id: String!
|
||||||
kraikenTotalSupply: BigInt!
|
kraikenTotalSupply: BigInt!
|
||||||
|
|
@ -428,4 +493,4 @@ input positionsFilter {
|
||||||
payout_lt: BigInt
|
payout_lt: BigInt
|
||||||
payout_gte: BigInt
|
payout_gte: BigInt
|
||||||
payout_lte: BigInt
|
payout_lte: BigInt
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,26 @@ import { TAX_RATE_OPTIONS } from 'kraiken-lib/taxRates';
|
||||||
export const HOURS_IN_RING_BUFFER = 168; // 7 days * 24 hours
|
export const HOURS_IN_RING_BUFFER = 168; // 7 days * 24 hours
|
||||||
const RING_BUFFER_SEGMENTS = 4; // ubi, minted, burned, tax
|
const RING_BUFFER_SEGMENTS = 4; // ubi, minted, burned, tax
|
||||||
|
|
||||||
|
export const stackMeta = onchainTable('stackMeta', t => ({
|
||||||
|
id: t.text().primaryKey(),
|
||||||
|
contractVersion: t
|
||||||
|
.integer()
|
||||||
|
.notNull()
|
||||||
|
.$default(() => 0),
|
||||||
|
ponderVersion: t
|
||||||
|
.text()
|
||||||
|
.notNull()
|
||||||
|
.$default(() => 'unknown'),
|
||||||
|
kraikenLibVersion: t
|
||||||
|
.integer()
|
||||||
|
.notNull()
|
||||||
|
.$default(() => 0),
|
||||||
|
updatedAt: t
|
||||||
|
.bigint()
|
||||||
|
.notNull()
|
||||||
|
.$default(() => 0n),
|
||||||
|
}));
|
||||||
|
|
||||||
// Global protocol stats - singleton with id "0x01"
|
// Global protocol stats - singleton with id "0x01"
|
||||||
export const stats = onchainTable('stats', t => ({
|
export const stats = onchainTable('stats', t => ({
|
||||||
id: t.text().primaryKey(), // Always "0x01"
|
id: t.text().primaryKey(), // Always "0x01"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { isCompatibleVersion, getVersionMismatchError, KRAIKEN_LIB_VERSION } from 'kraiken-lib/version';
|
import { createRequire } from 'node:module';
|
||||||
|
import { isCompatibleVersion, getVersionMismatchError, KRAIKEN_LIB_VERSION, STACK_META_ID } from 'kraiken-lib/version';
|
||||||
|
import { stackMeta } from 'ponder:schema';
|
||||||
import type { Context } from 'ponder:registry';
|
import type { Context } from 'ponder:registry';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const { version: PONDER_APP_VERSION } = require('../../package.json') as { version: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates that the deployed Kraiken contract version is compatible
|
* Validates that the deployed Kraiken contract version is compatible
|
||||||
* with this indexer's kraiken-lib version.
|
* with this indexer's kraiken-lib version.
|
||||||
|
|
@ -25,6 +30,24 @@ export async function validateContractVersion(context: Context): Promise<void> {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timestamp = BigInt(Math.floor(Date.now() / 1000));
|
||||||
|
const metaPayload = {
|
||||||
|
contractVersion: versionNumber,
|
||||||
|
ponderVersion: PONDER_APP_VERSION ?? 'dev',
|
||||||
|
kraikenLibVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingMeta = await context.db.find(stackMeta, { id: STACK_META_ID });
|
||||||
|
if (existingMeta) {
|
||||||
|
await context.db.update(stackMeta, { id: STACK_META_ID }).set(metaPayload);
|
||||||
|
} else {
|
||||||
|
await context.db.insert(stackMeta).values({
|
||||||
|
id: STACK_META_ID,
|
||||||
|
...metaPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`✓ Contract version validated: v${versionNumber} (kraiken-lib v${KRAIKEN_LIB_VERSION})`);
|
logger.info(`✓ Contract version validated: v${versionNumber} (kraiken-lib v${KRAIKEN_LIB_VERSION})`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to read contract VERSION:', error);
|
logger.error('Failed to read contract VERSION:', error);
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,17 @@ test.describe('Acquire & Stake', () => {
|
||||||
await expect(walletDisplay).toBeVisible({ timeout: 15_000 });
|
await expect(walletDisplay).toBeVisible({ timeout: 15_000 });
|
||||||
console.log('[TEST] Wallet connected successfully!');
|
console.log('[TEST] Wallet connected successfully!');
|
||||||
|
|
||||||
|
console.log('[TEST] Verifying stack version footer...');
|
||||||
|
const versionFooter = page.getByTestId('stack-version-footer');
|
||||||
|
await expect(versionFooter).toBeVisible({ timeout: 15_000 });
|
||||||
|
await expect(page.getByTestId('stack-version-contracts')).not.toHaveText(/Loading|—/i);
|
||||||
|
await expect(page.getByTestId('stack-version-ponder')).not.toHaveText(/Loading|—/i);
|
||||||
|
await expect(page.getByTestId('stack-version-kraiken-lib')).toHaveText(/^v\d+/i);
|
||||||
|
await expect(page.getByTestId('stack-version-web-app')).toHaveText(/^v/i);
|
||||||
|
const warningBanner = page.getByTestId('stack-version-warning');
|
||||||
|
await expect(warningBanner).toHaveCount(0);
|
||||||
|
console.log('[TEST] Stack version footer verified.');
|
||||||
|
|
||||||
console.log('[TEST] Navigating to cheats page...');
|
console.log('[TEST] Navigating to cheats page...');
|
||||||
await page.goto(`${STACK_WEBAPP_URL}/app/#/cheats`);
|
await page.goto(`${STACK_WEBAPP_URL}/app/#/cheats`);
|
||||||
await expect(page.getByRole('heading', { name: 'Cheat Console' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Cheat Console' })).toBeVisible();
|
||||||
|
|
|
||||||
2
web-app/env.d.ts
vendored
2
web-app/env.d.ts
vendored
|
|
@ -8,4 +8,6 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare const __APP_VERSION__: string;
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
124
web-app/src/components/layouts/StackVersionFooter.vue
Normal file
124
web-app/src/components/layouts/StackVersionFooter.vue
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<footer class="stack-version-footer" data-testid="stack-version-footer">
|
||||||
|
<div class="stack-version-footer__items">
|
||||||
|
<div class="stack-version-footer__item" v-for="item in footerItems" :key="item.key">
|
||||||
|
<span class="stack-version-footer__label">{{ item.label }}</span>
|
||||||
|
<span class="stack-version-footer__value" :class="valueClass(item.key)" :data-testid="`stack-version-${item.key}`">
|
||||||
|
{{ item.value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="footerMessage" class="stack-version-footer__warning" data-testid="stack-version-warning">
|
||||||
|
{{ footerMessage }}
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||||
|
import { resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
|
||||||
|
import { useVersionCheckOnMount } from '@/composables/useVersionCheck';
|
||||||
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
|
const endpointHint = ref<string | null>(null);
|
||||||
|
let endpoint: string | undefined;
|
||||||
|
try {
|
||||||
|
endpoint = resolveGraphqlEndpoint(DEFAULT_CHAIN_ID);
|
||||||
|
logger.info('Stack version footer resolved GraphQL endpoint', endpoint);
|
||||||
|
} catch (error) {
|
||||||
|
endpoint = undefined;
|
||||||
|
endpointHint.value = 'GraphQL endpoint unavailable – ensure the stack is running.';
|
||||||
|
logger.error('Failed to resolve GraphQL endpoint for stack version footer', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { versionStatus, stackVersions, isChecking, hasChecked } = useVersionCheckOnMount(endpoint);
|
||||||
|
|
||||||
|
const footerItems = computed(() => {
|
||||||
|
const loading = isChecking.value && !hasChecked.value;
|
||||||
|
const loadingText = loading ? 'Loading…' : '—';
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'contracts',
|
||||||
|
label: 'Contracts',
|
||||||
|
value: stackVersions.value.contractVersion !== null ? `v${stackVersions.value.contractVersion}` : loadingText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ponder',
|
||||||
|
label: 'Ponder',
|
||||||
|
value: stackVersions.value.ponderVersion ? `v${stackVersions.value.ponderVersion}` : loadingText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kraiken-lib',
|
||||||
|
label: 'kraiken-lib',
|
||||||
|
value: `v${stackVersions.value.kraikenLibVersion}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'web-app',
|
||||||
|
label: 'Web App',
|
||||||
|
value: `v${stackVersions.value.webAppVersion}`,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
});
|
||||||
|
|
||||||
|
const footerMessage = computed(() => {
|
||||||
|
if (!versionStatus.value.isValid) {
|
||||||
|
return versionStatus.value.error ?? 'Stack version mismatch detected.';
|
||||||
|
}
|
||||||
|
return endpointHint.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function valueClass(key: (typeof footerItems.value)[number]['key']) {
|
||||||
|
if (key === 'kraiken-lib') {
|
||||||
|
return versionStatus.value.isValid ? null : 'stack-version-footer__value--warning';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.stack-version-footer
|
||||||
|
padding: 12px 24px
|
||||||
|
background: rgba(7, 17, 27, 0.92)
|
||||||
|
color: #cbd5f5
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08)
|
||||||
|
font-size: 12px
|
||||||
|
letter-spacing: 0.04em
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 8px
|
||||||
|
align-items: center
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.stack-version-footer__items
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: center
|
||||||
|
gap: 16px
|
||||||
|
|
||||||
|
.stack-version-footer__item
|
||||||
|
display: flex
|
||||||
|
gap: 6px
|
||||||
|
align-items: baseline
|
||||||
|
|
||||||
|
.stack-version-footer__label
|
||||||
|
text-transform: uppercase
|
||||||
|
font-weight: 600
|
||||||
|
font-size: 11px
|
||||||
|
color: #94a3b8
|
||||||
|
|
||||||
|
.stack-version-footer__value
|
||||||
|
font-family: 'Fira Code', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace
|
||||||
|
font-weight: 600
|
||||||
|
color: #e2e8f0
|
||||||
|
|
||||||
|
.stack-version-footer__value--warning
|
||||||
|
color: #f97316
|
||||||
|
|
||||||
|
.stack-version-footer__warning
|
||||||
|
margin: 0
|
||||||
|
color: #f97316
|
||||||
|
font-size: 11px
|
||||||
|
text-align: center
|
||||||
|
</style>
|
||||||
|
|
@ -1,57 +1,162 @@
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { KRAIKEN_LIB_VERSION } from 'kraiken-lib/version';
|
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||||
|
import { resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
|
||||||
|
import { KRAIKEN_LIB_VERSION, STACK_META_ID } from 'kraiken-lib/version';
|
||||||
|
|
||||||
|
const WEB_APP_VERSION = __APP_VERSION__;
|
||||||
|
|
||||||
|
type GraphqlError = { message?: string | null };
|
||||||
|
type GraphqlResponse<T> = {
|
||||||
|
data?: T;
|
||||||
|
errors?: GraphqlError[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StackMetaRecord {
|
||||||
|
contractVersion?: number | null;
|
||||||
|
ponderVersion?: string | null;
|
||||||
|
kraikenLibVersion?: number | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StackMetaResult {
|
||||||
|
stackMeta?: StackMetaRecord | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StackVersions {
|
||||||
|
contractVersion: number | null;
|
||||||
|
ponderVersion: string | null;
|
||||||
|
indexerLibVersion: number | null;
|
||||||
|
kraikenLibVersion: number;
|
||||||
|
webAppVersion: string;
|
||||||
|
updatedAt: bigint | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VersionStatus {
|
export interface VersionStatus {
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
contractVersion?: number;
|
contractVersion?: number;
|
||||||
indexerVersion?: number;
|
indexerLibVersion?: number;
|
||||||
|
ponderVersion?: string | null;
|
||||||
libVersion: number;
|
libVersion: number;
|
||||||
|
webAppVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STACK_META_QUERY = `
|
||||||
|
query StackMeta($id: String!) {
|
||||||
|
stackMeta(id: $id) {
|
||||||
|
contractVersion
|
||||||
|
ponderVersion
|
||||||
|
kraikenLibVersion
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const versionStatus = ref<VersionStatus>({
|
const versionStatus = ref<VersionStatus>({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
libVersion: KRAIKEN_LIB_VERSION,
|
libVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
webAppVersion: WEB_APP_VERSION,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stackVersions = ref<StackVersions>({
|
||||||
|
contractVersion: null,
|
||||||
|
ponderVersion: null,
|
||||||
|
indexerLibVersion: null,
|
||||||
|
kraikenLibVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
webAppVersion: WEB_APP_VERSION,
|
||||||
|
updatedAt: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isChecking = ref(false);
|
const isChecking = ref(false);
|
||||||
const hasChecked = ref(false);
|
const hasChecked = ref(false);
|
||||||
|
|
||||||
/**
|
function formatError(error: unknown): string {
|
||||||
* Validates version compatibility between contract, indexer (Ponder), and frontend (kraiken-lib).
|
if (error instanceof Error && error.message) {
|
||||||
*
|
return error.message;
|
||||||
* Queries Ponder GraphQL for the contract version it indexed, then compares:
|
}
|
||||||
* 1. Frontend lib version vs Ponder's lib version (should match exactly)
|
if (typeof error === 'string' && error.length > 0) {
|
||||||
* 2. Contract version vs compatible versions list (should be in COMPATIBLE_CONTRACT_VERSIONS)
|
return error;
|
||||||
*/
|
}
|
||||||
|
return 'Failed to load stack version metadata';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeNumber(value: unknown): number | null {
|
||||||
|
const parsed = typeof value === 'number' ? value : Number(value ?? Number.NaN);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function useVersionCheck() {
|
export function useVersionCheck() {
|
||||||
async function checkVersions(_graphqlUrl: string) {
|
async function checkVersions(graphqlEndpointOverride?: string) {
|
||||||
if (isChecking.value || hasChecked.value) return versionStatus.value;
|
if (isChecking.value) return versionStatus.value;
|
||||||
|
|
||||||
isChecking.value = true;
|
isChecking.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For now, we don't have contract version in Ponder stats yet
|
const endpoint = resolveGraphqlEndpoint(DEFAULT_CHAIN_ID, graphqlEndpointOverride);
|
||||||
// This is a placeholder for when we add it to the schema
|
const response = await fetch(endpoint, {
|
||||||
// Just validate that kraiken-lib is loaded correctly
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: STACK_META_QUERY,
|
||||||
|
variables: { id: STACK_META_ID },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
versionStatus.value = {
|
if (!response.ok) {
|
||||||
isValid: true,
|
throw new Error(`GraphQL stackMeta request failed (HTTP ${response.status})`);
|
||||||
libVersion: KRAIKEN_LIB_VERSION,
|
}
|
||||||
|
|
||||||
|
const payload = (await response.json()) as GraphqlResponse<StackMetaResult>;
|
||||||
|
|
||||||
|
const apiErrors = payload.errors?.filter(err => err?.message);
|
||||||
|
if (apiErrors && apiErrors.length > 0) {
|
||||||
|
throw new Error(apiErrors.map(err => err.message).join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = payload.data?.stackMeta;
|
||||||
|
if (!meta) {
|
||||||
|
throw new Error('Stack metadata unavailable (stackMeta query returned null)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractVersion = sanitizeNumber(meta.contractVersion);
|
||||||
|
const ponderVersion = meta.ponderVersion ?? null;
|
||||||
|
const indexerLibVersion = sanitizeNumber(meta.kraikenLibVersion);
|
||||||
|
const updatedAt = meta.updatedAt ? BigInt(meta.updatedAt) : null;
|
||||||
|
|
||||||
|
const libMismatch = indexerLibVersion !== null && indexerLibVersion !== KRAIKEN_LIB_VERSION;
|
||||||
|
|
||||||
|
stackVersions.value = {
|
||||||
|
contractVersion,
|
||||||
|
ponderVersion,
|
||||||
|
indexerLibVersion,
|
||||||
|
kraikenLibVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
webAppVersion: WEB_APP_VERSION,
|
||||||
|
updatedAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Implement actual version check against Ponder GraphQL
|
versionStatus.value = {
|
||||||
// console.log(`✓ Frontend version check passed: v${KRAIKEN_LIB_VERSION}`);
|
isValid: !libMismatch,
|
||||||
hasChecked.value = true;
|
error: libMismatch
|
||||||
|
? `Stack mismatch: Ponder reports kraiken-lib v${indexerLibVersion} (frontend expects v${KRAIKEN_LIB_VERSION}). Restart the stack to rebuild containers.`
|
||||||
|
: undefined,
|
||||||
|
contractVersion: contractVersion ?? undefined,
|
||||||
|
indexerLibVersion: indexerLibVersion ?? undefined,
|
||||||
|
ponderVersion,
|
||||||
|
libVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
webAppVersion: WEB_APP_VERSION,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: Add proper error logging
|
|
||||||
// console.error('Version check failed:', error);
|
|
||||||
versionStatus.value = {
|
versionStatus.value = {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
error: error instanceof Error ? error.message : 'Failed to check version compatibility',
|
error: formatError(error),
|
||||||
|
contractVersion: stackVersions.value.contractVersion ?? undefined,
|
||||||
|
indexerLibVersion: stackVersions.value.indexerLibVersion ?? undefined,
|
||||||
|
ponderVersion: stackVersions.value.ponderVersion,
|
||||||
libVersion: KRAIKEN_LIB_VERSION,
|
libVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
webAppVersion: WEB_APP_VERSION,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
|
hasChecked.value = true;
|
||||||
isChecking.value = false;
|
isChecking.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,18 +165,15 @@ export function useVersionCheck() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
versionStatus,
|
versionStatus,
|
||||||
|
stackVersions,
|
||||||
checkVersions,
|
checkVersions,
|
||||||
isChecking,
|
isChecking,
|
||||||
hasChecked,
|
hasChecked,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function useVersionCheckOnMount(graphqlUrl?: string) {
|
||||||
* Vue composable that automatically checks versions on mount.
|
const { versionStatus, stackVersions, checkVersions, isChecking, hasChecked } = useVersionCheck();
|
||||||
* Shows a warning banner if versions are incompatible.
|
|
||||||
*/
|
|
||||||
export function useVersionCheckOnMount(graphqlUrl: string) {
|
|
||||||
const { versionStatus, checkVersions, isChecking } = useVersionCheck();
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkVersions(graphqlUrl);
|
await checkVersions(graphqlUrl);
|
||||||
|
|
@ -79,6 +181,9 @@ export function useVersionCheckOnMount(graphqlUrl: string) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
versionStatus,
|
versionStatus,
|
||||||
|
stackVersions,
|
||||||
isChecking,
|
isChecking,
|
||||||
|
hasChecked,
|
||||||
|
checkVersions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<div class="default-layout">
|
||||||
<slot></slot>
|
<main>
|
||||||
</main>
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
<StackVersionFooter />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import StackVersionFooter from '@/components/layouts/StackVersionFooter.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.default-layout
|
||||||
|
min-height: 100vh
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.default-layout > main
|
||||||
|
flex: 1 1 auto
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,8 @@
|
||||||
<main>
|
<main>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
|
<StackVersionFooter class="navbar-layout__footer" />
|
||||||
</div>
|
</div>
|
||||||
<!-- <footer>
|
|
||||||
<f-footer :dark="darkTheme"></f-footer>
|
|
||||||
</footer> -->
|
|
||||||
<div class="mobile-navigation-bar" v-if="isMobile">
|
<div class="mobile-navigation-bar" v-if="isMobile">
|
||||||
<div class="mobile-navigation-tab" @click="router.push('/')">
|
<div class="mobile-navigation-tab" @click="router.push('/')">
|
||||||
<div class="mobile-navigation-tab__icon">
|
<div class="mobile-navigation-tab__icon">
|
||||||
|
|
@ -33,6 +31,7 @@
|
||||||
import NavbarHeader from '@/components/layouts/NavbarHeader.vue';
|
import NavbarHeader from '@/components/layouts/NavbarHeader.vue';
|
||||||
import SlideoutPanel from '@/components/layouts/SlideoutPanel.vue';
|
import SlideoutPanel from '@/components/layouts/SlideoutPanel.vue';
|
||||||
import ConnectWallet from '@/components/layouts/ConnectWallet.vue';
|
import ConnectWallet from '@/components/layouts/ConnectWallet.vue';
|
||||||
|
import StackVersionFooter from '@/components/layouts/StackVersionFooter.vue';
|
||||||
import { useMobile } from '@/composables/useMobile';
|
import { useMobile } from '@/composables/useMobile';
|
||||||
import IconHome from '@/components/icons/IconHome.vue';
|
import IconHome from '@/components/icons/IconHome.vue';
|
||||||
import IconDocs from '@/components/icons/IconDocs.vue';
|
import IconDocs from '@/components/icons/IconDocs.vue';
|
||||||
|
|
@ -54,6 +53,16 @@ function openDocs() {
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
.navbar-layout
|
.navbar-layout
|
||||||
padding-top: 100px
|
padding-top: 100px
|
||||||
|
padding-bottom: 120px
|
||||||
|
min-height: 100vh
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.navbar-layout > main
|
||||||
|
flex: 1 1 auto
|
||||||
|
|
||||||
|
.navbar-layout__footer
|
||||||
|
margin-top: auto
|
||||||
|
|
||||||
.mobile-navigation-bar
|
.mobile-navigation-bar
|
||||||
height: 72px
|
height: 72px
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,59 @@
|
||||||
export function info(text: string, data: unknown = null) {
|
type LogLevel = 'info' | 'error' | 'contract';
|
||||||
if (data) {
|
|
||||||
// console.log(`%c ${text}`, 'color: #17a2b8', data);
|
interface LogEntry {
|
||||||
} else {
|
level: LogLevel;
|
||||||
// console.log(`%c ${text}`, 'color: #17a2b8');
|
message: string;
|
||||||
|
data?: unknown;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBrowser = typeof window !== 'undefined';
|
||||||
|
const MAX_BUFFER = 50;
|
||||||
|
const buffer: LogEntry[] = [];
|
||||||
|
|
||||||
|
function emit(entry: LogEntry) {
|
||||||
|
buffer.push(entry);
|
||||||
|
if (buffer.length > MAX_BUFFER) {
|
||||||
|
buffer.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBrowser && import.meta.env.DEV) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('kraiken:log', {
|
||||||
|
detail: entry,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function contract(text: string, data: unknown = null) {
|
function buildEntry(level: LogLevel, message: string, data?: unknown): LogEntry {
|
||||||
if (data) {
|
return {
|
||||||
// console.log(`%c ${text}`, 'color: #8732a8', data);
|
level,
|
||||||
} else {
|
message,
|
||||||
// console.log(`%c ${text}`, 'color: #8732a8');
|
data,
|
||||||
}
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function info(message: string, data?: unknown) {
|
||||||
|
emit(buildEntry('info', message, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contract(message: string, data?: unknown) {
|
||||||
|
emit(buildEntry('contract', message, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function error(message: string, data?: unknown) {
|
||||||
|
emit(buildEntry('error', message, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBufferedLogs(): LogEntry[] {
|
||||||
|
return [...buffer];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
info,
|
info,
|
||||||
contract,
|
contract,
|
||||||
|
error,
|
||||||
|
getBufferedLogs,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ import { fileURLToPath, URL } from 'node:url'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import packageJson from './package.json' assert { type: 'json' }
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(() => {
|
export default defineConfig(() => {
|
||||||
const localRpcProxyTarget = process.env.VITE_LOCAL_RPC_PROXY_TARGET
|
const localRpcProxyTarget = process.env.VITE_LOCAL_RPC_PROXY_TARGET
|
||||||
const localGraphqlProxyTarget = process.env.VITE_LOCAL_GRAPHQL_PROXY_TARGET ?? 'http://127.0.0.1:42069'
|
const localGraphqlProxyTarget = process.env.VITE_LOCAL_GRAPHQL_PROXY_TARGET ?? 'http://127.0.0.1:42069'
|
||||||
const localTxnProxyTarget = process.env.VITE_LOCAL_TXN_PROXY_TARGET ?? 'http://127.0.0.1:43069'
|
const localTxnProxyTarget = process.env.VITE_LOCAL_TXN_PROXY_TARGET ?? 'http://127.0.0.1:43069'
|
||||||
|
const appVersion = (packageJson as { version?: string }).version ?? 'dev'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// base: "/HarbergPublic/",
|
// base: "/HarbergPublic/",
|
||||||
|
|
@ -16,6 +18,9 @@ export default defineConfig(() => {
|
||||||
vue(),
|
vue(),
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
],
|
],
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(appVersion),
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue