From d72d8e83b083ec0978eba7027113007e193904f6 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 21 Jan 2025 15:20:13 -0300 Subject: [PATCH 1/6] initialization error events --- src/contexts/events.tsx | 34 ++++++++- src/hooks/offramp/useOfframpAdvancement.ts | 1 - src/hooks/useLocalStorage.ts | 1 + src/pages/swap/index.tsx | 55 +++++++++----- src/services/signingService.tsx | 83 ++++++++++++++++++---- 5 files changed, 143 insertions(+), 31 deletions(-) diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx index 66e9c805..9620958c 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: InializationErrorMessage; +} + +type InializationErrorMessage = + | '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 45b836b6..dbf6a015 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'; import { useSiweContext } from '../../contexts/siwe'; @@ -41,8 +41,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'; @@ -60,6 +58,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'; export const SwapPage = () => { const formRef = useRef(null); @@ -78,17 +82,47 @@ 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]); + 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) => { setInitializeFailedMessage( @@ -97,19 +131,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, From fbbc18c73d28663b14cae852f4978693eae3d8a8 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 21 Jan 2025 15:35:52 -0300 Subject: [PATCH 2/6] disable confirm button until services ready --- src/pages/swap/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index dbf6a015..0c5b4b21 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -73,7 +73,7 @@ export const SwapPage = () => { const { 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 [cachedId, setCachedId] = useState(undefined); const { trackEvent } = useEventsContext(); @@ -118,6 +118,7 @@ export const SwapPage = () => { useEffect(() => { if (api && !isSigningServiceError && !isSigningServiceLoading) { + console.log('API and signing service are ready.'); setIsReady(true); clearPersistentErrorEventStore(); } @@ -471,7 +472,9 @@ export const SwapPage = () => { ) : ( )} From 43c6059ccef51d8fb240b42070fe3a05330005ba Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 22 Jan 2025 13:49:09 -0300 Subject: [PATCH 3/6] trigger mock event --- src/pages/swap/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 0c5b4b21..4160f083 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -119,6 +119,7 @@ export const SwapPage = () => { useEffect(() => { if (api && !isSigningServiceError && !isSigningServiceLoading) { console.log('API and signing service are ready.'); + trackEvent({ event: 'initialization_error', error_message: 'pendulum_account_issue' }); // Testing only setIsReady(true); clearPersistentErrorEventStore(); } From 5c408149dbca3e447aa04c9eb84568b991b0c012 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 22 Jan 2025 13:59:50 -0300 Subject: [PATCH 4/6] remove test firing --- src/pages/swap/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 4160f083..787585cd 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -118,8 +118,6 @@ export const SwapPage = () => { useEffect(() => { if (api && !isSigningServiceError && !isSigningServiceLoading) { - console.log('API and signing service are ready.'); - trackEvent({ event: 'initialization_error', error_message: 'pendulum_account_issue' }); // Testing only setIsReady(true); clearPersistentErrorEventStore(); } From b87081eb62dff743eeda3d2e23a413ea082c39a2 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 29 Jan 2025 08:51:49 -0300 Subject: [PATCH 5/6] typos, comments --- src/contexts/events.tsx | 6 +++--- src/pages/swap/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx index 9620958c..809ebc0e 100644 --- a/src/contexts/events.tsx +++ b/src/contexts/events.tsx @@ -113,10 +113,10 @@ export interface FormErrorEvent { export interface InitializationErrorEvent { event: 'initialization_error'; - error_message: InializationErrorMessage; + error_message: InitializationErrorMessage; } -type InializationErrorMessage = +type InitializationErrorMessage = | 'node_connection_issue' | 'signer_service_issue' | 'moonbeam_account_issue' @@ -170,7 +170,7 @@ const useEvents = () => { } if (event.event === 'initialization_error') { - const eventsStored = storageService.getParsed>( + const eventsStored = storageService.getParsed>( LocalStorageKeys.FIRED_INITIALIZATION_EVENTS, ); const eventsSet = eventsStored ? new Set(eventsStored) : new Set(); diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 787585cd..af90917c 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -473,7 +473,7 @@ export const SwapPage = () => { text={offrampInitiating ? 'Confirming' : offrampStarted ? 'Processing Details' : 'Confirm'} disabled={ Boolean(getCurrentErrorMessage()) || !inputAmountIsStable || !!initializeFailedMessage || !isReady - } // !!initializeFailedMessage we disable when the initialize failed message is not null + } pending={offrampInitiating || offrampStarted || offrampState !== undefined} /> )} From a7a8b908c4671d7e2da6d7bb4ef8cb38392274f1 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Thu, 30 Jan 2025 15:27:28 -0300 Subject: [PATCH 6/6] lint --- src/pages/swap/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index f7a5aa5b..7af161c7 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -102,7 +102,7 @@ export const SwapPage = () => { if (pendulumNode.apiComponents?.api) { setApi(pendulumNode.apiComponents.api); } - }, [pendulumNode]); + }, [pendulumNode, trackEvent, setApiInitializeFailed]); useEffect(() => { if (isSigningServiceError && !isSigningServiceLoading) {