+
0
diff --git a/apps/tangle-dapp/src/types/restake.ts b/apps/tangle-dapp/src/types/restake.ts
index 232f2b76c2..1fcba73ae8 100644
--- a/apps/tangle-dapp/src/types/restake.ts
+++ b/apps/tangle-dapp/src/types/restake.ts
@@ -58,7 +58,7 @@ export type EvmDelegationFormFields = {
assetId: Address;
};
-export type EvmUnstakeFormFields = EvmDelegationFormFields;
+export type EvmUndelegateFormFields = EvmDelegationFormFields;
export type EvmWithdrawFormFields = {
amount: string;
diff --git a/libs/tangle-shared-ui/src/context/RestakeContext.tsx b/libs/tangle-shared-ui/src/context/RestakeContext.tsx
index 48355514bc..93b073c91b 100644
--- a/libs/tangle-shared-ui/src/context/RestakeContext.tsx
+++ b/libs/tangle-shared-ui/src/context/RestakeContext.tsx
@@ -20,6 +20,7 @@ import {
} from 'react';
import { useAccount, useChainId } from 'wagmi';
import { Address } from 'viem';
+import EVMChainId from '@tangle-network/dapp-types/EVMChainId';
import { useEnvioHealthCheckByChainId } from '../utils/checkEnvioHealth';
import {
useRestakingAssets,
@@ -31,6 +32,7 @@ import {
type RestakeAssetMap,
} from '../data/graphql/useRestakeAssets';
import { useDelegator, type Delegator } from '../data/graphql/useDelegator';
+import { useOnChainDelegator } from '../data/restake/useOnChainDelegator';
import { useOperatorMap, type Operator } from '../data/graphql/useOperators';
import {
useProtocolConfig,
@@ -100,7 +102,8 @@ interface RestakeProviderProps {
export const RestakeProvider: FC = ({ children }) => {
const chainId = useChainId();
const { address: userAddress } = useAccount();
- const isLocalDev = chainId === 84532 || chainId === 31337;
+ const isLocalDev =
+ chainId === EVMChainId.BaseSepolia || chainId === EVMChainId.AnvilLocal;
// Check indexer health
const { data: isHealthy, isLoading: isCheckingHealth } =
@@ -146,13 +149,57 @@ export const RestakeProvider: FC = ({ children }) => {
enabled: !isCheckingHealth,
});
- // Fetch delegator info (user's positions and pending requests)
+ // Get token addresses from assets for on-chain delegator query
+ const tokenAddresses = useMemo(() => {
+ return assetList.map((asset) => asset.id);
+ }, [assetList]);
+
+ // Fetch delegator info from GraphQL (for rich data like delegations, requests)
const {
- data: delegator,
- isLoading: isLoadingDelegator,
- refetch: refetchDelegator,
+ data: graphqlDelegator,
+ isLoading: isLoadingGraphqlDelegator,
+ refetch: refetchGraphqlDelegator,
} = useDelegator(userAddress);
+ // ALWAYS fetch delegator deposit data from on-chain for accurate amounts
+ // The indexer may have stale delegatedAmount values
+ const {
+ data: onChainDelegator,
+ isLoading: isLoadingOnChainDelegator,
+ refetch: refetchOnChainDelegator,
+ } = useOnChainDelegator(userAddress, {
+ tokenAddresses,
+ enabled: !!userAddress && tokenAddresses.length > 0,
+ });
+
+ // Merge data: use on-chain for accurate deposit/delegation amounts,
+ // GraphQL for rich data (delegations, requests, etc.)
+ const delegator = useMemo(() => {
+ // If we have on-chain data, use it for asset positions (source of truth)
+ if (onChainDelegator) {
+ // If we also have GraphQL data, merge them
+ if (graphqlDelegator) {
+ return {
+ ...graphqlDelegator,
+ // Override with on-chain data for accurate amounts
+ assetPositions: onChainDelegator.assetPositions,
+ totalDeposited: onChainDelegator.totalDeposited,
+ totalDelegated: onChainDelegator.totalDelegated,
+ };
+ }
+ // Only on-chain data available
+ return onChainDelegator;
+ }
+ return graphqlDelegator ?? null;
+ }, [onChainDelegator, graphqlDelegator]);
+
+ const isLoadingDelegator =
+ isLoadingOnChainDelegator || isLoadingGraphqlDelegator;
+
+ const refetchDelegator = useCallback(async () => {
+ await Promise.all([refetchOnChainDelegator(), refetchGraphqlDelegator()]);
+ }, [refetchOnChainDelegator, refetchGraphqlDelegator]);
+
// Fetch operators
const {
data: operatorMap,
diff --git a/libs/tangle-shared-ui/src/data/graphql/index.ts b/libs/tangle-shared-ui/src/data/graphql/index.ts
index a2ba2a3453..2af2a1f8e6 100644
--- a/libs/tangle-shared-ui/src/data/graphql/index.ts
+++ b/libs/tangle-shared-ui/src/data/graphql/index.ts
@@ -18,13 +18,13 @@ export {
useDelegatorDeposits,
useDelegatorDelegations,
useDelegatorWithdrawRequests,
- useDelegatorUnstakeRequests,
+ useDelegatorUndelegateRequests,
useDelegatorCount,
type Delegator,
type DelegatorAssetPosition,
type DelegationPosition,
type WithdrawRequest,
- type UnstakeRequest,
+ type UndelegateRequest,
type RequestStatus,
type LockDuration,
type BlueprintSelectionMode,
@@ -65,7 +65,7 @@ export {
export {
useProtocolConfig,
useWithdrawalDelay,
- useUnstakeDelay,
+ useUndelegateDelay,
type ProtocolConfig,
} from './useProtocolConfig';
diff --git a/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts b/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts
index bd9026f383..418171c59e 100644
--- a/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts
+++ b/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts
@@ -15,6 +15,7 @@ import {
} from '../../utils/executeEnvioGraphQL';
import { useEnvioHealthCheckByChainId } from '../../utils/checkEnvioHealth';
import useNetworkStore from '../../context/useNetworkStore';
+import { parseAddressLowercase } from '../../utils/safeParseAddress';
// Request status enum
export type RequestStatus = 'PENDING' | 'READY' | 'EXECUTED' | 'CANCELLED';
@@ -65,8 +66,8 @@ export interface WithdrawRequest {
executedAt: bigint | null;
}
-// Unstake request
-export interface UnstakeRequest {
+// Undelegate request (mapped from GraphQL unstakeRequests)
+export interface UndelegateRequest {
id: string;
operatorId: Address;
token: Address;
@@ -92,7 +93,7 @@ export interface Delegator {
assetPositions: DelegatorAssetPosition[];
delegations: DelegationPosition[];
withdrawRequests: WithdrawRequest[];
- unstakeRequests: UnstakeRequest[];
+ unstakeRequests: UndelegateRequest[];
}
// GraphQL query for delegator (Hasura uses _by_pk for single row queries)
@@ -212,12 +213,27 @@ interface DelegatorQueryResult {
} | null;
}
+/**
+ * Parses a string to Address, using lowercase normalization.
+ * Indexer data may have inconsistent casing, so we normalize to lowercase.
+ * Throws if the address is invalid to fail fast on bad data.
+ */
+const parseAddress = (value: string): Address => {
+ const parsed = parseAddressLowercase(value);
+
+ if (!parsed) {
+ throw new Error(`Invalid address from indexer: ${value}`);
+ }
+
+ return parsed;
+};
+
// Parse delegator from GraphQL response
const parseDelegator = (
raw: NonNullable,
): Delegator => ({
id: raw.id,
- address: raw.address as Address,
+ address: parseAddress(raw.address),
totalDeposited: BigInt(raw.totalDeposited),
totalDelegated: BigInt(raw.totalDelegated),
createdAt: BigInt(raw.createdAt),
@@ -226,7 +242,7 @@ const parseDelegator = (
unstakeNonce: BigInt(raw.unstakeNonce),
assetPositions: raw.assetPositions.map((pos) => ({
id: pos.id,
- token: pos.token as Address,
+ token: parseAddress(pos.token),
totalDeposited: BigInt(pos.totalDeposited),
delegatedAmount: BigInt(pos.delegatedAmount),
lockedAmount: BigInt(pos.lockedAmount),
@@ -234,8 +250,8 @@ const parseDelegator = (
})),
delegations: raw.delegations.map((del) => ({
id: del.id,
- operatorId: del.operator.id as Address,
- token: del.token as Address,
+ operatorId: parseAddress(del.operator.id),
+ token: parseAddress(del.token),
shares: BigInt(del.shares),
lastKnownAmount: BigInt(del.lastKnownAmount),
blueprintSelection: del.blueprintSelection,
@@ -247,7 +263,7 @@ const parseDelegator = (
})),
withdrawRequests: raw.withdrawRequests.map((req) => ({
id: req.id,
- token: req.token as Address,
+ token: parseAddress(req.token),
nonce: BigInt(req.nonce),
amount: BigInt(req.amount),
requestedRound: BigInt(req.requestedRound),
@@ -257,8 +273,8 @@ const parseDelegator = (
})),
unstakeRequests: raw.unstakeRequests.map((req) => ({
id: req.id,
- operatorId: req.operator.id as Address,
- token: req.token as Address,
+ operatorId: parseAddress(req.operator.id),
+ token: parseAddress(req.token),
nonce: BigInt(req.nonce),
shares: BigInt(req.shares),
estimatedAmount: BigInt(req.estimatedAmount),
@@ -383,8 +399,8 @@ export const useDelegatorWithdrawRequests = (
};
};
-// Hook to get delegator's pending unstake requests
-export const useDelegatorUnstakeRequests = (
+// Hook to get delegator's pending undelegate requests
+export const useDelegatorUndelegateRequests = (
address: Address | undefined,
options?: {
network?: EnvioNetwork;
diff --git a/libs/tangle-shared-ui/src/data/graphql/useProtocolConfig.ts b/libs/tangle-shared-ui/src/data/graphql/useProtocolConfig.ts
index 46918b68c9..dc2ab5205d 100644
--- a/libs/tangle-shared-ui/src/data/graphql/useProtocolConfig.ts
+++ b/libs/tangle-shared-ui/src/data/graphql/useProtocolConfig.ts
@@ -244,9 +244,9 @@ export const useWithdrawalDelay = () => {
};
/**
- * Hook to get unstake delay in human-readable format.
+ * Hook to get undelegate delay in human-readable format.
*/
-export const useUnstakeDelay = () => {
+export const useUndelegateDelay = () => {
const { data: config, isLoading } = useProtocolConfig();
if (isLoading || !config) {
diff --git a/libs/tangle-shared-ui/src/data/graphql/useRestakingAssets.ts b/libs/tangle-shared-ui/src/data/graphql/useRestakingAssets.ts
index 85c389ec87..a794c4943e 100644
--- a/libs/tangle-shared-ui/src/data/graphql/useRestakingAssets.ts
+++ b/libs/tangle-shared-ui/src/data/graphql/useRestakingAssets.ts
@@ -5,13 +5,14 @@
import { useQuery } from '@tanstack/react-query';
import { Address } from 'viem';
-import { useChainId } from 'wagmi';
+import { useAccount, useChainId } from 'wagmi';
import {
executeEnvioGraphQL,
gql,
EnvioNetwork,
getEnvioNetworkFromChainId,
} from '../../utils/executeEnvioGraphQL';
+import useNetworkStore from '../../context/useNetworkStore';
import type { RestakingAsset } from '../restake/types';
// Re-export for backwards compatibility
@@ -90,7 +91,10 @@ export const useRestakingAssets = (options?: {
}) => {
const { network, enabledOnly = true, enabled = true } = options ?? {};
const chainId = useChainId();
- const resolvedNetwork = network ?? getEnvioNetworkFromChainId(chainId);
+ const { isConnected } = useAccount();
+ const networkChainId = useNetworkStore((store) => store.network2?.evmChainId);
+ const activeChainId = isConnected ? chainId : (networkChainId ?? chainId);
+ const resolvedNetwork = network ?? getEnvioNetworkFromChainId(activeChainId);
return useQuery({
queryKey: ['envio', 'restakingAssets', resolvedNetwork, enabledOnly],
@@ -110,7 +114,10 @@ export const useRestakingAssetMap = (options?: {
}) => {
const { network, enabledOnly = true, enabled = true } = options ?? {};
const chainId = useChainId();
- const resolvedNetwork = network ?? getEnvioNetworkFromChainId(chainId);
+ const { isConnected } = useAccount();
+ const networkChainId = useNetworkStore((store) => store.network2?.evmChainId);
+ const activeChainId = isConnected ? chainId : (networkChainId ?? chainId);
+ const resolvedNetwork = network ?? getEnvioNetworkFromChainId(activeChainId);
return useQuery({
queryKey: ['envio', 'restakingAssetMap', resolvedNetwork, enabledOnly],
diff --git a/libs/tangle-shared-ui/src/data/tx/index.ts b/libs/tangle-shared-ui/src/data/tx/index.ts
index 175ce7e1c2..ad075ae58f 100644
--- a/libs/tangle-shared-ui/src/data/tx/index.ts
+++ b/libs/tangle-shared-ui/src/data/tx/index.ts
@@ -9,11 +9,11 @@ export { useDepositTx, type DepositParams } from './useDepositTx';
// Delegate
export { useDelegateTx, type DelegateParams } from './useDelegateTx';
-// Undelegate (unstake)
+// Undelegate
export {
- useScheduleUnstakeTx,
- useExecuteUnstakeTx,
- type ScheduleUnstakeParams,
+ useScheduleUndelegateTx,
+ useExecuteUndelegateTx,
+ type ScheduleUndelegateParams,
} from './useUndelegateTx';
// Withdraw
diff --git a/libs/tangle-shared-ui/src/data/tx/useUndelegateTx.ts b/libs/tangle-shared-ui/src/data/tx/useUndelegateTx.ts
index 4af904e597..de8d798b00 100644
--- a/libs/tangle-shared-ui/src/data/tx/useUndelegateTx.ts
+++ b/libs/tangle-shared-ui/src/data/tx/useUndelegateTx.ts
@@ -1,5 +1,5 @@
/**
- * Hooks for undelegating (scheduling unstake) from an operator.
+ * Hooks for undelegating from an operator.
* Replaces the Substrate-based useRestakeUndelegateTx hook.
*/
@@ -9,20 +9,20 @@ import MULTI_ASSET_DELEGATION_ABI from '../../abi/multiAssetDelegation';
import { getContractsByChainId } from '@tangle-network/dapp-config/contracts';
import { useChainId } from 'wagmi';
-export interface ScheduleUnstakeParams {
+export interface ScheduleUndelegateParams {
operator: Address;
token: Address;
amount: bigint;
}
/**
- * Hook to schedule unstaking shares from an operator.
- * This initiates the unstaking process - shares will be available
- * for withdrawal after the unstaking delay (in rounds).
+ * Hook to schedule undelegation from an operator.
+ * This initiates the undelegation process - shares will be available
+ * for withdrawal after the undelegation delay (in rounds).
*
* @example
* ```tsx
- * const { execute, status, error } = useScheduleUnstakeTx();
+ * const { execute, status, error } = useScheduleUndelegateTx();
*
* await execute({
* operator: '0x...',
@@ -31,13 +31,13 @@ export interface ScheduleUnstakeParams {
* });
* ```
*/
-export const useScheduleUnstakeTx = () => {
+export const useScheduleUndelegateTx = () => {
const chainId = useChainId();
const contracts = getContractsByChainId(chainId);
return useContractWrite(
MULTI_ASSET_DELEGATION_ABI,
- (params: ScheduleUnstakeParams, _activeAddress) => ({
+ (params: ScheduleUndelegateParams, _activeAddress) => ({
address: contracts.multiAssetDelegation,
abi: MULTI_ASSET_DELEGATION_ABI,
functionName: 'scheduleDelegatorUnstake' as const,
@@ -51,16 +51,16 @@ export const useScheduleUnstakeTx = () => {
['Token', params.token],
['Amount', params.amount.toString()],
]),
- getSuccessMessage: () => 'Successfully scheduled unstake',
+ getSuccessMessage: () => 'Successfully scheduled undelegate',
},
);
};
/**
- * Hook to execute a scheduled unstake after the delay period.
+ * Hook to execute a scheduled undelegate after the delay period.
* Call this after the readyAtRound has been reached.
*/
-export const useExecuteUnstakeTx = () => {
+export const useExecuteUndelegateTx = () => {
const chainId = useChainId();
const contracts = getContractsByChainId(chainId);
@@ -74,9 +74,9 @@ export const useExecuteUnstakeTx = () => {
}),
{
txName: 'restake execute undelegate',
- getSuccessMessage: () => 'Successfully executed unstake',
+ getSuccessMessage: () => 'Successfully executed undelegate',
},
);
};
-export default useScheduleUnstakeTx;
+export default useScheduleUndelegateTx;
diff --git a/libs/tangle-shared-ui/src/utils/fetchErc20TokenMetadata.ts b/libs/tangle-shared-ui/src/utils/fetchErc20TokenMetadata.ts
index abb1084ea2..671e1e7446 100644
--- a/libs/tangle-shared-ui/src/utils/fetchErc20TokenMetadata.ts
+++ b/libs/tangle-shared-ui/src/utils/fetchErc20TokenMetadata.ts
@@ -3,8 +3,10 @@ import { EvmAddress } from '@tangle-network/ui-components/types/address';
import { ContractFunctionName, erc20Abi, PublicClient } from 'viem';
import { z } from 'zod';
import { cacheTokenMetadata } from '@tangle-network/dapp-config/tokenMetadata';
+import EVMChainId from '@tangle-network/dapp-types/EVMChainId';
-const DEFAULT_CHAIN_ID = 1; // Fallback to Ethereum mainnet if chain ID unavailable
+/** Fallback to Ethereum mainnet if chain ID unavailable */
+const DEFAULT_CHAIN_ID = EVMChainId.EthereumMainNet;
type TokenMetadata = {
id: EvmAddress;
diff --git a/libs/tangle-shared-ui/src/utils/parseTransactionError.ts b/libs/tangle-shared-ui/src/utils/parseTransactionError.ts
new file mode 100644
index 0000000000..9e93d7e39d
--- /dev/null
+++ b/libs/tangle-shared-ui/src/utils/parseTransactionError.ts
@@ -0,0 +1,124 @@
+/**
+ * Parses transaction errors into user-friendly messages.
+ *
+ * This utility handles common blockchain transaction errors and provides
+ * clear, actionable messages for users.
+ */
+
+/** Common transaction error patterns and their user-friendly messages */
+const ERROR_PATTERNS: Array<{ pattern: string; message: string }> = [
+ {
+ pattern: 'intrinsic gas too low',
+ message: 'Transaction failed: Gas limit too low. Please try again.',
+ },
+ {
+ pattern: 'insufficient funds',
+ message: 'Insufficient funds to cover gas fees.',
+ },
+ {
+ pattern: 'user rejected',
+ message: 'Transaction was rejected by user.',
+ },
+ {
+ pattern: 'User rejected',
+ message: 'Transaction was rejected by user.',
+ },
+ {
+ pattern: 'Failed to fetch',
+ message:
+ 'Network request failed. Please check your connection and try again.',
+ },
+ {
+ pattern: 'nonce too low',
+ message: 'Transaction nonce conflict. Please try again.',
+ },
+ {
+ pattern: 'already claimed',
+ message: 'This address has already claimed its allocation.',
+ },
+ {
+ pattern: 'InvalidMerkleProof',
+ message:
+ 'Invalid Merkle proof for this account and amount. Double-check you selected the correct account.',
+ },
+ {
+ pattern: 'invalid proof',
+ message: 'Invalid proof. Please regenerate and try again.',
+ },
+ {
+ pattern: 'not in merkle tree',
+ message: 'Address not found in the merkle tree.',
+ },
+ {
+ pattern: 'claim period ended',
+ message: 'The claim period has ended.',
+ },
+ {
+ pattern: 'paused',
+ message: 'This operation is currently paused.',
+ },
+ {
+ pattern: 'execution reverted',
+ message: 'Transaction execution reverted. Please try again.',
+ },
+ {
+ pattern: 'timeout',
+ message: 'Transaction timed out. Please try again.',
+ },
+ {
+ pattern: 'replacement fee too low',
+ message: 'Gas price too low to replace pending transaction.',
+ },
+ {
+ pattern: 'already known',
+ message: 'Transaction already submitted. Please wait for confirmation.',
+ },
+];
+
+/** Maximum length for error messages before truncation */
+const MAX_ERROR_LENGTH = 100;
+
+/**
+ * Parses a transaction error into a user-friendly message.
+ *
+ * @param error - The error object or string to parse
+ * @returns A user-friendly error message
+ *
+ * @example
+ * ```ts
+ * try {
+ * await submitTransaction();
+ * } catch (err) {
+ * const message = parseTransactionError(err as Error);
+ * toast.error(message);
+ * }
+ * ```
+ */
+const parseTransactionError = (error: Error | string): string => {
+ const message = typeof error === 'string' ? error : error.message || '';
+ const lowerMessage = message.toLowerCase();
+
+ // Check for known error patterns
+ for (const { pattern, message: errorMessage } of ERROR_PATTERNS) {
+ if (lowerMessage.includes(pattern.toLowerCase())) {
+ return errorMessage;
+ }
+ }
+
+ // Extract "Details:" section if present (common in some RPC errors)
+ const detailsMatch = message.match(/Details:\s*([^V]+?)(?:Version:|$)/);
+
+ if (detailsMatch) {
+ return detailsMatch[1].trim();
+ }
+
+ // Fallback: truncate long messages
+ if (message.length > MAX_ERROR_LENGTH) {
+ return 'Transaction failed. Please try again.';
+ }
+
+ // Return the original message if it's reasonably short
+ return message || 'An unknown error occurred.';
+};
+
+export default parseTransactionError;
diff --git a/libs/tangle-shared-ui/src/utils/safeParseAddress.ts b/libs/tangle-shared-ui/src/utils/safeParseAddress.ts
new file mode 100644
index 0000000000..cb6157b5ca
--- /dev/null
+++ b/libs/tangle-shared-ui/src/utils/safeParseAddress.ts
@@ -0,0 +1,133 @@
+/**
+ * Safe address parsing utilities for converting strings to typed Address values.
+ *
+ * These utilities help reduce unsafe `as Address` type assertions by providing
+ * proper validation before type conversion.
+ */
+
+import { Address, getAddress, isAddress } from 'viem';
+
+/**
+ * Safely parses a string to a viem Address type.
+ * Returns the checksummed address if valid, or null if invalid.
+ *
+ * @param value - The string value to parse as an address
+ * @returns The checksummed Address or null if invalid
+ *
+ * @example
+ * ```ts
+ * const address = safeParseAddress(rawData.address);
+ * if (address) {
+ * // address is typed as Address
+ * console.log('Valid address:', address);
+ * }
+ * ```
+ */
+export const safeParseAddress = (value: string | undefined): Address | null => {
+ if (!value || !isAddress(value)) {
+ return null;
+ }
+
+ try {
+ return getAddress(value);
+ } catch {
+ return null;
+ }
+};
+
+/**
+ * Parses a string to an Address type, throwing an error if invalid.
+ * Use this when you expect the value to always be a valid address.
+ *
+ * @param value - The string value to parse as an address
+ * @param fieldName - Optional field name for better error messages
+ * @returns The checksummed Address
+ * @throws Error if the value is not a valid address
+ *
+ * @example
+ * ```ts
+ * // In a parser where address is required
+ * const address = parseAddressOrThrow(rawData.address, 'delegator address');
+ * ```
+ */
+export const parseAddressOrThrow = (
+ value: string | undefined,
+ fieldName = 'address',
+): Address => {
+ if (!value) {
+ throw new Error(`${fieldName} is required but was undefined`);
+ }
+
+ if (!isAddress(value)) {
+ throw new Error(`${fieldName} is not a valid EVM address: ${value}`);
+ }
+
+ try {
+ return getAddress(value);
+ } catch (error) {
+ throw new Error(
+ `Failed to parse ${fieldName}: ${error instanceof Error ? error.message : 'unknown error'}`,
+ );
+ }
+};
+
+/**
+ * Parses a string to a lowercase Address type.
+ * Useful for matching addresses that may have inconsistent casing (e.g., from indexers).
+ *
+ * @param value - The string value to parse as an address
+ * @returns The lowercase Address or null if invalid
+ *
+ * @example
+ * ```ts
+ * // For indexer data that may have inconsistent casing
+ * const address = parseAddressLowercase(indexerData.token);
+ * ```
+ */
+export const parseAddressLowercase = (
+ value: string | undefined,
+): Address | null => {
+ if (!value || !isAddress(value)) {
+ return null;
+ }
+
+ return value.toLowerCase() as Address;
+};
+
+/**
+ * Validates and normalizes an address, returning a consistent format.
+ * Useful for comparing addresses from different sources.
+ *
+ * @param value - The string value to normalize
+ * @returns The checksummed Address or null if invalid
+ */
+export const normalizeAddress = (value: string | undefined): Address | null => {
+ return safeParseAddress(value);
+};
+
+/**
+ * Checks if two addresses are equal (case-insensitive comparison).
+ *
+ * @param a - First address to compare
+ * @param b - Second address to compare
+ * @returns true if addresses are equal, false otherwise
+ *
+ * @example
+ * ```ts
+ * if (addressesEqual(userAddress, tokenAddress)) {
+ * // addresses match
+ * }
+ * ```
+ */
+export const addressesEqual = (
+ a: string | undefined,
+ b: string | undefined,
+): boolean => {
+ if (!a || !b) {
+ return false;
+ }
+
+ return a.toLowerCase() === b.toLowerCase();
+};
+
+export default safeParseAddress;