diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5950330d3..e2cbb36fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ changes.
### Added
+- Add support for ada handle in drep payment address [Issue 3155]()
+
### Fixed
### Changed
diff --git a/govtool/frontend/.storybook/preview.tsx b/govtool/frontend/.storybook/preview.tsx
index 52a7b97bc..bee0052bc 100644
--- a/govtool/frontend/.storybook/preview.tsx
+++ b/govtool/frontend/.storybook/preview.tsx
@@ -11,6 +11,7 @@ import { ModalProvider } from "../src/context/modal";
import { CardanoProvider } from "../src/context/wallet";
import i18n from "../src/i18n";
import { theme } from "../src/theme";
+import { AdaHandleProvider } from "../src/context/adaHandle";
const queryClient = new QueryClient();
@@ -29,32 +30,34 @@ const preview: Preview = {
-
-
-
-
-
-
-
-
-
- }
- />
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
diff --git a/govtool/frontend/src/context/adaHandle.tsx b/govtool/frontend/src/context/adaHandle.tsx
new file mode 100644
index 000000000..7d6f81c41
--- /dev/null
+++ b/govtool/frontend/src/context/adaHandle.tsx
@@ -0,0 +1,49 @@
+import { createContext, useContext, PropsWithChildren, useMemo } from "react";
+import { HandleObject } from "@/models/adaHandle";
+import { adaHandleService } from "@/services/AdaHandle";
+
+type AdaHandleContextType = {
+ isValidAdaHandle: (handle: string) => Promise;
+ getAdaHandleCIP105DRepId: (handle: string) => Promise;
+ getAdaHandleCIP129DRepId: (handle: string) => Promise;
+ getHandleDetails: (handle: string) => Promise;
+};
+
+const AdaHandleContext = createContext(null);
+
+/**
+ * Provides the AdaHandle context to its children components.
+ */
+export const AdaHandleProvider = ({ children }: PropsWithChildren) => {
+ const value = useMemo(
+ () => ({
+ isValidAdaHandle: adaHandleService.isValidAdaHandle,
+ getAdaHandleCIP105DRepId: adaHandleService.getAdaHandleCIP105DRepId,
+ getAdaHandleCIP129DRepId: adaHandleService.getAdaHandleCIP129DRepId,
+ getHandleDetails: adaHandleService.getHandleDetails,
+ }),
+ [adaHandleService],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+/**
+ * Custom hook that provides access to the AdaHandle context.
+ * Throws an error if used outside of an AdaHandleProvider.
+ */
+export const useAdaHandleContext = () => {
+ const context = useContext(AdaHandleContext);
+
+ if (!context) {
+ throw new Error(
+ "useAdaHandleContext must be used within an AdaHandleProvider",
+ );
+ }
+
+ return context;
+};
diff --git a/govtool/frontend/src/context/appContext.tsx b/govtool/frontend/src/context/appContext.tsx
index 745750b34..14c18c9ea 100644
--- a/govtool/frontend/src/context/appContext.tsx
+++ b/govtool/frontend/src/context/appContext.tsx
@@ -15,7 +15,8 @@ import {
PROTOCOL_PARAMS_KEY,
setItemToLocalStorage,
} from "@/utils";
-import { EpochParams, NetworkMetrics } from "@/models";
+import { EpochParams, NetworkMetrics, Network } from "@/models";
+import { adaHandleService } from "@/services/AdaHandle";
const BOOTSTRAPPING_PHASE_MAJOR = 9;
@@ -25,7 +26,7 @@ type AppContextType = {
isInBootstrapPhase: boolean;
isFullGovernance: boolean;
networkName: string;
- network: string;
+ network: Network;
cExplorerBaseUrl: string;
epochParams?: EpochParams;
networkMetrics?: NetworkMetrics;
@@ -58,6 +59,9 @@ const AppContextProvider = ({ children }: PropsWithChildren) => {
const { data: networkMetricsData } = await fetchNetworkMetrics();
if (networkMetricsData) {
setItemToLocalStorage(NETWORK_METRICS_KEY, networkMetricsData);
+
+ // Initialize ada handle service
+ adaHandleService.initialize(networkMetricsData.networkName);
}
setIsAppInitializing(false);
@@ -81,7 +85,7 @@ const AppContextProvider = ({ children }: PropsWithChildren) => {
(networkMetrics?.networkName as keyof typeof NETWORK_NAMES) ||
"preview"
],
- network: networkMetrics?.networkName || "preview",
+ network: networkMetrics?.networkName ?? Network.preview,
cExplorerBaseUrl:
CEXPLORER_BASE_URLS[
(networkMetrics?.networkName as keyof typeof NETWORK_NAMES) ||
diff --git a/govtool/frontend/src/context/contextProviders.tsx b/govtool/frontend/src/context/contextProviders.tsx
index 30f7f9a51..d38fa0acc 100644
--- a/govtool/frontend/src/context/contextProviders.tsx
+++ b/govtool/frontend/src/context/contextProviders.tsx
@@ -5,6 +5,7 @@ import { SnackbarProvider, useSnackbar } from "./snackbar";
import { DataActionsBarProvider } from "./dataActionsBar";
import { FeatureFlagProvider } from "./featureFlag";
import { GovernanceActionProvider } from "./governanceAction";
+import { AdaHandleProvider } from "./adaHandle";
interface Props {
children: React.ReactNode;
@@ -14,13 +15,15 @@ const ContextProviders = ({ children }: Props) => (
-
-
-
- {children}
-
-
-
+
+
+
+
+ {children}
+
+
+
+
diff --git a/govtool/frontend/src/context/featureFlag.test.tsx b/govtool/frontend/src/context/featureFlag.test.tsx
index 8840d6545..1ca6d4047 100644
--- a/govtool/frontend/src/context/featureFlag.test.tsx
+++ b/govtool/frontend/src/context/featureFlag.test.tsx
@@ -3,6 +3,7 @@ import { renderHook } from "@testing-library/react";
import { FeatureFlagProvider, useFeatureFlag } from "./featureFlag";
import { GovernanceActionType } from "@/types/governanceAction";
import { useAppContext } from "./appContext";
+import { Network } from "@/models";
vi.mock("./appContext");
@@ -13,7 +14,7 @@ const mockUseAppContextReturnValue = {
isAppInitializing: false,
isInBootstrapPhase: false,
isFullGovernance: true,
- network: "preview",
+ network: Network.preview,
networkName: "preview",
isMainnet: false,
};
diff --git a/govtool/frontend/src/models/adaHandle.ts b/govtool/frontend/src/models/adaHandle.ts
new file mode 100644
index 000000000..2c947a531
--- /dev/null
+++ b/govtool/frontend/src/models/adaHandle.ts
@@ -0,0 +1,58 @@
+export type ResolvedAddresses = {
+ ada?: string;
+ eth?: string;
+ btc?: string;
+};
+
+export type Virtual = {
+ expires_time: number;
+ public_mint: boolean;
+};
+
+export type HandleObject = {
+ hex: string;
+ name: string;
+ handle_type?: "handle" | "nft_subhandle";
+ virtual?: Virtual;
+ holder: string;
+ holder_type: "wallet" | "drep";
+ image: string;
+ standard_image: string;
+ image_hash: string;
+ standard_image_hash: string;
+ length: number;
+ og?: number;
+ og_number: number;
+ rarity: string;
+ characters: string;
+ numeric_modifiers: string;
+ sub_length: number;
+ sub_rarity: string;
+ sub_characters: string;
+ sub_numeric_modifiers: string;
+ payment_key_hash: string;
+ default_in_wallet: string;
+ pfp_image: string;
+ bg_image: string;
+ pfp_asset?: string;
+ bg_asset?: string;
+ resolved_addresses: ResolvedAddresses;
+ original_address?: string;
+ version: number;
+ svg_version: string;
+ utxo: string;
+ lovelace?: number;
+ has_datum: boolean;
+ created_slot_number: number;
+ updated_slot_number: number;
+ last_update_address?: string;
+ pz_enabled?: boolean;
+ last_edited_time?: number;
+ drep?: {
+ type: "drep";
+ cred: string;
+ hex: string;
+ cip_105: string;
+ cip_129: string;
+ };
+};
diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts
index 603cdd47d..32f710f41 100644
--- a/govtool/frontend/src/models/api.ts
+++ b/govtool/frontend/src/models/api.ts
@@ -78,6 +78,14 @@ export type TransactionStatus = {
| [];
};
+export enum Network {
+ samchonet = "sanchonet",
+ preview = "preview",
+ testnet = "testnet",
+ preprod = "preprod",
+ mainnet = "mainnet",
+}
+
export type NetworkMetrics = {
currentTime: string;
currentEpoch: number;
@@ -96,7 +104,7 @@ export type NetworkMetrics = {
totalRegisteredDirectVoters: number;
alwaysAbstainVotingPower: number;
alwaysNoConfidenceVotingPower: number;
- networkName: string;
+ networkName: Network;
noOfCommitteeMembers: number;
quorumNumerator: number;
quorumDenominator: number;
diff --git a/govtool/frontend/src/services/AdaHandle.ts b/govtool/frontend/src/services/AdaHandle.ts
new file mode 100644
index 000000000..a130ad1aa
--- /dev/null
+++ b/govtool/frontend/src/services/AdaHandle.ts
@@ -0,0 +1,109 @@
+import { NetworkMetrics } from "@/models";
+import { HandleObject } from "@/models/adaHandle";
+
+export const ADAHANDLE_BASE_URL = {
+ sanchonet: "",
+ preview: "https://preview.api.handle.me",
+ testnet: "",
+ preprod: "https://preprod.api.handle.me",
+ mainnet: "https://api.handle.me",
+};
+
+/**
+ * Service for interacting with AdaHandle API.
+ */
+class AdaHandleService {
+ private static instance: AdaHandleService;
+
+ private adaHandleBaseUrl: string | null = null;
+
+ /**
+ * Private constructor to enforce singleton pattern.
+ */
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
+ private constructor() {}
+
+ /**
+ * Get the instance of AdaHandleService.
+ * @returns The instance of AdaHandleService.
+ */
+ static getInstance(): AdaHandleService {
+ if (!AdaHandleService.instance) {
+ AdaHandleService.instance = new AdaHandleService();
+ }
+ return AdaHandleService.instance;
+ }
+
+ /**
+ * Initialize the AdaHandleService with the base URL for a specific network.
+ * @param network - The name of the network.
+ */
+ initialize(network: NetworkMetrics["networkName"]): void {
+ if (this.adaHandleBaseUrl !== ADAHANDLE_BASE_URL[network]) {
+ this.adaHandleBaseUrl = ADAHANDLE_BASE_URL[network];
+ }
+ }
+
+ /**
+ * Get the details of a handle from the AdaHandle API.
+ * @param handle - The handle to retrieve details for.
+ * @returns A Promise that resolves to the HandleObject or null if the handle is not found.
+ */
+ async getHandleDetails(handle: string): Promise {
+ if (!this.adaHandleBaseUrl) {
+ throw new Error("AdaHandleService is not initialized with a network.");
+ }
+ if (!handle) {
+ return null;
+ }
+
+ if (handle.startsWith("$")) {
+ handle = handle.slice(1);
+ }
+
+ try {
+ const response = await fetch(
+ `${this.adaHandleBaseUrl}/handles/${handle}`,
+ );
+ if (response.ok) {
+ return response.json();
+ }
+ return null;
+ } catch (error) {
+ console.error("Error fetching handle:", error);
+ return null;
+ }
+ }
+
+ /**
+ * Check if an Ada handle is valid.
+ * @param handle - The handle to check.
+ * @returns A Promise that resolves to true if the handle is valid, false otherwise.
+ */
+ async isValidAdaHandle(handle: string): Promise {
+ const handleObject = await this.getHandleDetails(handle);
+ return Boolean(handleObject);
+ }
+
+ /**
+ * Get the CIP-105 DRep ID associated with an Ada handle.
+ * @param handle - The handle to get the CIP-105 DRep ID for.
+ * @returns A Promise that resolves to the CIP-105 DRep ID or an empty string if not found.
+ */
+ async getAdaHandleCIP105DRepId(handle: string): Promise {
+ const handleObject = await this.getHandleDetails(handle);
+ return handleObject?.drep?.cip_105 ?? "";
+ }
+
+ /**
+ * Get the CIP-129 DRep ID associated with an Ada handle.
+ * @param handle - The handle to get the CIP-129 DRep ID for.
+ * @returns A Promise that resolves to the CIP-129 DRep ID or an empty string if not found.
+ */
+ async getAdaHandleCIP129DRepId(handle: string): Promise {
+ const handleObject = await this.getHandleDetails(handle);
+ return handleObject?.drep?.cip_129 ?? "";
+ }
+}
+
+export const adaHandleService = AdaHandleService.getInstance();
diff --git a/govtool/frontend/src/utils/drepSearchPhraseProcessor.ts b/govtool/frontend/src/utils/drepSearchPhraseProcessor.ts
index 1de314bd0..27d77a62a 100644
--- a/govtool/frontend/src/utils/drepSearchPhraseProcessor.ts
+++ b/govtool/frontend/src/utils/drepSearchPhraseProcessor.ts
@@ -1,3 +1,4 @@
+import { adaHandleService } from "@/services/AdaHandle";
import { decodeCIP129Identifier } from "./cip129identifier";
/**
@@ -14,6 +15,12 @@ export const dRepSearchPhraseProcessor = async (phrase: string) => {
let drepIDPhrase = phrase;
try {
+ const adaHandleCIP105DRepId =
+ await adaHandleService.getAdaHandleCIP105DRepId(phrase);
+ if (adaHandleCIP105DRepId) {
+ return adaHandleCIP105DRepId;
+ }
+
if (
drepIDPhrase.startsWith("drep_script") ||
drepIDPhrase.startsWith("drep")
diff --git a/govtool/frontend/src/utils/isValidFormat.ts b/govtool/frontend/src/utils/isValidFormat.ts
index da931998d..3f49710ef 100644
--- a/govtool/frontend/src/utils/isValidFormat.ts
+++ b/govtool/frontend/src/utils/isValidFormat.ts
@@ -4,6 +4,7 @@ import {
RewardAddress,
} from "@emurgo/cardano-serialization-lib-asmjs";
import i18n from "@/i18n";
+import { adaHandleService } from "@/services/AdaHandle";
export const URL_REGEX =
/^(?:(?:https?:\/\/)?(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(?:\/[^\s]*)?)|(?:ipfs:\/\/(?:[a-zA-Z0-9]+(?:\/[a-zA-Z0-9._-]+)*))$|^$/;
@@ -47,6 +48,12 @@ export async function isReceivingAddress(address?: string) {
if (!address) {
return true;
}
+ const isValidAdaHandle = await adaHandleService.isValidAdaHandle(address);
+
+ if (isValidAdaHandle) {
+ return true;
+ }
+
const receivingAddress = Address.from_bech32(address);
return receivingAddress
? true