diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx index 66e9c805..809ebc0e 100644 --- a/src/contexts/events.tsx +++ b/src/contexts/events.tsx @@ -8,6 +8,8 @@ import { calculateTotalReceive } from '../components/FeeCollapse'; import { QuoteService } from '../services/quotes'; import { useVortexAccount } from '../hooks/useVortexAccount'; import { Networks } from '../helpers/networks'; +import { storageService } from '../services/storage/local'; +import { LocalStorageKeys } from '../hooks/useLocalStorage'; declare global { interface Window { @@ -109,6 +111,18 @@ export interface FormErrorEvent { | 'more_than_maximum_withdrawal'; } +export interface InitializationErrorEvent { + event: 'initialization_error'; + error_message: InitializationErrorMessage; +} + +type InitializationErrorMessage = + | 'node_connection_issue' + | 'signer_service_issue' + | 'moonbeam_account_issue' + | 'stellar_account_issue' + | 'pendulum_account_issue'; + export type TrackableEvent = | AmountTypeEvent | ClickDetailsEvent @@ -122,7 +136,8 @@ export type TrackableEvent = | SigningRequestedEvent | TransactionSignedEvent | ProgressEvent - | NetworkChangeEvent; + | NetworkChangeEvent + | InitializationErrorEvent; type EventType = TrackableEvent['event']; @@ -154,6 +169,19 @@ const useEvents = () => { } } + if (event.event === 'initialization_error') { + const eventsStored = storageService.getParsed>( + LocalStorageKeys.FIRED_INITIALIZATION_EVENTS, + ); + const eventsSet = eventsStored ? new Set(eventsStored) : new Set(); + if (eventsSet.has(event.error_message)) { + return; + } else { + eventsSet.add(event.error_message); + storageService.set(LocalStorageKeys.FIRED_INITIALIZATION_EVENTS, Array.from(eventsSet)); + } + } + // Check if form error message has already been fired as we only want to fire each error message once if (event.event === 'form_error') { const { error_message } = event; @@ -301,3 +329,7 @@ export function createTransactionEvent( to_amount: calculateTotalReceive(Big(state.outputAmount.units), OUTPUT_TOKEN_CONFIG[state.outputTokenType]), }; } + +export function clearPersistentErrorEventStore() { + storageService.remove(LocalStorageKeys.FIRED_INITIALIZATION_EVENTS); +} diff --git a/src/hooks/offramp/useOfframpAdvancement.ts b/src/hooks/offramp/useOfframpAdvancement.ts index 2e5d8cbb..ca28c91b 100644 --- a/src/hooks/offramp/useOfframpAdvancement.ts +++ b/src/hooks/offramp/useOfframpAdvancement.ts @@ -35,7 +35,6 @@ export const useOfframpAdvancement = () => { if (isProcessingAdvance.current) return; isProcessingAdvance.current = true; if (!pendulumNode || !assetHubNode) { - console.error('Polkadot nodes not initialized'); return; } diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 4e45cc43..5c5bdf49 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -118,4 +118,5 @@ export enum LocalStorageKeys { SELECTED_NETWORK = 'SELECTED_NETWORK', SELECTED_POLKADOT_WALLET = 'SELECTED_POLKADOT_WALLET', SELECTED_POLKADOT_ACCOUNT = 'SELECTED_POLKADOT_ACCOUNT', + FIRED_INITIALIZATION_EVENTS = 'FIRED_INITIALIZATION_EVENTS', } diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index e831a91c..7af161c7 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -27,7 +27,7 @@ import { } from '../../constants/tokenConfig'; import { config } from '../../config'; -import { useEventsContext } from '../../contexts/events'; +import { useEventsContext, clearPersistentErrorEventStore } from '../../contexts/events'; import { useNetwork } from '../../contexts/network'; import { usePendulumNode } from '../../contexts/polkadotNode'; @@ -40,8 +40,6 @@ import { useTokenOutAmount } from '../../hooks/nabla/useTokenAmountOut'; import { useMainProcess } from '../../hooks/offramp/useMainProcess'; import { useSwapUrlParams } from './useSwapUrlParams'; -import { initialChecks } from '../../services/initialChecks'; - import { BaseLayout } from '../../layouts'; import { ProgressPage } from '../progress'; import { FailurePage } from '../failure'; @@ -59,6 +57,12 @@ import { swapConfirm } from './helpers/swapConfirm'; import { TrustedBy } from '../../components/TrustedBy'; import { WhyVortex } from '../../components/WhyVortex'; import { usePolkadotWalletState } from '../../contexts/polkadotWallet'; +import { + MoonbeamFundingAccountError, + PendulumFundingAccountError, + StellarFundingAccountError, + useSigningService, +} from '../../services/signingService'; import { OfframpSummaryDialog } from '../../components/OfframpSummaryDialog'; import satoshipayLogo from '../../assets/logo/satoshipay.svg'; @@ -71,7 +75,7 @@ export const SwapPage = () => { const { isDisconnected, address } = useVortexAccount(); const [initializeFailedMessage, setInitializeFailedMessage] = useState(null); const [apiInitializeFailed, setApiInitializeFailed] = useState(false); - const [_, setIsReady] = useState(false); + const [isReady, setIsReady] = useState(false); const [showCompareFees, setShowCompareFees] = useState(false); const [isOfframpSummaryDialogVisible, setIsOfframpSummaryDialogVisible] = useState(false); const [cachedAnchorUrl, setCachedAnchorUrl] = useState(undefined); @@ -81,16 +85,46 @@ export const SwapPage = () => { const { walletAccount } = usePolkadotWalletState(); const [termsAnimationKey, setTermsAnimationKey] = useState(0); + const { + error: signingServiceError, + isLoading: isSigningServiceLoading, + isError: isSigningServiceError, + } = useSigningService(); const { setTermsAccepted, toggleTermsChecked, termsChecked, termsAccepted, termsError, setTermsError } = useTermsAndConditions(); useEffect(() => { - setApiInitializeFailed(!pendulumNode.apiComponents?.api && pendulumNode?.isFetched); + if (!pendulumNode.apiComponents?.api && pendulumNode?.isFetched) { + setApiInitializeFailed(true); + trackEvent({ event: 'initialization_error', error_message: 'node_connection_issue' }); + } if (pendulumNode.apiComponents?.api) { setApi(pendulumNode.apiComponents.api); } - }, [pendulumNode]); + }, [pendulumNode, trackEvent, setApiInitializeFailed]); + + useEffect(() => { + if (isSigningServiceError && !isSigningServiceLoading) { + if (signingServiceError instanceof StellarFundingAccountError) { + trackEvent({ event: 'initialization_error', error_message: 'stellar_account_issue' }); + } else if (signingServiceError instanceof PendulumFundingAccountError) { + trackEvent({ event: 'initialization_error', error_message: 'pendulum_account_issue' }); + } else if (signingServiceError instanceof MoonbeamFundingAccountError) { + trackEvent({ event: 'initialization_error', error_message: 'moonbeam_account_issue' }); + } else { + trackEvent({ event: 'initialization_error', error_message: 'signer_service_issue' }); + } + setInitializeFailed(); + } + }, [isSigningServiceLoading, isSigningServiceError, signingServiceError, trackEvent]); + + useEffect(() => { + if (api && !isSigningServiceError && !isSigningServiceLoading) { + setIsReady(true); + clearPersistentErrorEventStore(); + } + }, [api, isSigningServiceError, isSigningServiceLoading]); // Maybe go into a state of UI errors?? const setInitializeFailed = useCallback((message?: string | null) => { @@ -100,19 +134,6 @@ export const SwapPage = () => { ); }, []); - useEffect(() => { - const initialize = async () => { - try { - await initialChecks(); - setIsReady(true); - } catch (error) { - setInitializeFailed(); - } - }; - - initialize(); - }, [setInitializeFailed]); - // Main process hook const { handleOnSubmit, diff --git a/src/services/signingService.tsx b/src/services/signingService.tsx index 8ba7f28e..4af19f16 100644 --- a/src/services/signingService.tsx +++ b/src/services/signingService.tsx @@ -1,3 +1,4 @@ +import { useQuery } from '@tanstack/react-query'; import { SIGNING_SERVICE_URL } from '../constants/constants'; import { OutputTokenType } from '../constants/tokenConfig'; @@ -26,28 +27,86 @@ export interface SignerServiceSep10Request { usesMemo?: boolean; } -// @todo: implement @tanstack/react-query +// Generic error for signing service +export class SigningServiceError extends Error { + constructor(message: string) { + super(message); + this.name = 'SigningServiceError'; + } +} + +// Specific errors for each funding account +export class StellarFundingAccountError extends SigningServiceError { + constructor() { + super('Stellar account is inactive'); + this.name = 'StellarFundingAccountError'; + } +} + +export class PendulumFundingAccountError extends SigningServiceError { + constructor() { + super('Pendulum account is inactive'); + this.name = 'PendulumFundingAccountError'; + } +} + +export class MoonbeamFundingAccountError extends SigningServiceError { + constructor() { + super('Moonbeam account is inactive'); + this.name = 'MoonbeamFundingAccountError'; + } +} + export const fetchSigningServiceAccountId = async (): Promise => { try { - const serviceResponse: SigningServiceStatus = await (await fetch(`${SIGNING_SERVICE_URL}/v1/status`)).json(); - const allServicesActive = Object.values(serviceResponse).every((service: AccountStatusResponse) => service.status); + const response = await fetch(`${SIGNING_SERVICE_URL}/v1/status`); + if (!response.ok) { + throw new SigningServiceError('Failed to fetch signing service status'); + } - if (allServicesActive) { - return { - stellar: serviceResponse.stellar, - pendulum: serviceResponse.pendulum, - moonbeam: serviceResponse.moonbeam, - }; + const serviceResponse: SigningServiceStatus = await response.json(); + + if (!serviceResponse.stellar?.status) { + throw new StellarFundingAccountError(); + } + if (!serviceResponse.pendulum?.status) { + throw new PendulumFundingAccountError(); + } + if (!serviceResponse.moonbeam?.status) { + throw new MoonbeamFundingAccountError(); } - // we really want to throw for both cases: accounts not funded, or service down. - throw new Error('One or more funding accounts are inactive'); + return { + stellar: serviceResponse.stellar, + pendulum: serviceResponse.pendulum, + moonbeam: serviceResponse.moonbeam, + }; } catch (error) { + if (error instanceof SigningServiceError) { + throw error; + } console.error('Signing service is down: ', error); - throw new Error('Signing service is down'); + throw new SigningServiceError('Signing service is down'); } }; +export const useSigningService = () => { + return useQuery({ + queryKey: ['signingService'], + queryFn: fetchSigningServiceAccountId, + retry: (failureCount, error) => { + if ( + error instanceof StellarFundingAccountError || + error instanceof PendulumFundingAccountError || + error instanceof MoonbeamFundingAccountError + ) { + return false; + } + return failureCount < 3; + }, + }); +}; + export const fetchSep10Signatures = async ({ challengeXDR, outToken,