From b9c7ead8162f91c8255008a12a57b4bf28afa856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sza=C5=82owski?= Date: Mon, 10 Mar 2025 10:48:18 +0100 Subject: [PATCH 1/3] feat(#3155): add integration and support of drep handles within drep registration --- CHANGELOG.md | 2 + govtool/frontend/.storybook/preview.tsx | 55 ++++----- govtool/frontend/src/context/adaHandle.tsx | 49 ++++++++ govtool/frontend/src/context/appContext.tsx | 6 +- .../frontend/src/context/contextProviders.tsx | 17 +-- govtool/frontend/src/context/wallet.tsx | 2 + govtool/frontend/src/models/adaHandle.ts | 58 ++++++++++ govtool/frontend/src/models/api.ts | 2 +- govtool/frontend/src/services/AdaHandle.ts | 109 ++++++++++++++++++ govtool/frontend/src/utils/isValidFormat.ts | 7 ++ 10 files changed, 272 insertions(+), 35 deletions(-) create mode 100644 govtool/frontend/src/context/adaHandle.tsx create mode 100644 govtool/frontend/src/models/adaHandle.ts create mode 100644 govtool/frontend/src/services/AdaHandle.ts 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..2d8cbfaf1 100644 --- a/govtool/frontend/src/context/appContext.tsx +++ b/govtool/frontend/src/context/appContext.tsx @@ -16,6 +16,7 @@ import { setItemToLocalStorage, } from "@/utils"; import { EpochParams, NetworkMetrics } 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: "sanchonet" | "preview" | "testnet" | "preprod" | "mainnet"; 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); 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/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index 77c3de83a..ead35c0d2 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, + useEffect, useMemo, useRef, useState, @@ -104,6 +105,7 @@ import { TransactionStateWithoutResource, usePendingTransaction, } from "./pendingTransaction"; +import { useAdaHandleContext } from "./adaHandle"; interface Props { children: React.ReactNode; 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..9d82fd180 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -96,7 +96,7 @@ export type NetworkMetrics = { totalRegisteredDirectVoters: number; alwaysAbstainVotingPower: number; alwaysNoConfidenceVotingPower: number; - networkName: string; + networkName: "sanchonet" | "preview" | "testnet" | "preprod" | "mainnet"; 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/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 From 9133aa2dcf2b568a26942a3d6de789f0cb440432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sza=C5=82owski?= Date: Mon, 10 Mar 2025 10:54:48 +0100 Subject: [PATCH 2/3] feat(#3155): add ada handles to drep search phrase processor --- govtool/frontend/src/utils/drepSearchPhraseProcessor.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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") From 86eb011b2b8f14c1ddf293a2f3e2ae5166bc7916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sza=C5=82owski?= Date: Mon, 10 Mar 2025 11:03:44 +0100 Subject: [PATCH 3/3] fix: tsc and lint errors --- govtool/frontend/src/context/appContext.tsx | 6 +++--- govtool/frontend/src/context/featureFlag.test.tsx | 3 ++- govtool/frontend/src/context/wallet.tsx | 2 -- govtool/frontend/src/models/api.ts | 10 +++++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/govtool/frontend/src/context/appContext.tsx b/govtool/frontend/src/context/appContext.tsx index 2d8cbfaf1..14c18c9ea 100644 --- a/govtool/frontend/src/context/appContext.tsx +++ b/govtool/frontend/src/context/appContext.tsx @@ -15,7 +15,7 @@ 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; @@ -26,7 +26,7 @@ type AppContextType = { isInBootstrapPhase: boolean; isFullGovernance: boolean; networkName: string; - network: "sanchonet" | "preview" | "testnet" | "preprod" | "mainnet"; + network: Network; cExplorerBaseUrl: string; epochParams?: EpochParams; networkMetrics?: NetworkMetrics; @@ -85,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/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/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index ead35c0d2..77c3de83a 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -2,7 +2,6 @@ import { createContext, useCallback, useContext, - useEffect, useMemo, useRef, useState, @@ -105,7 +104,6 @@ import { TransactionStateWithoutResource, usePendingTransaction, } from "./pendingTransaction"; -import { useAdaHandleContext } from "./adaHandle"; interface Props { children: React.ReactNode; diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index 9d82fd180..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: "sanchonet" | "preview" | "testnet" | "preprod" | "mainnet"; + networkName: Network; noOfCommitteeMembers: number; quorumNumerator: number; quorumDenominator: number;