@tippingchain/ui-react
Version:
TippingChain UI - React components
324 lines (288 loc) • 9.71 kB
text/typescript
// src/hooks/useRelayProgress.ts
import { useState, useEffect, useRef, useCallback } from 'react';
import { RelayStatus, RelayUpdate } from '@tippingchain/sdk';
import { useNotifications } from '../components/notifications';
export interface RelayProgressState {
status: RelayStatus['status'];
progress: number; // 0-100
relayId: string | null;
sourceTransactionHash: string | null;
destinationTransactionHash: string | null;
estimatedCompletionTime: number | null;
actualCompletionTime: number | null;
error: string | null;
isTracking: boolean;
}
export interface UseRelayProgressOptions {
enableNotifications?: boolean;
autoStart?: boolean;
maxWaitTime?: number;
onStatusChange?: (update: RelayUpdate) => void;
onComplete?: (status: RelayStatus) => void;
onError?: (error: string) => void;
}
export const useRelayProgress = (
relayId: string | null,
sourceChainId: number | null,
destinationChainId: number | null,
sourceTransactionHash: string | null,
sdk: any, // ApeChainTippingSDK instance
options: UseRelayProgressOptions = {}
) => {
const {
enableNotifications = true,
autoStart = true,
maxWaitTime = 600000, // 10 minutes
onStatusChange,
onComplete,
onError
} = options;
const [state, setState] = useState<RelayProgressState>({
status: 'initiated',
progress: 0,
relayId,
sourceTransactionHash,
destinationTransactionHash: null,
estimatedCompletionTime: null,
actualCompletionTime: null,
error: null,
isTracking: false
});
const { addNotification, updateNotification } = useNotifications();
const trackingPromiseRef = useRef<Promise<RelayStatus> | null>(null);
const notificationIdRef = useRef<string | null>(null);
// Get status display info
const getStatusDisplayInfo = useCallback((status: RelayStatus['status'], progress: number) => {
switch (status) {
case 'initiated':
return {
title: 'Relay Initiated',
message: 'Cross-chain relay has been started',
emoji: '🚀',
type: 'info' as const
};
case 'pending':
return {
title: 'Source Transaction Pending',
message: 'Waiting for source transaction confirmation',
emoji: '⏳',
type: 'pending' as const
};
case 'relaying':
return {
title: 'Relaying Across Chains',
message: `Bridging to ApeChain (${progress}% complete)`,
emoji: '🌉',
type: 'pending' as const
};
case 'completed':
return {
title: 'Relay Complete! ✅',
message: 'Funds successfully bridged to ApeChain',
emoji: '✅',
type: 'success' as const
};
case 'failed':
return {
title: 'Relay Failed',
message: 'Cross-chain relay encountered an error',
emoji: '❌',
type: 'error' as const
};
default:
return {
title: 'Relay Status Unknown',
message: `Status: ${status}`,
emoji: '❓',
type: 'info' as const
};
}
}, []);
// Start tracking relay progress
const startTracking = useCallback(async (
rId: string,
sourceChain: number,
destChain: number,
sourceTxHash: string
) => {
if (!sdk?.relayStatus) {
console.error('SDK relay status service not available');
return;
}
setState(prev => ({ ...prev, isTracking: true, error: null }));
try {
const sourceChainObj = sdk.getChainById(sourceChain);
const destChainObj = sdk.getChainById(destChain);
if (!sourceChainObj || !destChainObj) {
throw new Error(`Unsupported chain: source ${sourceChain}, dest ${destChain}`);
}
// Create initial notification
if (enableNotifications) {
notificationIdRef.current = addNotification({
type: 'pending',
title: 'Starting Cross-chain Relay 🚀',
message: 'Tracking relay progress to ApeChain...',
duration: 0,
transactionHash: sourceTxHash,
chainId: sourceChain
});
}
// Start tracking with callback
trackingPromiseRef.current = sdk.relayStatus.trackRelayWithCallback(
rId,
sourceChainObj,
destChainObj,
sourceTxHash,
(update: RelayUpdate) => {
setState(prev => ({
...prev,
status: update.status,
progress: update.progress,
destinationTransactionHash: update.destinationTransactionHash || prev.destinationTransactionHash,
error: update.error || null,
actualCompletionTime: update.status === 'completed' ? update.timestamp : null
}));
// Update notification
if (enableNotifications && notificationIdRef.current) {
const displayInfo = getStatusDisplayInfo(update.status, update.progress);
updateNotification(notificationIdRef.current, {
type: displayInfo.type,
title: displayInfo.title,
message: displayInfo.message,
transactionHash: update.destinationTransactionHash || sourceTxHash,
chainId: update.destinationTransactionHash ? destChain : sourceChain,
duration: displayInfo.type === 'success' || displayInfo.type === 'error' ? 8000 : 0
});
}
// Call custom callback
onStatusChange?.(update);
},
{ maxWaitTime }
);
const finalStatus = await trackingPromiseRef.current;
setState(prev => ({
...prev,
isTracking: false,
destinationTransactionHash: finalStatus.destinationTransactionHash || prev.destinationTransactionHash,
estimatedCompletionTime: finalStatus.estimatedCompletionTime || null,
actualCompletionTime: finalStatus.actualCompletionTime || Date.now()
}));
// Call completion callback
onComplete?.(finalStatus);
return finalStatus;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown relay error';
setState(prev => ({
...prev,
error: errorMessage,
isTracking: false,
status: 'failed'
}));
// Update notification with error
if (enableNotifications && notificationIdRef.current) {
updateNotification(notificationIdRef.current, {
type: 'error',
title: 'Relay Tracking Failed',
message: errorMessage,
duration: 8000
});
}
onError?.(errorMessage);
throw error;
}
}, [sdk, enableNotifications, maxWaitTime, getStatusDisplayInfo, addNotification, updateNotification, onStatusChange, onComplete, onError]);
// Stop tracking
const stopTracking = useCallback(() => {
if (relayId && sdk?.relayStatus) {
sdk.relayStatus.cancelRelayTracking(relayId);
}
trackingPromiseRef.current = null;
setState(prev => ({ ...prev, isTracking: false }));
}, [relayId, sdk]);
// Manual track function
const trackRelay = useCallback((
rId: string,
sourceChain: number,
destChain: number,
sourceTxHash: string
) => {
return startTracking(rId, sourceChain, destChain, sourceTxHash);
}, [startTracking]);
// Get estimated time remaining
const getEstimatedTimeRemaining = useCallback((): number | null => {
if (!state.estimatedCompletionTime) return null;
const remaining = state.estimatedCompletionTime - Date.now();
return Math.max(0, remaining);
}, [state.estimatedCompletionTime]);
// Format time remaining
const formatTimeRemaining = useCallback((): string | null => {
const remaining = getEstimatedTimeRemaining();
if (!remaining) return null;
const minutes = Math.floor(remaining / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
if (minutes > 0) {
return `~${minutes}m ${seconds}s`;
}
return `~${seconds}s`;
}, [getEstimatedTimeRemaining]);
// Auto-start tracking when parameters are available
useEffect(() => {
if (
autoStart &&
relayId &&
sourceChainId &&
destinationChainId &&
sourceTransactionHash &&
sdk
) {
startTracking(relayId, sourceChainId, destinationChainId, sourceTransactionHash);
}
return () => {
stopTracking();
};
}, [
relayId,
sourceChainId,
destinationChainId,
sourceTransactionHash,
autoStart,
sdk,
startTracking,
stopTracking
]);
// Reset state when key parameters change
useEffect(() => {
setState(prev => ({
...prev,
relayId,
sourceTransactionHash,
destinationTransactionHash: null,
estimatedCompletionTime: null,
actualCompletionTime: null,
status: 'initiated',
progress: 0,
error: null
}));
}, [relayId, sourceTransactionHash]);
return {
// State
...state,
// Control functions
trackRelay,
stopTracking,
// Helper functions
getEstimatedTimeRemaining,
formatTimeRemaining,
// Status helpers
isInitiated: state.status === 'initiated',
isPending: state.status === 'pending',
isRelaying: state.status === 'relaying',
isCompleted: state.status === 'completed',
isFailed: state.status === 'failed',
isActive: state.status === 'pending' || state.status === 'relaying',
// Progress helpers
progressPercent: `${state.progress}%`,
isNearComplete: state.progress >= 90,
hasDestinationTx: !!state.destinationTransactionHash
};
};