import { getMainAccount } from "@ledgerhq/live-common/account/index"; import { applyMemoToTransaction } from "@ledgerhq/live-common/bridge/descriptor/send/memo"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; import type { Memo } from "@ledgerhq/live-common/flows/send/types"; import type { TransactionStatus } from "@ledgerhq/live-common/generated/types"; import type { Account, AccountLike } from "@ledgerhq/types-live"; import { useCallback, useMemo, useRef, useState } from "react"; import type { BridgeValidationErrors, BridgeValidationWarnings } from "../types"; export type BridgeRecipientValidationResult = { errors: BridgeValidationErrors; warnings: BridgeValidationWarnings; isLoading: boolean; status: TransactionStatus | null; cleanup: () => void; }; type UseBridgeRecipientValidationProps = { recipient: string; account: AccountLike | null; parentAccount?: Account | null; memo?: Memo; enabled?: boolean; }; const DEBOUNCE_DELAY = 300; /** * Hook to validate recipient address using the bridge transaction status. * This hook leverages the existing bridge infrastructure to get * recipient and sender validation errors/warnings. */ export function useBridgeRecipientValidation({ recipient, account, parentAccount, memo, enabled = true, }: UseBridgeRecipientValidationProps): BridgeRecipientValidationResult { const [validationState, setValidationState] = useState<{ errors: BridgeValidationErrors; warnings: BridgeValidationWarnings; isLoading: boolean; status: TransactionStatus | null; }>({ errors: {}, warnings: {}, isLoading: false, status: null, }); const lastRecipientRef = useRef(""); const validationTriggeredRef = useRef(false); const debounceTimeoutRef = useRef(null); const abortControllerRef = useRef(null); // Cleanup function to clear timeout and abort pending validations const cleanup = useCallback(() => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); debounceTimeoutRef.current = null; } if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } }, []); const validateRecipient = useCallback(async () => { // Clear timeout reference when callback executes debounceTimeoutRef.current = null; if (!account || !recipient || !enabled) { setValidationState({ errors: {}, warnings: {}, isLoading: false, status: null, }); return; } // Cancel any pending validation if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; setValidationState(prev => ({ ...prev, isLoading: true })); try { const mainAccount = getMainAccount(account, parentAccount); const bridge = getAccountBridge(account, parentAccount); let transaction = bridge.createTransaction(mainAccount); transaction = bridge.updateTransaction(transaction, { recipient }); if (memo) { const memoUpdates = applyMemoToTransaction( transaction.family, memo.value, memo.type, transaction, ); transaction = bridge.updateTransaction(transaction, memoUpdates); } const preparedTransaction = await bridge.prepareTransaction(mainAccount, transaction); if (signal.aborted) return; const status = await bridge.getTransactionStatus(mainAccount, preparedTransaction); if (signal.aborted) return; const errors: BridgeValidationErrors = {}; const warnings: BridgeValidationWarnings = {}; if (status.errors.recipient) { errors.recipient = status.errors.recipient; } if (status.errors.sender) { errors.sender = status.errors.sender; } Object.entries(status.warnings).forEach(([key, value]) => { if (value) { warnings[key] = value; } }); setValidationState({ errors, warnings, isLoading: false, status, }); } catch (error) { if (signal.aborted) return; console.error("Bridge recipient validation failed:", error); setValidationState({ errors: {}, warnings: {}, isLoading: false, status: null, }); } }, [account, recipient, enabled, parentAccount, memo]); if (recipient !== lastRecipientRef.current) { lastRecipientRef.current = recipient; validationTriggeredRef.current = false; if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); debounceTimeoutRef.current = null; } if (!recipient) { setValidationState({ errors: {}, warnings: {}, isLoading: false, status: null, }); } } if (recipient && !validationTriggeredRef.current && enabled) { validationTriggeredRef.current = true; if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } setValidationState(prev => ({ ...prev, isLoading: true })); debounceTimeoutRef.current = setTimeout(() => { validateRecipient(); }, DEBOUNCE_DELAY); } return useMemo( () => ({ errors: validationState.errors, warnings: validationState.warnings, isLoading: validationState.isLoading, status: validationState.status, cleanup, }), [validationState, cleanup], ); }