From 853208773d1667257e562e4bce8e9397d4f53e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20St=C3=BCber?= <15174476+TorstenStueber@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:55:29 -0300 Subject: [PATCH 1/2] 46 integrate google analytics query into the prototype (#86) * Add basic event tracking * Add tracked events --- index.html | 27 ++++++ src/components/FeeCollapse/index.tsx | 7 +- src/components/NumericInput/index.tsx | 64 ++++++++------ src/contexts/events.tsx | 121 ++++++++++++++++++++++++++ src/hooks/useMainProcess.ts | 19 +++- src/main.tsx | 19 ++-- 6 files changed, 219 insertions(+), 38 deletions(-) create mode 100644 src/contexts/events.tsx diff --git a/index.html b/index.html index 0d8addc4..2eaa699a 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,22 @@ Vortex + + + + + + + + +
diff --git a/src/components/FeeCollapse/index.tsx b/src/components/FeeCollapse/index.tsx index 63158c1e..65de11bd 100644 --- a/src/components/FeeCollapse/index.tsx +++ b/src/components/FeeCollapse/index.tsx @@ -5,6 +5,7 @@ import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; import Big from 'big.js'; import { roundDownToSignificantDecimals } from '../../helpers/parseNumbers'; import { OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; +import { useEventsContext } from '../../contexts/events'; const FEES_RATE = 0.05; // 0.5% fee rate @@ -27,8 +28,12 @@ interface CollapseProps { export const FeeCollapse: FC = ({ fromAmount, toAmount, toCurrency }) => { const [isOpen, setIsOpen] = useState(false); + const { trackEvent } = useEventsContext(); - const toggleIsOpen = () => setIsOpen((state) => !state); + const toggleIsOpen = () => { + trackEvent({ event: 'click_details' }); + setIsOpen((state) => !state); + }; const outputToken = toCurrency ? OUTPUT_TOKEN_CONFIG[toCurrency] : undefined; diff --git a/src/components/NumericInput/index.tsx b/src/components/NumericInput/index.tsx index 7e80615e..6de9743b 100644 --- a/src/components/NumericInput/index.tsx +++ b/src/components/NumericInput/index.tsx @@ -1,5 +1,6 @@ import { Input } from 'react-daisyui'; import { UseFormRegisterReturn } from 'react-hook-form'; +import { useEventsContext } from '../../contexts/events'; export function exceedsMaxDecimals(value: unknown, maxDecimals: number) { if (value === undefined || value === null) return true; @@ -53,31 +54,38 @@ export const NumericInput = ({ autoFocus, disabled, disableStyles = false, -}: NumericInputProps) => ( -
- handleOnKeyPress(e, maxDecimals)} - onInput={handleOnInput} - pattern="^[0-9]*[.,]?[0-9]*$" - placeholder="0.0" - readOnly={readOnly} - spellcheck="false" - step="any" - type="text" - inputmode="decimal" - value={defaultValue} - autoFocus={autoFocus} - disabled={disabled} - {...register} - /> -
-); +}: NumericInputProps) => { + const { trackEvent } = useEventsContext(); + + return ( +
+ handleOnKeyPress(e, maxDecimals)} + onInput={(e: KeyboardEvent) => { + trackEvent({ event: 'amount_type' }); + handleOnInput(e); + }} + pattern="^[0-9]*[.,]?[0-9]*$" + placeholder="0.0" + readOnly={readOnly} + spellcheck="false" + step="any" + type="text" + inputmode="decimal" + value={defaultValue} + autoFocus={autoFocus} + disabled={disabled} + {...register} + /> +
+ ); +}; diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx new file mode 100644 index 00000000..d737f5f9 --- /dev/null +++ b/src/contexts/events.tsx @@ -0,0 +1,121 @@ +import { createContext } from 'preact'; +import { PropsWithChildren, useCallback, useContext, useEffect, useRef, useState } from 'preact/compat'; +import { useAccount } from 'wagmi'; +import { INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG } from '../constants/tokenConfig'; +import { OfframpingState } from '../services/offrampingFlow'; + +declare global { + interface Window { + dataLayer: Record[]; + } +} + +const UNIQUE_EVENT_TYPES = ['amount_type', 'click_details', 'click_support']; + +export interface AmountTypeEvent { + event: `amount_type`; +} + +export interface ClickDetailsEvent { + event: 'click_details'; +} + +export interface WalletConnectEvent { + event: 'wallet_connect'; + wallet_action: 'connect' | 'disconnect' | 'change'; +} + +export interface TransactionEvent { + event: 'transaction_confirmation' | 'kyc_completed' | 'transaction_success' | 'transaction_failure'; + from_asset: string; + to_asset: string; + from_amount: string; + to_amount: string; +} + +export interface ClickSupportEvent { + event: 'click_support'; + transaction_status: 'success' | 'failure'; +} + +export type TrackableEvent = + | AmountTypeEvent + | ClickDetailsEvent + | WalletConnectEvent + | TransactionEvent + | ClickSupportEvent; + +type EventType = TrackableEvent['event']; + +type UseEventsContext = ReturnType; +const useEvents = () => { + const [_, setTrackedEventTypes] = useState>(new Set()); + + const previousAddress = useRef<`0x${string}` | undefined>(undefined); + const { address } = useAccount(); + + const trackEvent = useCallback( + (event: TrackableEvent) => { + setTrackedEventTypes((trackedEventTypes) => { + if (UNIQUE_EVENT_TYPES.includes(event.event)) { + if (trackedEventTypes.has(event.event)) { + return trackedEventTypes; + } else { + trackedEventTypes = new Set(trackedEventTypes); + trackedEventTypes.add(event.event); + } + } + console.log('Push data layer', event); + + window.dataLayer.push(event); + + return trackedEventTypes; + }); + }, + [setTrackedEventTypes], + ); + + useEffect(() => { + const wasConnected = previousAddress.current !== undefined; + const isConnected = address !== undefined; + + if (!isConnected) { + trackEvent({ event: 'wallet_connect', wallet_action: 'disconnect' }); + } else { + trackEvent({ event: 'wallet_connect', wallet_action: wasConnected ? 'change' : 'connect' }); + } + + previousAddress.current = address; + }, [address]); + + return { + trackEvent, + }; +}; + +const Context = createContext(undefined); + +export const useEventsContext = () => { + const contextValue = useContext(Context); + if (contextValue === undefined) { + throw new Error('Context must be inside a Provider'); + } + + return contextValue; +}; + +export function EventsProvider({ children }: PropsWithChildren) { + const useEventsResult = useEvents(); + + return {children}; +} + +export function createTransactionEvent(type: TransactionEvent['event'], state: OfframpingState) { + return { + event: type, + from_asset: INPUT_TOKEN_CONFIG[state.inputTokenType].assetSymbol, + to_asset: OUTPUT_TOKEN_CONFIG[state.outputTokenType].stellarAsset.code.string, + from_amount: state.inputAmount.units, + to_amount: state.outputAmount.units, + }; +} diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index e8397a3f..562c7fe6 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react'; // Configs, Types, constants import { createStellarEphemeralSecret, sep24First } from '../services/anchor'; import { ExecutionInput } from '../types'; -import { OUTPUT_TOKEN_CONFIG } from '../constants/tokenConfig'; +import { INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG } from '../constants/tokenConfig'; import { fetchTomlValues, sep10, sep24Second } from '../services/anchor'; // Utils @@ -20,6 +20,7 @@ import { } from '../services/offrampingFlow'; import { EventStatus, GenericEvent } from '../components/GenericEvent'; import Big from 'big.js'; +import { createTransactionEvent, useEventsContext } from '../contexts/events'; export const useMainProcess = () => { // EXAMPLE mocking states @@ -39,12 +40,19 @@ export const useMainProcess = () => { const [sep24Url, setSep24Url] = useState(undefined); const [sep24Id, setSep24Id] = useState(undefined); const wagmiConfig = useConfig(); + const { trackEvent } = useEventsContext(); const [events, setEvents] = useState([]); const updateHookStateFromState = (state: OfframpingState | undefined) => { setOfframpingPhase(state?.phase); setSep24Id(state?.sep24Id); + + if (state?.phase === 'success') { + trackEvent(createTransactionEvent('transaction_success', state)); + } else if (state?.phase === 'failure') { + trackEvent(createTransactionEvent('transaction_failure', state)); + } }; useEffect(() => { @@ -64,6 +72,13 @@ export const useMainProcess = () => { (async () => { setOfframpingStarted(true); + trackEvent({ + event: 'transaction_confirmation', + from_asset: INPUT_TOKEN_CONFIG[inputTokenType].assetSymbol, + to_asset: OUTPUT_TOKEN_CONFIG[outputTokenType].stellarAsset.code.string, + from_amount: amountInUnits, + to_amount: Big(minAmountOutUnits).round(2, 0).toFixed(2, 0), + }); try { const stellarEphemeralSecret = createStellarEphemeralSecret(); @@ -99,6 +114,8 @@ export const useMainProcess = () => { sepResult: secondSep24Response, }); + trackEvent(createTransactionEvent('kyc_completed', initialState)); + updateHookStateFromState(initialState); } catch (error) { console.error('Some error occurred initializing the offramping process', error); diff --git a/src/main.tsx b/src/main.tsx index efc5752a..fb0083bd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,6 +14,7 @@ import { App } from './app'; import defaultTheme from './theme'; import { GlobalState, GlobalStateContext, GlobalStateProvider } from './GlobalStateProvider'; import { wagmiConfig } from './wagmiConfig'; +import { EventsProvider } from './contexts/events'; const queryClient = new QueryClient(); @@ -24,14 +25,16 @@ render( - - - {(globalState) => { - const { tenantRPC, getThemeName = () => undefined } = globalState as GlobalState; - return ; - }} - - + + + + {(globalState) => { + const { tenantRPC, getThemeName = () => undefined } = globalState as GlobalState; + return ; + }} + + + From bbfd121904d423734672f024cd17f64c80b5a1ff Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz <43585069+Sharqiewicz@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:50:29 +0200 Subject: [PATCH 2/2] Signing dialog box (#87) * add basic dialogbox component * implement styles for signing dialog * show SignerPopup when txs are pending * fix imports * improve signingPhase logic * remove console log --- .../src/api/services/stellar.service.js | 3 +- src/components/ExchangeRate/index.tsx | 2 +- src/components/GenericEvent.tsx | 2 +- src/components/Nabla/useSwapForm.tsx | 8 +- src/components/SigningBox/index.tsx | 61 +++++++++++++ src/hooks/useMainProcess.ts | 19 ++-- src/pages/progress/index.tsx | 5 +- src/pages/swap/index.tsx | 88 +++---------------- src/services/evmTransactions.ts | 2 +- src/services/offrampingFlow.ts | 6 +- src/services/polkadot/ephemeral.tsx | 2 +- src/services/squidrouter/process.ts | 23 +++-- tailwind.config.js | 2 +- 13 files changed, 121 insertions(+), 102 deletions(-) create mode 100644 src/components/SigningBox/index.tsx diff --git a/signer-service/src/api/services/stellar.service.js b/signer-service/src/api/services/stellar.service.js index ffe632c1..850aeab2 100644 --- a/signer-service/src/api/services/stellar.service.js +++ b/signer-service/src/api/services/stellar.service.js @@ -6,7 +6,6 @@ const { Networks, Asset, Memo, - Transaction, Account, } = require('stellar-sdk'); const { HORIZON_URL, BASE_FEE } = require('../../constants/constants'); @@ -31,7 +30,7 @@ async function buildCreationStellarTx(fundingSecret, ephemeralAccountId, maxTime const fundingSequence = fundingAccount.sequence; // add a setOption oeration in order to make this a 2-of-2 multisig account where the // funding account is a cosigner - let createAccountTransaction = new TransactionBuilder(fundingAccount, { + const createAccountTransaction = new TransactionBuilder(fundingAccount, { fee: BASE_FEE, networkPassphrase: NETWORK_PASSPHRASE, }) diff --git a/src/components/ExchangeRate/index.tsx b/src/components/ExchangeRate/index.tsx index c3d4d305..188088ff 100644 --- a/src/components/ExchangeRate/index.tsx +++ b/src/components/ExchangeRate/index.tsx @@ -1,6 +1,6 @@ +import { FC } from 'preact/compat'; import { InputTokenDetails, OutputTokenDetails } from '../../constants/tokenConfig'; import { UseTokenOutAmountResult } from '../../hooks/nabla/useTokenAmountOut'; -import { FC } from 'preact/compat'; interface ExchangeRateProps { fromToken?: InputTokenDetails; diff --git a/src/components/GenericEvent.tsx b/src/components/GenericEvent.tsx index 1715522d..3883c17e 100644 --- a/src/components/GenericEvent.tsx +++ b/src/components/GenericEvent.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from 'preact/compat'; export enum EventStatus { Success = 'success', diff --git a/src/components/Nabla/useSwapForm.tsx b/src/components/Nabla/useSwapForm.tsx index 5696c527..9a666ef8 100644 --- a/src/components/Nabla/useSwapForm.tsx +++ b/src/components/Nabla/useSwapForm.tsx @@ -1,13 +1,13 @@ +import { yupResolver } from '@hookform/resolvers/yup'; import Big from 'big.js'; +import { useCallback, useMemo, useState } from 'preact/compat'; import { Resolver, useForm, useWatch } from 'react-hook-form'; -import { useState, useCallback, useMemo } from 'preact/compat'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; import { storageKeys } from '../../constants/localStorage'; +import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; import { debounce } from '../../helpers/function'; -import schema, { SwapFormValues } from './schema'; import { storageService } from '../../services/storage/local'; +import schema, { SwapFormValues } from './schema'; interface SwapSettings { from: string; diff --git a/src/components/SigningBox/index.tsx b/src/components/SigningBox/index.tsx new file mode 100644 index 00000000..af37718c --- /dev/null +++ b/src/components/SigningBox/index.tsx @@ -0,0 +1,61 @@ +import AccountBalanceWalletOutlinedIcon from '@mui/icons-material/AccountBalanceWalletOutlined'; +import { Progress } from 'react-daisyui'; +import { FC } from 'preact/compat'; + +import { SigningPhase } from '../../hooks/useMainProcess'; + +const progressValues: Record = { + started: '25', + approved: '50', + signed: '75', + finished: '100', +}; + +function getProgressValue(step: SigningPhase): string { + return progressValues[step] || '0'; +} + +function getSignatureNumber(step: SigningPhase): string { + return step === 'started' ? '1' : '2'; +} + +interface SigningBoxProps { + step?: SigningPhase; +} + +export const SigningBox: FC = ({ step }) => { + if (!step) return <>; + if (step !== 'started' && step !== 'approved' && step !== 'signed') return <>; + + return ( +
+
+
+

Action Required

+
+
+
+
+ +
+
+

Please sign the transaction in

+

your connected wallet to proceed

+
+
+
+ +
+
+
+ +

Waiting for signature {getSignatureNumber(step)}/2

+
+
+
+ ); +}; diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index 562c7fe6..344d9558 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback } from 'preact/compat'; // Configs, Types, constants import { createStellarEphemeralSecret, sep24First } from '../services/anchor'; @@ -22,6 +22,8 @@ import { EventStatus, GenericEvent } from '../components/GenericEvent'; import Big from 'big.js'; import { createTransactionEvent, useEventsContext } from '../contexts/events'; +export type SigningPhase = 'started' | 'approved' | 'signed' | 'finished'; + export const useMainProcess = () => { // EXAMPLE mocking states @@ -39,12 +41,18 @@ export const useMainProcess = () => { const [offrampingPhase, setOfframpingPhase] = useState(undefined); const [sep24Url, setSep24Url] = useState(undefined); const [sep24Id, setSep24Id] = useState(undefined); + + const [signingPhase, setSigningPhase] = useState(undefined); + const wagmiConfig = useConfig(); const { trackEvent } = useEventsContext(); - const [events, setEvents] = useState([]); + const [, setEvents] = useState([]); const updateHookStateFromState = (state: OfframpingState | undefined) => { + if (state?.phase === 'success' || state?.phase === 'failure') { + setSigningPhase(undefined); + } setOfframpingPhase(state?.phase); setSep24Id(state?.sep24Id); @@ -123,7 +131,7 @@ export const useMainProcess = () => { } })(); }, - [], + [offrampingPhase, offrampingStarted], ); const finishOfframping = useCallback(() => { @@ -136,10 +144,10 @@ export const useMainProcess = () => { useEffect(() => { (async () => { - const nextState = await advanceOfframpingState({ renderEvent: addEvent, wagmiConfig }); + const nextState = await advanceOfframpingState({ renderEvent: addEvent, wagmiConfig, setSigningPhase }); updateHookStateFromState(nextState); })(); - }, [offrampingPhase]); + }, [offrampingPhase, wagmiConfig]); return { handleOnSubmit, @@ -148,5 +156,6 @@ export const useMainProcess = () => { offrampingStarted, sep24Id, finishOfframping, + signingPhase, }; }; diff --git a/src/pages/progress/index.tsx b/src/pages/progress/index.tsx index 4d226ad2..b3774f39 100644 --- a/src/pages/progress/index.tsx +++ b/src/pages/progress/index.tsx @@ -1,5 +1,4 @@ import { useEffect } from 'preact/hooks'; -import { useNavigate } from 'react-router-dom'; import { ExclamationCircleIcon } from '@heroicons/react/20/solid'; import { Box } from '../../components/Box'; import { BaseLayout } from '../../layouts'; @@ -9,15 +8,13 @@ const handleTabClose = (event: Event) => { }; export const ProgressPage = () => { - const navigate = useNavigate(); - useEffect(() => { window.addEventListener('beforeunload', handleTabClose); return () => { window.removeEventListener('beforeunload', handleTabClose); }; - }, [navigate]); + }, []); const main = (
diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 08aba742..7309c213 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -1,5 +1,4 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; -import { useAccount } from 'wagmi'; import Big from 'big.js'; import { ArrowDownIcon } from '@heroicons/react/20/solid'; @@ -13,7 +12,7 @@ import { PoolSelectorModal } from '../../components/InputKeys/SelectionModal'; import { ExchangeRate } from '../../components/ExchangeRate'; import { AssetNumericInput } from '../../components/AssetNumericInput'; import { SwapSubmitButton } from '../../components/buttons/SwapSubmitButton'; -import { BankDetails } from './sections/BankDetails'; +import { SigningBox } from '../../components/SigningBox'; import { config } from '../../config'; import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; import { BaseLayout } from '../../layouts'; @@ -31,16 +30,11 @@ const Arrow = () => ( ); export const SwapPage = () => { - const [isSubmitButtonDisabled, setIsSubmitButtonDisabled] = useState(true); - const [isExchangeSectionSubmitted, setIsExchangeSectionSubmitted] = useState(false); - const [isExchangeSectionSubmittedError, setIsExchangeSectionSubmittedError] = useState(false); const [isQuoteSubmitted, setIsQuoteSubmitted] = useState(false); const formRef = useRef(null); const [api, setApi] = useState(null); - const { isDisconnected } = useAccount(); - useEffect(() => { const initializeApiManager = async () => { const manager = await getApiManagerInstance(); @@ -52,7 +46,8 @@ export const SwapPage = () => { }, []); // Main process hook - const { handleOnSubmit, finishOfframping, offrampingStarted, sep24Url, sep24Id, offrampingPhase } = useMainProcess(); + const { handleOnSubmit, finishOfframping, offrampingStarted, sep24Url, sep24Id, offrampingPhase, signingPhase } = + useMainProcess(); const { tokensModal: [modalType, setModalType], @@ -63,19 +58,11 @@ export const SwapPage = () => { fromAmountString, from, to, - reset, } = useSwapForm(); const fromToken = from ? INPUT_TOKEN_CONFIG[from] : undefined; const toToken = to ? OUTPUT_TOKEN_CONFIG[to] : undefined; - useEffect(() => { - if (form.formState.isDirty && isExchangeSectionSubmitted && isDisconnected) { - setIsExchangeSectionSubmitted(false); - reset(); - } - }, [form.formState.isDirty, isDisconnected, isExchangeSectionSubmitted, reset]); - const tokenOutData = useTokenOutAmount({ wantsSwap: true, api, @@ -92,31 +79,10 @@ export const SwapPage = () => { const inputAmountIsStable = tokenOutData.actualAmountInRaw !== undefined && BigInt(tokenOutData.actualAmountInRaw) > 0n; - // Check only the first part of the form (without Bank Details) - const isFormValidWithoutBankDetails = useMemo(() => { - const errors = form.formState.errors; - const noErrors = !errors.from && !errors.to && !errors.fromAmount && !errors.toAmount; - const isValid = - Boolean(from) && Boolean(to) && Boolean(fromAmount) && Boolean(tokenOutData.data?.amountOut.preciseString); - - return noErrors && isValid; - }, [form.formState.errors, from, fromAmount, to, tokenOutData.data?.amountOut.preciseString]); - function onSubmit(e: Event) { e.preventDefault(); - if (isSubmitButtonDisabled || !inputAmountIsStable || tokenOutData.actualAmountInRaw === undefined) return; - - if (!isExchangeSectionSubmitted) { - if (isFormValidWithoutBankDetails) { - setIsExchangeSectionSubmittedError(false); - setIsExchangeSectionSubmitted(true); - } else { - setIsExchangeSectionSubmittedError(true); - } - - return; - } + if (!inputAmountIsStable || tokenOutData.actualAmountInRaw === undefined) return; if (fromAmount === undefined) { console.log('Input amount is undefined'); @@ -140,12 +106,6 @@ export const SwapPage = () => { }); } - useEffect(() => { - if (isExchangeSectionSubmitted) { - formRef.current && formRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' }); - } - }, [isExchangeSectionSubmitted]); - useEffect(() => { if (tokenOutData.data) { const toAmount = tokenOutData.data.amountOut.preciseBigDecimal.round(2, 0); @@ -157,20 +117,6 @@ export const SwapPage = () => { } }, [form, tokenOutData.data]); - // Check if the Submit button should be enabled - useEffect(() => { - // Validate only the first part of the form (without Bank Details) - if (!isExchangeSectionSubmitted && isFormValidWithoutBankDetails) { - setIsSubmitButtonDisabled(false); - } - // Validate the whole form (with Bank Details) - else if (isExchangeSectionSubmitted /*&& form.formState.isValid*/) { - setIsSubmitButtonDisabled(false); - } else { - setIsSubmitButtonDisabled(true); - } - }, [form.formState, form.formState.isValid, isExchangeSectionSubmitted, isFormValidWithoutBankDetails]); - const ReceiveNumericInput = useMemo( () => ( { ); function getCurrentErrorMessage() { - if (isExchangeSectionSubmittedError) { - return 'You must first enter the amount you wish to withdraw.'; - } - const amountOut = tokenOutData.data?.amountOut; if (amountOut !== undefined && toToken !== undefined) { @@ -242,7 +184,7 @@ export const SwapPage = () => { setModalType(undefined)} isLoading={false} @@ -257,12 +199,13 @@ export const SwapPage = () => { return ; } - if (offrampingPhase !== undefined || offrampingStarted) { + if ((offrampingPhase !== undefined || offrampingStarted) && signingPhase === 'finished') { return ; } const main = (
+
{
- {isExchangeSectionSubmitted ? ( - - ) : ( - <> - )} {sep24Url !== undefined ? ( { ) : ( )} diff --git a/src/services/evmTransactions.ts b/src/services/evmTransactions.ts index 95c744d8..a9ae5e54 100644 --- a/src/services/evmTransactions.ts +++ b/src/services/evmTransactions.ts @@ -2,5 +2,5 @@ import { waitForTransactionReceipt } from '@wagmi/core'; import { Config } from 'wagmi'; export async function waitForEvmTransaction(hash: `0x${string}`, wagmiConfig: Config) { - const result = await waitForTransactionReceipt(wagmiConfig, { hash }); + await waitForTransactionReceipt(wagmiConfig, { hash }); } diff --git a/src/services/offrampingFlow.ts b/src/services/offrampingFlow.ts index f6c9b2c8..50aa408f 100644 --- a/src/services/offrampingFlow.ts +++ b/src/services/offrampingFlow.ts @@ -12,6 +12,7 @@ import { executeSpacewalkRedeem } from './polkadot'; import { fetchSigningServiceAccountId } from './signingService'; import { Keypair } from 'stellar-sdk'; import { storageService } from './storage/local'; +import { SigningPhase } from '../hooks/useMainProcess'; export type OfframpingPhase = | 'squidRouter' @@ -88,6 +89,7 @@ const STATE_ADVANCEMENT_HANDLERS: Record void; } const OFFRAMPING_STATE_LOCAL_STORAGE_KEY = 'offrampingState'; @@ -196,8 +198,8 @@ export async function advanceOfframpingState(context: ExecutionContext): Promise let newState: OfframpingState | undefined; try { newState = await STATE_ADVANCEMENT_HANDLERS[phase](state, context); - } catch (error) { - if ((error as any)?.message === 'Wallet not connected') { + } catch (error: unknown) { + if ((error as Error)?.message === 'Wallet not connected') { // TODO: transmit error to caller console.error('Wallet not connected. Try to connect wallet'); return state; diff --git a/src/services/polkadot/ephemeral.tsx b/src/services/polkadot/ephemeral.tsx index 4ede7f49..738e5850 100644 --- a/src/services/polkadot/ephemeral.tsx +++ b/src/services/polkadot/ephemeral.tsx @@ -1,7 +1,7 @@ import { Keyring } from '@polkadot/api'; import { mnemonicGenerate } from '@polkadot/util-crypto'; import { getApiManagerInstance } from './polkadotApi'; -import { getPendulumCurrencyId, INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; +import { getPendulumCurrencyId, INPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; import Big from 'big.js'; import { ExecutionContext, OfframpingState } from '../offrampingFlow'; import { waitForEvmTransaction } from '../evmTransactions'; diff --git a/src/services/squidrouter/process.ts b/src/services/squidrouter/process.ts index 4162973b..71284ca0 100644 --- a/src/services/squidrouter/process.ts +++ b/src/services/squidrouter/process.ts @@ -1,14 +1,17 @@ import { writeContract, sendTransaction, getAccount } from '@wagmi/core'; +import { Keyring } from '@polkadot/api'; -import { ExecutionContext, OfframpingState } from '../offrampingFlow'; -import erc20ABI from '../../contracts/ERC20'; import { INPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; -import { getRouteTransactionRequest } from './route'; -import { waitForEvmTransaction } from '../evmTransactions'; -import { Keyring } from '@polkadot/api'; +import erc20ABI from '../../contracts/ERC20'; import { getApiManagerInstance } from '../polkadot/polkadotApi'; +import { ExecutionContext, OfframpingState } from '../offrampingFlow'; +import { waitForEvmTransaction } from '../evmTransactions'; +import { getRouteTransactionRequest } from './route'; -export async function squidRouter(state: OfframpingState, { wagmiConfig }: ExecutionContext): Promise { +export async function squidRouter( + state: OfframpingState, + { wagmiConfig, setSigningPhase }: ExecutionContext, +): Promise { const inputToken = INPUT_TOKEN_CONFIG[state.inputTokenType]; const fromTokenErc20Address = inputToken.erc20AddressSourceChain; @@ -32,6 +35,8 @@ export async function squidRouter(state: OfframpingState, { wagmiConfig }: Execu console.log('Asking for approval of', transactionRequest?.target, fromTokenErc20Address, state.inputAmount.units); + setSigningPhase?.('started'); + const approvalHash = await writeContract(wagmiConfig, { abi: erc20ABI, address: fromTokenErc20Address, @@ -39,6 +44,8 @@ export async function squidRouter(state: OfframpingState, { wagmiConfig }: Execu args: [transactionRequest?.target, state.inputAmount.raw], }); + setSigningPhase?.('approved'); + await waitForEvmTransaction(approvalHash, wagmiConfig); const swapHash = await sendTransaction(wagmiConfig, { @@ -48,9 +55,13 @@ export async function squidRouter(state: OfframpingState, { wagmiConfig }: Execu gas: BigInt(transactionRequest.gasLimit) * BigInt(2), }); + setSigningPhase?.('signed'); + const axelarScanLink = 'https://axelarscan.io/gmp/' + swapHash; console.log(`Squidrouter Swap Initiated! Check Axelarscan for details: ${axelarScanLink}`); + setSigningPhase?.('finished'); + return { ...state, squidRouterApproveHash: approvalHash, diff --git a/tailwind.config.js b/tailwind.config.js index 08c6468f..b88c0dc6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -53,7 +53,7 @@ module.exports = { // Undefined colors will be chosen by daisyUI automatically. { pendulum: { - primary: '#907EA0', + primary: '#0F4DC0', 'primary-content': '#fff', secondary: '#F4F5F6', 'secondary-content': '#58667E',