1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Merge pull request #628 from oceanprotocol/feature/multinetwork

Multinetwork Interface
This commit is contained in:
Matthias Kretschmann 2021-07-29 12:38:07 +02:00 committed by GitHub
commit 4c653b6ae8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 5165 additions and 1774 deletions

View File

@ -1,7 +1,3 @@
# Default network, possible values:
# "development", "ropsten", "rinkeby", "mainnet", "polygon", "moonbeamalpha",
# "gaiaxtestnet", "mumbai", "bsc"
GATSBY_NETWORK="rinkeby"
#GATSBY_INFURA_PROJECT_ID="xxx"
#GATSBY_MARKET_FEE_ADDRESS="0xxx"

View File

@ -16,7 +16,6 @@ jobs:
- run: npm run build
env:
GATSBY_NETWORK: ${{ secrets.GATSBY_NETWORK }}
GATSBY_INFURA_PROJECT_ID: ${{ secrets.GATSBY_INFURA_PROJECT_ID }}
GATSBY_PORTIS_ID: ${{ secrets.GATSBY_PORTIS_ID }}

2
.gitignore vendored
View File

@ -14,3 +14,5 @@ public/storybook
repo-metadata.json
content/networks-metadata.json
src/@types/apollo
graphql.schema.json
src/@types/graph.types.ts

View File

@ -31,7 +31,7 @@
## 🏄 Get Started
The app is a React app built with [Gatsby.js](https://www.gatsbyjs.org) + TypeScript + CSS modules and will connect to Ocean components in Rinkeby by default.
The app is a React app built with [Gatsby.js](https://www.gatsbyjs.org) + TypeScript + CSS modules and will connect to Ocean remote components by default.
To start local development:
@ -73,7 +73,7 @@ Barge will deploy contracts to the local Ganache node which will take some time.
Finally, set environment variables to use this local connection in `.env` in the app:
```bash
# modify env variables, setting GATSBY_NETWORK="development"
# modify env variables
cp .env.example .env
npm start
@ -111,7 +111,7 @@ All this data then comes from multiple sources:
### Aquarius
All initial data sets and their metadata (DDO) is retrieved client-side on run-time from the [Aquarius](https://github.com/oceanprotocol/aquarius) instance for each network. All app calls to Aquarius are done with 2 internal methods which mimic the same methods in ocean.js, but allow us:
All initial data sets and their metadata (DDO) is retrieved client-side on run-time from the [Aquarius](https://github.com/oceanprotocol/aquarius) instance, defined in `app.config.js`. All app calls to Aquarius are done with 2 internal methods which mimic the same methods in ocean.js, but allow us:
- to cancel requests when components get unmounted in combination with [axios](https://github.com/axios/axios)
- hit Aquarius as early as possible without relying on any ocean.js initialization
@ -127,25 +127,22 @@ const queryLatest = {
offset: 9,
query: {
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html
query_string: { query: `-isInPurgatory:true` }
},
sort: { created: -1 }
}
function Component() {
const { config } = useOcean()
const { appConfig } = useSiteMetadata()
const [result, setResult] = useState<QueryResult>()
useEffect(() => {
if (!config?.metadataCacheUri) return
if (!appConfig.metadataCacheUri) return
const source = axios.CancelToken.source()
async function init() {
const result = await queryMetadata(
query,
config.metadataCacheUri,
source.token
)
const result = await queryMetadata(query, source.token)
setResult(result)
}
init()
@ -153,7 +150,7 @@ function Component() {
return () => {
source.cancel()
}
}, [config?.metadataCacheUri, query])
}, [appConfig.metadataCacheUri, query])
return <div>{result}</div>
}
@ -174,10 +171,10 @@ function Component() {
Most financial data in the market is retrieved with GraphQL from [our own subgraph](https://github.com/oceanprotocol/ocean-subgraph), rendered on top of the initial data coming from Aquarius.
The app has [Apollo Client](https://www.apollographql.com/docs/react/) setup to query the respective subgraph based on network. In any component this client can be used like so:
The app has [Urql Client](https://formidable.com/open-source/urql/docs/basics/react-preact/) setup to query the respective subgraph based on network. In any component this client can be used like so:
```tsx
import { gql, useQuery } from '@apollo/client'
import { gql, useQuery } from 'urql'
const query = gql`
query PoolLiquidity($id: ID!, $shareId: ID) {
@ -266,10 +263,14 @@ Within components this metadata can be queried for under `allNetworksMetadataJso
```tsx
export default function NetworkName(): ReactElement {
const { networkDisplayName, isTestnet } = useWeb3()
const { networkId, isTestnet } = useWeb3()
const { networksList } = useNetworkMetadata()
const networkData = getNetworkDataById(networksList, networkId)
const networkName = getNetworkDisplayName(networkData, networkId)
return (
<>
{networkDisplayName} {isTestnet && `(Test)`}
{networkName} {isTestnet && `(Test)`}
</>
)
}

View File

@ -1,9 +1,20 @@
module.exports = {
// The default network and its associated config the app should connect to
// on start. App will automatically switch network configs when user switches
// networks in their wallet.
// Ocean Protocol contracts are deployed for: 'mainnet', 'rinkeby', 'development'
network: process.env.GATSBY_NETWORK || 'mainnet',
// URI of single metadata cache instance for all networks.
// While ocean.js includes this value for each network as part of its ConfigHelper,
// it is assumed to be the same for all networks.
// In components can be accessed with the useSiteMetadata hook:
// const { appConfig } = useSiteMetadata()
// return appConfig.metadataCacheUri
metadataCacheUri:
process.env.METADATACACHE_URI || 'https://aquarius.oceanprotocol.com',
// List of chainIds which metadata cache queries will return by default.
// This preselects the Chains user preferences.
chainIds: [1, 137, 56],
// List of all supported chainIds. Used to populate the Chains user preferences list.
chainIdsSupported: [1, 3, 4, 137, 80001, 1287, 56],
rbacUrl: process.env.GATSBY_RBAC_URL,
infuraProjectId: process.env.GATSBY_INFURA_PROJECT_ID || 'xxx',

22
codegen.yml Normal file
View File

@ -0,0 +1,22 @@
overwrite: true
schema: 'https://subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph'
documents:
- './src/utils/subgraph.ts'
- './src/components/pages/History/PoolShares.tsx'
- './src/components/pages/History/Downloads.tsx'
- './src/components/pages/History/ComputeJobs/index.tsx'
- './src/ //Users/bogdanfazakas/Sites/ocean-market/src/components/organisms/AssetContent/EditHistory.tsx'
# - './src/components/organisms/AssetActions/Pool/index.tsx'
- './src/components/organisms/AssetActions/Pool/Graph.tsx'
- './src/components/organisms/AssetActions/Consume.tsx'
- './src/components/molecules/PoolTransactions.tsx'
- './src/components/molecules/MarketStats.tsx'
generates:
./src/@types/graph.types.ts:
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'
./graphql.schema.json:
plugins:
- 'introspection'

View File

@ -1,5 +1,6 @@
{
"title": "Publish",
"description": "Highlight the important features of your data set or algorithm to make it more discoverable and catch the interest of data consumers.",
"warning": "Given the beta status, publishing on Ropsten or Rinkeby first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms)."
"warning": "Given the beta status, publishing on Ropsten or Rinkeby first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms).",
"tooltipNetwork": "Assets are published into the network your wallet is connected to. Switch your wallet's network to publish into another one."
}

View File

@ -7,6 +7,9 @@ execSync(`node ./scripts/write-repo-metadata > repo-metadata.json`, {
stdio: 'inherit'
})
// Generate GraphQl typings for urql
// execSync(`npm run graphql:graphTypes`, { stdio: 'inherit' })
// Generate Apollo typings
execSync(`npm run apollo:codegen`, { stdio: 'inherit' })

2398
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"build": "gatsby build && cp _redirects public/_redirects",
"serve": "serve -s public/",
"jest": "NODE_ENV=test jest -c tests/unit/jest.config.js",
"test-graphql": "npm run graphql:graphTypes && npm run lint && npm run jest",
"test": "npm run apollo:codegen && npm run lint && npm run jest",
"test:watch": "npm run lint && npm run jest -- --watch",
"lint": "npm run write:repoMetadata && eslint --ignore-path .gitignore --ext .js --ext .ts --ext .tsx . && npm run type-check",
@ -19,11 +20,11 @@
"storybook:build": "build-storybook -c .storybook -o public/storybook",
"write:repoMetadata": "node ./scripts/write-repo-metadata > repo-metadata.json",
"deploy:s3": "./scripts/deploy-s3.sh",
"postinstall": "husky install",
"apollo:codegen": "apollo client:codegen --target typescript --tsFileExtension=d.ts --outputFlat src/@types/apollo/",
"postinstall": "husky install"
"graphql:graphTypes": "graphql-codegen --config codegen.yml"
},
"dependencies": {
"@apollo/client": "^3.3.19",
"@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "^5.15.0",
"@oceanprotocol/art": "^3.0.0",
@ -32,6 +33,7 @@
"@portis/web3": "^4.0.4",
"@sindresorhus/slugify": "^2.1.0",
"@tippyjs/react": "^4.2.5",
"@urql/introspection": "^0.3.0",
"@walletconnect/web3-provider": "^1.5.0-rc.7",
"axios": "^0.21.1",
"chart.js": "^2.9.4",
@ -60,6 +62,7 @@
"gatsby-transformer-remark": "^2.16.1",
"gatsby-transformer-sharp": "^2.12.1",
"graphql": "14.7.0",
"graphql-schema-typescript": "^1.5.2",
"is-url-superb": "^6.0.0",
"jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8",
@ -81,12 +84,18 @@
"shortid": "^2.2.16",
"slugify": "^1.5.3",
"swr": "^0.5.6",
"urql": "^2.0.3",
"use-dark-mode": "^2.3.1",
"web3": "^1.4.0",
"web3modal": "^1.9.3",
"yup": "^0.32.9"
},
"devDependencies": {
"@graphql-codegen/cli": "1.21.6",
"@graphql-codegen/introspection": "1.18.2",
"@graphql-codegen/typescript": "1.22.4",
"@graphql-codegen/typescript-operations": "^1.18.3",
"@graphql-codegen/typescript-react-apollo": "2.3.0",
"@svgr/webpack": "^5.5.0",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7",

View File

@ -3,4 +3,5 @@ import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Comput
export interface ComputeJobMetaData extends ComputeJob {
assetName: string
assetDtSymbol: string
networkId: number
}

View File

@ -7,10 +7,9 @@ import Styles from '../global/Styles'
import { useWeb3 } from '../providers/Web3'
import { useSiteMetadata } from '../hooks/useSiteMetadata'
import { useAccountPurgatory } from '../hooks/useAccountPurgatory'
import NetworkBanner from './molecules/NetworkBanner'
import styles from './App.module.css'
import AnnouncementBanner from './atoms/AnnouncementBanner'
import { useGraphSyncStatus } from '../hooks/useGraphSyncStatus'
import styles from './App.module.css'
const contentQuery = graphql`
query AppQuery {
@ -41,24 +40,15 @@ export default function App({
const { warning } = useSiteMetadata()
const { accountId } = useWeb3()
const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId)
const { isGraphSynced, blockHead, blockGraph } = useGraphSyncStatus()
// const { isGraphSynced, blockHead, blockGraph } = useGraphSyncStatus()
return (
<Styles>
<div className={styles.app}>
{!isGraphSynced && (
<AnnouncementBanner
text={`The data for this network has only synced to Ethereum block ${blockGraph} (out of ${blockHead}). Please check back soon.`}
state="error"
/>
)}
{!location.pathname.includes('/asset/did') && <NetworkBanner />}
<Header />
{(props as PageProps).uri === '/' && (
<Alert text={warning.main} state="info" />
<AnnouncementBanner text={warning.main} />
)}
<Header />
{isInPurgatory && (
<Alert

View File

@ -10,7 +10,7 @@
.typeLabel {
display: inline-block;
text-transform: uppercase;
border-right: 1px solid var(--border-color);
padding-right: calc(var(--spacer) / 3.5);
margin-right: calc(var(--spacer) / 4);
border-left: 1px solid var(--border-color);
padding-left: calc(var(--spacer) / 3.5);
margin-left: calc(var(--spacer) / 4);
}

View File

@ -21,9 +21,6 @@ export default function AssetType({
})
return (
<div className={styleClasses}>
<div className={styles.typeLabel}>
{type === 'dataset' ? 'data set' : 'algorithm'}
</div>
{accessType === 'access' ? (
<Download role="img" aria-label="Download" className={styles.icon} />
) : accessType === 'compute' && type === 'algorithm' ? (
@ -31,6 +28,10 @@ export default function AssetType({
) : (
<Compute role="img" aria-label="Compute" className={styles.icon} />
)}
<div className={styles.typeLabel}>
{type === 'dataset' ? 'data set' : 'algorithm'}
</div>
</div>
)
}

View File

@ -4,7 +4,6 @@
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
box-shadow: 0 6px 17px 0 var(--box-shadow-color);
overflow: hidden;
padding: calc(var(--spacer) / 1.5);
}

View File

@ -78,6 +78,7 @@
color: var(--brand-pink);
box-shadow: none;
cursor: pointer;
min-width: auto;
}
/* Size Modifiers */

View File

@ -4,10 +4,12 @@ import classNames from 'classnames/bind'
import { ConfigHelperConfig } from '@oceanprotocol/lib'
import { useOcean } from '../../providers/Ocean'
import styles from './ExplorerLink.module.css'
import { getOceanConfig } from '../../utils/ocean'
const cx = classNames.bind(styles)
export default function ExplorerLink({
networkId,
path,
children,
className
@ -17,22 +19,29 @@ export default function ExplorerLink({
children: ReactNode
className?: string
}): ReactElement {
const { config } = useOcean()
const { config, ocean } = useOcean()
const [url, setUrl] = useState<string>()
const [oceanConfig, setOceanConfig] = useState<ConfigHelperConfig>()
const styleClasses = cx({
link: true,
[className]: className
})
useEffect(() => {
setUrl((config as ConfigHelperConfig).explorerUri)
}, [config])
async function initOcean() {
const oceanInitialConfig = getOceanConfig(networkId)
setOceanConfig(oceanInitialConfig)
setUrl(oceanInitialConfig?.explorerUri)
}
if (oceanConfig === undefined) {
initOcean()
}
}, [config, networkId, ocean])
return (
<a
href={`${url}/${path}`}
title={`View on ${(config as ConfigHelperConfig).explorerUri}`}
title={`View on ${oceanConfig?.explorerUri}`}
target="_blank"
rel="noreferrer"
className={styleClasses}

View File

@ -27,6 +27,13 @@ export interface InputProps {
| ChangeEvent<HTMLSelectElement>
| ChangeEvent<HTMLTextAreaElement>
): void
onKeyPress?(
e:
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLSelectElement>
| React.KeyboardEvent<HTMLTextAreaElement>
): void
rows?: number
multiple?: boolean
pattern?: string

View File

@ -1,6 +1,6 @@
.logo {
width: 4rem;
height: 4rem;
width: 2.5rem;
height: 2.5rem;
margin: 0;
}

View File

@ -0,0 +1,28 @@
.network {
text-transform: capitalize;
display: inline-flex;
align-items: center;
color: var(--color-secondary);
}
.icon {
display: inline-block;
width: 1.2em;
height: 1.2em;
fill: currentColor;
margin-right: calc(var(--spacer) / 8);
}
.minimal {
position: relative;
}
.minimal .name {
opacity: 0;
width: 0;
}
.minimal:hover .name {
width: auto;
opacity: 1;
}

View File

@ -0,0 +1,48 @@
import React, { ReactElement } from 'react'
import { ReactComponent as EthIcon } from '../../images/eth.svg'
import { ReactComponent as PolygonIcon } from '../../images/polygon.svg'
import { ReactComponent as MoonbeamIcon } from '../../images/moonbeam.svg'
import { ReactComponent as BscIcon } from '../../images/bsc.svg'
import { getNetworkDataById, getNetworkDisplayName } from '../../utils/web3'
import styles from './NetworkName.module.css'
import useNetworkMetadata from '../../hooks/useNetworkMetadata'
export function NetworkIcon({ name }: { name: string }): ReactElement {
const IconMapped = name.includes('ETH')
? EthIcon
: name.includes('Polygon')
? PolygonIcon
: name.includes('Moon')
? MoonbeamIcon
: name.includes('BSC')
? BscIcon
: EthIcon // ETH icon as fallback
return IconMapped ? <IconMapped className={styles.icon} /> : null
}
export default function NetworkName({
networkId,
minimal,
className
}: {
networkId: number
minimal?: boolean
className?: string
}): ReactElement {
const { networksList } = useNetworkMetadata()
const networkData = getNetworkDataById(networksList, networkId)
const networkName = getNetworkDisplayName(networkData, networkId)
return (
<span
className={`${styles.network} ${minimal ? styles.minimal : null} ${
className || ''
}`}
title={networkName}
>
<NetworkIcon name={networkName} />{' '}
<span className={styles.name}>{networkName}</span>
</span>
)
}

View File

@ -6,6 +6,9 @@
font-weight: var(--font-weight-base);
}
.removeTvlPadding {
padding-left: 0 !important;
}
/* fiat currency symbol */
.conversion strong span {
font-weight: var(--font-weight-base);

View File

@ -10,11 +10,13 @@ const cx = classNames.bind(styles)
export default function Conversion({
price,
className,
hideApproximateSymbol
hideApproximateSymbol,
showTVLLabel
}: {
price: string // expects price in OCEAN, not wei
className?: string
hideApproximateSymbol?: boolean
showTVLLabel?: boolean
}): ReactElement {
const { prices } = usePrices()
const { currency, locale } = useUserPreferences()
@ -27,6 +29,7 @@ export default function Conversion({
const styleClasses = cx({
conversion: true,
removeTvlPadding: showTVLLabel,
[className]: className
})
@ -61,6 +64,7 @@ export default function Conversion({
className={styleClasses}
title="Approximation based on current OCEAN spot price on Coingecko"
>
{showTVLLabel && 'TVL'}
{!hideApproximateSymbol && '≈ '}
<strong dangerouslySetInnerHTML={{ __html: priceConverted }} />{' '}
{!isFiat && currency}

View File

@ -10,11 +10,11 @@
}
.icon {
width: 1rem;
height: 1rem;
width: 1em;
height: 1em;
cursor: help;
display: inline-block;
margin-bottom: -0.1rem;
margin-bottom: -0.1em;
margin-left: calc(var(--spacer) / 6);
fill: var(--color-secondary);
}

View File

@ -28,9 +28,7 @@ export default function Tooltip({
trigger,
disabled,
className,
placement,
link,
reference
placement
}: {
content: ReactNode
children?: ReactNode
@ -38,8 +36,6 @@ export default function Tooltip({
disabled?: boolean
className?: string
placement?: Placement
link?: string
reference?: string
}): ReactElement {
const [props, setSpring] = useSpring(() => animation.from)
@ -76,7 +72,6 @@ export default function Tooltip({
<animated.div style={props}>
<div className={styles.content} {...attrs}>
{content}
{link && <a href={link}>{reference}</a>}
<div className={styles.arrow} data-popper-arrow />
</div>
</animated.div>

View File

@ -5,6 +5,7 @@ import React, { ReactElement, useEffect, useState } from 'react'
import { getAssetsNames } from '../../utils/aquarius'
import styles from './AssetListTitle.module.css'
import axios from 'axios'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
export default function AssetListTitle({
ddo,
@ -15,11 +16,11 @@ export default function AssetListTitle({
did?: string
title?: string
}): ReactElement {
const { config } = useOcean()
const { appConfig } = useSiteMetadata()
const [assetTitle, setAssetTitle] = useState<string>(title)
useEffect(() => {
if (title || !config?.metadataCacheUri) return
if (title || !appConfig.metadataCacheUri) return
if (ddo) {
const { attributes } = ddo.findServiceByType('metadata')
setAssetTitle(attributes.main.name)
@ -29,11 +30,7 @@ export default function AssetListTitle({
const source = axios.CancelToken.source()
async function getAssetName() {
const title = await getAssetsNames(
[did],
config.metadataCacheUri,
source.token
)
const title = await getAssetsNames([did], source.token)
setAssetTitle(title[did])
}
@ -42,7 +39,7 @@ export default function AssetListTitle({
return () => {
source.cancel()
}
}, [assetTitle, config?.metadataCacheUri, ddo, did, title])
}, [assetTitle, appConfig.metadataCacheUri, ddo, did, title])
return (
<h3 className={styles.title}>

View File

@ -57,11 +57,6 @@
display: block;
}
.date {
font-size: var(--font-size-mini);
margin-top: calc(var(--spacer) / 2);
}
.typeDetails {
position: absolute;
top: calc(var(--spacer) / 3);
@ -69,3 +64,10 @@
width: auto;
font-size: var(--font-size-mini);
}
.network {
font-size: var(--font-size-mini);
position: absolute;
right: calc(var(--spacer) / 3);
bottom: calc(var(--spacer) / 3);
}

View File

@ -2,12 +2,13 @@ import React from 'react'
import { Link } from 'gatsby'
import Dotdotdot from 'react-dotdotdot'
import Price from '../atoms/Price'
import styles from './AssetTeaser.module.css'
import { DDO, BestPrice } from '@oceanprotocol/lib'
import removeMarkdown from 'remove-markdown'
import Publisher from '../atoms/Publisher'
import Time from '../atoms/Time'
import AssetType from '../atoms/AssetType'
import NetworkName from '../atoms/NetworkName'
import { useOcean } from '../../providers/Ocean'
import styles from './AssetTeaser.module.css'
declare type AssetTeaserProps = {
ddo: DDO
@ -52,9 +53,7 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({
<footer className={styles.foot}>
<Price price={price} small />
<p className={styles.date}>
<Time date={ddo?.created} relative />
</p>
<NetworkName networkId={ddo.chainId} className={styles.network} />
</footer>
</Link>
</article>

View File

@ -1,17 +1,21 @@
import { useUserPreferences } from '../../providers/UserPreferences'
import React, { ReactElement, useEffect, useState } from 'react'
import Table from '../atoms/Table'
import { DDO, Logger, ConfigHelperConfig } from '@oceanprotocol/lib'
import { useOcean } from '../../providers/Ocean'
import { DDO, Logger, BestPrice } from '@oceanprotocol/lib'
import Price from '../atoms/Price'
import Tooltip from '../atoms/Tooltip'
import AssetTitle from './AssetListTitle'
import { queryMetadata } from '../../utils/aquarius'
import {
queryMetadata,
transformChainIdsListToQuery
} from '../../utils/aquarius'
import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph'
import axios, { CancelToken } from 'axios'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
async function getAssetsBookmarked(
bookmarks: string[],
metadataCacheUri: string,
chainIds: number[],
cancelToken: CancelToken
) {
const searchDids = JSON.stringify(bookmarks)
@ -26,7 +30,9 @@ async function getAssetsBookmarked(
offset: 100,
query: {
query_string: {
query: searchDids,
query: `(${searchDids}) AND (${transformChainIdsListToQuery(
chainIds
)})`,
fields: ['dataToken'],
default_operator: 'OR'
}
@ -35,11 +41,7 @@ async function getAssetsBookmarked(
}
try {
const result = await queryMetadata(
queryBookmarks,
metadataCacheUri,
cancelToken
)
const result = await queryMetadata(queryBookmarks, cancelToken)
return result
} catch (error) {
@ -50,19 +52,19 @@ async function getAssetsBookmarked(
const columns = [
{
name: 'Data Set',
selector: function getAssetRow(row: DDO) {
const { attributes } = row.findServiceByType('metadata')
return <AssetTitle title={attributes.main.name} ddo={row} />
selector: function getAssetRow(row: AssetListPrices) {
const { attributes } = row.ddo.findServiceByType('metadata')
return <AssetTitle title={attributes.main.name} ddo={row.ddo} />
},
maxWidth: '45rem',
grow: 1
},
{
name: 'Datatoken Symbol',
selector: function getAssetRow(row: DDO) {
selector: function getAssetRow(row: AssetListPrices) {
return (
<Tooltip content={row.dataTokenInfo.name}>
{row.dataTokenInfo.symbol}
<Tooltip content={row.ddo.dataTokenInfo.name}>
{row.ddo.dataTokenInfo.symbol}
</Tooltip>
)
},
@ -70,7 +72,7 @@ const columns = [
},
{
name: 'Price',
selector: function getAssetRow(row: DDO) {
selector: function getAssetRow(row: AssetListPrices) {
return <Price price={row.price} small />
},
right: true
@ -78,21 +80,20 @@ const columns = [
]
export default function Bookmarks(): ReactElement {
const { config } = useOcean()
const { appConfig } = useSiteMetadata()
const { bookmarks } = useUserPreferences()
const [pinned, setPinned] = useState<DDO[]>()
const [pinned, setPinned] = useState<AssetListPrices[]>()
const [isLoading, setIsLoading] = useState<boolean>()
const networkName = (config as ConfigHelperConfig)?.network
const { chainIds } = useUserPreferences()
useEffect(() => {
if (!config?.metadataCacheUri || !networkName || bookmarks === {}) return
if (!appConfig.metadataCacheUri || bookmarks === []) return
const source = axios.CancelToken.source()
async function init() {
if (!bookmarks[networkName]?.length) {
if (!bookmarks?.length) {
setPinned([])
return
}
@ -101,11 +102,14 @@ export default function Bookmarks(): ReactElement {
try {
const resultPinned = await getAssetsBookmarked(
bookmarks[networkName],
config.metadataCacheUri,
bookmarks,
chainIds,
source.token
)
setPinned(resultPinned?.results)
const pinnedAssets: AssetListPrices[] = await getAssetsBestPrices(
resultPinned?.results
)
setPinned(pinnedAssets)
} catch (error) {
Logger.error(error.message)
}
@ -117,7 +121,7 @@ export default function Bookmarks(): ReactElement {
return () => {
source.cancel()
}
}, [bookmarks, config.metadataCacheUri, networkName])
}, [bookmarks, chainIds])
return (
<Table

View File

@ -4,25 +4,27 @@ import { useField } from 'formik'
import { toast } from 'react-toastify'
import FileInfo from './Info'
import FileInput from './Input'
import { useOcean } from '../../../../providers/Ocean'
import { InputProps } from '../../../atoms/Input'
import { fileinfo } from '../../../../utils/provider'
import { useWeb3 } from '../../../../providers/Web3'
import { getOceanConfig } from '../../../../utils/ocean'
export default function FilesInput(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
const [isLoading, setIsLoading] = useState(false)
const [fileUrl, setFileUrl] = useState<string>()
const { config } = useOcean()
const { chainId } = useWeb3()
function loadFileInfo() {
const source = axios.CancelToken.source()
const config = getOceanConfig(chainId || 1)
async function validateUrl() {
try {
setIsLoading(true)
const checkedFile = await fileinfo(
fileUrl,
config.providerUri,
config?.providerUri,
source.token
)
checkedFile && helpers.setValue([checkedFile])
@ -43,7 +45,7 @@ export default function FilesInput(props: InputProps): ReactElement {
useEffect(() => {
loadFileInfo()
}, [fileUrl, config.providerUri])
}, [fileUrl])
async function handleButtonClick(e: React.SyntheticEvent, url: string) {
// hack so the onBlur-triggered validation does not show,

View File

@ -1,18 +1,42 @@
.stats {
margin-bottom: calc(var(--spacer) * 2);
}
/* specificity sledgehammer override without !important */
.stats,
.stats *,
.statsList * {
font-size: var(--font-size-small);
line-height: 2;
color: var(--color-secondary);
margin-left: 0;
}
.stats > div > div {
display: inline-block;
.tooltipStats {
margin-bottom: calc(var(--spacer) / 3);
padding-bottom: calc(var(--spacer) / 3);
border-bottom: 1px solid var(--border-color);
}
.total {
color: var(--color-secondary) !important;
font-size: var(--font-size-small) !important;
.network {
font-weight: var(--font-weight-bold);
}
.info {
width: 0.85rem;
}
.statsList,
.note {
padding: calc(var(--spacer) / 4);
}
.statsList {
padding-bottom: 0;
}
.note {
margin-bottom: 0;
padding-top: 0;
font-size: var(--font-size-mini);
color: var(--color-secondary);
}

View File

@ -1,9 +1,15 @@
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './MarketStats.module.css'
import { gql, useQuery } from '@apollo/client'
import { gql, OperationContext } from 'urql'
import Conversion from '../atoms/Price/Conversion'
import PriceUnit from '../atoms/Price/PriceUnit'
import Tooltip from '../atoms/Tooltip'
import NetworkName from '../atoms/NetworkName'
import { fetchData, getSubgrahUri } from '../../utils/subgraph'
import { filterNetworksByType } from './UserPreferences/Networks/index'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import useNetworkMetadata from '../../hooks/useNetworkMetadata'
import { Logger } from '@oceanprotocol/lib'
import styles from './MarketStats.module.css'
const getTotalPoolsValues = gql`
query PoolsData {
@ -15,32 +21,160 @@ const getTotalPoolsValues = gql`
}
`
interface Value {
[chainId: number]: string
}
function MarketNetworkStats({
totalValueLocked,
poolCount,
totalOceanLiquidity
}: {
totalValueLocked: string
poolCount: string
totalOceanLiquidity: string
}): ReactElement {
return (
<>
<Conversion price={totalValueLocked} hideApproximateSymbol />{' '}
<abbr title="Total Value Locked">TVL</abbr> across{' '}
<strong>{poolCount}</strong> asset pools that contain{' '}
<PriceUnit price={totalOceanLiquidity} small className={styles.total} />,
plus datatokens for each pool.
</>
)
}
function MarketNetworkStatsTooltip({
totalValueLocked,
poolCount,
totalOceanLiquidity,
mainChainIds
}: {
totalValueLocked: Value
poolCount: Value
totalOceanLiquidity: Value
mainChainIds: number[]
}): ReactElement {
return (
<>
<ul className={styles.statsList}>
{totalValueLocked &&
totalOceanLiquidity &&
poolCount &&
mainChainIds?.map((chainId, key) => (
<li className={styles.tooltipStats} key={key}>
<NetworkName networkId={chainId} className={styles.network} />
<br />
<Conversion
price={totalValueLocked[chainId] || '0'}
hideApproximateSymbol
/>{' '}
<abbr title="Total Value Locked">TVL</abbr>
{' | '}
<strong>{poolCount[chainId] || '0'}</strong> pools
{' | '}
<PriceUnit price={totalOceanLiquidity[chainId] || '0'} small />
</li>
))}
</ul>
<p className={styles.note}>
Counted on-chain from our pool factory. Does not filter out assets in{' '}
<a href="https://github.com/oceanprotocol/list-purgatory">
list-purgatory
</a>
</p>
</>
)
}
export default function MarketStats(): ReactElement {
const [totalValueLocked, setTotalValueLocked] = useState<string>()
const [totalOceanLiquidity, setTotalOceanLiquidity] = useState<string>()
const [poolCount, setPoolCount] = useState<number>()
const { data } = useQuery(getTotalPoolsValues, { pollInterval: 20000 })
const [totalValueLocked, setTotalValueLocked] = useState<Value>()
const [totalOceanLiquidity, setTotalOceanLiquidity] = useState<Value>()
const [poolCount, setPoolCount] = useState<Value>()
const [totalValueLockedSum, setTotalValueLockedSum] = useState<string>()
const [totalOceanLiquiditySum, setTotalOceanLiquiditySum] = useState<string>()
const [poolCountSum, setPoolCountSum] = useState<string>()
const [mainChainIds, setMainChainIds] = useState<number[]>()
const { appConfig } = useSiteMetadata()
const { networksList } = useNetworkMetadata()
async function getMarketStats() {
const mainChainIdsList = await filterNetworksByType(
'mainnet',
appConfig.chainIdsSupported,
networksList
)
setMainChainIds(mainChainIdsList)
let newTotalValueLockedSum = 0
let newTotalOceanLiquiditySum = 0
let newPoolCountSum = 0
for (const chainId of mainChainIdsList) {
const context: OperationContext = {
url: `${getSubgrahUri(
chainId
)}/subgraphs/name/oceanprotocol/ocean-subgraph`,
requestPolicy: 'network-only'
}
try {
const response = await fetchData(getTotalPoolsValues, null, context)
if (!response) continue
const { totalValueLocked, totalOceanLiquidity, finalizedPoolCount } =
response?.data?.poolFactories[0]
await setTotalValueLocked((prevState) => ({
...prevState,
[chainId]: totalValueLocked
}))
await setTotalOceanLiquidity((prevState) => ({
...prevState,
[chainId]: totalOceanLiquidity
}))
await setPoolCount((prevState) => ({
...prevState,
[chainId]: finalizedPoolCount
}))
newTotalValueLockedSum += parseInt(totalValueLocked)
newTotalOceanLiquiditySum += parseInt(totalOceanLiquidity)
newPoolCountSum += parseInt(finalizedPoolCount)
} catch (error) {
Logger.error('Error fetchData: ', error.message)
}
}
setTotalValueLockedSum(`${newTotalValueLockedSum}`)
setTotalOceanLiquiditySum(`${newTotalOceanLiquiditySum}`)
setPoolCountSum(`${newPoolCountSum}`)
}
useEffect(() => {
if (!data || !data.poolFactories || data.poolFactories.length === 0) return
setTotalValueLocked(data.poolFactories[0].totalValueLocked)
setTotalOceanLiquidity(data.poolFactories[0].totalOceanLiquidity)
setPoolCount(data.poolFactories[0].finalizedPoolCount)
}, [data])
getMarketStats()
}, [])
return (
<div className={styles.stats}>
<Conversion price={`${totalValueLocked}`} hideApproximateSymbol />{' '}
<abbr title="Total Value Locked">TVL</abbr> across{' '}
<strong>{poolCount}</strong> data set pools that contain{' '}
<PriceUnit price={totalOceanLiquidity} small className={styles.total} />,
plus datatokens for each pool.
<>
<MarketNetworkStats
totalValueLocked={totalValueLockedSum || '0'}
totalOceanLiquidity={totalOceanLiquiditySum || '0'}
poolCount={poolCountSum || '0'}
/>{' '}
<Tooltip
className={styles.info}
content="Counted on-chain from our pool factory. Does not filter out data sets in "
reference="list-purgatory"
link="https://github.com/oceanprotocol/list-purgatory"
content={
<MarketNetworkStatsTooltip
totalValueLocked={totalValueLocked}
poolCount={poolCount}
totalOceanLiquidity={totalOceanLiquidity}
mainChainIds={mainChainIds}
/>
}
/>
</>
</div>
)
}

View File

@ -1,32 +1,66 @@
.menu {
width: 100%;
}
.menu > div {
padding: calc(var(--spacer) / 2);
display: flex;
align-items: center;
justify-content: space-between;
align-items: center;
padding-top: calc(var(--spacer) / 2);
padding-bottom: calc(var(--spacer) / 2);
flex-wrap: wrap;
}
.logoUnit {
.logo {
order: 1;
white-space: nowrap;
display: flex;
align-items: center;
}
.logoUnit svg {
margin-left: -0.5rem;
margin-right: 0.5rem;
.navigation {
order: 3;
margin-top: calc(var(--spacer) / 2);
text-align: center;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
margin-left: -1rem;
margin-right: -1rem;
width: calc(100% + 2rem);
}
.actions {
order: 2;
display: flex;
}
.title {
display: none;
}
@media screen and (min-width: 40rem) {
@media screen and (min-width: 42rem) {
.menu {
justify-content: start;
}
.navigation {
order: 2;
width: auto;
margin: 0;
text-align: left;
border: none;
}
.actions {
order: 3;
margin-left: auto;
}
}
@media screen and (min-width: 55rem) {
.menu {
padding: var(--spacer);
}
.title {
margin: 0;
margin-right: var(--spacer);
display: block;
color: var(--color-secondary);
font-size: var(--font-size-h4);
@ -38,6 +72,7 @@
overflow-y: hidden;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: none;
}
.navigation::-webkit-scrollbar,
@ -48,19 +83,13 @@
.navigation li {
display: inline-block;
vertical-align: middle;
margin-left: var(--spacer);
margin-right: calc(var(--spacer) / 3);
margin-left: calc(var(--spacer) / 3);
}
@media screen and (min-width: 60rem) {
.navigation li {
margin-left: calc(var(--spacer) * 2);
}
}
.navigation button,
.link {
display: block;
padding: calc(var(--spacer) / 2);
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
text-transform: uppercase;
color: var(--color-secondary);
font-weight: var(--font-weight-bold);
@ -69,16 +98,11 @@
z-index: 1;
}
.navigation button {
text-transform: none;
padding-top: calc(var(--spacer) / 4);
padding-bottom: calc(var(--spacer) / 4);
}
.actions,
.link:hover,
.link:focus,
.link:active {
color: var(--brand-grey);
color: var(--font-color-text);
}
.link[aria-current],
@ -90,3 +114,11 @@
.link:last-child {
padding-right: 0;
}
.logo svg {
margin-right: calc(var(--spacer) / 3);
}
.actions button {
text-transform: none;
}

View File

@ -4,10 +4,11 @@ import { useLocation } from '@reach/router'
import loadable from '@loadable/component'
import styles from './Menu.module.css'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import Container from '../atoms/Container'
import UserPreferences from './UserPreferences'
import Badge from '../atoms/Badge'
import Logo from '../atoms/Logo'
import Networks from './UserPreferences/Networks'
import SearchBar from './SearchBar'
const Wallet = loadable(() => import('./Wallet'))
@ -36,11 +37,10 @@ export default function Menu(): ReactElement {
return (
<nav className={styles.menu}>
<Container>
<Link to="/" className={styles.logoUnit}>
<Logo />
<Link to="/" className={styles.logo}>
<Logo noWordmark />
<h1 className={styles.title}>
{siteTitle} <Badge label="beta" />
{siteTitle} <Badge label="v3" />
</h1>
</Link>
@ -50,14 +50,14 @@ export default function Menu(): ReactElement {
<MenuLink item={item} />
</li>
))}
<li>
<Wallet />
</li>
<li>
<UserPreferences />
</li>
</ul>
</Container>
<div className={styles.actions}>
<SearchBar />
<Networks />
<Wallet />
<UserPreferences />
</div>
</nav>
)
}

View File

@ -33,7 +33,6 @@
}
.datatoken {
margin-top: calc(var(--spacer) / 8);
margin-bottom: 0;
color: var(--color-secondary);
}

View File

@ -3,7 +3,6 @@ import { File as FileMetadata } from '@oceanprotocol/lib/dist/node/ddo/interface
import Markdown from '../atoms/Markdown'
import Tags from '../atoms/Tags'
import MetaItem from '../organisms/AssetContent/MetaItem'
import styles from './MetadataPreview.module.css'
import File from '../atoms/File'
import {
MetadataPublishFormDataset,
@ -11,6 +10,11 @@ import {
} from '../../@types/MetaData'
import Button from '../atoms/Button'
import { transformTags } from '../../utils/metadata'
import NetworkName from '../atoms/NetworkName'
import { useWeb3 } from '../../providers/Web3'
import styles from './MetadataPreview.module.css'
import Web3Feedback from './Web3Feedback'
import { useAsset } from '../../providers/Asset'
function Description({ description }: { description: string }) {
const [fullDescription, setFullDescription] = useState<boolean>(false)
@ -92,10 +96,14 @@ export function MetadataPreview({
}: {
values: Partial<MetadataPublishFormDataset>
}): ReactElement {
const { networkId } = useWeb3()
const { isAssetNetwork } = useAsset()
return (
<div className={styles.preview}>
<h2 className={styles.previewTitle}>Preview</h2>
<header>
{networkId && <NetworkName networkId={networkId} />}
{values.name && <h3 className={styles.title}>{values.name}</h3>}
{values.dataTokenOptions?.name && (
<p
@ -121,6 +129,9 @@ export function MetadataPreview({
</header>
<MetaFull values={values} />
{isAssetNetwork === false && (
<Web3Feedback isAssetNetwork={isAssetNetwork} />
)}
</div>
)
}
@ -130,10 +141,13 @@ export function MetadataAlgorithmPreview({
}: {
values: Partial<MetadataPublishFormAlgorithm>
}): ReactElement {
const { networkId } = useWeb3()
return (
<div className={styles.preview}>
<h2 className={styles.previewTitle}>Preview</h2>
<header>
{networkId && <NetworkName networkId={networkId} />}
{values.name && <h3 className={styles.title}>{values.name}</h3>}
{values.dataTokenOptions?.name && (
<p

View File

@ -1,84 +0,0 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useWeb3 } from '../../providers/Web3'
import { addCustomNetwork, NetworkObject } from '../../utils/web3'
import { getOceanConfig } from '../../utils/ocean'
import { useOcean } from '../../providers/Ocean'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import AnnouncementBanner, {
AnnouncementAction
} from '../atoms/AnnouncementBanner'
const networkMatic: NetworkObject = {
chainId: 137,
name: 'Matic Network',
urlList: [
'https://rpc-mainnet.matic.network',
'https://rpc-mainnet.maticvigil.com/'
]
}
export default function NetworkBanner(): ReactElement {
const { web3Provider, web3ProviderInfo } = useWeb3()
const { config, connect } = useOcean()
const { announcement } = useSiteMetadata()
const [text, setText] = useState<string>(announcement.main)
const [action, setAction] = useState<AnnouncementAction>()
const addCustomNetworkAction = {
name: 'Add custom network',
handleAction: () => addCustomNetwork(web3Provider, networkMatic)
}
const switchToPolygonAction = {
name: 'Switch to Polygon',
handleAction: async () => {
const config = getOceanConfig('polygon')
await connect(config)
}
}
const switchToEthAction = {
name: 'Switch to ETH',
handleAction: async () => {
const config = getOceanConfig('mainnet')
await connect(config)
}
}
function setBannerForMatic() {
setText(announcement.polygon)
setAction(undefined)
}
useEffect(() => {
if (!web3ProviderInfo || (!web3Provider && !config)) return
switch (web3ProviderInfo.name) {
case 'Web3':
if (config.networkId !== 137) {
setText(announcement.main)
setAction(switchToPolygonAction)
} else {
setText(announcement.polygon)
setAction(switchToEthAction)
}
break
case 'MetaMask':
if (config.networkId === 137) {
setBannerForMatic()
} else {
setText(announcement.main)
setAction(addCustomNetworkAction)
}
break
default:
if (config.networkId === 137) {
setBannerForMatic()
} else {
setText(announcement.main)
setAction(undefined)
}
}
}, [web3Provider, web3ProviderInfo, config, announcement])
return <AnnouncementBanner text={text} action={action} />
}

View File

@ -1,229 +0,0 @@
import { useOcean } from '../../providers/Ocean'
import React, { ReactElement, useEffect, useState } from 'react'
import ExplorerLink from '../atoms/ExplorerLink'
import Time from '../atoms/Time'
import Table from '../atoms/Table'
import AssetTitle from './AssetListTitle'
import styles from './PoolTransactions.module.css'
import { useUserPreferences } from '../../providers/UserPreferences'
import { Ocean } from '@oceanprotocol/lib'
import { formatPrice } from '../atoms/Price/PriceUnit'
import { gql, useQuery } from '@apollo/client'
import {
TransactionHistory,
TransactionHistory_poolTransactions as TransactionHistoryPoolTransactions
} from '../../@types/apollo/TransactionHistory'
import web3 from 'web3'
import { useWeb3 } from '../../providers/Web3'
const txHistoryQueryByPool = gql`
query TransactionHistoryByPool($user: String, $pool: String) {
poolTransactions(
orderBy: timestamp
orderDirection: desc
where: { userAddress: $user, poolAddress: $pool }
first: 1000
) {
tx
event
timestamp
poolAddress {
datatokenAddress
}
tokens {
value
type
tokenAddress
}
}
}
`
const txHistoryQuery = gql`
query TransactionHistory($user: String) {
poolTransactions(
orderBy: timestamp
orderDirection: desc
where: { userAddress: $user }
first: 1000
) {
tx
event
timestamp
poolAddress {
datatokenAddress
}
tokens {
value
type
tokenAddress
}
}
}
`
async function getSymbol(ocean: Ocean, tokenAddress: string) {
const symbol =
ocean.pool.oceanAddress.toLowerCase() === tokenAddress.toLowerCase()
? 'OCEAN'
: await ocean.datatokens.getSymbol(tokenAddress)
return symbol
}
async function getTitle(
ocean: Ocean,
row: TransactionHistoryPoolTransactions,
locale: string
) {
let title = ''
switch (row.event) {
case 'swap': {
const inToken = row.tokens.filter((x) => x.type === 'in')[0]
const inTokenSymbol = await getSymbol(ocean, inToken.tokenAddress)
const outToken = row.tokens.filter((x) => x.type === 'out')[0]
const outTokenSymbol = await getSymbol(ocean, outToken.tokenAddress)
title += `Swap ${formatPrice(
Math.abs(inToken.value).toString(),
locale
)}${inTokenSymbol} for ${formatPrice(
Math.abs(outToken.value).toString(),
locale
)}${outTokenSymbol}`
break
}
case 'setup': {
const firstToken = row.tokens.filter(
(x) =>
x.tokenAddress.toLowerCase() === ocean.pool.oceanAddress.toLowerCase()
)[0]
const firstTokenSymbol = await getSymbol(ocean, firstToken.tokenAddress)
const secondToken = row.tokens.filter(
(x) =>
x.tokenAddress.toLowerCase() !== ocean.pool.oceanAddress.toLowerCase()
)[0]
const secondTokenSymbol = await getSymbol(ocean, secondToken.tokenAddress)
title += `Create pool with ${formatPrice(
Math.abs(firstToken.value).toString(),
locale
)}${firstTokenSymbol} and ${formatPrice(
Math.abs(secondToken.value).toString(),
locale
)}${secondTokenSymbol}`
break
}
case 'join':
case 'exit': {
for (let i = 0; i < row.tokens.length; i++) {
const tokenSymbol = await getSymbol(ocean, row.tokens[i].tokenAddress)
if (i > 0) title += '\n'
title += `${row.event === 'join' ? 'Add' : 'Remove'} ${formatPrice(
Math.abs(row.tokens[i].value).toString(),
locale
)}${tokenSymbol}`
}
break
}
}
return title
}
function Title({ row }: { row: TransactionHistoryPoolTransactions }) {
const { networkId } = useWeb3()
const { ocean } = useOcean()
const [title, setTitle] = useState<string>()
const { locale } = useUserPreferences()
useEffect(() => {
if (!ocean || !locale || !row) return
async function init() {
const title = await getTitle(ocean, row, locale)
setTitle(title)
}
init()
}, [ocean, row, locale])
return title ? (
<ExplorerLink networkId={networkId} path={`/tx/${row.tx}`}>
<span className={styles.titleText}>{title}</span>
</ExplorerLink>
) : null
}
const columns = [
{
name: 'Title',
selector: function getTitleRow(row: TransactionHistoryPoolTransactions) {
return <Title row={row} />
}
},
{
name: 'Data Set',
selector: function getAssetRow(row: TransactionHistoryPoolTransactions) {
const did = web3.utils
.toChecksumAddress(row.poolAddress.datatokenAddress)
.replace('0x', 'did:op:')
return <AssetTitle did={did} />
}
},
{
name: 'Time',
selector: function getTimeRow(row: TransactionHistoryPoolTransactions) {
return (
<Time
className={styles.time}
date={row.timestamp.toString()}
relative
isUnix
/>
)
},
maxWidth: '10rem'
}
]
// hack! if we use a function to omit one field this will display a strange refresh to the enduser for each row
const columnsMinimal = [columns[0], columns[2]]
export default function PoolTransactions({
poolAddress,
minimal
}: {
poolAddress?: string
minimal?: boolean
}): ReactElement {
const { accountId } = useWeb3()
const [logs, setLogs] = useState<TransactionHistoryPoolTransactions[]>()
const { data, loading } = useQuery<TransactionHistory>(
poolAddress ? txHistoryQueryByPool : txHistoryQuery,
{
variables: {
user: accountId?.toLowerCase(),
pool: poolAddress?.toLowerCase()
},
pollInterval: 20000
}
)
useEffect(() => {
if (!data) return
setLogs(data.poolTransactions)
}, [data, loading])
return (
<Table
columns={minimal ? columnsMinimal : columns}
data={logs}
isLoading={loading}
noTableHead={minimal}
dense={minimal}
pagination={minimal ? logs?.length >= 4 : logs?.length >= 9}
paginationPerPage={minimal ? 5 : 10}
emptyMessage="Your pool transactions will show up here"
/>
)
}

View File

@ -0,0 +1,3 @@
.titleText {
white-space: pre;
}

View File

@ -0,0 +1,88 @@
import React, { useState, useEffect, ReactElement } from 'react'
import { Datatoken, PoolTransaction } from '.'
import { useUserPreferences } from '../../../providers/UserPreferences'
import ExplorerLink from '../../atoms/ExplorerLink'
import { formatPrice } from '../../atoms/Price/PriceUnit'
import styles from './Title.module.css'
function getSymbol(tokenId: Datatoken) {
const symbol = tokenId === null ? 'OCEAN' : tokenId.symbol
return symbol
}
async function getTitle(row: PoolTransaction, locale: string) {
let title = ''
switch (row.event) {
case 'swap': {
const inToken = row.tokens.filter((x) => x.type === 'in')[0]
const inTokenSymbol = getSymbol(inToken.poolToken.tokenId)
const outToken = row.tokens.filter((x) => x.type === 'out')[0]
const outTokenSymbol = getSymbol(outToken.poolToken.tokenId)
title += `Swap ${formatPrice(
Math.abs(inToken.value).toString(),
locale
)}${inTokenSymbol} for ${formatPrice(
Math.abs(outToken.value).toString(),
locale
)}${outTokenSymbol}`
break
}
case 'setup': {
const firstToken = row.tokens.filter(
(x) =>
x.tokenAddress.toLowerCase() !==
row.poolAddress.datatokenAddress.toLowerCase()
)[0]
const firstTokenSymbol = await getSymbol(firstToken.poolToken.tokenId)
const secondToken = row.tokens.filter(
(x) =>
x.tokenAddress.toLowerCase() ===
row.poolAddress.datatokenAddress.toLowerCase()
)[0]
const secondTokenSymbol = await getSymbol(secondToken.poolToken.tokenId)
title += `Create pool with ${formatPrice(
Math.abs(firstToken.value).toString(),
locale
)}${firstTokenSymbol} and ${formatPrice(
Math.abs(secondToken.value).toString(),
locale
)}${secondTokenSymbol}`
break
}
case 'join':
case 'exit': {
for (let i = 0; i < row.tokens.length; i++) {
const tokenSymbol = await getSymbol(row.tokens[i].poolToken.tokenId)
if (i > 0) title += '\n'
title += `${row.event === 'join' ? 'Add' : 'Remove'} ${formatPrice(
Math.abs(row.tokens[i].value).toString(),
locale
)}${tokenSymbol}`
}
break
}
}
return title
}
export default function Title({ row }: { row: PoolTransaction }): ReactElement {
const [title, setTitle] = useState<string>()
const { locale } = useUserPreferences()
useEffect(() => {
if (!locale || !row) return
async function init() {
const title = await getTitle(row, locale)
setTitle(title)
}
init()
}, [row, locale])
return title ? (
<ExplorerLink networkId={row.networkId} path={`/tx/${row.tx}`}>
<span className={styles.titleText}>{title}</span>
</ExplorerLink>
) : null
}

View File

@ -1,7 +1,3 @@
.time {
color: var(--color-secondary);
}
.titleText {
white-space: pre;
}

View File

@ -0,0 +1,232 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Time from '../../atoms/Time'
import Table from '../../atoms/Table'
import AssetTitle from '../AssetListTitle'
import { useUserPreferences } from '../../../providers/UserPreferences'
import { gql } from 'urql'
import { TransactionHistory_poolTransactions as TransactionHistoryPoolTransactions } from '../../../@types/apollo/TransactionHistory'
import web3 from 'web3'
import { useWeb3 } from '../../../providers/Web3'
import { fetchDataForMultipleChains } from '../../../utils/subgraph'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import NetworkName from '../../atoms/NetworkName'
import { retrieveDDO } from '../../../utils/aquarius'
import axios from 'axios'
import Title from './Title'
import styles from './index.module.css'
const REFETCH_INTERVAL = 20000
const txHistoryQueryByPool = gql`
query TransactionHistoryByPool($user: String, $pool: String) {
poolTransactions(
orderBy: timestamp
orderDirection: desc
where: { userAddress: $user, poolAddress: $pool }
first: 1000
) {
tokens {
poolToken {
tokenId {
symbol
}
}
}
tx
event
timestamp
poolAddress {
datatokenAddress
}
tokens {
value
type
tokenAddress
}
}
}
`
const txHistoryQuery = gql`
query TransactionHistory($user: String) {
poolTransactions(
orderBy: timestamp
orderDirection: desc
where: { userAddress: $user }
first: 1000
) {
tokens {
poolToken {
tokenId {
symbol
}
}
}
tx
event
timestamp
poolAddress {
datatokenAddress
}
tokens {
value
type
tokenAddress
}
}
}
`
export interface Datatoken {
symbol: string
}
export interface PoolTransaction extends TransactionHistoryPoolTransactions {
networkId: number
}
const columns = [
{
name: 'Title',
selector: function getTitleRow(row: PoolTransaction) {
return <Title row={row} />
}
},
{
name: 'Data Set',
selector: function getAssetRow(row: PoolTransaction) {
const did = web3.utils
.toChecksumAddress(row.poolAddress.datatokenAddress)
.replace('0x', 'did:op:')
return <AssetTitle did={did} />
}
},
{
name: 'Network',
selector: function getNetwork(row: PoolTransaction) {
return <NetworkName networkId={row.networkId} />
},
maxWidth: '12rem'
},
{
name: 'Time',
selector: function getTimeRow(row: PoolTransaction) {
return (
<Time
className={styles.time}
date={row.timestamp.toString()}
relative
isUnix
/>
)
},
maxWidth: '10rem'
}
]
// hack! if we use a function to omit one field this will display a strange refresh to the enduser for each row
const columnsMinimal = [columns[0], columns[3]]
export default function PoolTransactions({
poolAddress,
minimal
}: {
poolAddress?: string
minimal?: boolean
}): ReactElement {
const { accountId } = useWeb3()
const [logs, setLogs] = useState<PoolTransaction[]>()
const [isLoading, setIsLoading] = useState<boolean>(false)
const { chainIds } = useUserPreferences()
const { appConfig } = useSiteMetadata()
const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>()
const [data, setData] = useState<PoolTransaction[]>()
async function fetchPoolTransactionData() {
const variables = { user: accountId?.toLowerCase() }
const transactions: PoolTransaction[] = []
const result = await fetchDataForMultipleChains(
poolAddress ? txHistoryQueryByPool : txHistoryQuery,
variables,
chainIds
)
for (let i = 0; i < result.length; i++) {
result[i].poolTransactions.forEach((poolTransaction: PoolTransaction) => {
transactions.push(poolTransaction)
})
}
if (JSON.stringify(data) !== JSON.stringify(transactions)) {
setData(transactions)
}
}
function refetchPoolTransactions() {
if (!dataFetchInterval) {
setDataFetchInterval(
setInterval(function () {
fetchPoolTransactionData()
}, REFETCH_INTERVAL)
)
}
}
useEffect(() => {
return () => {
clearInterval(dataFetchInterval)
}
}, [dataFetchInterval])
useEffect(() => {
if (!appConfig.metadataCacheUri) return
async function getTransactions() {
const poolTransactions: PoolTransaction[] = []
const source = axios.CancelToken.source()
try {
setIsLoading(true)
if (!data) {
await fetchPoolTransactionData()
return
}
const poolTransactionsData = data.map((obj) => ({ ...obj }))
for (let i = 0; i < poolTransactionsData.length; i++) {
const did = web3.utils
.toChecksumAddress(
poolTransactionsData[i].poolAddress.datatokenAddress
)
.replace('0x', 'did:op:')
const ddo = await retrieveDDO(did, source.token)
poolTransactionsData[i].networkId = ddo.chainId
poolTransactions.push(poolTransactionsData[i])
}
const sortedTransactions = poolTransactions.sort(
(a, b) => b.timestamp - a.timestamp
)
setLogs(sortedTransactions)
refetchPoolTransactions()
} catch (error) {
console.error('Error fetching pool transactions: ', error.message)
} finally {
setIsLoading(false)
}
}
getTransactions()
}, [accountId, chainIds, appConfig.metadataCacheUri, poolAddress, data])
return accountId ? (
<Table
columns={minimal ? columnsMinimal : columns}
data={logs}
isLoading={isLoading}
noTableHead={minimal}
dense={minimal}
pagination={minimal ? logs?.length >= 4 : logs?.length >= 9}
paginationPerPage={minimal ? 5 : 10}
/>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -1,17 +1,72 @@
.form {
margin-bottom: var(--spacer);
.search {
display: flex;
position: relative;
}
.button {
color: var(--color-secondary);
cursor: pointer;
background: var(--background-content);
border: none;
box-shadow: none;
padding: 0;
position: absolute;
padding: calc(var(--spacer) / 4);
width: 100%;
max-width: 30rem;
right: 1px;
left: 1px;
top: 1px;
bottom: 1px;
z-index: -1;
}
.form > div > div {
.button:hover,
.button:focus {
color: var(--font-color-text);
}
.input {
background-color: transparent;
height: 36px;
margin: 0;
outline: 0;
padding-right: var(--spacer);
width: 0;
transition: none;
}
.form label {
display: none;
}
.form input {
.input:focus {
width: calc(100% - var(--spacer));
background-color: var(--background-content);
position: fixed;
left: calc(var(--spacer) / 2);
right: 0;
z-index: 2;
}
@media screen and (min-width: 78rem) {
.input,
.input:focus {
width: auto;
position: relative;
left: initial;
right: initial;
}
.button {
width: auto;
left: auto;
background: none;
}
.input:focus + .button {
z-index: 3;
}
}
.searchIcon {
fill: currentColor;
transition: 0.2s ease-out;
width: var(--font-size-h5);
height: var(--font-size-h5);
}

View File

@ -10,5 +10,3 @@ export default {
export const Normal = () => <SearchBar />
export const WithInitialValue = () => <SearchBar initialValue="Water" />
export const WithFilters = () => <SearchBar filters />

View File

@ -1,35 +1,17 @@
import React, { useState, ChangeEvent, FormEvent, ReactElement } from 'react'
import React, {
useState,
useEffect,
ChangeEvent,
FormEvent,
KeyboardEvent,
ReactElement
} from 'react'
import { navigate } from 'gatsby'
import styles from './SearchBar.module.css'
import Button from '../atoms/Button'
import Input from '../atoms/Input'
import InputGroup from '../atoms/Input/InputGroup'
import queryString from 'query-string'
import { addExistingParamsToUrl } from '../templates/Search/utils'
export default function SearchBar({
placeholder,
initialValue,
filters,
size
}: {
placeholder?: string
initialValue?: string
filters?: boolean
size?: 'small' | 'large'
}): ReactElement {
let [value, setValue] = useState(initialValue || '')
async function startSearch(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
if (value === '') value = ' '
const urlEncodedValue = encodeURIComponent(value)
const url = await addExistingParamsToUrl(location, [
'text',
'owner',
'tags'
])
navigate(`${url}&text=${urlEncodedValue}`)
}
import { ReactComponent as SearchIcon } from '../../images/search.svg'
import InputElement from '../atoms/Input/InputElement'
import styles from './SearchBar.module.css'
async function emptySearch() {
const searchParams = new URLSearchParams(window.location.href)
@ -44,33 +26,67 @@ export default function SearchBar({
}
}
export default function SearchBar({
placeholder,
initialValue
}: {
placeholder?: string
initialValue?: string
}): ReactElement {
const [value, setValue] = useState(initialValue || '')
const parsed = queryString.parse(location.search)
const { text, owner } = parsed
useEffect(() => {
;(text || owner) && setValue((text || owner) as string)
}, [text, owner])
async function startSearch(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
if (value === '') setValue(' ')
const urlEncodedValue = encodeURIComponent(value)
const url = await addExistingParamsToUrl(location, [
'text',
'owner',
'tags'
])
navigate(`${url}&text=${urlEncodedValue}`)
}
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
e.target.value === '' && emptySearch()
}
async function handleKeyPress(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
await startSearch(e)
}
}
async function handleButtonClick(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
await startSearch(e)
}
return (
<form className={styles.form}>
<InputGroup>
<Input
<form className={styles.search}>
<InputElement
type="search"
name="search"
placeholder={placeholder || 'What are you looking for?'}
placeholder={placeholder || 'Search...'}
value={value}
onChange={handleChange}
required
size={size}
size="small"
className={styles.input}
onKeyPress={handleKeyPress}
/>
<Button
onClick={async (e: FormEvent<HTMLButtonElement>) =>
await startSearch(e)
}
>
Search
</Button>
</InputGroup>
{filters && <fieldset className={styles.filters}>Type, Price</fieldset>}
<button onClick={handleButtonClick} className={styles.button}>
<SearchIcon className={styles.searchIcon} />
</button>
</form>
)
}

View File

@ -1,30 +0,0 @@
.buttons {
composes: buttons from './Appearance.module.css';
}
.button {
composes: button from './Appearance.module.css';
}
.button span {
display: block;
font-size: var(--font-size-small);
font-family: var(--font-family-base);
font-weight: var(--font-weight-base);
margin-top: calc(var(--spacer) / 10);
}
.selected {
composes: selected from './Appearance.module.css';
}
.chains div[class*='boxSelectionsWrapper'] {
display: grid;
gap: calc(var(--spacer) / 4);
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
padding-bottom: calc(var(--spacer) / 8);
}
.chains label[class*='boxSelection'] {
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4) !important;
}

View File

@ -1,65 +0,0 @@
import { ConfigHelperConfig } from '@oceanprotocol/lib'
import React, { ReactElement, ChangeEvent } from 'react'
import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3'
import { getOceanConfig } from '../../../utils/ocean'
import FormHelp from '../../atoms/Input/Help'
import Label from '../../atoms/Input/Label'
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
import styles from './Chain.module.css'
export default function Chain(): ReactElement {
const { web3 } = useWeb3()
const { config, connect } = useOcean()
async function connectOcean(event: ChangeEvent<HTMLInputElement>) {
const config = getOceanConfig(event.target.value)
await connect(config)
}
function isNetworkSelected(oceanConfig: string) {
return (config as ConfigHelperConfig).network === oceanConfig
}
const options: BoxSelectionOption[] = [
{
name: 'mainnet',
checked: isNetworkSelected('mainnet'),
title: 'ETH',
text: 'Mainnet'
},
{
name: 'polygon',
checked: isNetworkSelected('polygon'),
title: 'Polygon/Matic',
text: 'Mainnet'
},
{
name: 'bsc',
checked: isNetworkSelected('bsc'),
title: 'BSC',
text: 'Mainnet'
},
{
name: 'moonbeamalpha',
checked: isNetworkSelected('moonbeamalpha'),
title: 'Moonbase Alpha',
text: 'Testnet'
}
]
// TODO: to fully solve https://github.com/oceanprotocol/market/issues/432
// there are more considerations for users with a wallet connected (wallet network vs. setting network).
// For now, only show the setting for non-wallet users.
return !web3 ? (
<li className={styles.chains}>
<Label htmlFor="">Chain</Label>
<BoxSelection
options={options}
name="chain"
handleChange={connectOcean}
/>
<FormHelp>Switch the data source for the interface.</FormHelp>
</li>
) : null
}

View File

@ -1,23 +1,21 @@
import React, { ReactElement } from 'react'
import { useUserPreferences } from '../../../providers/UserPreferences'
import FormHelp from '../../atoms/Input/Help'
import InputElement from '../../atoms/Input/InputElement'
import Input from '../../atoms/Input'
export default function Debug(): ReactElement {
const { debug, setDebug } = useUserPreferences()
return (
<li>
<InputElement
<Input
label="Debug"
help="Show geeky information in some places, and in your console."
name="debug"
type="checkbox"
options={['Debug Mode']}
options={['Activate Debug Mode']}
defaultChecked={debug === true}
onChange={() => setDebug(!debug)}
/>
<FormHelp>
Show geeky information in some places, and in your console.
</FormHelp>
</li>
)
}

View File

@ -0,0 +1,31 @@
.radioWrap {
composes: radioWrap from '../../../atoms/Input/InputElement.module.css';
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 3);
border-bottom: 1px solid var(--border-color);
}
.radioWrap:last-child {
border-bottom: none;
}
.input {
composes: checkbox from '../../../atoms/Input/InputElement.module.css';
vertical-align: baseline;
margin-right: calc(var(--spacer) / 3);
margin-top: 0;
}
.radioLabel {
composes: radioLabel from '../../../atoms/Input/InputElement.module.css';
font-weight: var(--font-weight-base);
display: flex;
align-items: center;
margin: 0;
padding: 0;
width: 100%;
}
.input:checked + span {
color: var(--font-color-text);
font-weight: var(--font-weight-bold);
}

View File

@ -0,0 +1,42 @@
import React, { ChangeEvent, ReactElement } from 'react'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import { removeItemFromArray } from '../../../../utils'
import NetworkName from '../../../atoms/NetworkName'
import styles from './NetworkItem.module.css'
export default function NetworkItem({
chainId
}: {
chainId: number
}): ReactElement {
const { chainIds, setChainIds } = useUserPreferences()
function handleNetworkChanged(e: ChangeEvent<HTMLInputElement>) {
const { value } = e.target
// storing all chainId everywhere as a number so convert from here
const valueAsNumber = Number(value)
const newChainIds = chainIds.includes(valueAsNumber)
? [...removeItemFromArray(chainIds, valueAsNumber)]
: [...chainIds, valueAsNumber]
setChainIds(newChainIds)
}
return (
<div className={styles.radioWrap} key={chainId}>
<label className={styles.radioLabel} htmlFor={`opt-${chainId}`}>
<input
className={styles.input}
id={`opt-${chainId}`}
type="checkbox"
name="chainIds"
value={chainId}
onChange={handleNetworkChanged}
defaultChecked={chainIds.includes(chainId)}
/>
<NetworkName key={chainId} networkId={chainId} />
</label>
</div>
)
}

View File

@ -0,0 +1,12 @@
.titleGroup {
font-size: var(--font-size-small);
margin-bottom: calc(var(--spacer) / 6);
margin-top: calc(var(--spacer) / 2);
color: var(--color-secondary);
}
.networks {
composes: box from '../../../atoms/Box.module.css';
box-shadow: none;
padding: 0;
}

View File

@ -0,0 +1,22 @@
import React, { ReactElement } from 'react'
import NetworkItem from './NetworkItem'
import styles from './NetworksList.module.css'
export default function NetworksList({
title,
networks
}: {
title: string
networks: number[]
}): ReactElement {
const content = networks.map((chainId) => (
<NetworkItem key={chainId} chainId={chainId} />
))
return (
<>
<h4 className={styles.titleGroup}>{title}</h4>
<div className={styles.networks}>{content}</div>
</>
)
}

View File

@ -0,0 +1,22 @@
.networks {
margin-right: calc(var(--spacer) / 3);
position: relative;
overflow: hidden;
}
.chainsSelected {
text-align: center;
position: absolute;
bottom: -8px;
left: 0;
width: 100%;
}
.chainsSelectedIndicator {
width: 4px;
height: 4px;
border-radius: 50%;
margin: 0 1px;
display: inline-block;
background-color: var(--color-primary);
}

View File

@ -0,0 +1,76 @@
import React, { ReactElement } from 'react'
import Label from '../../../atoms/Input/Label'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import FormHelp from '../../../atoms/Input/Help'
import { EthereumListsChain, getNetworkDataById } from '../../../../utils/web3'
import Tooltip from '../../../atoms/Tooltip'
import { ReactComponent as Caret } from '../../../../images/caret.svg'
import { ReactComponent as Network } from '../../../../images/network.svg'
import NetworksList from './NetworksList'
import stylesIndex from '../index.module.css'
import styles from './index.module.css'
import useNetworkMetadata from '../../../../hooks/useNetworkMetadata'
import { useUserPreferences } from '../../../../providers/UserPreferences'
export function filterNetworksByType(
type: 'mainnet' | 'testnet',
chainIds: number[],
networksList: { node: EthereumListsChain }[]
) {
const finalNetworks = chainIds.filter((chainId: number) => {
const networkData = getNetworkDataById(networksList, chainId)
// HEADS UP! Only networkData.network === 'mainnet' is consistent
// while not every test network in the network data has 'testnet'
// in its place. So for the 'testnet' case filter for all non-'mainnet'.
return type === 'mainnet'
? networkData.network === type
: networkData.network !== 'mainnet'
})
return finalNetworks
}
export default function Networks(): ReactElement {
const { networksList } = useNetworkMetadata()
const { appConfig } = useSiteMetadata()
const { chainIds } = useUserPreferences()
const networksMain = filterNetworksByType(
'mainnet',
appConfig.chainIdsSupported,
networksList
)
const networksTest = filterNetworksByType(
'testnet',
appConfig.chainIdsSupported,
networksList
)
return (
<Tooltip
content={
<ul className={stylesIndex.preferencesDetails}>
<li>
<Label htmlFor="chains">Networks</Label>
<FormHelp>Switch the data source for the interface.</FormHelp>
<NetworksList title="Main" networks={networksMain} />
<NetworksList title="Test" networks={networksTest} />
</li>
</ul>
}
trigger="click focus"
className={`${stylesIndex.preferences} ${styles.networks}`}
>
<Network aria-label="Networks" className={stylesIndex.icon} />
<Caret aria-hidden="true" className={stylesIndex.caret} />
<div className={styles.chainsSelected}>
{chainIds.map((chainId) => (
<span className={styles.chainsSelectedIndicator} key={chainId} />
))}
</div>
</Tooltip>
)
}

View File

@ -1,7 +1,11 @@
.preferences {
padding: calc(var(--spacer) / 5) calc(var(--spacer) / 4)
calc(var(--spacer) / 5) 0;
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 3);
cursor: pointer;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
margin-left: calc(var(--spacer) / 3);
background-color: var(--background-content);
white-space: nowrap;
}
.preferences svg {
@ -13,19 +17,28 @@
transform: rotate(180deg);
}
.preferences svg:last-child {
width: 1em;
height: 1em;
.caret,
svg.caret {
width: var(--font-size-small);
height: var(--font-size-small);
fill: var(--border-color);
margin-left: calc(var(--spacer) / 4);
transition: transform 0.2s ease-out;
display: none;
}
@media screen and (min-width: 42rem) {
.caret,
svg.caret {
display: inline-block;
}
}
.icon {
fill: var(--brand-grey-light);
transition: 0.2s ease-out;
width: 1.2em;
height: 1.2em;
width: 1em;
height: 1em;
}
.preferences:hover .icon,
@ -40,16 +53,16 @@
max-width: 20rem;
}
.preferencesDetails li > div,
.preferencesDetails li p {
margin: 0;
.preferencesDetails > li > div,
.preferencesDetails p:last-child {
margin-bottom: 0;
}
.preferencesDetails li p {
margin-top: calc(var(--spacer) / 8);
}
.preferencesDetails li {
.preferencesDetails > li {
padding-top: calc(var(--spacer) / 3);
padding-bottom: calc(var(--spacer) / 3);
}

View File

@ -8,10 +8,9 @@ import { ReactComponent as Caret } from '../../../images/caret.svg'
import useDarkMode from 'use-dark-mode'
import Appearance from './Appearance'
import { darkModeConfig } from '../../../../app.config'
import Chain from './Chain'
export default function UserPreferences(): ReactElement {
// Calling this here because <Theme /> is not mounted on first load
// Calling this here because <Style /> is not mounted on first load
const darkMode = useDarkMode(false, darkModeConfig)
return (
@ -20,7 +19,6 @@ export default function UserPreferences(): ReactElement {
<ul className={styles.preferencesDetails}>
<Currency />
<Appearance darkMode={darkMode} />
<Chain />
<Debug />
</ul>
}
@ -28,7 +26,7 @@ export default function UserPreferences(): ReactElement {
className={styles.preferences}
>
<Cog aria-label="Preferences" className={styles.icon} />
<Caret aria-hidden="true" />
<Caret aria-hidden="true" className={styles.caret} />
</Tooltip>
)
}

View File

@ -5,14 +5,15 @@
text-transform: uppercase;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: calc(var(--spacer) / 4);
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 3);
white-space: nowrap;
background: var(--background-content);
margin: 0;
transition: border 0.2s ease-out;
cursor: pointer;
min-width: 190px;
height: 100%;
display: flex;
align-items: center;
}
.button,
@ -30,6 +31,16 @@
color: var(--color-primary);
}
.button.initial span {
display: none;
}
@media screen and (min-width: 42rem) {
.button.initial span {
display: inline;
}
}
.blockies {
width: var(--font-size-large);
height: var(--font-size-large);
@ -37,15 +48,25 @@
overflow: hidden;
display: inline-block;
vertical-align: middle;
margin-right: calc(var(--spacer) / 6);
}
.address {
display: none;
text-transform: none;
border-right: 1px solid var(--border-color);
padding-right: calc(var(--spacer) / 3);
}
@media screen and (min-width: 60rem) {
.address {
display: inline-block;
}
.blockies {
margin-right: calc(var(--spacer) / 6);
}
}
.button svg {
width: 1em;
height: 1em;
@ -65,3 +86,15 @@
position: relative;
top: 1px;
}
.caret,
svg.caret {
display: none;
}
@media screen and (min-width: 42rem) {
.caret,
svg.caret {
display: inline-block;
}
}

View File

@ -35,7 +35,7 @@ const Account = React.forwardRef((props, ref: any) => {
return !accountId && web3Modal?.cachedProvider ? (
// Improve user experience for cached provider when connecting takes some time
<button className={styles.button} onClick={(e) => e.preventDefault()}>
<Loader message="Reconnecting wallet..." />
<Loader message="Reconnecting..." />
</button>
) : accountId ? (
<button
@ -48,7 +48,7 @@ const Account = React.forwardRef((props, ref: any) => {
<span className={styles.address} title={accountId}>
{accountTruncate(accountId)}
</span>
<Caret aria-hidden="true" />
<Caret aria-hidden="true" className={styles.caret} />
</button>
) : (
<button
@ -58,7 +58,7 @@ const Account = React.forwardRef((props, ref: any) => {
// the Tippy to show in this state.
ref={ref}
>
Connect Wallet
Connect <span>Wallet</span>
</button>
)
})

View File

@ -1,14 +1,14 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { formatCurrency } from '@coingecko/cryptoformat'
import { useOcean } from '../../../providers/Ocean'
import { useUserPreferences } from '../../../providers/UserPreferences'
import Button from '../../atoms/Button'
import AddToken from '../../atoms/AddToken'
import Conversion from '../../atoms/Price/Conversion'
import { useWeb3 } from '../../../providers/Web3'
import Web3Feedback from './Feedback'
import Web3Feedback from '../Web3Feedback'
import styles from './Details.module.css'
import { getOceanConfig } from '../../../utils/ocean'
export default function Details(): ReactElement {
const {
@ -17,20 +17,34 @@ export default function Details(): ReactElement {
web3Modal,
connect,
logout,
networkData,
networkId,
networkData
balance
} = useWeb3()
const { balance, config } = useOcean()
const { locale } = useUserPreferences()
const [mainCurrency, setMainCurrency] = useState<string>()
const [oceanTokenMetadata, setOceanTokenMetadata] =
useState<{
address: string
symbol: string
}>()
// const [portisNetwork, setPortisNetwork] = useState<string>()
useEffect(() => {
if (!networkId) return
const symbol =
networkId === 2021000 ? 'GX' : networkData?.nativeCurrency.symbol
setMainCurrency(symbol)
const oceanConfig = getOceanConfig(networkId)
oceanConfig &&
setOceanTokenMetadata({
address: oceanConfig.oceanTokenAddress,
symbol: oceanConfig.oceanTokenSymbol
})
}, [networkData, networkId])
// Handle network change for Portis
@ -49,7 +63,7 @@ export default function Details(): ReactElement {
{Object.entries(balance).map(([key, value]) => (
<li className={styles.balance} key={key}>
<span className={styles.symbol}>
{key === 'eth' ? mainCurrency : config.oceanTokenSymbol}
{key === 'eth' ? mainCurrency : oceanTokenMetadata?.symbol}
</span>{' '}
{formatCurrency(Number(value), '', locale, false, {
significantFigures: 4
@ -76,8 +90,8 @@ export default function Details(): ReactElement {
)} */}
{web3ProviderInfo?.name === 'MetaMask' && (
<AddToken
address={config.oceanTokenAddress}
symbol={config.oceanTokenSymbol}
address={oceanTokenMetadata?.address}
symbol={oceanTokenMetadata?.symbol}
logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/token.png"
className={styles.addToken}
/>

View File

@ -1,51 +0,0 @@
import React, { ReactElement } from 'react'
import { useOcean } from '../../../providers/Ocean'
import Status from '../../atoms/Status'
import styles from './Feedback.module.css'
export declare type Web3Error = {
status: 'error' | 'warning' | 'success'
title: string
message?: string
}
export default function Web3Feedback({
isBalanceSufficient
}: {
isBalanceSufficient?: boolean
}): ReactElement {
const { account, ocean } = useOcean()
const showFeedback = !account || !ocean || isBalanceSufficient === false
const state = !account
? 'error'
: account && isBalanceSufficient
? 'success'
: 'warning'
const title = !account
? 'No account connected'
: !ocean
? 'Error connecting to Ocean'
: account
? isBalanceSufficient === false
? 'Insufficient balance'
: 'Connected to Ocean'
: 'Something went wrong'
const message = !account
? 'Please connect your Web3 wallet.'
: !ocean
? 'Please try again.'
: isBalanceSufficient === false
? 'You do not have enough OCEAN in your wallet to purchase this asset.'
: 'Something went wrong.'
return showFeedback ? (
<section className={styles.feedback}>
<Status state={state} aria-hidden />
<h3 className={styles.title}>{title}</h3>
{message && <p className={styles.error}>{message}</p>}
</section>
) : null
}

View File

@ -3,7 +3,7 @@
border-right: none;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 3);
white-space: nowrap;
margin-right: -3px;
display: inline-flex;
@ -12,8 +12,6 @@
.name {
font-size: var(--font-size-small);
display: inline-block;
text-transform: capitalize;
}
.badge {

View File

@ -1,41 +1,36 @@
import React, { useState, useEffect, ReactElement } from 'react'
import { useOcean } from '../../../providers/Ocean'
import Status from '../../atoms/Status'
import { ConfigHelper, ConfigHelperConfig } from '@oceanprotocol/lib'
import styles from './Network.module.css'
import Badge from '../../atoms/Badge'
import Tooltip from '../../atoms/Tooltip'
import { useWeb3 } from '../../../providers/Web3'
import NetworkName from '../../atoms/NetworkName'
import styles from './Network.module.css'
import { getOceanConfig } from '../../../utils/ocean'
export default function Network(): ReactElement {
const { networkId, networkDisplayName, isTestnet } = useWeb3()
const { config } = useOcean()
const networkIdConfig = (config as ConfigHelperConfig).networkId
const { networkId, isTestnet } = useWeb3()
const [isEthMainnet, setIsEthMainnet] = useState<boolean>()
const [isSupportedNetwork, setIsSupportedNetwork] = useState<boolean>()
const [isSupportedOceanNetwork, setIsSupportedOceanNetwork] =
useState<boolean>()
useEffect(() => {
// take network from user when present,
// otherwise use the default configured one of app
const network = networkId || networkIdConfig
const isEthMainnet = network === 1
setIsEthMainnet(isEthMainnet)
// take network from user when present
const network = networkId || 1
// Check networkId against ocean.js ConfigHelper configs
// to figure out if network is supported.
const isSupportedNetwork = Boolean(new ConfigHelper().getConfig(network))
setIsSupportedNetwork(isSupportedNetwork)
}, [networkId, networkIdConfig])
const isSupportedOceanNetwork = Boolean(getOceanConfig(network))
setIsSupportedOceanNetwork(isSupportedOceanNetwork)
}, [networkId])
return !isEthMainnet && networkDisplayName ? (
return networkId ? (
<div className={styles.network}>
{!isSupportedNetwork && (
{!isSupportedOceanNetwork && (
<Tooltip content="No Ocean Protocol contracts are deployed to this network.">
<Status state="error" className={styles.warning} />
</Tooltip>
)}
<span className={styles.name}>{networkDisplayName}</span>
<NetworkName className={styles.name} networkId={networkId} minimal />
{isTestnet && <Badge label="Test" className={styles.badge} />}
</div>
) : null

View File

@ -1,3 +1,4 @@
.wallet {
display: flex;
align-self: stretch;
}

View File

@ -1,5 +1,5 @@
import React from 'react'
import Web3Feedback from './Feedback'
import Web3Feedback from '../Web3Feedback'
import web3Mock from '../../../../tests/unit/__mocks__/web3'
import { Center } from '../../../../.storybook/helpers'

View File

@ -1,4 +1,4 @@
import React, { ReactElement, useState } from 'react'
import React, { ReactElement } from 'react'
import Account from './Account'
import Details from './Details'
import Tooltip from '../../atoms/Tooltip'

View File

@ -0,0 +1,5 @@
.text {
color: var(--color-secondary);
margin-top: calc(var(--spacer) / 4);
margin-bottom: calc(var(--spacer) / 2);
}

View File

@ -0,0 +1,51 @@
import React, { ReactElement } from 'react'
import { useWeb3 } from '../../providers/Web3'
import {
addCustomNetwork,
getNetworkConfigObject,
getNetworkDisplayName,
getNetworkDataById
} from '../../utils/web3'
import Button from '../atoms/Button'
import styles from './WalletNetworkSwitcher.module.css'
import useNetworkMetadata from '../../hooks/useNetworkMetadata'
import { getOceanConfig } from '../../utils/ocean'
import { useAsset } from '../../providers/Asset'
export default function WalletNetworkSwitcher(): ReactElement {
const { networkId, web3Provider } = useWeb3()
const { ddo } = useAsset()
const oceanConfig = getOceanConfig(ddo.chainId)
const { networksList } = useNetworkMetadata()
const ddoNetworkData = getNetworkDataById(networksList, ddo.chainId)
const walletNetworkData = getNetworkDataById(networksList, networkId)
const ddoNetworkName = (
<strong>{getNetworkDisplayName(ddoNetworkData, ddo.chainId)}</strong>
)
const walletNetworkName = (
<strong>{getNetworkDisplayName(walletNetworkData, networkId)}</strong>
)
async function switchWalletNetwork() {
const networkNode = networksList.find(
(data) => data.node.chainId === ddo.chainId
).node
const network = { ...networkNode, providerUri: oceanConfig.providerUri }
const networkConfig = getNetworkConfigObject(network)
addCustomNetwork(web3Provider, networkConfig)
}
return (
<>
<p className={styles.text}>
This asset is published on {ddoNetworkName} but your wallet is connected
to {walletNetworkName}. Connect to {ddoNetworkName} to interact with
this asset.
</p>
<Button size="small" onClick={() => switchWalletNetwork()}>
Switch to {ddoNetworkName}
</Button>
</>
)
}

View File

@ -2,8 +2,7 @@
font-size: var(--font-size-small);
padding-left: var(--spacer);
padding-top: calc(var(--spacer) / 1.5);
margin-top: var(--spacer);
border-top: 1px solid var(--border-color);
margin-top: calc(var(--spacer) / 2);
position: relative;
width: 100%;
}
@ -11,7 +10,7 @@
.feedback i {
position: absolute;
left: 0;
top: calc(var(--spacer) / 1.5);
top: calc(var(--spacer) / 1.45);
}
.title {

View File

@ -0,0 +1,70 @@
import React, { ReactElement, useEffect } from 'react'
import { useWeb3 } from '../../providers/Web3'
import Status from '../atoms/Status'
import styles from './Web3Feedback.module.css'
import WalletNetworkSwitcher from './WalletNetworkSwitcher'
import { useGraphSyncStatus } from '../../hooks/useGraphSyncStatus'
export declare type Web3Error = {
status: 'error' | 'warning' | 'success'
title: string
message?: string
}
export default function Web3Feedback({
isBalanceSufficient,
isAssetNetwork
}: {
isBalanceSufficient?: boolean
isAssetNetwork?: boolean
}): ReactElement {
const { accountId } = useWeb3()
const { isGraphSynced, blockGraph, blockHead } = useGraphSyncStatus()
const showFeedback =
!accountId ||
// !ocean ||
isBalanceSufficient === false ||
isAssetNetwork === false ||
isGraphSynced === false
const state =
!accountId || !isGraphSynced
? 'error'
: accountId && isBalanceSufficient && isAssetNetwork
? 'success'
: 'warning'
const title = !accountId
? 'No account connected'
: // : !ocean
// ? 'Error connecting to Ocean'
accountId && isAssetNetwork === false
? 'Not connected to asset network'
: isGraphSynced === false
? `Data out of sync`
: accountId
? isBalanceSufficient === false
? 'Insufficient balance'
: 'Connected to Ocean'
: 'Something went wrong'
const message = !accountId
? 'Please connect your Web3 wallet.'
: isBalanceSufficient === false
? 'You do not have enough OCEAN in your wallet to purchase this asset.'
: isGraphSynced === false
? `The data for this network has only synced to Ethereum block ${blockGraph} (out of ${blockHead}). Transactions may fail! Please check back soon`
: 'Something went wrong.'
return showFeedback ? (
<section className={styles.feedback}>
<Status state={state} aria-hidden />
<h3 className={styles.title}>{title}</h3>
{isAssetNetwork === false ? (
<WalletNetworkSwitcher />
) : (
message && <p className={styles.error}>{message}</p>
)}
</section>
) : null
}

View File

@ -94,7 +94,7 @@ export default function FormStartCompute({
const { isValid, values }: FormikContextType<{ algorithm: string }> =
useFormikContext()
const { price, ddo } = useAsset()
const { price, ddo, isAssetNetwork } = useAsset()
const [totalPrice, setTotalPrice] = useState(price?.value)
const { accountId } = useWeb3()
const { ocean } = useOcean()
@ -175,7 +175,10 @@ export default function FormStartCompute({
<ButtonBuy
action="compute"
disabled={
isComputeButtonDisabled || !isValid || algorithmConsumableStatus > 0
isComputeButtonDisabled ||
!isValid ||
!isAssetNetwork ||
algorithmConsumableStatus > 0
}
hasPreviousOrder={hasPreviousOrder}
hasDatatoken={hasDatatoken}

View File

@ -10,7 +10,6 @@ import { toast } from 'react-toastify'
import Price from '../../../atoms/Price'
import File from '../../../atoms/File'
import Alert from '../../../atoms/Alert'
import Web3Feedback from '../../../molecules/Wallet/Feedback'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
@ -39,6 +38,7 @@ import { secondsToString } from '../../../../utils/metadata'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import AlgorithmDatasetsListForCompute from '../../AssetContent/AlgorithmDatasetsListForCompute'
import { getPreviousOrders, getPrice } from '../../../../utils/subgraph'
import { chainIds } from '../../../../../app.config'
const SuccessAction = () => (
<Button style="text" to="/history?defaultTab=ComputeJobs" size="small">
@ -63,8 +63,8 @@ export default function Compute({
}): ReactElement {
const { appConfig } = useSiteMetadata()
const { accountId } = useWeb3()
const { ocean, account, config } = useOcean()
const { price, type, ddo } = useAsset()
const { ocean, account } = useOcean()
const { price, type, ddo, isAssetNetwork } = useAsset()
const { buyDT, pricingError, pricingStepText } = usePricing()
const [isJobStarting, setIsJobStarting] = useState(false)
const [error, setError] = useState<string>()
@ -142,7 +142,8 @@ export default function Compute({
}
function getQuerryString(
trustedAlgorithmList: publisherTrustedAlgorithm[]
trustedAlgorithmList: publisherTrustedAlgorithm[],
chainId?: number
): SearchQuery {
let algoQuerry = ''
trustedAlgorithmList.forEach((trusteAlgo) => {
@ -157,7 +158,7 @@ export default function Compute({
offset: 500,
query: {
query_string: {
query: `${algorithmQuery} service.attributes.main.type:algorithm -isInPurgatory:true`
query: `${algorithmQuery} service.attributes.main.type:algorithm AND chainId:${chainId} -isInPurgatory:true`
}
},
sort: { created: -1 }
@ -180,9 +181,9 @@ export default function Compute({
} else {
const gueryResults = await queryMetadata(
getQuerryString(
computeService.attributes.main.privacy.publisherTrustedAlgorithms
computeService.attributes.main.privacy.publisherTrustedAlgorithms,
ddo.chainId
),
config.metadataCacheUri,
source.token
)
setDdoAlgorithmList(gueryResults.results)
@ -190,7 +191,6 @@ export default function Compute({
algorithmSelectionList = await transformDDOToAssetSelection(
datasetComputeService?.serviceEndpoint,
gueryResults.results,
config.metadataCacheUri,
[]
)
}
@ -472,9 +472,6 @@ export default function Compute({
action={<SuccessAction />}
/>
)}
{type !== 'algorithm' && (
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
)}
</footer>
</>
)

View File

@ -6,7 +6,6 @@
.info {
display: flex;
width: auto;
padding: 0 calc(var(--spacer)) 0 calc(var(--spacer));
}
.filewrapper {
@ -15,4 +14,5 @@
.feedback {
width: 100%;
margin-top: calc(var(--spacer));
}

View File

@ -3,12 +3,9 @@ import { toast } from 'react-toastify'
import { File as FileMetadata, DDO, BestPrice } from '@oceanprotocol/lib'
import File from '../../atoms/File'
import Price from '../../atoms/Price'
import Web3Feedback from '../../molecules/Wallet/Feedback'
import styles from './Consume.module.css'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useAsset } from '../../../providers/Asset'
import { secondsToString } from '../../../utils/metadata'
import { gql, useQuery } from '@apollo/client'
import { gql, useQuery } from 'urql'
import { OrdersData } from '../../../@types/apollo/OrdersData'
import BigNumber from 'bignumber.js'
import { useOcean } from '../../../providers/Ocean'
@ -16,7 +13,9 @@ import { useWeb3 } from '../../../providers/Web3'
import { usePricing } from '../../../hooks/usePricing'
import { useConsume } from '../../../hooks/useConsume'
import ButtonBuy from '../../atoms/ButtonBuy'
import { secondsToString } from '../../../utils/metadata'
import AlgorithmDatasetsListForCompute from '../AssetContent/AlgorithmDatasetsListForCompute'
import styles from './Consume.module.css'
const previousOrderQuery = gql`
query PreviousOrder($id: String!, $account: String!) {
@ -54,7 +53,7 @@ export default function Consume({
const { appConfig } = useSiteMetadata()
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
const [previousOrderId, setPreviousOrderId] = useState<string>()
const { isInPurgatory, price, type } = useAsset()
const { isInPurgatory, price, type, isAssetNetwork } = useAsset()
const { buyDT, pricingStepText, pricingError, pricingIsLoading } =
usePricing()
const { consumeStepText, consume, consumeError, isLoading } = useConsume()
@ -63,13 +62,15 @@ export default function Consume({
const [maxDt, setMaxDT] = useState<number>(1)
const [isConsumablePrice, setIsConsumablePrice] = useState(true)
const [assetTimeout, setAssetTimeout] = useState('')
const { data } = useQuery<OrdersData>(previousOrderQuery, {
const [result] = useQuery<OrdersData>({
query: previousOrderQuery,
variables: {
id: ddo.dataToken?.toLowerCase(),
account: accountId?.toLowerCase()
},
pollInterval: 5000
}
// pollInterval: 5000
})
const { data } = result
async function checkMaxAvaialableTokens(price: BestPrice) {
if (!ocean || !price) return
@ -122,6 +123,7 @@ export default function Consume({
!isConsumable ||
((!ocean ||
!isBalanceSufficient ||
!isAssetNetwork ||
typeof consumeStepText !== 'undefined' ||
pricingIsLoading ||
(!hasPreviousOrder && !hasDatatoken && !(maxDt >= 1)) ||
@ -133,6 +135,7 @@ export default function Consume({
ocean,
hasPreviousOrder,
isBalanceSufficient,
isAssetNetwork,
consumeStepText,
pricingIsLoading,
isConsumablePrice,
@ -198,9 +201,6 @@ export default function Consume({
{type === 'algorithm' && (
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} dataset={ddo} />
)}
<footer className={styles.feedback}>
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
</footer>
</aside>
)
}

View File

@ -20,6 +20,7 @@ import {
setMinterToDispenser,
setMinterToPublisher
} from '../../../../utils/freePrice'
import Web3Feedback from '../../../molecules/Web3Feedback'
const contentQuery = graphql`
query EditAvanceSettingsQuery {
@ -72,7 +73,7 @@ export default function EditAdvancedSettings({
const { debug } = useUserPreferences()
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { metadata, ddo, refreshDdo, price } = useAsset()
const { isAssetNetwork, ddo, refreshDdo, price } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const { appConfig } = useSiteMetadata()
@ -168,7 +169,7 @@ export default function EditAdvancedSettings({
setShowEdit={setShowEdit}
/>
</article>
<Web3Feedback isAssetNetwork={isAssetNetwork} />
{debug === true && (
<div className={styles.grid}>
<DebugEditCredential

View File

@ -20,6 +20,7 @@ import {
setMinterToDispenser,
setMinterToPublisher
} from '../../../../utils/freePrice'
import Web3Feedback from '../../../molecules/Web3Feedback'
const contentQuery = graphql`
query EditComputeDataQuery {
@ -66,7 +67,7 @@ export default function EditComputeDataset({
const { debug } = useUserPreferences()
const { ocean } = useOcean()
const { accountId } = useWeb3()
const { ddo, refreshDdo, price } = useAsset()
const { ddo, price, isAssetNetwork, refreshDdo } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
@ -169,7 +170,7 @@ export default function EditComputeDataset({
setShowEdit={setShowEdit}
/>
</article>
<Web3Feedback isAssetNetwork={isAssetNetwork} />
{debug === true && (
<div className={styles.grid}>
<DebugEditCompute values={values} ddo={ddo} />

View File

@ -0,0 +1,20 @@
.actions {
margin-left: -2rem;
margin-right: -2rem;
border-top: 1px solid var(--border-color);
padding: calc(var(--spacer) / 2) var(--spacer) 0;
display: flex;
justify-content: center;
}
@media (min-width: 40rem) {
.actions {
padding-top: var(--spacer);
}
}
.actions a,
.actions button {
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2);
}

View File

@ -0,0 +1,37 @@
import { FormikContextType, useFormikContext } from 'formik'
import React, { ReactElement } from 'react'
import { AdvancedSettingsForm } from '../../../../models/FormEditCredential'
import { useAsset } from '../../../../providers/Asset'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import Button from '../../../atoms/Button'
import styles from './FormActions.module.css'
export default function FormActions({
setShowEdit,
handleClick
}: {
setShowEdit: (show: boolean) => void
handleClick?: () => void
}): ReactElement {
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { isAssetNetwork } = useAsset()
const { isValid }: FormikContextType<Partial<AdvancedSettingsForm>> =
useFormikContext()
return (
<footer className={styles.actions}>
<Button
style="primary"
disabled={!ocean || !accountId || !isValid || !isAssetNetwork}
onClick={handleClick}
>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>
Cancel
</Button>
</footer>
)
}

View File

@ -1,12 +1,10 @@
import React, { ChangeEvent, ReactElement } from 'react'
import styles from './FormEditMetadata.module.css'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Button from '../../../atoms/Button'
import Input from '../../../atoms/Input'
import { FormFieldProps } from '../../../../@types/Form'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import { AdvancedSettingsForm } from '../../../../models/FormEditCredential'
import FormActions from './FormActions'
export default function FormAdvancedSettings({
data,
@ -15,10 +13,7 @@ export default function FormAdvancedSettings({
data: FormFieldProps[]
setShowEdit: (show: boolean) => void
}): ReactElement {
const { accountId } = useWeb3()
const { ocean, config } = useOcean()
const {
isValid,
validateField,
setFieldValue
}: FormikContextType<Partial<AdvancedSettingsForm>> = useFormikContext()
@ -46,14 +41,7 @@ export default function FormAdvancedSettings({
/>
))}
<footer className={styles.actions}>
<Button style="primary" disabled={!ocean || !accountId || !isValid}>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>
Cancel
</Button>
</footer>
<FormActions setShowEdit={setShowEdit} />
</Form>
)
}

View File

@ -1,9 +1,6 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Button from '../../../atoms/Button'
import Input from '../../../atoms/Input'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import { FormFieldProps } from '../../../../@types/Form'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import stylesIndex from './index.module.css'
@ -16,6 +13,8 @@ import { useAsset } from '../../../../providers/Asset'
import { ComputePrivacyForm } from '../../../../models/FormEditComputeDataset'
import { publisherTrustedAlgorithm as PublisherTrustedAlgorithm } from '@oceanprotocol/lib'
import axios from 'axios'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import FormActions from './FormActions'
export default function FormEditComputeDataset({
data,
@ -26,11 +25,9 @@ export default function FormEditComputeDataset({
title: string
setShowEdit: (show: boolean) => void
}): ReactElement {
const { accountId } = useWeb3()
const { ocean, config } = useOcean()
const { appConfig } = useSiteMetadata()
const { ddo } = useAsset()
const { isValid, values }: FormikContextType<ComputePrivacyForm> =
useFormikContext()
const { values }: FormikContextType<ComputePrivacyForm> = useFormikContext()
const [allAlgorithms, setAllAlgorithms] = useState<AssetSelectionAsset[]>()
const { publisherTrustedAlgorithms } =
@ -44,21 +41,16 @@ export default function FormEditComputeDataset({
offset: 500,
query: {
query_string: {
query: `service.attributes.main.type:algorithm -isInPurgatory:true`
query: `service.attributes.main.type:algorithm AND chainId:${ddo.chainId} -isInPurgatory:true`
}
},
sort: { created: -1 }
}
const querryResult = await queryMetadata(
query,
config.metadataCacheUri,
source.token
)
const querryResult = await queryMetadata(query, source.token)
const datasetComputeService = ddo.findServiceByType('compute')
const algorithmSelectionList = await transformDDOToAssetSelection(
datasetComputeService?.serviceEndpoint,
querryResult.results,
config.metadataCacheUri,
publisherTrustedAlgorithms
)
return algorithmSelectionList
@ -68,7 +60,7 @@ export default function FormEditComputeDataset({
getAlgorithmList(publisherTrustedAlgorithms).then((algorithms) => {
setAllAlgorithms(algorithms)
})
}, [config.metadataCacheUri, publisherTrustedAlgorithms])
}, [appConfig.metadataCacheUri, publisherTrustedAlgorithms])
return (
<Form className={styles.form}>
@ -90,14 +82,8 @@ export default function FormEditComputeDataset({
component={Input}
/>
))}
<footer className={styles.actions}>
<Button style="primary" disabled={!ocean || !accountId || !isValid}>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>
Cancel
</Button>
</footer>
<FormActions setShowEdit={setShowEdit} />
</Form>
)
}

View File

@ -2,27 +2,6 @@
composes: box from '../../../atoms/Box.module.css';
}
.actions {
margin-left: -2rem;
margin-right: -2rem;
border-top: 1px solid var(--border-color);
padding: calc(var(--spacer) / 2) var(--spacer) 0;
display: flex;
justify-content: center;
}
@media (min-width: 40rem) {
.actions {
padding-top: var(--spacer);
}
}
.actions a,
.actions button {
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2);
}
select[multiple] {
.form select[multiple] {
height: 130px;
}

View File

@ -1,13 +1,12 @@
import React, { ChangeEvent, ReactElement } from 'react'
import styles from './FormEditMetadata.module.css'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Button from '../../../atoms/Button'
import { useOcean } from '../../../../providers/Ocean'
import Input from '../../../atoms/Input'
import { FormFieldProps } from '../../../../@types/Form'
import { MetadataPublishFormDataset } from '../../../../@types/MetaData'
import { checkIfTimeoutInPredefinedValues } from '../../../../utils/metadata'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import FormActions from './FormActions'
import styles from './FormEditMetadata.module.css'
function handleTimeoutCustomOption(
data: FormFieldProps[],
@ -56,10 +55,8 @@ export default function FormEditMetadata({
values: Partial<MetadataPublishFormDataset>
showPrice: boolean
}): ReactElement {
const { accountId } = useWeb3()
const { ocean, config } = useOcean()
const { config } = useOcean()
const {
isValid,
validateField,
setFieldValue
}: FormikContextType<Partial<MetadataPublishFormDataset>> = useFormikContext()
@ -96,18 +93,10 @@ export default function FormEditMetadata({
)
)}
<footer className={styles.actions}>
<Button
style="primary"
disabled={!ocean || !accountId || !isValid}
onClick={() => setTimeoutStringValue(values.timeout)}
>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>
Cancel
</Button>
</footer>
<FormActions
setShowEdit={setShowEdit}
handleClick={() => setTimeoutStringValue(values.timeout)}
/>
</Form>
)
}

View File

@ -9,7 +9,7 @@ import { useAsset } from '../../../../providers/Asset'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import { MetadataPreview } from '../../../molecules/MetadataPreview'
import Debug from './DebugEditMetadata'
import Web3Feedback from '../../../molecules/Wallet/Feedback'
import Web3Feedback from '../../../molecules/Web3Feedback'
import FormEditMetadata from './FormEditMetadata'
import { mapTimeoutStringToSeconds } from '../../../../utils/metadata'
import styles from './index.module.css'

View File

@ -13,6 +13,7 @@ import { FormAddLiquidity } from '.'
import { PoolBalance } from '../../../../../@types/TokenBalance'
import UserLiquidity from '../../../../atoms/UserLiquidity'
import { useOcean } from '../../../../../providers/Ocean'
import { useWeb3 } from '../../../../../providers/Web3'
export default function FormAdd({
coin,
@ -37,7 +38,8 @@ export default function FormAdd({
setNewPoolTokens: (value: string) => void
setNewPoolShare: (value: string) => void
}): ReactElement {
const { ocean, balance } = useOcean()
const { balance } = useWeb3()
const { ocean } = useOcean()
// Connect with form
const {

View File

@ -65,8 +65,8 @@ export default function Add({
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childContentJson.pool.add
const { accountId } = useWeb3()
const { ocean, balance } = useOcean()
const { accountId, balance } = useWeb3()
const { ocean } = useOcean()
const { debug } = useUserPreferences()
const [txId, setTxId] = useState<string>()
const [coin, setCoin] = useState('OCEAN')

View File

@ -8,7 +8,6 @@ import {
ChartTooltipItem,
ChartTooltipOptions
} from 'chart.js'
import styles from './Graph.module.css'
import Loader from '../../../atoms/Loader'
import { formatPrice } from '../../../atoms/Price/PriceUnit'
import { useUserPreferences } from '../../../../providers/UserPreferences'
@ -17,8 +16,10 @@ import { darkModeConfig } from '../../../../../app.config'
import Button from '../../../atoms/Button'
import { Logger } from '@oceanprotocol/lib'
import { useAsset } from '../../../../providers/Asset'
import { gql, useQuery } from '@apollo/client'
import { gql, OperationResult } from 'urql'
import { PoolHistory } from '../../../../@types/apollo/PoolHistory'
import { fetchData, getQueryContext } from '../../../../utils/subgraph'
import styles from './Graph.module.css'
declare type GraphType = 'liquidity' | 'price'
@ -27,6 +28,8 @@ defaults.global.defaultFontFamily = `'Sharp Sans', -apple-system, BlinkMacSystem
'Segoe UI', Helvetica, Arial, sans-serif`
defaults.global.animation = { easing: 'easeInOutQuart', duration: 1000 }
const REFETCH_INTERVAL = 10000
const lineStyle: Partial<ChartDataSets> = {
fill: false,
lineTension: 0.1,
@ -121,22 +124,53 @@ export default function Graph(): ReactElement {
const [options, setOptions] = useState<ChartOptions>()
const [graphType, setGraphType] = useState<GraphType>('liquidity')
const { price } = useAsset()
const { price, ddo } = useAsset()
const [lastBlock, setLastBlock] = useState<number>(0)
const [priceHistory, setPriceHistory] = useState([])
const [error, setError] = useState<Error>()
const [liquidityHistory, setLiquidityHistory] = useState([])
const [timestamps, setTimestamps] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [dataHistory, setDataHistory] = useState<PoolHistory>()
const [graphData, setGraphData] = useState<ChartData>()
const [graphFetchInterval, setGraphFetchInterval] = useState<NodeJS.Timeout>()
const { data, refetch, error } = useQuery<PoolHistory>(poolHistory, {
variables: {
async function getPoolHistory() {
try {
const queryContext = getQueryContext(ddo.chainId)
const queryVariables = {
id: price.address.toLowerCase(),
block: lastBlock
},
pollInterval: 20000
})
}
const queryResult: OperationResult<PoolHistory> = await fetchData(
poolHistory,
queryVariables,
queryContext
)
setDataHistory(queryResult?.data)
} catch (error) {
console.error('Error fetchData: ', error.message)
setError(error)
}
}
function refetchGraph() {
if (!graphFetchInterval) {
setGraphFetchInterval(
setInterval(function () {
getPoolHistory()
}, REFETCH_INTERVAL)
)
}
}
useEffect(() => {
return () => {
clearInterval(graphFetchInterval)
}
}, [graphFetchInterval])
useEffect(() => {
Logger.log('Fired GraphOptions!')
@ -145,7 +179,16 @@ export default function Graph(): ReactElement {
}, [locale, darkMode.value])
useEffect(() => {
if (!data) return
getPoolHistory()
}, [lastBlock])
useEffect(() => {
async function init() {
const data: PoolHistory = dataHistory
if (!data) {
await getPoolHistory()
return
}
Logger.log('Fired GraphData!')
const latestTimestamps = [
@ -178,7 +221,6 @@ export default function Graph(): ReactElement {
setLastBlock(
data.poolTransactions[data.poolTransactions.length - 1].block
)
refetch()
} else {
setGraphData({
labels: latestTimestamps.slice(0),
@ -196,8 +238,11 @@ export default function Graph(): ReactElement {
]
})
setIsLoading(false)
refetchGraph()
}
}, [data, graphType])
}
init()
}, [dataHistory, graphType])
function handleGraphTypeSwitch(e: ChangeEvent<HTMLButtonElement>) {
e.preventDefault()

View File

@ -11,7 +11,8 @@ export default function TokenList({
dtSymbol,
poolShares,
conversion,
highlight
highlight,
showTVLLabel
}: {
title: string | ReactNode
children: ReactNode
@ -21,6 +22,7 @@ export default function TokenList({
poolShares: string
conversion: number
highlight?: boolean
showTVLLabel?: boolean
}): ReactElement {
return (
<div className={`${styles.tokenlist} ${highlight ? styles.highlight : ''}`}>
@ -33,6 +35,7 @@ export default function TokenList({
<Conversion
price={`${conversion}`}
className={styles.totalLiquidity}
showTVLLabel={showTVLLabel}
/>
)}
</div>

View File

@ -15,10 +15,13 @@ import { PoolBalance } from '../../../../@types/TokenBalance'
import Transactions from './Transactions'
import Graph from './Graph'
import { useAsset } from '../../../../providers/Asset'
import { gql, useQuery } from '@apollo/client'
import { gql, OperationResult } from 'urql'
import { PoolLiquidity } from '../../../../@types/apollo/PoolLiquidity'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import { fetchData, getQueryContext } from '../../../../utils/subgraph'
const REFETCH_INTERVAL = 5000
const contentQuery = graphql`
query PoolQuery {
@ -62,9 +65,10 @@ export default function Pool(): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childContentJson.pool
const { accountId, networkId } = useWeb3()
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { isInPurgatory, ddo, owner, price, refreshInterval } = useAsset()
const { isInPurgatory, ddo, owner, price, refreshInterval, isAssetNetwork } =
useAsset()
const dtSymbol = ddo?.dataTokenInfo.symbol
const [poolTokens, setPoolTokens] = useState<string>()
@ -88,20 +92,50 @@ export default function Pool(): ReactElement {
const [creatorLiquidity, setCreatorLiquidity] = useState<PoolBalance>()
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
const [dataLiquidity, setdataLiquidity] = useState<PoolLiquidity>()
const [liquidityFetchInterval, setLiquidityFetchInterval] =
useState<NodeJS.Timeout>()
// the purpose of the value is just to trigger the effect
const [refreshPool, setRefreshPool] = useState(false)
const { data: dataLiquidity } = useQuery<PoolLiquidity>(poolLiquidityQuery, {
variables: {
async function getPoolLiquidity() {
const queryContext = getQueryContext(ddo.chainId)
const queryVariables = {
id: price.address.toLowerCase(),
shareId: `${price.address.toLowerCase()}-${ddo.publicKey[0].owner.toLowerCase()}`
},
pollInterval: 5000
})
}
const queryResult: OperationResult<PoolLiquidity> = await fetchData(
poolLiquidityQuery,
queryVariables,
queryContext
)
setdataLiquidity(queryResult?.data)
}
function refetchLiquidity() {
if (!liquidityFetchInterval) {
setLiquidityFetchInterval(
setInterval(function () {
getPoolLiquidity()
}, REFETCH_INTERVAL)
)
}
}
useEffect(() => {
return () => {
clearInterval(liquidityFetchInterval)
}
}, [liquidityFetchInterval])
useEffect(() => {
async function init() {
if (!dataLiquidity || !dataLiquidity.pool) return
if (!dataLiquidity || !dataLiquidity.pool) {
await getPoolLiquidity()
return
}
// Total pool shares
const totalPoolTokens = dataLiquidity.pool.totalShares
@ -149,6 +183,7 @@ export default function Pool(): ReactElement {
creatorLiquidity &&
((Number(creatorPoolTokens) / Number(totalPoolTokens)) * 100).toFixed(2)
setCreatorPoolShare(creatorPoolShare)
refetchLiquidity()
}
init()
}, [dataLiquidity, ddo.dataToken, price.datatoken, price.ocean, price?.value])
@ -243,15 +278,15 @@ export default function Pool(): ReactElement {
<Tooltip content={content.tooltips.price} />
<div className={styles.dataTokenLinks}>
<ExplorerLink
networkId={networkId}
networkId={ddo.chainId}
path={`address/${price?.address}`}
>
Pool
</ExplorerLink>
<ExplorerLink
networkId={networkId}
networkId={ddo.chainId}
path={
networkId === 137 || networkId === 1287
ddo.chainId === 2021000 || ddo.chainId === 1287
? `tokens/${ddo.dataToken}`
: `token/${ddo.dataToken}`
}
@ -284,7 +319,7 @@ export default function Pool(): ReactElement {
</TokenList>
<TokenList
title="Pool Creator Liquidity"
title="Pool Creator Statistics"
ocean={`${creatorLiquidity?.ocean}`}
dt={`${creatorLiquidity?.datatoken}`}
dtSymbol={dtSymbol}
@ -314,6 +349,7 @@ export default function Pool(): ReactElement {
dtSymbol={dtSymbol}
poolShares={totalPoolTokens}
conversion={totalLiquidityInOcean}
showTVLLabel
>
<Token symbol="% swap fee" balance={swapFee} noIcon />
</TokenList>
@ -330,14 +366,18 @@ export default function Pool(): ReactElement {
style="primary"
size="small"
onClick={() => setShowAdd(true)}
disabled={isInPurgatory}
disabled={isInPurgatory || !isAssetNetwork}
>
Add Liquidity
</Button>
)}
{hasAddedLiquidity && !isRemoveDisabled && (
<Button size="small" onClick={() => setShowRemove(true)}>
<Button
size="small"
onClick={() => setShowRemove(true)}
disabled={!isAssetNetwork}
>
Remove
</Button>
)}

View File

@ -14,6 +14,7 @@ import { FormTradeData, initialValues } from '../../../../models/FormTrade'
import Decimal from 'decimal.js'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import { useAsset } from '../../../../providers/Asset'
const contentQuery = graphql`
query TradeQuery {
@ -49,6 +50,7 @@ export default function FormTrade({
const content = data.content.edges[0].node.childContentJson.trade
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { isAssetNetwork } = useAsset()
const { debug } = useUserPreferences()
const [txId, setTxId] = useState<string>()
@ -139,7 +141,7 @@ export default function FormTrade({
</div>
)}
<Actions
isDisabled={!isWarningAccepted}
isDisabled={!isWarningAccepted || !isAssetNetwork}
isLoading={isSubmitting}
loaderMessage="Swapping tokens..."
successMessage="Successfully swapped tokens."

View File

@ -6,8 +6,8 @@ import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
export default function Trade(): ReactElement {
const { accountId } = useWeb3()
const { ocean, balance } = useOcean()
const { accountId, balance } = useWeb3()
const { ocean } = useOcean()
const [tokenBalance, setTokenBalance] = useState<PoolBalance>()
const { price, ddo } = useAsset()
const [maxDt, setMaxDt] = useState(0)

View File

@ -11,13 +11,14 @@ import Trade from './Trade'
import { useAsset } from '../../../providers/Asset'
import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3'
import Web3Feedback from '../../molecules/Web3Feedback'
import { getFileInfo } from '../../../utils/provider'
import axios from 'axios'
export default function AssetActions(): ReactElement {
const { accountId } = useWeb3()
const { config, ocean, balance, account } = useOcean()
const { price, ddo } = useAsset()
const { accountId, balance } = useWeb3()
const { ocean, config, account } = useOcean()
const { price, ddo, isAssetNetwork } = useAsset()
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>()
@ -31,7 +32,7 @@ export default function AssetActions(): ReactElement {
useEffect(() => {
if (!ddo || !accountId) return
async function checkIsConsumable() {
const consumable = await ocean.assets.isConsumable(
const consumable: any = await ocean.assets.isConsumable(
ddo,
accountId.toLowerCase()
)
@ -72,6 +73,7 @@ export default function AssetActions(): ReactElement {
// Get and set user DT balance
useEffect(() => {
if (!ocean || !accountId) return
async function init() {
try {
const dtBalance = await ocean.datatokens.balance(
@ -84,7 +86,7 @@ export default function AssetActions(): ReactElement {
}
}
init()
}, [ocean, accountId, ddo.dataToken])
}, [ocean, accountId, ddo.dataToken, isAssetNetwork])
// Check user balance against price
useEffect(() => {
@ -141,8 +143,14 @@ export default function AssetActions(): ReactElement {
)
return (
<>
<Permission eventType="consume">
<Tabs items={tabs} className={styles.actions} />
</Permission>
<Web3Feedback
isBalanceSufficient={isBalanceSufficient}
isAssetNetwork={isAssetNetwork}
/>
</>
)
}

View File

@ -3,7 +3,6 @@ import styles from './AlgorithmDatasetsListForCompute.module.css'
import { getAlgorithmDatasetsForCompute } from '../../../utils/aquarius'
import { AssetSelectionAsset } from '../../molecules/FormFields/AssetSelection'
import AssetComputeList from '../../molecules/AssetComputeList'
import { useOcean } from '../../../providers/Ocean'
import { useAsset } from '../../../providers/Asset'
import { DDO } from '@oceanprotocol/lib'
@ -14,7 +13,6 @@ export default function AlgorithmDatasetsListForCompute({
algorithmDid: string
dataset: DDO
}): ReactElement {
const { config } = useOcean()
const { type } = useAsset()
const [datasetsForCompute, setDatasetsForCompute] =
useState<AssetSelectionAsset[]>()
@ -28,7 +26,7 @@ export default function AlgorithmDatasetsListForCompute({
const datasets = await getAlgorithmDatasetsForCompute(
algorithmDid,
datasetComputeService?.serviceEndpoint,
config.metadataCacheUri
dataset?.chainId
)
setDatasetsForCompute(datasets)
}

View File

@ -1,6 +1,6 @@
.bookmark {
position: absolute;
top: -10px;
top: -3px;
right: calc(var(--spacer) / 8);
appearance: none;
background: none;
@ -20,7 +20,7 @@
.bookmark:hover,
.bookmark:focus {
transform: translate3d(0, 6px, 0);
transform: translate3d(0, -3px, 0);
}
.bookmark.active svg {

View File

@ -2,15 +2,12 @@ import { useUserPreferences } from '../../../providers/UserPreferences'
import React, { ReactElement } from 'react'
import styles from './Bookmark.module.css'
import { ReactComponent as BookmarkIcon } from '../../../images/bookmark.svg'
import { ConfigHelperConfig } from '@oceanprotocol/lib'
import { useOcean } from '../../../providers/Ocean'
export default function Bookmark({ did }: { did: string }): ReactElement {
const { config } = useOcean()
const { bookmarks, addBookmark, removeBookmark } = useUserPreferences()
const isBookmarked =
bookmarks &&
bookmarks[(config as ConfigHelperConfig).network]?.includes(did)
const isBookmarked = bookmarks && bookmarks?.includes(did)
function handleBookmark() {
isBookmarked ? removeBookmark(did) : addBookmark(did)

View File

@ -3,7 +3,7 @@ import { useAsset } from '../../../providers/Asset'
import ExplorerLink from '../../atoms/ExplorerLink'
import Time from '../../atoms/Time'
import styles from './EditHistory.module.css'
import { gql, useQuery } from '@apollo/client'
import { gql, useQuery } from 'urql'
import { ReceiptData_datatokens_updates as ReceiptData } from '../../../@types/apollo/ReceiptData'
import { useWeb3 } from '../../../providers/Web3'
@ -22,9 +22,11 @@ const getReceipts = gql`
export default function EditHistory(): ReactElement {
const { networkId } = useWeb3()
const { ddo } = useAsset()
const { data } = useQuery(getReceipts, {
const [result] = useQuery({
query: getReceipts,
variables: { address: ddo?.dataToken.toLowerCase() }
})
const { data } = result
const [receipts, setReceipts] = useState<ReceiptData[]>()
const [creationTx, setCreationTx] = useState<string>()
@ -48,14 +50,14 @@ export default function EditHistory(): ReactElement {
<ul className={styles.history}>
{receipts?.map((receipt) => (
<li key={receipt.id} className={styles.item}>
<ExplorerLink networkId={networkId} path={`/tx/${receipt.tx}`}>
<ExplorerLink networkId={ddo.chainId} path={`/tx/${receipt.tx}`}>
edited{' '}
<Time date={receipt.timestamp.toString()} relative isUnix />
</ExplorerLink>
</li>
))}
<li className={styles.item}>
<ExplorerLink networkId={networkId} path={`/tx/${creationTx}`}>
<ExplorerLink networkId={ddo.chainId} path={`/tx/${creationTx}`}>
published <Time date={ddo.created} relative />
</ExplorerLink>
</li>

View File

@ -9,7 +9,7 @@ import AssetType from '../../atoms/AssetType'
import styles from './MetaMain.module.css'
export default function MetaMain(): ReactElement {
const { ddo, owner, type } = useAsset()
const { ddo, owner, type, isAssetNetwork } = useAsset()
const { networkId, web3ProviderInfo } = useWeb3()
const isCompute = Boolean(ddo?.findServiceByType('compute'))
const accessType = isCompute ? 'compute' : 'access'
@ -34,7 +34,7 @@ export default function MetaMain(): ReactElement {
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
</ExplorerLink>
{web3ProviderInfo?.name === 'MetaMask' && (
{web3ProviderInfo?.name === 'MetaMask' && isAssetNetwork && (
<span className={styles.addWrap}>
<AddToken
address={ddo?.dataTokenInfo.address}

View File

@ -23,8 +23,8 @@ export default function Dynamic({
ddo: DDO
content: any
}): ReactElement {
const { networkId } = useWeb3()
const { account, balance } = useOcean()
const { networkId, balance } = useWeb3()
const { account } = useOcean()
const [firstPrice, setFirstPrice] = useState<string>()
// Connect with form

View File

@ -1,8 +1,13 @@
.networkWrap {
display: block;
margin-top: -1rem;
}
.grid {
display: grid;
gap: calc(var(--spacer) * 1.5);
position: relative;
margin-top: -1.5rem;
margin-top: -1rem;
}
.grid > div {

View File

@ -19,6 +19,7 @@ import { useWeb3 } from '../../../providers/Web3'
import styles from './index.module.css'
import EditAdvancedSettings from '../AssetActions/Edit/EditAdvancedSettings'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import NetworkName from '../../atoms/NetworkName'
export interface AssetContentProps {
path?: string
@ -46,7 +47,7 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const content = data.purgatory.edges[0].node.childContentJson.asset
const { debug } = useUserPreferences()
const { accountId } = useWeb3()
const { owner, isInPurgatory, purgatoryData } = useAsset()
const { owner, isInPurgatory, purgatoryData, isAssetNetwork } = useAsset()
const [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>()
const [showEditCompute, setShowEditCompute] = useState<boolean>()
@ -87,6 +88,11 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
) : showEditAdvancedSettings ? (
<EditAdvancedSettings setShowEdit={setShowEditAdvancedSettings} />
) : (
<>
<div className={styles.networkWrap}>
<NetworkName networkId={ddo.chainId} className={styles.network} />
</div>
<article className={styles.grid}>
<div>
{showPricing && <Pricing ddo={ddo} />}
@ -110,9 +116,13 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
<MetaSecondary />
{isOwner && (
{isOwner && isAssetNetwork && (
<div className={styles.ownerActions}>
<Button style="text" size="small" onClick={handleEditButton}>
<Button
style="text"
size="small"
onClick={handleEditButton}
>
Edit Metadata
</Button>
{appConfig.allowAdvancedSettings === 'true' && (
@ -154,5 +164,6 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
<AssetActions />
</div>
</article>
</>
)
}

View File

@ -14,7 +14,8 @@ export default function Footer(): ReactElement {
return (
<footer className={styles.footer}>
<div className={styles.content}>
<SyncStatus /> | <BuildId />
{/* <SyncStatus /> | */}
<BuildId />
<MarketStats />
<div className={styles.copyright}>
© {year} <Markdown text={copyright} /> {' '}

View File

@ -10,6 +10,7 @@ import { retrieveDDO } from '../../../../utils/aquarius'
import { useOcean } from '../../../../providers/Ocean'
import Results from './Results'
import styles from './Details.module.css'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
function Asset({
title,
@ -41,7 +42,7 @@ function Asset({
}
function DetailsAssets({ job }: { job: ComputeJobMetaData }) {
const { config } = useOcean()
const { appConfig } = useSiteMetadata()
const [algoName, setAlgoName] = useState<string>()
const [algoDtSymbol, setAlgoDtSymbol] = useState<string>()
@ -49,18 +50,14 @@ function DetailsAssets({ job }: { job: ComputeJobMetaData }) {
async function getAlgoMetadata() {
const source = axios.CancelToken.source()
const ddo = await retrieveDDO(
job.algoDID,
config.metadataCacheUri,
source.token
)
const ddo = await retrieveDDO(job.algoDID, source.token)
setAlgoDtSymbol(ddo.dataTokenInfo.symbol)
const { attributes } = ddo.findServiceByType('metadata')
setAlgoName(attributes?.main.name)
}
getAlgoMetadata()
}, [config?.metadataCacheUri, job.algoDID])
}, [appConfig.metadataCacheUri, job.algoDID])
return (
<>

Some files were not shown because too many files have changed in this diff Show More