Skip to content
Open
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
463 changes: 463 additions & 0 deletions config/external-config/__tests__/validate.test.ts

Large diffs are not rendered by default.

66 changes: 19 additions & 47 deletions config/external-config/frontend-fallback.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,13 @@
import { useMemo } from 'react';
import {
Manifest,
ManifestConfig,
ManifestConfigPage,
ManifestConfigPageList,
ManifestEntry,
getManifestKey,
isManifestValid,
} from 'config/external-config';
import { getDexConfig } from 'features/withdrawals/request/withdrawal-rates';
import { EARN_VAULTS } from 'features/earn/consts';
import invariant from 'tiny-invariant';

import FallbackLocalManifest from 'IPFS.json';

export const getBackwardCompatibleConfig = (
config: ManifestEntry['config'],
): ManifestEntry['config'] => {
const pages = (Object.keys(config?.pages ?? {}) as ManifestConfigPage[])
.filter((key) => ManifestConfigPageList.has(key))
.reduce(
(acc, key) => {
if (acc) {
acc[key] = { ...config.pages[key] };
}

return acc;
},
{} as ManifestConfig['pages'],
);

return {
enabledWithdrawalDexes:
config.enabledWithdrawalDexes?.filter((dex) => !!getDexConfig(dex)) ?? [],
featureFlags: { ...(config?.featureFlags ?? {}) },
multiChainBanner: config?.multiChainBanner ?? [],
earnVaultsBanner: config?.earnVaultsBanner ?? {},
earnVaults:
config.earnVaults?.filter((vault) => EARN_VAULTS.includes(vault.name)) ??
[],
pages: { ...pages },
api: { ...(config?.api ?? {}) },
};
};
import { ManifestSchema } from './validate';
import { getLocalFallbackManifest, getManifestKey } from './utils';
import type { ManifestConfig, ManifestEntry } from './types';

export const overrideManifestConfig = (
config: ManifestEntry['config'],
override: Partial<ManifestEntry['config']> = {},
override: Partial<ManifestConfig> = {},
): ManifestEntry['config'] => {
return {
...config,
Expand All @@ -65,10 +27,20 @@ export const getFallbackedManifestEntry = (
defaultChain: number,
manifestOverride?: string,
): ManifestEntry => {
const isValid = isManifestValid(prefetchedManifest, defaultChain);
return isValid
? prefetchedManifest[getManifestKey(defaultChain, manifestOverride)]
: (FallbackLocalManifest as unknown as Manifest)[defaultChain];
const key = getManifestKey(defaultChain, manifestOverride);
const parsing = ManifestSchema.safeParse(prefetchedManifest);

if (parsing.success && parsing.data[key]) {
return parsing.data[key];
}

const fallbackManifest = getLocalFallbackManifest();
invariant(
fallbackManifest[key],
`Fallback manifest entry not found for key ${key}`,
);

return fallbackManifest[key];
};

export const useFallbackManifestEntry = (
Expand Down
19 changes: 11 additions & 8 deletions config/external-config/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
export { useExternalConfigContext } from './use-external-config-context';
export { getFallbackedManifestEntry } from './frontend-fallback';
export type {
ManifestConfig,
Manifest,
ManifestEntry,
ExternalConfig,
ManifestConfig,
ManifestConfigVaultEntry,
ManifestConfigVaultApyType,
ManifestConfigPage,
ManifestConfigDex,
ManifestConfigEarnVault,
} from './types';
export { ManifestConfigPageList, ManifestConfigPageEnum } from './types';
export {
isManifestValid,
getManifestKey,
isManifestEntryValid,
isEnabledDexesValid,
isFeatureFlagsValid,
isMultiChainBannerValid,
isPagesValid,
getLocalFallbackManifest,
shouldRedirectToRoot,
} from './utils';
export {
ManifestSchema,
ManifestConfigPages,
ManifestConfigWithdrawalDexes,
} from './validate';
88 changes: 18 additions & 70 deletions config/external-config/types.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,30 @@
import type { UseQueryResult } from '@tanstack/react-query';
import type { DexWithdrawalApi } from 'features/withdrawals/request/withdrawal-rates';
import type { EarnVaultKey } from 'features/earn/consts';
import type { z } from 'zod';
import type {
ManifestSchema,
ManifestConfigWithdrawalDexes,
ManifestConfigPages,
} from './validate';

export type Manifest = Record<string, ManifestEntry>;
export type Manifest = z.infer<typeof ManifestSchema>;

export type ManifestEntry = {
cid?: string;
ens?: string;
leastSafeVersion?: string;
config: ManifestConfig;
};
export type ManifestEntry = NonNullable<Manifest[keyof Manifest]>;

export type VaultAPYType = 'daily' | 'weekly' | 'weekly_moving_average';
type VaultAPY = {
type?: VaultAPYType;
};
export type ManifestConfig = ManifestEntry['config'];

export type EarnVaultConfigEntry = {
name: EarnVaultKey;
deposit?: boolean;
withdraw?: boolean;
depositPauseReasonText?: string;
withdrawPauseReasonText?: string;
listWarningText?: string;
apy?: VaultAPY;
showNew?: boolean;
deprecated?: boolean;
disabled?: boolean;
};
export type ManifestConfigVaultEntry =
ManifestEntry['config']['earnVaults'][number];

export type ManifestConfig = {
enabledWithdrawalDexes: DexWithdrawalApi[];
multiChainBanner: number[];
earnVaults: EarnVaultConfigEntry[];
earnVaultsBanner: {
showOnStakeForm: boolean;
showAfterStake: boolean;
};
featureFlags: {
ledgerLiveL2?: boolean;
disableSendCalls?: boolean;
dgBannerEnabled?: boolean;
dgWarningState?: boolean;
rewardsMaintenance?: boolean;
holidayDecorEnabled?: boolean;
forceAllowance?: boolean;
amountBannerEnabled?: boolean;
};
pages: {
[page in ManifestConfigPage]?: {
shouldDisable?: boolean;
showNew?: boolean;
sections?: [string, ...string[]];
};
};
api: {
validation: {
version: string;
};
};
};
export type ManifestConfigEarnVault = ManifestConfigVaultEntry['name'];

export enum ManifestConfigPageEnum {
Stake = '/',
Wrap = '/wrap',
Withdrawals = '/withdrawals',
Rewards = '/rewards',
Settings = '/settings',
Referral = '/referral',
Earn = '/earn',
EarnNew = '/earn-new',
}
export type ManifestConfigVaultApyType =
ManifestConfigVaultEntry['apy']['type'];

export type ManifestConfigPage = `${ManifestConfigPageEnum}`;
export type ManifestConfigPage =
(typeof ManifestConfigPages)[keyof typeof ManifestConfigPages];

export const ManifestConfigPageList = new Set<ManifestConfigPage>(
Object.values(ManifestConfigPageEnum),
);
export type ManifestConfigDex =
(typeof ManifestConfigWithdrawalDexes)[keyof typeof ManifestConfigWithdrawalDexes];

export type ExternalConfig = Omit<ManifestEntry, 'config'> &
ManifestConfig & {
Expand Down
42 changes: 24 additions & 18 deletions config/external-config/use-external-config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@ import { useQuery } from '@tanstack/react-query';
import { config } from 'config';
import { STRATEGY_LAZY } from 'consts/react-query-strategies';
import { IPFS_MANIFEST_URL } from 'consts/external-links';
import { getManifestKey, isManifestEntryValid } from 'config/external-config';
import { standardFetcher } from 'utils/standardFetcher';
import { useEarnRuntimeState } from 'features/earn/shared/hooks/use-earn-state';
import { EARN_PATH } from 'consts/urls';

import {
getBackwardCompatibleConfig,
overrideManifestConfig,
useFallbackManifestEntry,
} from './frontend-fallback';

import type { ExternalConfig, ManifestEntry } from './types';
import { ManifestSchema } from './validate';
import { getManifestKey } from './utils';
import type { ExternalConfig, ManifestConfig, ManifestEntry } from './types';

export const useExternalConfigContext = (
prefetchedManifest?: unknown,
Expand All @@ -42,11 +41,16 @@ export const useExternalConfigContext = (
},
);

const entry = result[getManifestKey(defaultChain, manifestOverride)];
if (isManifestEntryValid(entry)) return entry;
const manifestKey = getManifestKey(defaultChain, manifestOverride);

const parsing = ManifestSchema.safeParse(result);

if (parsing.success && parsing.data[manifestKey]) {
return parsing.data[manifestKey];
}

throw new Error(
'[useExternalConfig] received invalid manifest',
`[useExternalConfig] received invalid manifest ${parsing.error?.message}`,
result,
);
} catch (err) {
Expand All @@ -61,23 +65,25 @@ export const useExternalConfigContext = (
});

return useMemo(() => {
const { config: rawConfig, ...rest } = queryResult.data ?? fallbackData;
const { config: cleanConfig, ...rest } = queryResult.data ?? fallbackData;

const cleanConfig = getBackwardCompatibleConfig(rawConfig);
const EARN_CONFIG = cleanConfig.pages[EARN_PATH];

// TODO: replace this logic with useEarnState hook, which is a single source of truth for Earn state.
// Current blocker: the Navigation component (navigation.tsx) uses external config to disable pages,
// we don't want to couple it with specific Earn state logic.
const overridePages = isEarnDisabledByRuntimeContext
? {
pages: {
[EARN_PATH]: {
...cleanConfig.pages[EARN_PATH],
shouldDisable: true,
const overridePages: Partial<ManifestConfig> | undefined =
isEarnDisabledByRuntimeContext
? {
pages: {
[EARN_PATH]: {
showNew: EARN_CONFIG?.showNew ?? false,
sections: EARN_CONFIG?.sections ?? [],
shouldDisable: true,
},
},
},
}
: undefined;
}
: undefined;

const overrideConfig = overrideManifestConfig(cleanConfig, {
...overridePages,
Expand Down
Loading
Loading