-
Validators
+
+
+
Validators
+
+ List of validators on SKALE Network
+
+
+
-
List of validators on SKALE Network
(() => {
@@ -47,14 +48,13 @@ export function useApps(chainsMeta: types.ChainsMetadataMap) {
return apps.sort((a, b) => (b.added || 0) - (a.added || 0))
}, [chainsMeta])
- const trendingAppIds = getTrendingApps()
-
- const trendingApps = useMemo(() => {
+ const mostLikedAppIds = getMostLikedApps()
+ const mostLikedApps = useMemo(() => {
const appMap = new Map(allApps.map((app) => [getAppId(app.chain, app.appName), app]))
- return trendingAppIds
+ return mostLikedAppIds
.map((id) => appMap.get(id))
.filter((app): app is types.AppWithChainAndName => app !== undefined)
- }, [allApps, trendingAppIds, getAppId])
+ }, [allApps, mostLikedAppIds, getAppId])
const favoriteApps = useMemo(() => {
if (!isSignedIn) return []
@@ -62,5 +62,25 @@ export function useApps(chainsMeta: types.ChainsMetadataMap) {
return apps.sort((a, b) => a.alias.localeCompare(b.alias))
}, [allApps, likedApps, isSignedIn, getAppId])
- return { allApps, newApps, trendingApps, favoriteApps, isSignedIn }
+ const trendingApps = useMemo(() => {
+ if (!metrics) return []
+
+ const appsWithTransactions = allApps.map((app) => {
+ const chainMetrics = metrics.metrics[app.chain]
+ if (!chainMetrics || !chainMetrics.apps_counters[app.appName]) {
+ return { ...app, transactions_last_7_days: 0 }
+ }
+ const totalCounters = getTotalAppCounters(chainMetrics.apps_counters[app.appName])
+ return {
+ ...app,
+ transactions_last_7_days: totalCounters ? totalCounters.transactions_last_7_days : 0
+ }
+ })
+
+ return appsWithTransactions
+ .sort((a, b) => b.transactions_last_7_days - a.transactions_last_7_days)
+ .slice(0, MAX_APPS_DEFAULT)
+ }, [allApps, metrics])
+
+ return { allApps, newApps, mostLikedApps, favoriteApps, trendingApps, isSignedIn }
}
diff --git a/src/useSFuel.tsx b/src/useSFuel.tsx
new file mode 100644
index 00000000..e47b4815
--- /dev/null
+++ b/src/useSFuel.tsx
@@ -0,0 +1,191 @@
+/**
+ * @license
+ * SKALE portal
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * @file useSFuel.tsx
+ * @copyright SKALE Labs 2024-Present
+ */
+
+import { useState, useEffect, useCallback, useMemo } from 'react'
+import { Logger, type ILogObj } from 'tslog'
+import { useWagmiAccount, type MetaportCore, Station } from '@skalenetwork/metaport'
+import { DEFAULT_MIN_SFUEL_WEI, SFUEL_CHECK_INTERVAL } from './core/constants'
+import { types } from '@/core'
+
+const log = new Logger({ name: 'useSFuel' })
+
+const CHAINS_TO_SKIP = ['turbulent-unique-scheat'] // todo: tmp fix, remove later
+
+interface SFuelState {
+ sFuelOk: boolean
+ isMining: boolean
+ chainsWithFaucet: string[]
+ totalChainsWithStation: number
+ chainsWithEnoughSFuel: number
+ currentAddress: types.AddressType | null
+ loading: boolean
+ intervalId: number | null
+}
+
+export function usesFuel(mpc: MetaportCore) {
+ const { address } = useWagmiAccount()
+ const [state, setState] = useState({
+ sFuelOk: true,
+ isMining: false,
+ chainsWithFaucet: [] as string[],
+ totalChainsWithStation: 0,
+ chainsWithEnoughSFuel: 0,
+ currentAddress: null,
+ loading: true,
+ intervalId: null
+ })
+
+ const checkFaucetAvailability = useCallback(async () => {
+ const chainsWithFaucet = await Promise.all(
+ mpc.config.chains
+ .filter((chain) => !CHAINS_TO_SKIP.includes(chain))
+ .map(async (chain) => {
+ const station = new Station(chain, mpc)
+ return (await station.isFaucetAvailable()) ? chain : null
+ })
+ ).then((chains) => chains.filter((chain): chain is string => chain !== null))
+ setState((prev) => ({
+ ...prev,
+ chainsWithFaucet,
+ totalChainsWithStation: chainsWithFaucet.length
+ }))
+ }, [mpc.config.chains, mpc.config.skaleNetwork])
+
+ const checkSFuelBalance = useCallback(
+ async (currentAddress: types.AddressType): Promise => {
+ if (!currentAddress || state.chainsWithFaucet.length === 0) return
+ if (state.currentAddress !== currentAddress) {
+ setState((prev) => ({ ...prev, currentAddress, loading: true }))
+ }
+ let chainsWithEnoughSFuel = 0
+ let sFuelOk = true
+ for (const chain of state.chainsWithFaucet) {
+ if (CHAINS_TO_SKIP.includes(chain)) continue
+ const { balance } = await new Station(chain, mpc).getData(currentAddress)
+ if (balance >= DEFAULT_MIN_SFUEL_WEI) {
+ chainsWithEnoughSFuel++
+ } else {
+ sFuelOk = false
+ }
+ }
+ setState((prev) => ({ ...prev, sFuelOk, chainsWithEnoughSFuel, loading: false }))
+ },
+ [state.chainsWithFaucet, mpc]
+ )
+
+ const mineSFuel = useCallback(async () => {
+ if (!address) return
+ setState((prev) => ({ ...prev, isMining: true }))
+ let errorOccurred = false
+ let chainsWithEnoughSFuel = 0
+
+ for (const chain of state.chainsWithFaucet) {
+ if (CHAINS_TO_SKIP.includes(chain)) continue
+ try {
+ const station = new Station(chain, mpc)
+ const { balance } = await station.getData(address)
+ if (balance < DEFAULT_MIN_SFUEL_WEI) {
+ log.info(`Mining sFuel on chain ${chain}`)
+ const powResult = await station.doPoW(address)
+ if (!powResult.ok) {
+ log.error(`Failed to mine sFuel on chain ${chain}: ${powResult.message}`)
+ errorOccurred = true
+ } else {
+ chainsWithEnoughSFuel++
+ }
+ } else {
+ chainsWithEnoughSFuel++
+ }
+ } catch (error) {
+ log.error(`Error processing chain ${chain}:`, error)
+ errorOccurred = true
+ }
+ setState((prev) => ({
+ ...prev,
+ chainsWithEnoughSFuel
+ }))
+ }
+
+ if (errorOccurred) {
+ log.error('sFuel mining encountered errors on one or more chains')
+ } else {
+ log.info('sFuel mining completed successfully on all required chains')
+ }
+
+ setState((prev) => ({
+ ...prev,
+ sFuelOk: !errorOccurred,
+ isMining: false,
+ chainsWithEnoughSFuel
+ }))
+ }, [address, state.chainsWithFaucet, mpc])
+
+ useEffect(() => {
+ checkFaucetAvailability()
+ }, [checkFaucetAvailability])
+
+ useEffect(() => {
+ const checkAndSetInterval = async () => {
+ if (address !== state.currentAddress && state.intervalId) {
+ clearInterval(state.intervalId)
+ setState((prev) => ({
+ ...prev,
+ intervalId: null
+ }))
+ }
+ if (address) {
+ await checkSFuelBalance(address)
+ setState((prev) => ({ ...prev, intervalId: newIntervalId }))
+ const newIntervalId = setInterval(checkSFuelBalance, SFUEL_CHECK_INTERVAL)
+ } else {
+ setState((prev) => ({
+ ...prev,
+ sFuelOk: true,
+ chainsWithEnoughSFuel: 0,
+ isChecking: false,
+ currentAddress: null,
+ intervalId: null
+ }))
+ }
+ }
+ checkAndSetInterval()
+ return () => {
+ if (state.intervalId) {
+ clearInterval(state.intervalId)
+ }
+ }
+ }, [address, checkSFuelBalance])
+
+ const sFuelCompletionPercentage = useMemo(() => {
+ if (state.totalChainsWithStation === 0) return 100
+ return Math.round((state.chainsWithEnoughSFuel / state.totalChainsWithStation) * 100)
+ }, [state.chainsWithEnoughSFuel, state.totalChainsWithStation])
+
+ return {
+ ...state,
+ mineSFuel,
+ totalChainsWithStation: state.chainsWithFaucet.length,
+ chainsWithEnoughSFuel: state.chainsWithEnoughSFuel,
+ sFuelCompletionPercentage
+ }
+}
diff --git a/vercel.json b/vercel.json
index 3f4e75aa..05a5fecc 100644
--- a/vercel.json
+++ b/vercel.json
@@ -19,5 +19,11 @@
"source": "/(.*)",
"destination": "/"
}
+ ],
+ "redirects": [
+ {
+ "source": "/submit",
+ "destination": "https://github.com/skalenetwork/skale-network/issues/new?assignees=dmytrotkk&labels=metadata&projects=&template=app_submission.yml&title=App+Metadata+Submission"
+ }
]
}
\ No newline at end of file