diff --git a/app.config.js b/app.config.js index 7eb5da50..4ac3cca7 100644 --- a/app.config.js +++ b/app.config.js @@ -95,5 +95,16 @@ module.exports = { }, // Display alert banner for the developer preview deployment - showPreviewAlert: process.env.NEXT_PUBLIC_SHOW_PREVIEW_ALERT || 'false' + showPreviewAlert: process.env.NEXT_PUBLIC_SHOW_PREVIEW_ALERT || 'false', + + networkAlertConfig: { + // Refresh interval for network status - 30 sec + refreshInterval: 30000, + // Margin of error for block count (how much difference between min / max block numbers before showing an alert) + errorMargin: 10, + // Map chainIds to their respective status endpoints + statusEndpoints: { + 100: 'https://status.genx.delta-dao.com/api/check-blocks' + } + } } diff --git a/content/site.json b/content/site.json index d65dac7e..61458420 100644 --- a/content/site.json +++ b/content/site.json @@ -26,70 +26,104 @@ "subItems": [ { "name": "Pontus-X", - "link": "https://pontus-x.eu/" + "link": "https://pontus-x.eu/", + "isLive": true }, { "name": "GX4M moveID", "link": "https://portal.moveid.eu/", "description": "moveID aims to develop a decentralized digital mobility identity infrastructure for Europe.", "image": "moveid-logo.webp", - "category": "mobility" + "category": "mobility", + "isLive": true }, { "name": "Cooperants", "link": "https://cooperants.pontus-x.eu/", "description": "The world's only collaborative alliance in the Aeronautics and space sector that solves pressing digital collaboration problems by creating a common data space.", "image": "cooperants-logo.webp", - "category": "space" + "category": "manufacturing", + "isLive": true }, { "name": "Airbus", "link": "https://airbus.pontus-x.eu/", "description": "For those who make this world a safer place. We pioneer defense and space for their mission success.", "image": "airbus-logo.webp", - "category": "space" + "category": "manufacturing", + "isLive": true }, { "name": "Service-Meister", "link": "https://servicemeister.pontus-x.eu/", "description": "An AI-based Service Ecosystem for Technical Service in the Age of Industry 4.0.", "image": "service-meister-logo.webp", - "category": "manufacturing" - }, - { - "name": "Future Mobility Marketplace", - "link": "https://marketplace.future-mobility-alliance.org/", - "description": "Find or publish datasets and AI algorithms for solving challenges in mobility.", - "image": "fmdm-logo.webp", - "category": "mobility" + "category": "AI", + "isLive": true }, { "name": "Berlin State Library", "link": "https://sbb.pontus-x.eu/", "description": "Explore the Open Science Ecosystem, a collaboration of libraries, universities, and science pioneers under Gaia-X principles for limitless shared knowledge.", "image": "sbb-logo.webp", - "category": "science" + "category": "language models", + "isLive": true + }, + { + "name": "Flex4Res", + "link": "https://flex4res.pontus-x.eu/", + "description": "Flex4Res stands for Data spaces for FLEXible production lines and supply chains FOR RESilient manufacturing. Flex4Res develops an open platform for secure and sovereign data exchange", + "image": "flex4res-logo.webp", + "category": "manufacturing", + "isLive": true + }, + { + "name": "Future Mobility Marketplace", + "link": "https://marketplace.future-mobility-alliance.org/", + "description": "Find or publish datasets and AI algorithms for solving challenges in mobility.", + "image": "fmdm-logo.webp", + "category": "mobility", + "isLive": true }, { "name": "EnergySHR", "link": "https://www.energyshr.nl/", "description": "A platform for sharing (SHR) datasets and AI algorithms that contribute to solving challenges of the energy transition.", "image": "energy-shr-logo.webp", - "category": "science" + "category": "energy", + "isLive": true }, { "name": "Clinical Insights Exchange", "link": "https://cix.triall.io/", "description": "A federated data platform to change clinical data assets and analytics services in a trusted, privacy-friendly environment.", "image": "clinical-insights-exchange-logo.webp", - "category": "health" + "category": "health", + "isLive": true }, { "name": "UdL Research Data Space", "link": "https://dataspace.angliru.udl.cat/", "description": "An open research platform following the FAIR guiding principles of findability, accessibility, interoperability and reusability, while guaranteeing data sovereignty.", "image": "udl-logo.webp", - "category": "science" + "category": "agriculture", + "isLive": true + }, + { + "name": "Under Construction: ACCURATE", + "link": "https://ec.europa.eu/info/funding-tenders/opportunities/portal/screen/how-to-participate/org-details/999999999/project/101138269/program/43108390/details", + "description": "Gaia-X Lighthouse ACCURATE aims to boost the competitiveness of European manufacturing companies with DSS and Manufacturing as a Service frameworks. ", + "image": "accurate-logo.webp", + "category": "manufacturing", + "isLive": false + }, + { + "name": "deltaDAO AI marketplace", + "link": "https://market.delta-dao.com/", + "description": "With deltaDAO's AI Marketplace, you have the freedom to decide how your AI service is offered. By keeping your AI private, you retain full control and do not disclose your valuable IP.", + "image": "deltadao-logo.webp", + "category": "AI", + "isLive": false } ] } diff --git a/src/@context/Automation/AutomationProvider.tsx b/src/@context/Automation/AutomationProvider.tsx index f75f6702..db4fb3c3 100644 --- a/src/@context/Automation/AutomationProvider.tsx +++ b/src/@context/Automation/AutomationProvider.tsx @@ -21,16 +21,11 @@ export enum AUTOMATION_MODES { ADVANCED = 'advanced' } -export interface NativeTokenBalance { - symbol: string - balance: string -} export interface AutomationProviderValue { autoWallet: Wallet autoWalletAddress: string isAutomationEnabled: boolean balance: UserBalance - nativeBalance: NativeTokenBalance isLoading: boolean decryptPercentage: number hasValidEncryptedWallet: boolean @@ -65,8 +60,12 @@ function AutomationProvider({ children }) { address: autoWallet?.address as `0x${string}` }) - const [nativeBalance, setNativeBalance] = useState() - const [balance, setBalance] = useState({}) + const [balance, setBalance] = useState({ + native: { + symbol: 'eth', + balance: '0' + } + }) const [hasDeleteRequest, setHasDeleteRequest] = useState(false) @@ -98,16 +97,22 @@ function AutomationProvider({ children }) { if (!autoWallet) return try { + const newBalance: UserBalance = { + native: { + symbol: 'eth', + balance: '0' + } + } if (balanceNativeToken) - setNativeBalance({ - symbol: balanceNativeToken?.symbol.toLowerCase() || 'ETH', - balance: balanceNativeToken?.formatted - }) + newBalance.native.symbol = + balanceNativeToken?.symbol.toLowerCase() || 'eth' + newBalance.native.balance = balanceNativeToken?.formatted if (approvedBaseTokens?.length > 0) { - const newBalance = await getApprovedTokenBalances(autoWallet?.address) - setBalance(newBalance) - } else setBalance(undefined) + const approved = await getApprovedTokenBalances(autoWallet?.address) + newBalance.approved = approved + } + setBalance(newBalance) } catch (error) { LoggerInstance.error('[AutomationProvider] Error: ', error.message) } @@ -207,7 +212,6 @@ function AutomationProvider({ children }) { autoWallet, autoWalletAddress, balance, - nativeBalance, isAutomationEnabled, isLoading, decryptPercentage, diff --git a/src/@context/MarketMetadata/_types.ts b/src/@context/MarketMetadata/_types.ts index 3151179f..89998700 100644 --- a/src/@context/MarketMetadata/_types.ts +++ b/src/@context/MarketMetadata/_types.ts @@ -36,6 +36,16 @@ export interface AppConfig { roughTxGasEstimate: number } showPreviewAlert: string + networkAlertConfig: { + // Refresh interval for network status - 30 sec + refreshInterval: number + // Margin of error for block count (how much difference between min / max block numbers before showing an alert) + errorMargin: number + // Map chainIds to their respective status endpoints + statusEndpoints: { + [chainId: number]: string + } + } } export interface SiteContent { siteTitle: string @@ -52,12 +62,14 @@ export interface SiteContent { description?: string image?: string category?: string + isLive?: boolean subItems?: { name: string link: string description?: string image?: string category?: string + isLive?: boolean }[] }[] }[] diff --git a/src/@hooks/useBalance.tsx b/src/@hooks/useBalance.tsx index 2eacb2f3..9462ae76 100644 --- a/src/@hooks/useBalance.tsx +++ b/src/@hooks/useBalance.tsx @@ -11,7 +11,7 @@ import { getTokenBalance } from '@utils/wallet' interface BalanceProviderValue { balance: UserBalance - getApprovedTokenBalances: (address: string) => Promise + getApprovedTokenBalances: (address: string) => Promise } function useBalance(): BalanceProviderValue { @@ -22,12 +22,15 @@ function useBalance(): BalanceProviderValue { const { chain } = useNetwork() const [balance, setBalance] = useState({ - eth: '0' + native: { + symbol: 'eth', + balance: '0' + } }) const getApprovedTokenBalances = useCallback( - async (address: string): Promise => { - const newBalance: UserBalance = {} + async (address: string): Promise => { + const newBalance: TokenBalances = {} if (approvedBaseTokens?.length > 0) { await Promise.allSettled( @@ -64,11 +67,13 @@ function useBalance(): BalanceProviderValue { try { const userBalance = balanceNativeToken?.formatted const key = balanceNativeToken?.symbol.toLowerCase() - const newNativeBalance: UserBalance = { [key]: userBalance } - const newBalance = { - ...newNativeBalance, - ...(await getApprovedTokenBalances(address)) + const newBalance: UserBalance = { + native: { + symbol: key, + balance: userBalance + }, + approved: await getApprovedTokenBalances(address) } setBalance(newBalance) diff --git a/src/@types/AssetExtended.d.ts b/src/@types/AssetExtended.d.ts index 71d63e26..abb730c4 100644 --- a/src/@types/AssetExtended.d.ts +++ b/src/@types/AssetExtended.d.ts @@ -1,4 +1,4 @@ -import { Asset } from '@oceanprotocol/lib' +import { Asset, Metadata } from '@oceanprotocol/lib' // declaring into global scope to be able to use this as // ambiant types despite the above imports @@ -6,7 +6,7 @@ declare global { interface AssetExtended extends Asset { accessDetails?: AccessDetails views?: number - metadata: MetadataExtended + metadata: Metadata services: ServiceExtended[] } } diff --git a/src/@types/TokenBalance.d.ts b/src/@types/TokenBalance.d.ts index d51e735d..bd117f8c 100644 --- a/src/@types/TokenBalance.d.ts +++ b/src/@types/TokenBalance.d.ts @@ -1,3 +1,11 @@ interface UserBalance { + native: { + symbol: string + balance: string + } + approved?: TokenBalances +} + +interface TokenBalances { [key: string]: string } diff --git a/src/@utils/subgraph.ts b/src/@utils/subgraph.ts index 3cc58875..613e31e9 100644 --- a/src/@utils/subgraph.ts +++ b/src/@utils/subgraph.ts @@ -58,6 +58,7 @@ const OpcsApprovedTokensQuery = gql` export const tokenAddressesEUROe = { 100: '0xe974c4894996e012399dedbda0be7314a73bbff1', 137: '0x820802Fa8a99901F52e39acD21177b0BE6EE2974', + 32456: '0x8A4826071983655805bF4f29828577Cd6b1aC0cB', 80001: '0xA089a21902914C3f3325dBE2334E9B466071E5f1' } @@ -187,7 +188,8 @@ export async function getOpcsApprovedTokens( ...approvedTokens, { address: tokenAddressesEUROe[chainId], - decimals: 6, + // TODO: revert once decimals changed to 6 on pontus-x + decimals: chainId === 32456 ? 18 : 6, name: 'EUROe', symbol: 'EUROe' } diff --git a/src/@utils/wallet/index.ts b/src/@utils/wallet/index.ts index b37f4187..9121e1af 100644 --- a/src/@utils/wallet/index.ts +++ b/src/@utils/wallet/index.ts @@ -176,12 +176,12 @@ export async function getTokenBalance( } } -export function getTokenBalanceFromSymbol( +export function getApprovedTokenBalanceFromSymbol( balance: UserBalance, symbol: string ): string { if (!symbol) return - - const baseTokenBalance = balance?.[symbol.toLocaleLowerCase()] + const { approved } = balance + const baseTokenBalance = approved?.[symbol.toLocaleLowerCase()] return baseTokenBalance || '0' } diff --git a/src/components/@shared/AssetList/index.tsx b/src/components/@shared/AssetList/index.tsx index e02f1bf2..83f1a5f1 100644 --- a/src/components/@shared/AssetList/index.tsx +++ b/src/components/@shared/AssetList/index.tsx @@ -1,5 +1,5 @@ import AssetTeaser from '@shared/AssetTeaser' -import { ReactElement, useState } from 'react' +import { ReactElement, useEffect, useState } from 'react' import Pagination from '@shared/Pagination' import styles from './index.module.css' import AssetTitle from '@shared/AssetListTitle' @@ -10,8 +10,20 @@ import { getServiceByName } from '@utils/ddo' import AssetViewSelector, { AssetViewOptions } from './AssetViewSelector' import Time from '../atoms/Time' import Loader from '../atoms/Loader' +import NetworkName from '../NetworkName' +import { useUserPreferences } from '../../../@context/UserPreferences' +import { ChainDoesNotSupportMulticallError } from 'wagmi' -const columns: TableOceanColumn[] = [ +const networkColumn: TableOceanColumn = { + name: 'Network', + selector: (row) => { + const { chainId } = row + return + }, + maxWidth: '10rem' +} + +const tableColumns: TableOceanColumn[] = [ { name: 'Dataset', selector: (row) => { @@ -94,6 +106,17 @@ export default function AssetList({ showAssetViewSelector, defaultAssetView }: AssetListProps): ReactElement { + const { chainIds } = useUserPreferences() + + const [columns, setColumns] = useState(tableColumns) + + useEffect(() => { + if (chainIds.length > 1) { + const [datasetColumn, ...otherColumns] = tableColumns + setColumns([datasetColumn, networkColumn, ...otherColumns]) + } else setColumns(tableColumns) + }, [chainIds]) + const [activeAssetView, setActiveAssetView] = useState( defaultAssetView || AssetViewOptions.Grid ) diff --git a/src/components/@shared/MenuDropdown/index.tsx b/src/components/@shared/MenuDropdown/index.tsx index 55022108..0c68695d 100644 --- a/src/components/@shared/MenuDropdown/index.tsx +++ b/src/components/@shared/MenuDropdown/index.tsx @@ -40,17 +40,13 @@ export default function MenuDropdown({ content={
    {items.map((item, i) => { - const { name, link, subItems } = item + const { name, subItems } = item return (
  • {subItems && subItems.length > 0 ? ( ) : ( - + )}
  • ) diff --git a/src/components/@shared/NetworkStatus/index.tsx b/src/components/@shared/NetworkStatus/index.tsx new file mode 100644 index 00000000..73ad4f8e --- /dev/null +++ b/src/components/@shared/NetworkStatus/index.tsx @@ -0,0 +1,84 @@ +import { ReactElement, useCallback, useEffect, useState } from 'react' +import { useNetwork } from 'wagmi' +import { useMarketMetadata } from '../../../@context/MarketMetadata' +import Alert from '../atoms/Alert' +import axios from 'axios' +import { LoggerInstance } from '@oceanprotocol/lib' + +export default function NetworkStatus({ + className +}: { + className?: string +}): ReactElement { + const [showNetworkAlert, setShowNetworkAlert] = useState(false) + const [network, setNetwork] = useState() + const { appConfig } = useMarketMetadata() + const { chain } = useNetwork() + + const { networkAlertConfig } = appConfig + + const fetchNetworkStatus = useCallback( + async (chainId: number) => { + if (!chainId) return + setNetwork(chain?.name) + const apiEndpoint = networkAlertConfig.statusEndpoints[chainId] + if (!apiEndpoint) return + LoggerInstance.log(`[NetworkStatus] retrieving network status`, { + apiEndpoint + }) + try { + const result = await axios.get(apiEndpoint) + const { Nodes } = result.data + const { nodes }: { nodes: { [node: string]: number } } = Nodes + let minBlock: number + let maxBlock: number + Object.values(nodes).forEach((block) => { + if (!minBlock || block < minBlock) minBlock = block + if (!maxBlock || block > maxBlock) maxBlock = block + }) + const hasError = maxBlock - minBlock > networkAlertConfig.errorMargin + setShowNetworkAlert(hasError) + LoggerInstance.log(`[NetworkStatus] network status updated:`, { + minBlock, + maxBlock, + hasError + }) + } catch (error) { + LoggerInstance.error( + `[NetworkStatus] could not retrieve network status:`, + error.message + ) + } + }, + [networkAlertConfig, chain] + ) + + useEffect(() => { + if (!chain?.id) return + + fetchNetworkStatus(chain?.id) + + // init periodic refresh for network status + const networkStatusInterval = setInterval( + () => fetchNetworkStatus(chain?.id), + networkAlertConfig.refreshInterval + ) + + return () => { + clearInterval(networkStatusInterval) + } + }, [chain, fetchNetworkStatus]) + + return ( + showNetworkAlert && ( + setShowNetworkAlert(false)} + className={className} + /> + ) + ) +} diff --git a/src/components/@shared/Page/PageHeader.module.css b/src/components/@shared/Page/PageHeader.module.css index ec95acd3..fa68c029 100644 --- a/src/components/@shared/Page/PageHeader.module.css +++ b/src/components/@shared/Page/PageHeader.module.css @@ -11,7 +11,6 @@ .header { margin-bottom: var(--spacer); - max-width: 50rem; } .homeWrapper .header { @@ -75,3 +74,7 @@ margin-top: calc(var(--spacer) / 2); max-width: 30rem; } + +.networkAlert { + margin: var(--spacer) auto; +} diff --git a/src/components/@shared/Page/PageHeader.tsx b/src/components/@shared/Page/PageHeader.tsx index a364e131..89c9eab9 100644 --- a/src/components/@shared/Page/PageHeader.tsx +++ b/src/components/@shared/Page/PageHeader.tsx @@ -1,8 +1,9 @@ -import { ReactElement } from 'react' +import SearchBar from '@components/Header/SearchBar' +import Markdown from '@shared/Markdown' import classNames from 'classnames/bind' +import { ReactElement } from 'react' +import NetworkStatus from '../NetworkStatus' import styles from './PageHeader.module.css' -import Markdown from '@shared/Markdown' -import SearchBar from '@components/Header/SearchBar' const cx = classNames.bind(styles) @@ -36,7 +37,10 @@ export default function PageHeader({ ) : ( -

    {title}

    +
    +

    {title}

    + +
    )} {description && ( diff --git a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx index 49aae0c7..2b79c543 100644 --- a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx +++ b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx @@ -10,7 +10,7 @@ import { useAsset } from '@context/Asset' import content from '../../../../../content/pages/startComputeDataset.json' import { Asset, ComputeEnvironment, ZERO_ADDRESS } from '@oceanprotocol/lib' import { getAccessDetails } from '@utils/accessDetailsAndPricing' -import { getTokenBalanceFromSymbol } from '@utils/wallet' +import { getApprovedTokenBalanceFromSymbol } from '@utils/wallet' import { MAX_DECIMALS } from '@utils/constants' import Decimal from 'decimal.js' import { useAccount } from 'wagmi' @@ -273,7 +273,7 @@ export default function FormStartCompute({ totalPrices.forEach((price) => { const balanceToUse = isAutomationEnabled ? automationBalance : balance - const baseTokenBalance = getTokenBalanceFromSymbol( + const baseTokenBalance = getApprovedTokenBalanceFromSymbol( balanceToUse, price.symbol ) diff --git a/src/components/Asset/AssetActions/index.tsx b/src/components/Asset/AssetActions/index.tsx index f05cc638..e62e3f82 100644 --- a/src/components/Asset/AssetActions/index.tsx +++ b/src/components/Asset/AssetActions/index.tsx @@ -11,7 +11,7 @@ import { useIsMounted } from '@hooks/useIsMounted' import styles from './index.module.css' import { useFormikContext } from 'formik' import { FormPublishData } from '@components/Publish/_types' -import { getTokenBalanceFromSymbol } from '@utils/wallet' +import { getApprovedTokenBalanceFromSymbol } from '@utils/wallet' import AssetStats from './AssetStats' import { isAddressWhitelisted } from '@utils/ddo' import { useAccount, useProvider, useNetwork, useSigner } from 'wagmi' @@ -154,7 +154,7 @@ export default function AssetActions({ const balanceToUse = isAutomationEnabled ? automationBalance : balance - const baseTokenBalance = getTokenBalanceFromSymbol( + const baseTokenBalance = getApprovedTokenBalanceFromSymbol( balanceToUse, asset?.accessDetails?.baseToken?.symbol ) diff --git a/src/components/Header/Menu.tsx b/src/components/Header/Menu.tsx index 929c5563..39415474 100644 --- a/src/components/Header/Menu.tsx +++ b/src/components/Header/Menu.tsx @@ -11,7 +11,6 @@ import MenuDropdown from '@components/@shared/MenuDropdown' import SearchButton from './SearchButton' import Button from '@components/@shared/atoms/Button' import UserPreferences from './UserPreferences' -import { useAutomation } from '../../@context/Automation/AutomationProvider' import Automation from './UserPreferences/Automation' const Wallet = loadable(() => import('./Wallet')) @@ -25,9 +24,10 @@ declare type MenuItem = { image?: string category?: string className?: string + isLive?: boolean } -export function MenuLink({ name, link, className }: MenuItem) { +export function MenuLink({ name, link, className, isLive }: MenuItem) { const router = useRouter() const basePath = router?.pathname.split(/[/?]/)[1] @@ -39,7 +39,9 @@ export function MenuLink({ name, link, className }: MenuItem) { [className]: className }) - return ( + return isLive === false ? ( + <> + ) : ( + {live ? ( + + ) : ( + + )} diff --git a/src/components/Home/Ecosystem/index.tsx b/src/components/Home/Ecosystem/index.tsx index 664cf3e4..de3ca496 100644 --- a/src/components/Home/Ecosystem/index.tsx +++ b/src/components/Home/Ecosystem/index.tsx @@ -18,6 +18,7 @@ export default function Ecosystem() { image={portal.image} link={portal.link} title={portal.name} + live={portal.isLive} /> ))}