Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namadillo: Checks for indexer and keychain compatibility #1449

Merged
merged 3 commits into from
Dec 27, 2024
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
23 changes: 23 additions & 0 deletions apps/namadillo/src/App/Common/FixedWarningBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ReactNode } from "react";
import { IoWarning } from "react-icons/io5";

type FixedWarningBannerProps = {
errorMessage: ReactNode;
};

export const FixedWarningBanner = ({
errorMessage,
}: FixedWarningBannerProps): JSX.Element => {
if (!errorMessage) return <></>;

return (
<div className="fixed bottom-0 left-0 w-full bg-yellow z-[9999]">
<div className="flex flex-row justify-center items-center gap-1 px-12 py-3 text-sm [&_a]:underline">
<strong className="inline-flex items-center">
<IoWarning /> WARNING:{" "}
</strong>
<div>{errorMessage}</div>
</div>
</div>
);
};
4 changes: 4 additions & 0 deletions apps/namadillo/src/App/Layout/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FixedWarningBanner } from "App/Common/FixedWarningBanner";
import { useCompatibilityErrors } from "hooks/useCompatibilityErrors";
import { ReactNode, useState } from "react";
import { twMerge } from "tailwind-merge";
import { AppHeader } from "./AppHeader";
Expand All @@ -10,6 +12,7 @@ export const AppLayout = ({
children: ReactNode;
}): JSX.Element => {
const [displayNavigation, setDisplayNavigation] = useState(false);
const compatibilityErrors = useCompatibilityErrors();

return (
<div className="custom-container pb-2">
Expand Down Expand Up @@ -42,6 +45,7 @@ export const AppLayout = ({
</aside>
<main className="min-h-full">{children}</main>
</div>
<FixedWarningBanner errorMessage={compatibilityErrors} />
</div>
);
};
13 changes: 8 additions & 5 deletions apps/namadillo/src/atoms/settings/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SettingsStorage } from "types";
import {
clearShieldedContext,
fetchDefaultTomlConfig,
isIndexerAlive,
getIndexerHealth,
isMaspIndexerAlive,
isRpcAlive,
} from "./services";
Expand Down Expand Up @@ -176,7 +176,10 @@ export const maspIndexerUrlAtom = atom((get) => {
export const updateIndexerUrlAtom = atomWithMutation(() => {
return {
mutationKey: ["update-indexer-url"],
mutationFn: changeSettingsUrl("indexerUrl", isIndexerAlive),
mutationFn: changeSettingsUrl(
"indexerUrl",
async (url: string): Promise<boolean> => !!(await getIndexerHealth(url))
),
};
});

Expand All @@ -201,9 +204,9 @@ export const indexerHeartbeatAtom = atomWithQuery((get) => {
refetchOnWindowFocus: true,
refetchInterval: 10_000,
queryFn: async () => {
const valid = await isIndexerAlive(indexerUrl);
if (!valid) throw "Unable to verify indexer heartbeat";
return true;
const indexerInfo = await getIndexerHealth(indexerUrl);
if (!indexerInfo) throw "Unable to verify indexer heartbeat";
return indexerInfo;
},
};
});
Expand Down
16 changes: 11 additions & 5 deletions apps/namadillo/src/atoms/settings/services.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { Configuration, DefaultApi } from "@namada/indexer-client";
import { isUrlValid } from "@namada/utils";
import toml from "toml";
import { SettingsTomlOptions } from "types";
import { SettingsTomlOptions, TempIndexerHealthType } from "types";
import { getSdkInstance } from "utils/sdk";

export const isIndexerAlive = async (url: string): Promise<boolean> => {
export const getIndexerHealth = async (
url: string
): Promise<TempIndexerHealthType | undefined> => {
if (!isUrlValid(url)) {
return false;
return;
}

try {
const configuration = new Configuration({ basePath: url });
const api = new DefaultApi(configuration);
const response = await api.healthGet();
return response.status === 200;

// TODO:update when indexer swagger is fixed
// @ts-expect-error Indexer swagger is out of date
return response.data as TempIndexerHealthType;
} catch {
return false;
return;
}
};

Expand Down
4 changes: 4 additions & 0 deletions apps/namadillo/src/compatibility.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"keychain": "0.3.x",
"indexer": "1.1.x"
}
47 changes: 47 additions & 0 deletions apps/namadillo/src/hooks/useCompatibilityErrors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { indexerHeartbeatAtom } from "atoms/settings";
import { useAtomValue } from "jotai";
import { useEffect, useState } from "react";
import {
checkIndexerCompatibilityErrors,
checkKeychainCompatibilityError,
} from "utils/compatibility";
import { useNamadaKeychain } from "./useNamadaKeychain";

export const useCompatibilityErrors = (): React.ReactNode | undefined => {
const indexerHealth = useAtomValue(indexerHeartbeatAtom);
const keychain = useNamadaKeychain();
const [errorMessage, setErrorMessage] = useState<
React.ReactNode | undefined
>();

const verifyKeychainVersion = async (): Promise<void> => {
const namadaKeychain = await keychain.namadaKeychain.get();
if (namadaKeychain) {
const version = namadaKeychain.version();
const versionErrorMessage = checkKeychainCompatibilityError(version);
if (versionErrorMessage) {
setErrorMessage(versionErrorMessage);
}
}
};

const verifyIndexerVersion = async (): Promise<void> => {
const versionErrorMessage = checkIndexerCompatibilityErrors(
indexerHealth.data?.version || ""
);

if (versionErrorMessage) {
setErrorMessage(versionErrorMessage);
}
};

useEffect(() => {
verifyKeychainVersion();
}, [keychain]);

useEffect(() => {
indexerHealth.isSuccess && verifyIndexerVersion();
}, [indexerHealth]);

return errorMessage;
};
6 changes: 6 additions & 0 deletions apps/namadillo/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,9 @@ export type LocalnetToml = {
chain_1_channel: string;
chain_2_channel: string;
};

// TODO: remove this after indexer swagger gets fixed
export type TempIndexerHealthType = {
version: string;
commit: string;
};
104 changes: 104 additions & 0 deletions apps/namadillo/src/utils/compatibility.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { routes } from "App/routes";
import compatibilty from "compatibility.json";
import { wallets } from "integrations";
import { Link } from "react-router-dom";
import semverSatisfies from "semver/functions/satisfies";
import semverLtr from "semver/ranges/ltr";
import { isFirefox } from "./etc";

enum CompatibilityOutput {
IncompatibleVersion = -1,
Compatible = 0,
InterfaceOutdated = 1,
}

// Checks if the versions are compatible using semantic versioning (semver).
// Returns true if the versions are compatible, -1 if the version is outdated,
// or 1 if a required version is lower than the version provided.
const checkVersionsCompatible = (
currentVersion: string,
requiredVersion: string
): CompatibilityOutput => {
if (semverSatisfies(currentVersion, requiredVersion)) {
return CompatibilityOutput.Compatible;
}
return semverLtr(currentVersion, requiredVersion) ?
CompatibilityOutput.IncompatibleVersion
: CompatibilityOutput.InterfaceOutdated;
};

export const checkIndexerCompatibilityErrors = (
indexerVersion: string
): React.ReactNode => {
const requiredVersion = compatibilty.indexer;
const checkResult = checkVersionsCompatible(indexerVersion, requiredVersion);

if (checkResult === CompatibilityOutput.IncompatibleVersion) {
return (
<>
You&apos;re using an outdated version of Namada Indexer. Please update
your indexer URL in the{" "}
<Link to={routes.settingsAdvanced}>Advanced Settings</Link> section.
</>
);
}

if (checkResult === CompatibilityOutput.InterfaceOutdated) {
return (
<>
Your Namadillo version is not compatible with the current Namada
Indexer. Please upgrade your web interface or pick a different one from
the <a href="https://namada.net/apps#interfaces">Namada Apps</a> list.
</>
);
}

return "";
};

export const checkKeychainCompatibilityError = (
keychainVersion: string
): React.ReactNode => {
const targetKeychainVersion = compatibilty.keychain;
const checkResult = checkVersionsCompatible(
keychainVersion,
targetKeychainVersion
);

if (checkResult === CompatibilityOutput.IncompatibleVersion) {
return (
<>
Your Namada Keychain version is outdated. Please upgrade it using{" "}
{isFirefox() ?
<a
href={wallets.namada.downloadUrl.firefox}
target="_blank"
rel="nofollow noreferrer"
>
Firefox addons
</a>
: <a
href={wallets.namada.downloadUrl.chrome}
target="_blank"
rel="nofollow noreferrer"
>
Chrome store
</a>
}{" "}
or websites.
</>
);
}

if (checkResult === CompatibilityOutput.InterfaceOutdated) {
return (
<>
Your Namadillo version is not compatible with the keychain installed.
Please upgrade your web interface or pick a different one from the{" "}
<a href="https://namada.net/apps#interfaces">Namada Apps</a> list.
</>
);
}

return "";
};
1 change: 1 addition & 0 deletions apps/namadillo/src/utils/etc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isFirefox = (): boolean => /firefox/i.test(navigator.userAgent);
Loading