diff --git a/apps/web/index.html b/apps/web/index.html index dc80b1bf2..a5fc01a4d 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -20,7 +20,8 @@ + + diff --git a/apps/web/src/components/LoaderSpinner.vue b/apps/web/src/components/LoaderSpinner.vue new file mode 100644 index 000000000..2abef0072 --- /dev/null +++ b/apps/web/src/components/LoaderSpinner.vue @@ -0,0 +1,91 @@ + + + + + + \ No newline at end of file diff --git a/apps/web/src/composables/analytics.ts b/apps/web/src/composables/analytics.ts index 79ba6e304..f891311e7 100644 --- a/apps/web/src/composables/analytics.ts +++ b/apps/web/src/composables/analytics.ts @@ -1,14 +1,15 @@ -import { readonly, ref, watchEffect } from 'vue' +import { readonly, ref } from 'vue' import { UserAnalyticsData } from '@casimir/types' import useEnvironment from '@/composables/environment' import useTxData from '../mockData/mock_transaction_data' const { usersUrl } = useEnvironment() const { mockData, txData } = useTxData() +const loadingInitializeAnalytics = ref(false) +const loadingInitializeAnalyticsError = ref(false) export default function useAnalytics() { const finishedComputingUerAnalytics = ref(false) - const getUserAnalyticsError = ref(null) const rawUserAnalytics = ref(null) const userAnalytics = ref({ oneMonth: { @@ -153,7 +154,6 @@ export default function useAnalytics() { // const { error, message, data: athenaData } = await response.json() // console.log('data from analytics :>> ', data) // userAnalytics.value = athenaData - // getUserAnalyticsError.value = error // if (error) throw new Error(`Error in getUserAnalytics: ${message}`) // TODO: Get events, actions, and contract data from the API @@ -180,12 +180,19 @@ export default function useAnalytics() { } async function initializeAnalyticsComposable() { - resetUserAnalytics() - await getUserAnalytics() + try { + loadingInitializeAnalytics.value = true + resetUserAnalytics() + await getUserAnalytics() + loadingInitializeAnalytics.value = false + } catch (error) { + loadingInitializeAnalyticsError.value = true + loadingInitializeAnalytics.value = false + throw new Error('Error initializing analytics') + } } function resetUserAnalytics() { - getUserAnalyticsError.value = null userAnalytics.value = { oneMonth: { labels: [], @@ -208,10 +215,11 @@ export default function useAnalytics() { return { finishedComputingUerAnalytics: readonly(finishedComputingUerAnalytics), + loadingInitializeAnalytics: readonly(loadingInitializeAnalytics), + loadingInitializeAnalyticsError: readonly(loadingInitializeAnalyticsError), + rawUserAnalytics, userAnalytics: readonly(userAnalytics), - getUserAnalyticsError: readonly(getUserAnalyticsError), + initializeAnalyticsComposable, updateAnalytics, - rawUserAnalytics, - initializeAnalyticsComposable } } \ No newline at end of file diff --git a/apps/web/src/composables/breakdownMetrics.ts b/apps/web/src/composables/breakdownMetrics.ts index 3979a8d1a..ce0b9b8da 100644 --- a/apps/web/src/composables/breakdownMetrics.ts +++ b/apps/web/src/composables/breakdownMetrics.ts @@ -13,7 +13,11 @@ const { getCurrentPrice } = usePrice() const provider = new ethers.providers.JsonRpcProvider(ethereumUrl) +const loadingInitializeBreakdownMetrics = ref(false) +const loadingInitializeBreakdownMetricsError = ref(false) + export default function useBreakdownMetrics() { + const userValue = ref() const currentStaked = ref({ @@ -238,10 +242,18 @@ export default function useBreakdownMetrics() { async function initializeComposable(user: UserWithAccountsAndOperators){ userValue.value = toValue(user) - provider.removeAllListeners('block') - provider.on('block', blockListener as ethers.providers.Listener) - listenForContractEvents() - await refreshBreakdown() + try { + loadingInitializeBreakdownMetrics.value = true + provider.removeAllListeners('block') + provider.on('block', blockListener as ethers.providers.Listener) + listenForContractEvents() + await refreshBreakdown() + loadingInitializeBreakdownMetrics.value = false + } catch (error) { + loadingInitializeBreakdownMetricsError.value = true + console.log('Error initializing breakdown metrics :>> ', error) + loadingInitializeBreakdownMetrics.value = false + } } async function uninitializeComposable(){ @@ -252,6 +264,8 @@ export default function useBreakdownMetrics() { return { currentStaked: readonly(currentStaked), + loadingInitializeBreakdownMetrics: readonly(loadingInitializeBreakdownMetrics), + loadingInitializeBreakdownMetricsError: readonly(loadingInitializeBreakdownMetricsError), stakingRewards: readonly(stakingRewards), totalWalletBalance: readonly(totalWalletBalance), initializeComposable, diff --git a/apps/web/src/composables/contracts.ts b/apps/web/src/composables/contracts.ts index 1084e1163..6c1483437 100644 --- a/apps/web/src/composables/contracts.ts +++ b/apps/web/src/composables/contracts.ts @@ -1,5 +1,5 @@ -import { ref } from 'vue' -import { BigNumberish, ethers } from 'ethers' +import { ref, readonly } from 'vue' +import { ethers } from 'ethers' import { CasimirManager, CasimirRegistry, CasimirViews } from '@casimir/ethereum/build/@types' import ICasimirManagerAbi from '@casimir/ethereum/build/abi/ICasimirManager.json' import ICasimirRegistryAbi from '@casimir/ethereum/build/abi/ICasimirRegistry.json' @@ -12,13 +12,6 @@ import useWalletConnectV2 from './walletConnectV2' import { ProviderString } from '@casimir/types' import { Operator } from '@casimir/ssv' -interface RegisterOperatorWithCasimirParams { - walletProvider: ProviderString - address: string - operatorId: BigNumberish - collateral: string -} - const { ethereumUrl, managerAddress, registryAddress, viewsAddress } = useEnvironment() const provider = new ethers.providers.JsonRpcProvider(ethereumUrl) const manager: CasimirManager & ethers.Contract = new ethers.Contract(managerAddress, ICasimirManagerAbi, provider) as CasimirManager @@ -26,14 +19,12 @@ const views: CasimirViews & ethers.Contract = new ethers.Contract(viewsAddress, const registry: CasimirRegistry & ethers.Contract = new ethers.Contract(registryAddress, ICasimirRegistryAbi, provider) as CasimirRegistry const operators = ref([]) - -const loadingRegisteredOperators = ref(false) +const { ethersProviderList, getEthersBrowserSigner } = useEthers() +const { getEthersLedgerSigner } = useLedger() +const { getEthersTrezorSigner } = useTrezor() +const { getWalletConnectSignerV2 } = useWalletConnectV2() export default function useContracts() { - const { ethersProviderList, getEthersBrowserSigner } = useEthers() - const { getEthersLedgerSigner } = useLedger() - const { getEthersTrezorSigner } = useTrezor() - const { getWalletConnectSignerV2 } = useWalletConnectV2() async function deposit({ amount, walletProvider }: { amount: string, walletProvider: ProviderString }) { try { @@ -51,21 +42,12 @@ export default function useContracts() { } else { signer = signerCreator(walletProvider) } - console.log('signer :>> ', signer) const managerSigner = manager.connect(signer as ethers.Signer) - console.log('managerSigner :>> ', managerSigner) const fees = await getDepositFees() - console.log('fees :>> ', fees) const depositAmount = parseFloat(amount) * ((100 + fees) / 100) - console.log('depositAmount :>> ', depositAmount) const value = ethers.utils.parseEther(depositAmount.toString()) - console.log('value :>> ', value) - const result = await managerSigner.depositStake({ value, type: 2 }) - console.log('result :>> ', result) - await result.wait(2) - return true + return await managerSigner.depositStake({ value, type: 2 }) } catch (err) { - console.log('err :>> ', err) console.error(`There was an error in deposit function: ${JSON.stringify(err)}`) return false } @@ -135,33 +117,6 @@ export default function useContracts() { } } - async function registerOperatorWithCasimir({ walletProvider, address, operatorId, collateral }: RegisterOperatorWithCasimirParams) { - loadingRegisteredOperators.value = true - try { - const signerCreators = { - 'Browser': getEthersBrowserSigner, - 'Ledger': getEthersLedgerSigner, - 'Trezor': getEthersTrezorSigner, - 'WalletConnect': getWalletConnectSignerV2 - } - const signerType = ethersProviderList.includes(walletProvider) ? 'Browser' : walletProvider - const signerCreator = signerCreators[signerType as keyof typeof signerCreators] - let signer - if (walletProvider === 'WalletConnect') { - signer = await signerCreator(walletProvider) - } else { - signer = signerCreator(walletProvider) - } - const result = await registry.connect(signer as ethers.Signer).registerOperator(operatorId, { from: address, value: ethers.utils.parseEther(collateral)}) - loadingRegisteredOperators.value = false - // TODO: @shanejearley - How many confirmations do we want to wait? - await result?.wait(1) - } catch (err) { - console.error(`There was an error in registerOperatorWithCasimir function: ${JSON.stringify(err)}`) - loadingRegisteredOperators.value = false - } - } - async function withdraw({ amount, walletProvider }: { amount: string, walletProvider: ProviderString }) { const signerCreators = { 'Browser': getEthersBrowserSigner, @@ -185,7 +140,6 @@ export default function useContracts() { } return { - loadingRegisteredOperators, manager, operators, registry, @@ -193,7 +147,6 @@ export default function useContracts() { deposit, getDepositFees, getUserStake, - registerOperatorWithCasimir, withdraw } } \ No newline at end of file diff --git a/apps/web/src/composables/operators.ts b/apps/web/src/composables/operators.ts index e90313ad5..c6625c318 100644 --- a/apps/web/src/composables/operators.ts +++ b/apps/web/src/composables/operators.ts @@ -1,20 +1,33 @@ -import { readonly, ref, watchEffect, watch } from 'vue' +import { readonly, ref } from 'vue' import useEnvironment from '@/composables/environment' import useContracts from '@/composables/contracts' import { Operator, Scanner } from '@casimir/ssv' -import { RegisteredOperator, Pool, Account, UserWithAccountsAndOperators } from '@casimir/types' +import { Account, Pool, RegisteredOperator, RegisterOperatorWithCasimirParams, UserWithAccountsAndOperators } from '@casimir/types' import { ethers } from 'ethers' +import useEthers from '@/composables/ethers' +import useLedger from '@/composables/ledger' +import useTrezor from '@/composables/trezor' -export default function useOperators() { - const { ethereumUrl, ssvNetworkAddress, ssvNetworkViewsAddress, usersUrl } = useEnvironment() - const { manager, registry, views } = useContracts() +const { manager, registry, views } = useContracts() +const { ethereumUrl, ssvNetworkAddress, ssvNetworkViewsAddress, usersUrl } = useEnvironment() +const { ethersProviderList, getEthersBrowserSigner } = useEthers() +const { getEthersLedgerSigner } = useLedger() +const { getEthersTrezorSigner } = useTrezor() +const loadingInitializeOperators = ref(false) +const loadingInitializeOperatorsError = ref(false) +export default function useOperators() { + const loadingAddOperator = ref(false) + const loadingAddOperatorError = ref(false) + const loadingRegisteredOperators = ref(false) + const loadingRegisteredOperatorsError = ref(false) const nonregisteredOperators = ref([]) const registeredOperators = ref([]) async function addOperator({ address, nodeUrl }: { address: string, nodeUrl: string }) { try { + loadingAddOperator.value = true const requestOptions = { method: 'POST', headers: { @@ -24,9 +37,11 @@ export default function useOperators() { } const response = await fetch(`${usersUrl}/user/add-operator`, requestOptions) const { error, message } = await response.json() + loadingAddOperator.value = false return { error, message } } catch (error: any) { throw new Error(error.message || 'Error adding operator') + loadingAddOperatorError.value = true } } @@ -117,14 +132,55 @@ export default function useOperators() { } async function initializeComposable(user: UserWithAccountsAndOperators){ - listenForContractEvents(user) - await getUserOperators(user) + try { + loadingInitializeOperators.value = true + listenForContractEvents(user) + await getUserOperators(user) + loadingInitializeOperators.value = false + } catch (error) { + loadingInitializeOperatorsError.value = true + console.log('Error initializing operators :>> ', error) + loadingInitializeOperators.value = false + } + } + + // TODO: Move this to operators.ts to combine with AddOperator method + async function registerOperatorWithCasimir({ walletProvider, address, operatorId, collateral, nodeUrl }: RegisterOperatorWithCasimirParams) { + loadingRegisteredOperators.value = true + try { + const signerCreators = { + 'Browser': getEthersBrowserSigner, + 'Ledger': getEthersLedgerSigner, + 'Trezor': getEthersTrezorSigner + } + const signerType = ethersProviderList.includes(walletProvider) ? 'Browser' : walletProvider + const signerCreator = signerCreators[signerType as keyof typeof signerCreators] + let signer + if (walletProvider === 'WalletConnect') { + // signer = nonReactiveWalletConnectWeb3Provider + } else { + signer = signerCreator(walletProvider) + } + const result = await registry.connect(signer as ethers.Signer).registerOperator(operatorId, { from: address, value: ethers.utils.parseEther(collateral)}) + // TODO: @shanejearley - How many confirmations do we want to wait? + await result?.wait(1) + await addOperator({address, nodeUrl}) + loadingRegisteredOperators.value = false + } catch (err) { + loadingRegisteredOperatorsError.value = true + console.error(`There was an error in registerOperatorWithCasimir function: ${JSON.stringify(err)}`) + loadingRegisteredOperators.value = false + } } return { nonregisteredOperators: readonly(nonregisteredOperators), registeredOperators: readonly(registeredOperators), - addOperator, - initializeComposable + loadingAddOperator: readonly(loadingAddOperator), + loadingAddOperatorError: readonly(loadingAddOperatorError), + loadingInitializeOperators: readonly(loadingInitializeOperators), + loadingInitializeOperatorsError: readonly(loadingInitializeOperatorsError), + initializeComposable, + registerOperatorWithCasimir, } } \ No newline at end of file diff --git a/apps/web/src/composables/user.ts b/apps/web/src/composables/user.ts index e84e19d42..9731496fb 100644 --- a/apps/web/src/composables/user.ts +++ b/apps/web/src/composables/user.ts @@ -1,4 +1,4 @@ -import { onMounted, onUnmounted, readonly, ref, watch } from 'vue' +import { onMounted, onUnmounted, readonly, ref } from 'vue' import * as Session from 'supertokens-web-js/recipe/session' import { ethers } from 'ethers' import { Account, LoginCredentials, UserWithAccountsAndOperators } from '@casimir/types' @@ -18,6 +18,10 @@ const { loginWithWalletConnectV2, initializeWalletConnect, uninitializeWalletCon const initializeComposable = ref(false) const provider = new ethers.providers.JsonRpcProvider(ethereumUrl) const user = ref(undefined) +const loadingSessionLogin = ref(false) +const loadingSessionLoginError = ref(false) +const loadingSessionLogout = ref(false) +const loadingSessionLogoutError = ref(false) export default function useUser() { async function addAccountToUser({ provider, address, currency }: { provider: string, address: string, currency: string}) { @@ -96,8 +100,17 @@ export default function useUser() { } async function logout() { - await Session.signOut() - user.value = undefined + // Loader + try { + loadingSessionLogout.value = true + await Session.signOut() + user.value = undefined + loadingSessionLogout.value = false + } catch (error) { + loadingSessionLogoutError.value = true + console.log('Error logging out user :>> ', error) + loadingSessionLogout.value = false + } // TODO: Fix bug that doesn't allow you to log in without refreshing page after a user logs out window.location.reload() } @@ -105,9 +118,18 @@ export default function useUser() { onMounted(async () => { if (!initializeComposable.value) { initializeComposable.value = true - const session = await Session.doesSessionExist() - if (session) await getUser() - await initializeWalletConnect() + // Loader + try { + loadingSessionLogin.value = true + const session = await Session.doesSessionExist() + if (session) await getUser() + await initializeWalletConnect() + loadingSessionLogin.value = false + } catch (error) { + loadingSessionLoginError.value = true + console.log('error getting user :>> ', error) + loadingSessionLogin.value = false + } } }) @@ -192,9 +214,13 @@ export default function useUser() { return { user: readonly(user), + loadingSessionLogin: readonly(loadingSessionLogin), + loadingSessionLoginError: readonly(loadingSessionLoginError), + loadingSessionLogout: readonly(loadingSessionLogout), + loadingSessionLogoutError: readonly(loadingSessionLogoutError), addAccountToUser, login, logout, - updateUserAgreement + updateUserAgreement, } } \ No newline at end of file diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 65ff61589..3d8575357 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -5,12 +5,26 @@ :root { font-family: 'IBM Plex Sans', sans-serif; font-synthesis: none; + +max-width: 1400px; +margin: auto; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } +:root::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 319px; + background-color: #000; + z-index: -1; + } + .tooltip_container{ position: relative; } @@ -34,4 +48,27 @@ text-rendering: optimizeLegibility; opacity: 0; transition: opacity 0.4s ease-in-out; z-index: 5; +} + + +.skeleton_box { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: shimmer 2.5s infinite forwards; + backdrop-filter: blur(10px); +} + +@keyframes shimmer { + 0% { + background-position: +200% 0; + } + 100% { + background-position: -200% 0; + } } \ No newline at end of file diff --git a/apps/web/src/pages/operators/Operator.vue b/apps/web/src/pages/operators/Operator.vue index 364ddbf98..4a4b26045 100644 --- a/apps/web/src/pages/operators/Operator.vue +++ b/apps/web/src/pages/operators/Operator.vue @@ -2,18 +2,15 @@ import { onMounted, ref, watch } from 'vue' import VueFeather from 'vue-feather' import { ProviderString } from '@casimir/types' -import useContracts from '@/composables/contracts' import useFiles from '@/composables/files' import useFormat from '@/composables/format' import useUser from '@/composables/user' import useOperators from '@/composables/operators' import { UserWithAccountsAndOperators} from '@casimir/types' - -const { registerOperatorWithCasimir, loadingRegisteredOperators } = useContracts() const { exportFile } = useFiles() const { convertString } = useFormat() -const { user, } = useUser() +const { user, loadingSessionLogin } = useUser() // Form inputs const selectedWallet = ref({address: '', wallet_provider: ''}) @@ -105,7 +102,7 @@ onMounted(async () => { } }) -const { addOperator, initializeComposable, nonregisteredOperators, registeredOperators } = useOperators() +const {initializeComposable, nonregisteredOperators, registeredOperators, registerOperatorWithCasimir, loadingInitializeOperators } = useOperators() watch(user, async () => { if (user.value) { @@ -231,7 +228,8 @@ async function submitRegisterOperatorForm() { walletProvider: selectedWallet.value.wallet_provider as ProviderString, address: selectedWallet.value.address, operatorId: parseInt(selectedOperatorID.value), - collateral: selectedCollateral.value + collateral: selectedCollateral.value, + nodeUrl: selectedPublicNodeURL.value }) openAddOperatorModal.value = false } catch (error) { @@ -239,12 +237,6 @@ async function submitRegisterOperatorForm() { openAddOperatorModal.value = false } - // Add the nodeUrl to the operator - await addOperator({ - address: selectedWallet.value.address, - nodeUrl: selectedPublicNodeURL.value - }) - if (selectedWallet.value.address === '') { const primaryAccount = user.value?.accounts.find(item => { item.address === user.value?.address}) selectedWallet.value = {address: primaryAccount?.address as string, wallet_provider: primaryAccount?.walletProvider as string} @@ -255,20 +247,42 @@ async function submitRegisterOperatorForm() { availableOperatorIDs.value = [] } +const showSkeleton = ref(true) + +watch([loadingSessionLogin || loadingInitializeOperators], () =>{ + setTimeout(() => { + if(loadingSessionLogin || loadingInitializeOperators){ + showSkeleton.value = false + } + }, 500) +}) +