diff --git a/package.json b/package.json index e1e4def1..c046555f 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "viem": "^2.21.43", "wagmi": "^2.12.29", "web3": "^4.10.0", - "yup": "^1.4.0" + "yup": "^1.4.0", + "zustand": "^5.0.2" }, "devDependencies": { "@babel/core": "^7.20.12", diff --git a/src/hooks/offramp/useMainProcess.ts b/src/hooks/offramp/useMainProcess.ts index 47d5d068..15c8cb2e 100644 --- a/src/hooks/offramp/useMainProcess.ts +++ b/src/hooks/offramp/useMainProcess.ts @@ -1,27 +1,15 @@ -import { useState, useEffect, useCallback, StateUpdater, useRef } from 'preact/compat'; -import { useConfig } from 'wagmi'; +import { useEffect, StateUpdater } from 'preact/compat'; import Big from 'big.js'; -import { EventStatus, GenericEvent } from '../../components/GenericEvent'; - -import { getInputTokenDetailsOrDefault, InputTokenType, OutputTokenType } from '../../constants/tokenConfig'; -import { OFFRAMPING_PHASE_SECONDS } from '../../pages/progress'; - -import { createTransactionEvent, useEventsContext } from '../../contexts/events'; -import { useAssetHubNode, usePendulumNode } from '../../contexts/polkadotNode'; -import { usePolkadotWalletState } from '../../contexts/polkadotWallet'; -import { Networks, useNetwork } from '../../contexts/network'; - -import { - clearOfframpingState, - recoverFromFailure, - readCurrentState, - advanceOfframpingState, - OfframpingState, -} from '../../services/offrampingFlow'; +import { InputTokenType, OutputTokenType } from '../../constants/tokenConfig'; +import { useNetwork } from '../../contexts/network'; +import { recoverFromFailure, readCurrentState } from '../../services/offrampingFlow'; import { useSEP24 } from './useSEP24'; import { useSubmitOfframp } from './useSubmitOfframp'; +import { useOfframpEvents } from './useOfframpEvents'; +import { useOfframpAdvancement } from './useOfframpAdvancement'; +import { useOfframpActions, useOfframpState } from '../../stores/offrampStore'; export type SigningPhase = 'started' | 'approved' | 'signed' | 'finished'; @@ -34,187 +22,51 @@ export interface ExecutionInput { } export const useMainProcess = () => { - const [offrampingStarted, setOfframpingStarted] = useState(false); - const [isInitiating, setIsInitiating] = useState(false); - const [offrampingState, setOfframpingState] = useState(undefined); - const [signingPhase, setSigningPhase] = useState(undefined); - const isProcessingAdvance = useRef(false); - - const { selectedNetwork, setOnSelectedNetworkChange } = useNetwork(); - const { walletAccount } = usePolkadotWalletState(); - - const { apiComponents: pendulumNode } = usePendulumNode(); - const { apiComponents: assetHubNode } = useAssetHubNode(); - - const wagmiConfig = useConfig(); - const { trackEvent, resetUniqueEvents } = useEventsContext(); - - const [, setEvents] = useState([]); - const { - firstSep24IntervalRef, - firstSep24Response, - setFirstSep24Response, - setExecutionInput, - setAnchorSessionParams, - cleanSep24FirstVariables, - handleOnAnchorWindowOpen: sep24HandleOnAnchorWindowOpen, - } = useSEP24(); + const { updateOfframpHookStateFromState, resetOfframpState, setOfframpStarted } = useOfframpActions(); - const handleOnSubmit = useSubmitOfframp({ - firstSep24IntervalRef, - setFirstSep24Response, - setExecutionInput, - setAnchorSessionParams, - cleanSep24FirstVariables, - offrampingStarted, - offrampingState, - setOfframpingStarted, - setIsInitiating, - }); + const offrampState = useOfframpState(); - const updateHookStateFromState = useCallback( - (state: OfframpingState | undefined) => { - if (state === undefined || state.phase === 'success' || state.failure !== undefined) { - setSigningPhase(undefined); - } - - setOfframpingState(state); + // Contexts + const { setOnSelectedNetworkChange } = useNetwork(); - if (state?.phase === 'success') { - trackEvent(createTransactionEvent('transaction_success', state, selectedNetwork)); - } else if (state?.failure !== undefined) { - const currentPhase = state?.phase; - const currentPhaseIndex = Object.keys(OFFRAMPING_PHASE_SECONDS).indexOf(currentPhase); - - trackEvent({ - ...createTransactionEvent('transaction_failure', state, selectedNetwork), - event: 'transaction_failure', - phase_name: currentPhase, - phase_index: currentPhaseIndex, - from_asset: getInputTokenDetailsOrDefault(selectedNetwork, state.inputTokenType).assetSymbol, - error_message: state.failure.message, - }); - } - }, - [trackEvent, selectedNetwork], - ); + // Custom hooks + const events = useOfframpEvents(); + const sep24 = useSEP24(); + // Initialize state from storage useEffect(() => { - const state = readCurrentState(); - updateHookStateFromState(state); - }, [updateHookStateFromState]); - - const addEvent = (message: string, status: EventStatus) => { - console.log('Add event', message, status); - setEvents((prevEvents) => [...prevEvents, { value: message, status }]); - }; - - const resetOfframpingState = useCallback(() => { - setOfframpingState(undefined); - setOfframpingStarted(false); - setIsInitiating(false); - setAnchorSessionParams(undefined); - setFirstSep24Response(undefined); - setExecutionInput(undefined); - cleanSep24FirstVariables(); - clearOfframpingState(); - setSigningPhase(undefined); - }, [ - setOfframpingState, - setOfframpingStarted, - setIsInitiating, - setAnchorSessionParams, - setFirstSep24Response, - setExecutionInput, - cleanSep24FirstVariables, - setSigningPhase, - ]); - - const handleOnAnchorWindowOpen = useCallback(async () => { - if (!pendulumNode) { - console.error('Pendulum node not initialized'); - return; - } - - await sep24HandleOnAnchorWindowOpen(selectedNetwork, setOfframpingStarted, updateHookStateFromState, pendulumNode); - }, [selectedNetwork, setOfframpingStarted, updateHookStateFromState, pendulumNode, sep24HandleOnAnchorWindowOpen]); - - const finishOfframping = useCallback(() => { - (async () => { - clearOfframpingState(); - resetUniqueEvents(); - setOfframpingStarted(false); - updateHookStateFromState(undefined); - })(); - }, [updateHookStateFromState, resetUniqueEvents]); - - const continueFailedFlow = useCallback(() => { - const nextState = recoverFromFailure(offrampingState); - updateHookStateFromState(nextState); - }, [updateHookStateFromState, offrampingState]); + const recoveryState = readCurrentState(); + updateOfframpHookStateFromState(recoveryState); + events.trackOfframpingEvent(recoveryState); + }, [updateOfframpHookStateFromState, events]); + // Reset offramping state when the network is changed useEffect(() => { - if (selectedNetwork == Networks.Polygon && wagmiConfig.state.status !== 'connected') return; - if (selectedNetwork == Networks.AssetHub && !walletAccount?.address) return; - - (async () => { - try { - if (isProcessingAdvance.current) return; - isProcessingAdvance.current = true; - - if (!pendulumNode || !assetHubNode) { - console.error('Polkadot nodes not initialized'); - return; - } + setOnSelectedNetworkChange(resetOfframpState); + }, [setOnSelectedNetworkChange, resetOfframpState]); - const nextState = await advanceOfframpingState(offrampingState, { - renderEvent: addEvent, - wagmiConfig, - setSigningPhase, - trackEvent, - pendulumNode, - assetHubNode, - walletAccount, - }); - - if (JSON.stringify(offrampingState) !== JSON.stringify(nextState)) { - updateHookStateFromState(nextState); - } - } catch (error) { - console.error('Error advancing offramping state:', error); - } finally { - isProcessingAdvance.current = false; - } - })(); - }, [ - offrampingState, - trackEvent, - updateHookStateFromState, - wagmiConfig, - pendulumNode, - assetHubNode, - wagmiConfig.state.status, - walletAccount?.address, - ]); - - const maybeCancelSep24First = useCallback(() => { - if (firstSep24IntervalRef.current !== undefined) { - setOfframpingStarted(false); - cleanSep24FirstVariables(); - } - }, [firstSep24IntervalRef, cleanSep24FirstVariables]); + // Determines the current offramping phase + useOfframpAdvancement(); return { - handleOnSubmit, - firstSep24ResponseState: firstSep24Response, - offrampingState, - offrampingStarted, - isInitiating, - setIsInitiating, - finishOfframping, - continueFailedFlow, - handleOnAnchorWindowOpen, - signingPhase, - maybeCancelSep24First, + handleOnSubmit: useSubmitOfframp({ + ...sep24, + }), + firstSep24ResponseState: sep24.firstSep24Response, + finishOfframping: () => { + events.resetUniqueEvents(); + resetOfframpState(); + }, + continueFailedFlow: () => { + updateOfframpHookStateFromState(recoverFromFailure(offrampState)); + }, + handleOnAnchorWindowOpen: sep24.handleOnAnchorWindowOpen, + // @todo: why do we need this? + maybeCancelSep24First: () => { + if (sep24.firstSep24IntervalRef.current !== undefined) { + setOfframpStarted(false); + sep24.cleanSep24FirstVariables(); + } + }, }; }; diff --git a/src/hooks/offramp/useOfframpAdvancement.ts b/src/hooks/offramp/useOfframpAdvancement.ts new file mode 100644 index 00000000..e30d0d0b --- /dev/null +++ b/src/hooks/offramp/useOfframpAdvancement.ts @@ -0,0 +1,57 @@ +import { useEffect } from 'preact/hooks'; +import { useConfig } from 'wagmi'; + +import { advanceOfframpingState } from '../../services/offrampingFlow'; + +import { usePolkadotWalletState } from '../../contexts/polkadotWallet'; +import { useAssetHubNode } from '../../contexts/polkadotNode'; +import { usePendulumNode } from '../../contexts/polkadotNode'; +import { useEventsContext } from '../../contexts/events'; + +import { useOfframpActions, useOfframpState } from '../../stores/offrampStore'; +import { EventStatus } from '../../components/GenericEvent'; + +export const useOfframpAdvancement = () => { + const { walletAccount } = usePolkadotWalletState(); + const { trackEvent } = useEventsContext(); + const wagmiConfig = useConfig(); + + const { apiComponents: pendulumNode } = usePendulumNode(); + const { apiComponents: assetHubNode } = useAssetHubNode(); + + const offrampState = useOfframpState(); + const { updateOfframpHookStateFromState, setOfframpSigningPhase } = useOfframpActions(); + + useEffect(() => { + if (wagmiConfig.state.status !== 'connected') return; + + (async () => { + if (!pendulumNode || !assetHubNode) { + console.error('Polkadot nodes not initialized'); + return; + } + + const nextState = await advanceOfframpingState(offrampState, { + wagmiConfig, + setOfframpSigningPhase, + trackEvent, + pendulumNode, + assetHubNode, + walletAccount, + renderEvent: (message: string, status: EventStatus) => { + console.log('renderEvent: ', message, status); + }, + }); + + if (JSON.stringify(offrampState) !== JSON.stringify(nextState)) { + updateOfframpHookStateFromState(nextState); + } + })(); + + // @todo: investigate and remove this + // This effect has dependencies that are used inside the async function (assetHubNode, pendulumNode, walletAccount) + // but we intentionally exclude them from the dependency array to prevent unnecessary re-renders. + // These dependencies are stable and won't change during the lifecycle of this hook. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [offrampState, trackEvent, updateOfframpHookStateFromState, wagmiConfig]); +}; diff --git a/src/hooks/offramp/useOfframpEvents.ts b/src/hooks/offramp/useOfframpEvents.ts new file mode 100644 index 00000000..511c0ba3 --- /dev/null +++ b/src/hooks/offramp/useOfframpEvents.ts @@ -0,0 +1,35 @@ +import { useCallback } from 'preact/compat'; +import { createTransactionEvent } from '../../contexts/events'; +import { useEventsContext } from '../../contexts/events'; +import { useNetwork } from '../../contexts/network'; + +import { getInputTokenDetailsOrDefault } from '../../constants/tokenConfig'; +import { OfframpingState } from '../../services/offrampingFlow'; +import { OFFRAMPING_PHASE_SECONDS } from '../../pages/progress'; + +export const useOfframpEvents = () => { + const { trackEvent, resetUniqueEvents } = useEventsContext(); + const { selectedNetwork } = useNetwork(); + + const trackOfframpingEvent = useCallback( + (state: OfframpingState | undefined) => { + if (!state) return; + + if (state.phase === 'success') { + trackEvent(createTransactionEvent('transaction_success', state, selectedNetwork)); + } else if (state.failure) { + trackEvent({ + ...createTransactionEvent('transaction_failure', state, selectedNetwork), + event: 'transaction_failure', + phase_name: state.phase, + phase_index: Object.keys(OFFRAMPING_PHASE_SECONDS).indexOf(state.phase), + from_asset: getInputTokenDetailsOrDefault(selectedNetwork, state.inputTokenType).assetSymbol, + error_message: state.failure.message, + }); + } + }, + [trackEvent, selectedNetwork], + ); + + return { trackOfframpingEvent, trackEvent, resetUniqueEvents }; +}; diff --git a/src/hooks/offramp/useSEP24/useAnchorWindowHandler.ts b/src/hooks/offramp/useSEP24/useAnchorWindowHandler.ts index 600256ad..457e9d55 100644 --- a/src/hooks/offramp/useSEP24/useAnchorWindowHandler.ts +++ b/src/hooks/offramp/useSEP24/useAnchorWindowHandler.ts @@ -1,16 +1,17 @@ import { useCallback } from 'preact/compat'; -import { ApiPromise } from '@polkadot/api'; import Big from 'big.js'; -import { Networks } from '../../../contexts/network'; +import { useNetwork } from '../../../contexts/network'; -import { constructInitialState, OfframpingState } from '../../../services/offrampingFlow'; +import { constructInitialState } from '../../../services/offrampingFlow'; import { sep24Second } from '../../../services/anchor'; import { showToast, ToastMessage } from '../../../helpers/notifications'; import { UseSEP24StateReturn } from './useSEP24State'; import { useTrackSEP24Events } from './useTrackSEP24Events'; +import { usePendulumNode } from '../../../contexts/polkadotNode'; +import { useOfframpActions } from '../../../stores/offrampStore'; import { useVortexAccount } from '../../useVortexAccount'; const handleAmountMismatch = (setOfframpingStarted: (started: boolean) => void): void => { @@ -25,50 +26,63 @@ const handleError = (error: unknown, setOfframpingStarted: (started: boolean) => export const useAnchorWindowHandler = (sep24State: UseSEP24StateReturn, cleanupFn: () => void) => { const { trackKYCStarted, trackKYCCompleted } = useTrackSEP24Events(); + const { selectedNetwork } = useNetwork(); + const { apiComponents: pendulumNode } = usePendulumNode(); + const { setOfframpStarted, updateOfframpHookStateFromState } = useOfframpActions(); + const { firstSep24Response, anchorSessionParams, executionInput } = sep24State; const { address } = useVortexAccount(); - return useCallback( - async ( - selectedNetwork: Networks, - setOfframpingStarted: (started: boolean) => void, - updateHookStateFromState: (state: OfframpingState | undefined) => void, - pendulumNode: { ss58Format: number; api: ApiPromise; decimals: number }, - ) => { - if (!firstSep24Response || !anchorSessionParams || !executionInput) { - return; - } - - trackKYCStarted(executionInput, selectedNetwork); - cleanupFn(); + return useCallback(async () => { + if (!firstSep24Response || !anchorSessionParams || !executionInput) { + return; + } - try { - const secondSep24Response = await sep24Second(firstSep24Response, anchorSessionParams); + if (!pendulumNode) { + console.error('Pendulum node not initialized'); + return; + } - if (!Big(secondSep24Response.amount).eq(executionInput.offrampAmount)) { - handleAmountMismatch(setOfframpingStarted); - return; - } + trackKYCStarted(executionInput, selectedNetwork); + cleanupFn(); - const initialState = await constructInitialState({ - sep24Id: firstSep24Response.id, - stellarEphemeralSecret: executionInput.stellarEphemeralSecret, - inputTokenType: executionInput.inputTokenType, - outputTokenType: executionInput.outputTokenType, - amountIn: executionInput.amountInUnits, - amountOut: executionInput.offrampAmount, - sepResult: secondSep24Response, - network: selectedNetwork, - pendulumNode, - offramperAddress: address!, - }); + try { + const secondSep24Response = await sep24Second(firstSep24Response, anchorSessionParams); - trackKYCCompleted(initialState, selectedNetwork); - updateHookStateFromState(initialState); - } catch (error) { - handleError(error, setOfframpingStarted); + if (!Big(secondSep24Response.amount).eq(executionInput.offrampAmount)) { + handleAmountMismatch(setOfframpStarted); + return; } - }, - [firstSep24Response, anchorSessionParams, executionInput, trackKYCStarted, cleanupFn, trackKYCCompleted], - ); + + const initialState = await constructInitialState({ + sep24Id: firstSep24Response.id, + stellarEphemeralSecret: executionInput.stellarEphemeralSecret, + inputTokenType: executionInput.inputTokenType, + outputTokenType: executionInput.outputTokenType, + amountIn: executionInput.amountInUnits, + amountOut: executionInput.offrampAmount, + sepResult: secondSep24Response, + network: selectedNetwork, + pendulumNode, + offramperAddress: address!, + }); + + trackKYCCompleted(initialState, selectedNetwork); + updateOfframpHookStateFromState(initialState); + } catch (error) { + handleError(error, setOfframpStarted); + } + }, [ + firstSep24Response, + anchorSessionParams, + executionInput, + pendulumNode, + trackKYCStarted, + cleanupFn, + trackKYCCompleted, + selectedNetwork, + setOfframpStarted, + updateOfframpHookStateFromState, + address, + ]); }; diff --git a/src/hooks/offramp/useSubmitOfframp.ts b/src/hooks/offramp/useSubmitOfframp.ts index 70c1c5ad..f3b31f2a 100644 --- a/src/hooks/offramp/useSubmitOfframp.ts +++ b/src/hooks/offramp/useSubmitOfframp.ts @@ -16,7 +16,7 @@ import { sep24First, } from '../../services/anchor'; -import { OfframpingState } from '../../services/offrampingFlow'; +import { useOfframpActions, useOfframpStarted, useOfframpState } from '../../stores/offrampStore'; import { ExtendedExecutionInput } from './useSEP24/useSEP24State'; import { ExecutionInput } from './useSEP24'; import { useVortexAccount } from '../useVortexAccount'; @@ -27,10 +27,6 @@ interface UseSubmitOfframpProps { setExecutionInput: (input: ExtendedExecutionInput | undefined) => void; setAnchorSessionParams: (params: IAnchorSessionParams | undefined) => void; cleanSep24FirstVariables: () => void; - offrampingStarted: boolean; - offrampingState: OfframpingState | undefined; - setOfframpingStarted: (started: boolean) => void; - setIsInitiating: (isInitiating: boolean) => void; } export const useSubmitOfframp = ({ @@ -39,16 +35,15 @@ export const useSubmitOfframp = ({ setExecutionInput, setAnchorSessionParams, cleanSep24FirstVariables, - offrampingStarted, - offrampingState, - setOfframpingStarted, - setIsInitiating, }: UseSubmitOfframpProps) => { const { selectedNetwork } = useNetwork(); const { switchChain } = useSwitchChain(); const { trackEvent } = useEventsContext(); const { address } = useVortexAccount(); const { checkAndWaitForSignature, forceRefreshAndWaitForSignature } = useSiweContext(); + const offrampStarted = useOfframpStarted(); + const offrampState = useOfframpState(); + const { setOfframpStarted, setOfframpInitiating } = useOfframpActions(); const addEvent = (message: string, status: string) => { console.log('Add event', message, status); @@ -58,14 +53,14 @@ export const useSubmitOfframp = ({ (executionInput: ExecutionInput) => { const { inputTokenType, amountInUnits, outputTokenType, offrampAmount, setInitializeFailed } = executionInput; - if (offrampingStarted || offrampingState !== undefined) { - setIsInitiating(false); + if (offrampStarted || offrampState !== undefined) { + setOfframpInitiating(false); return; } (async () => { switchChain({ chainId: polygon.id }); - setOfframpingStarted(true); + setOfframpStarted(true); trackEvent({ event: 'transaction_confirmation', @@ -122,25 +117,25 @@ export const useSubmitOfframp = ({ } catch (error) { console.error('Error finalizing the initial state of the offramping process', error); setInitializeFailed(true); - setOfframpingStarted(false); + setOfframpStarted(false); cleanSep24FirstVariables(); } finally { - setIsInitiating(false); + setOfframpInitiating(false); } } catch (error) { console.error('Error initializing the offramping process', error); setInitializeFailed(true); - setOfframpingStarted(false); - setIsInitiating(false); + setOfframpStarted(false); + setOfframpInitiating(false); } })(); }, [ - offrampingStarted, - offrampingState, - setIsInitiating, + offrampStarted, + offrampState, + setOfframpInitiating, switchChain, - setOfframpingStarted, + setOfframpStarted, trackEvent, selectedNetwork, address, diff --git a/src/pages/swap/helpers/swapConfirm/index.ts b/src/pages/swap/helpers/swapConfirm/index.ts index 247f88ba..a7a94c38 100644 --- a/src/pages/swap/helpers/swapConfirm/index.ts +++ b/src/pages/swap/helpers/swapConfirm/index.ts @@ -28,7 +28,7 @@ interface SwapConfirmParams { requiresSquidRouter: boolean; selectedNetwork: Networks; setInitializeFailed: StateUpdater; - setIsInitiating: StateUpdater; + setOfframpInitiating: (initiating: boolean) => void; setTermsAccepted: (accepted: boolean) => void; to: OutputTokenType; tokenOutAmount: { data: TokenOutData | undefined }; @@ -48,7 +48,7 @@ export function swapConfirm(e: Event, params: SwapConfirmParams) { requiresSquidRouter, selectedNetwork, setInitializeFailed, - setIsInitiating, + setOfframpInitiating, setTermsAccepted, to, tokenOutAmount, @@ -59,7 +59,7 @@ export function swapConfirm(e: Event, params: SwapConfirmParams) { return; } - setIsInitiating(true); + setOfframpInitiating(true); const outputToken = OUTPUT_TOKEN_CONFIG[to]; const inputToken = getInputTokenDetailsOrDefault(selectedNetwork, from); @@ -96,7 +96,7 @@ export function swapConfirm(e: Event, params: SwapConfirmParams) { }) .catch((_error) => { console.error('Error during swap confirmation:', _error); - setIsInitiating(false); + setOfframpInitiating(false); setInitializeFailed(true); }); } diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 1e5a1983..0f588451 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -48,6 +48,13 @@ import { BaseLayout } from '../../layouts'; import { ProgressPage } from '../progress'; import { FailurePage } from '../failure'; import { SuccessPage } from '../success'; +import { + useOfframpActions, + useOfframpSigningPhase, + useOfframpState, + useOfframpStarted, + useOfframpInitiating, +} from '../../stores/offrampStore'; import { swapConfirm } from './helpers/swapConfirm'; const Arrow = () => ( @@ -99,16 +106,17 @@ export const SwapPage = () => { handleOnSubmit, finishOfframping, continueFailedFlow, - offrampingStarted, firstSep24ResponseState, handleOnAnchorWindowOpen, - offrampingState, - isInitiating, - signingPhase, - setIsInitiating, maybeCancelSep24First, } = useMainProcess(); + const offrampStarted = useOfframpStarted(); + const offrampState = useOfframpState(); + const offrampSigningPhase = useOfframpSigningPhase(); + const offrampInitiating = useOfframpInitiating(); + const { setOfframpInitiating } = useOfframpActions(); + // Store the id as it is cleared after the user opens the anchor window useEffect(() => { if (firstSep24ResponseState?.id != undefined) { @@ -198,10 +206,10 @@ export const SwapPage = () => { }, []); useEffect(() => { - if (offrampingState?.phase !== undefined) { + if (offrampState?.phase !== undefined) { setNetworkSelectorDisabled(true); } - }, [offrampingState, setNetworkSelectorDisabled]); + }, [offrampState, setNetworkSelectorDisabled]); const ReceiveNumericInput = useMemo( () => ( @@ -302,31 +310,31 @@ export const SwapPage = () => { ); - if (offrampingState?.phase === 'success') { + if (offrampState?.phase === 'success') { return ; } - if (offrampingState?.failure !== undefined) { + if (offrampState?.failure !== undefined) { return ( ); } - if (offrampingState !== undefined || offrampingStarted) { + if (offrampState !== undefined || offrampStarted) { const isAssetHubFlow = selectedNetwork === Networks.AssetHub && - (offrampingState?.phase === 'pendulumFundEphemeral' || offrampingState?.phase === 'executeAssetHubXCM'); + (offrampState?.phase === 'pendulumFundEphemeral' || offrampState?.phase === 'executeAssetHubXCM'); const showMainScreenAnyway = - offrampingState === undefined || - ['prepareTransactions', 'squidRouter'].includes(offrampingState.phase) || + offrampState === undefined || + ['prepareTransactions', 'squidRouter'].includes(offrampState.phase) || isAssetHubFlow; if (!showMainScreenAnyway) { - return ; + return ; } } @@ -352,7 +360,7 @@ export const SwapPage = () => { selectedNetwork, fromAmountString, requiresSquidRouter: selectedNetwork === Networks.Polygon, - setIsInitiating, + setOfframpInitiating, setInitializeFailed, handleOnSubmit, setTermsAccepted, @@ -362,7 +370,7 @@ export const SwapPage = () => { const main = (
- +
{ ) : ( )} diff --git a/src/services/offrampingFlow.ts b/src/services/offrampingFlow.ts index c9f79721..12f618f3 100644 --- a/src/services/offrampingFlow.ts +++ b/src/services/offrampingFlow.ts @@ -7,7 +7,6 @@ import { decodeAddress } from '@polkadot/util-crypto'; import { ApiPromise } from '@polkadot/api'; import { u8aToHex } from '@polkadot/util'; -import { RenderEventHandler } from '../components/GenericEvent'; import { SigningPhase } from '../hooks/offramp/useMainProcess'; import { TrackableEvent } from '../contexts/events'; import { Networks } from '../contexts/network'; @@ -40,6 +39,7 @@ import { pendulumCleanup, createPendulumEphemeralSeed, } from './phases/polkadot/ephemeral'; +import { RenderEventHandler } from '../components/GenericEvent'; export interface FailureType { type: 'recoverable' | 'unrecoverable'; @@ -66,7 +66,7 @@ export type FinalOfframpingPhase = 'success'; export interface ExecutionContext { wagmiConfig: Config; renderEvent: RenderEventHandler; - setSigningPhase: (n: SigningPhase) => void; + setOfframpSigningPhase: (n: SigningPhase) => void; trackEvent: (event: TrackableEvent) => void; pendulumNode: { ss58Format: number; api: ApiPromise; decimals: number }; assetHubNode: { api: ApiPromise }; diff --git a/src/services/phases/polkadot/assethub.ts b/src/services/phases/polkadot/assethub.ts index 6949c49e..8232160c 100644 --- a/src/services/phases/polkadot/assethub.ts +++ b/src/services/phases/polkadot/assethub.ts @@ -30,7 +30,7 @@ export function createAssethubAssetTransfer(assethubApi: ApiPromise, receiverAdd } export async function executeAssetHubXCM(state: OfframpingState, context: ExecutionContext): Promise { - const { assetHubNode, walletAccount, setSigningPhase } = context; + const { assetHubNode, walletAccount, setOfframpSigningPhase } = context; const { pendulumEphemeralAddress } = state; if (!walletAccount) { @@ -40,7 +40,7 @@ export async function executeAssetHubXCM(state: OfframpingState, context: Execut throw new Error('AssetHub node not available'); } - setSigningPhase?.('started'); + setOfframpSigningPhase?.('started'); const didInputTokenArrivedOnPendulum = async () => { const inputBalanceRaw = await getRawInputBalance(state, context); @@ -52,9 +52,9 @@ export async function executeAssetHubXCM(state: OfframpingState, context: Execut if (assetHubXcmTransactionHash === undefined) { const tx = createAssethubAssetTransfer(assetHubNode.api, pendulumEphemeralAddress, inputAmount.raw); - context.setSigningPhase('started'); + context.setOfframpSigningPhase('started'); const { hash } = await tx.signAndSend(walletAccount.address, { signer: walletAccount.signer as Signer }); - setSigningPhase?.('finished'); + setOfframpSigningPhase?.('finished'); return { ...state, assetHubXcmTransactionHash: hash.toString() }; } diff --git a/src/services/phases/squidrouter/process.ts b/src/services/phases/squidrouter/process.ts index 96c0fb24..c25ab5d2 100644 --- a/src/services/phases/squidrouter/process.ts +++ b/src/services/phases/squidrouter/process.ts @@ -11,7 +11,7 @@ import { getRouteTransactionRequest } from './route'; export async function squidRouter( state: OfframpingState, - { wagmiConfig, setSigningPhase, trackEvent }: ExecutionContext, + { wagmiConfig, setOfframpSigningPhase, trackEvent }: ExecutionContext, ): Promise { const inputToken = getInputTokenDetails(state.network, state.inputTokenType); if (!inputToken || !isEvmInputTokenDetails(inputToken)) { @@ -37,7 +37,7 @@ export async function squidRouter( state.inputAmount.units, ); - setSigningPhase?.('started'); + setOfframpSigningPhase?.('started'); let approvalHash; try { @@ -68,7 +68,7 @@ export async function squidRouter( return { ...state, failure: { type: 'unrecoverable', message: e?.toString() } }; } - setSigningPhase?.('approved'); + setOfframpSigningPhase?.('approved'); await waitForTransactionReceipt(wagmiConfig, { hash: approvalHash }); @@ -100,12 +100,12 @@ export async function squidRouter( return { ...state, failure: { type: 'unrecoverable', message: e?.toString() } }; } - setSigningPhase?.('signed'); + setOfframpSigningPhase?.('signed'); const axelarScanLink = 'https://axelarscan.io/gmp/' + swapHash; console.log(`Squidrouter Swap Initiated! Check Axelarscan for details: ${axelarScanLink}`); - setSigningPhase?.('finished'); + setOfframpSigningPhase?.('finished'); return { ...state, diff --git a/src/stores/offrampStore.ts b/src/stores/offrampStore.ts new file mode 100644 index 00000000..4e33c480 --- /dev/null +++ b/src/stores/offrampStore.ts @@ -0,0 +1,60 @@ +import { create } from 'zustand'; +import { OfframpState, OfframpActions } from '../types/offramp'; +import { clearOfframpingState } from '../services/offrampingFlow'; + +interface OfframpStore extends OfframpState { + actions: OfframpActions; +} + +const useOfframpStore = create()((set) => ({ + // Initial state + offrampStarted: false, + offrampInitiating: false, + offrampState: undefined, + offrampSigningPhase: undefined, + offrampAnchorSessionParams: undefined, + offrampFirstSep24Response: undefined, + offrampExecutionInput: undefined, + + actions: { + // Simple setters + setOfframpStarted: (started) => set({ offrampStarted: started }), + setOfframpInitiating: (initiating) => set({ offrampInitiating: initiating }), + setOfframpState: (state) => set({ offrampState: state }), + setOfframpSigningPhase: (phase) => set({ offrampSigningPhase: phase }), + + // Complex setters + setOfframpSep24Params: (params) => set((state) => ({ ...state, ...params })), + + // Business logic + updateOfframpHookStateFromState: (state) => { + if (!state || state.phase === 'success' || state.failure !== undefined) { + set({ offrampSigningPhase: undefined }); + } + set({ offrampState: state }); + }, + + resetOfframpState: async () => { + await clearOfframpingState(); + set({ + offrampStarted: false, + offrampInitiating: false, + offrampState: undefined, + offrampSigningPhase: undefined, + offrampAnchorSessionParams: undefined, + offrampFirstSep24Response: undefined, + offrampExecutionInput: undefined, + }); + }, + }, +})); + +export const useOfframpSigningPhase = () => useOfframpStore((state) => state.offrampSigningPhase); +export const useOfframpState = () => useOfframpStore((state) => state.offrampState); +export const useOfframpStarted = () => useOfframpStore((state) => state.offrampStarted); +export const useOfframpInitiating = () => useOfframpStore((state) => state.offrampInitiating); +export const useOfframpFirstSep24Response = () => useOfframpStore((state) => state.offrampFirstSep24Response); +export const useOfframpExecutionInput = () => useOfframpStore((state) => state.offrampExecutionInput); +export const useOfframpAnchorSessionParams = () => useOfframpStore((state) => state.offrampAnchorSessionParams); + +export const useOfframpActions = () => useOfframpStore((state) => state.actions); diff --git a/src/types/offramp.ts b/src/types/offramp.ts new file mode 100644 index 00000000..22587612 --- /dev/null +++ b/src/types/offramp.ts @@ -0,0 +1,42 @@ +import { StateUpdater } from 'preact/hooks'; +import Big from 'big.js'; +import { OfframpingState } from '../services/offrampingFlow'; +import { InputTokenType, OutputTokenType } from '../constants/tokenConfig'; +import { ISep24Intermediate, IAnchorSessionParams } from '../services/anchor'; + +export type OfframpSigningPhase = 'started' | 'approved' | 'signed' | 'finished'; + +export interface OfframpExecutionInput { + inputTokenType: InputTokenType; + outputTokenType: OutputTokenType; + amountInUnits: string; + offrampAmount: Big; + setInitializeFailed: StateUpdater; +} + +export interface OfframpState { + // Core state + offrampStarted: boolean; + offrampInitiating: boolean; + offrampState: OfframpingState | undefined; + offrampSigningPhase: OfframpSigningPhase | undefined; + + // SEP24 related state @todo move to separate store + offrampAnchorSessionParams: IAnchorSessionParams | undefined; + offrampFirstSep24Response: ISep24Intermediate | undefined; + offrampExecutionInput: OfframpExecutionInput | undefined; +} + +export interface OfframpActions { + setOfframpStarted: (started: boolean) => void; + setOfframpInitiating: (initiating: boolean) => void; + setOfframpState: (state: OfframpingState | undefined) => void; + setOfframpSigningPhase: (phase: OfframpSigningPhase | undefined) => void; + setOfframpSep24Params: ( + params: Partial< + Pick + >, + ) => void; + updateOfframpHookStateFromState: (state: OfframpingState | undefined) => void; + resetOfframpState: () => void; +} diff --git a/yarn.lock b/yarn.lock index 15df6d94..e84d0649 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15114,6 +15114,7 @@ __metadata: wagmi: "npm:^2.12.29" web3: "npm:^4.10.0" yup: "npm:^1.4.0" + zustand: "npm:^5.0.2" languageName: unknown linkType: soft @@ -15822,3 +15823,24 @@ __metadata: checksum: 10/be75ef4d1b218b143314467bb9e23641231043cad2d5c3a4b2219c46d1609ee799cd8dc9acec9b23d55ec3a2a619a06616e593aea4049f3b7323938af9a33bfe languageName: node linkType: hard + +"zustand@npm:^5.0.2": + version: 5.0.2 + resolution: "zustand@npm:5.0.2" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 10/9fb60796b9770dcc3f78dd794e7f424ff735e5676784cbc9726761037613942b62470b24a9ca9e98534ee4369a3b5429be570ff34281cb3c9d6d4e8df559ec3f + languageName: node + linkType: hard