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

Merge branch 'main' into 'feature/enforce-docker-containers'

This commit is contained in:
Bogdan Fazakas 2022-09-27 16:54:39 +03:00
commit 9fbe671574
86 changed files with 20417 additions and 24743 deletions

View File

@ -53,7 +53,8 @@
"object": true, "object": true,
"array": false "array": false
} }
] ],
"testing-library/no-node-access": "off"
} }
} }
] ]

View File

@ -23,13 +23,13 @@ jobs:
node: ['16'] node: ['16']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v2 uses: actions/cache@v3
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
with: with:
@ -37,7 +37,7 @@ jobs:
key: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}- restore-keys: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-
- run: npm ci --legacy-peer-deps - run: npm ci
- run: npm run build - run: npm run build
test: test:
@ -50,13 +50,13 @@ jobs:
node: ['16'] node: ['16']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v2 uses: actions/cache@v3
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
with: with:
@ -64,11 +64,11 @@ jobs:
key: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}- restore-keys: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-
- run: npm ci --legacy-peer-deps - run: npm ci
- run: npm test - run: npm test
- name: Upload coverage artifact - name: Upload coverage artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: coverage-${{ runner.os }} name: coverage-${{ runner.os }}
path: coverage/ path: coverage/
@ -79,12 +79,12 @@ jobs:
if: ${{ success() && github.actor != 'dependabot[bot]' }} if: ${{ success() && github.actor != 'dependabot[bot]' }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
node-version: '16' node-version: '16'
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v2 uses: actions/cache@v3
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
with: with:
@ -92,11 +92,11 @@ jobs:
key: ${{ runner.os }}-coverage-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-coverage-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-coverage-${{ env.cache-name }}- restore-keys: ${{ runner.os }}-coverage-${{ env.cache-name }}-
- uses: actions/download-artifact@v2 - uses: actions/download-artifact@v3
with: with:
name: coverage-${{ runner.os }} name: coverage-${{ runner.os }}
- run: npm ci --legacy-peer-deps - run: npm ci
- run: npm run codegen:apollo - run: npm run codegen:apollo
- uses: paambaati/codeclimate-action@v3.0.0 - uses: paambaati/codeclimate-action@v3.0.0
@ -113,13 +113,13 @@ jobs:
node: ['16'] node: ['16']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v2 uses: actions/cache@v3
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
with: with:
@ -127,6 +127,6 @@ jobs:
key: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}- restore-keys: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-
- run: npm ci --legacy-peer-deps - run: npm ci
- run: npm run pregenerate - run: npm run pregenerate
- run: npm run storybook:build - run: npm run storybook:build

View File

@ -35,11 +35,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -64,4 +64,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

View File

@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
- run: npm ci --legacy-peer-deps - run: npm ci
- run: npm run build:static - run: npm run build:static
env: env:

View File

@ -14,7 +14,7 @@ const customJestConfig = {
moduleDirectories: ['node_modules', '<rootDir>/src'], moduleDirectories: ['node_modules', '<rootDir>/src'],
testEnvironment: 'jest-environment-jsdom', testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: { moduleNameMapper: {
'\\.svg': '<rootDir>/.jest/__mocks__/svgrMock.tsx', '^.+\\.(svg)$': '<rootDir>/.jest/__mocks__/svgrMock.tsx',
// '^@/components/(.*)$': '<rootDir>/components/$1', // '^@/components/(.*)$': '<rootDir>/components/$1',
'@shared(.*)$': '<rootDir>/src/components/@shared/$1', '@shared(.*)$': '<rootDir>/src/components/@shared/$1',
'@hooks/(.*)$': '<rootDir>/src/@hooks/$1', '@hooks/(.*)$': '<rootDir>/src/@hooks/$1',

12
.jest/testRender.ts Normal file
View File

@ -0,0 +1,12 @@
import { render } from '@testing-library/react'
import { ReactElement } from 'react'
const testRender = (component: ReactElement): void => {
it('renders without crashing', () => {
const { container } = render(component)
expect(container.firstChild).toBeInTheDocument()
})
}
export default testRender

View File

@ -1,14 +0,0 @@
import initStoryshots from '@storybook/addon-storyshots'
import { render, waitFor } from '@testing-library/react'
// Stories are render-tested with @testing-library/react,
// overwriting default snapshot testing behavior
initStoryshots({
asyncJest: true,
test: async ({ story, done }) => {
const storyElement = story.render()
// render the story with @testing-library/react
render(storyElement)
await waitFor(() => done())
}
})

View File

@ -16,7 +16,7 @@
- [🦀 Data Sources](#-data-sources) - [🦀 Data Sources](#-data-sources)
- [Aquarius](#aquarius) - [Aquarius](#aquarius)
- [Ocean Protocol Subgraph](#ocean-protocol-subgraph) - [Ocean Protocol Subgraph](#ocean-protocol-subgraph)
- [3Box](#3box) - [ENS](#ens)
- [Purgatory](#purgatory) - [Purgatory](#purgatory)
- [Network Metadata](#network-metadata) - [Network Metadata](#network-metadata)
- [👩‍🎤 Storybook](#-storybook) - [👩‍🎤 Storybook](#-storybook)
@ -194,37 +194,21 @@ function Component() {
} }
``` ```
### 3Box ### ENS
Publishers can create a profile on [3Box Hub](https://www.3box.io/hub) and when found, it will be displayed in the app. Publishers can fill their account's [ENS domain](https://ens.domains) profile and when found, it will be displayed in the app.
For this our own [3box-proxy](https://github.com/oceanprotocol/3box-proxy) is used, within the app the utility method `get3BoxProfile()` can be used to get all info: For this our own [ens-proxy](https://github.com/oceanprotocol/ens-proxy) is used, within the app the utility method `getEnsProfile()` is called as part of the `useProfile()` hook:
```tsx ```tsx
import get3BoxProfile from '@utils/profile' import { useProfile } from '@context/Profile'
function Component() { function Component() {
const [profile, setProfile] = useState<Profile>() const { profile } = useProfile()
useEffect(() => {
if (!account) return
const source = axios.CancelToken.source()
async function get3Box() {
const profile = await get3BoxProfile(account, source.token)
if (!profile) return
setProfile(profile)
}
get3Box()
return () => {
source.cancel()
}
}, [account])
return ( return (
<div> <div>
{profile.emoji} {profile.name} {profile.avatar} {profile.name}
</div> </div>
) )
} }

View File

@ -66,7 +66,7 @@ module.exports = {
// Refers to Coingecko API tokenIds. // Refers to Coingecko API tokenIds.
coingeckoTokenIds: ['ocean-protocol', 'h2o', 'ethereum', 'matic-network'], coingeckoTokenIds: ['ocean-protocol', 'h2o', 'ethereum', 'matic-network'],
// Config for https://github.com/donavon/use-dark-mode // Config for https://github.com/oceanprotocol/use-dark-mode
darkModeConfig: { darkModeConfig: {
classNameDark: 'dark', classNameDark: 'dark',
classNameLight: 'light', classNameLight: 'light',

43562
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write", "format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"deploy:s3": "bash scripts/deploy-s3.sh", "deploy:s3": "bash scripts/deploy-s3.sh",
"postinstall": "husky install", "postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql",
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.ropsten.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/", "codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.ropsten.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet", "storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
"storybook:build": "cross-env NODE_ENV=test build-storybook" "storybook:build": "cross-env NODE_ENV=test build-storybook"
@ -28,25 +28,25 @@
"@oceanprotocol/art": "^3.2.0", "@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^2.0.2", "@oceanprotocol/lib": "^2.0.2",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@oceanprotocol/use-dark-mode": "^2.4.3",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"@urql/exchange-refocus": "^0.2.5", "@urql/exchange-refocus": "^1.0.0",
"@walletconnect/web3-provider": "^1.7.8", "@walletconnect/web3-provider": "^1.8.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"classnames": "^2.3.1", "classnames": "^2.3.2",
"date-fns": "^2.29.1", "date-fns": "^2.29.3",
"decimal.js": "^10.3.1", "decimal.js": "^10.3.1",
"dom-confetti": "^0.2.2", "dom-confetti": "^0.2.2",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"filesize": "^9.0.1", "filesize": "^9.0.11",
"formik": "^2.2.9", "formik": "^2.2.9",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-url-superb": "^6.1.0", "is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"myetherwallet-blockies": "^0.1.1", "myetherwallet-blockies": "^0.1.1",
"next": "^12.1.6", "next": "12.3.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-clipboard.js": "^2.0.16", "react-clipboard.js": "^2.0.16",
@ -64,58 +64,52 @@
"remove-markdown": "^0.5.0", "remove-markdown": "^0.5.0",
"slugify": "^1.6.5", "slugify": "^1.6.5",
"swr": "^1.3.0", "swr": "^1.3.0",
"urql": "^2.2.1", "urql": "^3.0.3",
"use-dark-mode": "^2.3.1", "web3": "^1.8.0",
"web3": "^1.7.4", "web3modal": "^1.9.9",
"web3modal": "^1.9.8",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^6.5.7", "@storybook/addon-essentials": "^6.5.12",
"@storybook/addon-storyshots": "^6.5.9", "@storybook/builder-webpack5": "^6.5.12",
"@storybook/builder-webpack5": "^6.5.9", "@storybook/manager-webpack5": "^6.5.12",
"@storybook/manager-webpack5": "^6.5.7", "@storybook/react": "^6.5.12",
"@storybook/react": "^6.5.7", "@svgr/webpack": "^6.3.1",
"@storybook/testing-library": "^0.0.11", "@testing-library/jest-dom": "^5.16.5",
"@storybook/testing-react": "^1.3.0", "@testing-library/react": "^13.4.0",
"@svgr/webpack": "^6.2.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@types/loadable__component": "^5.13.4", "@types/loadable__component": "^5.13.4",
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/lodash.omit": "^4.5.7", "@types/lodash.omit": "^4.5.7",
"@types/node": "^17.0.41", "@types/node": "^18.7.18",
"@types/react": "^18.0.14", "@types/react": "^18.0.14",
"@types/react-dom": "^18.0.5", "@types/react-dom": "^18.0.5",
"@types/react-modal": "^3.13.1", "@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1", "@types/react-paginate": "^7.1.1",
"@types/remove-markdown": "^0.3.1", "@types/remove-markdown": "^0.3.1",
"@types/yup": "^0.29.14", "@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.38.0",
"@typescript-eslint/parser": "^5.27.1",
"apollo": "^2.34.0", "apollo": "^2.34.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.21.0", "eslint": "^8.23.1",
"eslint-config-oceanprotocol": "^2.0.3", "eslint-config-oceanprotocol": "^2.0.3",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.2", "eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.0", "eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.5.1", "eslint-plugin-testing-library": "^5.6.4",
"file-loader": "^6.2.0",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"husky": "^8.0.1", "husky": "^8.0.1",
"jest": "^28.1.2", "jest": "^29.0.3",
"jest-environment-jsdom": "^28.1.2", "jest-environment-jsdom": "^29.0.3",
"prettier": "^2.6.2", "prettier": "^2.7.1",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"process": "^0.11.10", "process": "^0.11.10",
"serve": "^13.0.2", "serve": "^14.0.1",
"stream-http": "^3.2.0", "stream-http": "^3.2.0",
"tsconfig-paths-webpack-plugin": "^3.5.2", "tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.7.3" "typescript": "^4.8.3"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -7,15 +7,18 @@ import React, {
useCallback, useCallback,
ReactNode ReactNode
} from 'react' } from 'react'
import { getUserSales, getUserTokenOrders } from '@utils/subgraph' import { getUserTokenOrders } from '@utils/subgraph'
import { useUserPreferences } from './UserPreferences' import { useUserPreferences } from '../UserPreferences'
import { Asset, LoggerInstance } from '@oceanprotocol/lib' import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { getDownloadAssets, getPublishedAssets } from '@utils/aquarius' import {
import { accountTruncate } from '@utils/web3' getDownloadAssets,
getPublishedAssets,
getUserSales
} from '@utils/aquarius'
import axios, { CancelToken } from 'axios' import axios, { CancelToken } from 'axios'
import get3BoxProfile from '@utils/profile'
import web3 from 'web3' import web3 from 'web3'
import { useMarketMetadata } from './MarketMetadata' import { useMarketMetadata } from '../MarketMetadata'
import { getEnsProfile } from '@utils/ens'
interface ProfileProviderValue { interface ProfileProviderValue {
profile: Profile profile: Profile
@ -32,6 +35,14 @@ const ProfileContext = createContext({} as ProfileProviderValue)
const refreshInterval = 10000 // 10 sec. const refreshInterval = 10000 // 10 sec.
const clearedProfile: Profile = {
name: null,
avatar: null,
url: null,
description: null,
links: null
}
function ProfileProvider({ function ProfileProvider({
accountId, accountId,
accountEns, accountEns,
@ -56,9 +67,9 @@ function ProfileProvider({
}, [accountId]) }, [accountId])
// //
// User profile: ENS + 3Box // User profile: ENS
// //
const [profile, setProfile] = useState<Profile>() const [profile, setProfile] = useState<Profile>({ name: accountEns })
useEffect(() => { useEffect(() => {
if (!accountEns) return if (!accountEns) return
@ -66,53 +77,22 @@ function ProfileProvider({
}, [accountId, accountEns]) }, [accountId, accountEns])
useEffect(() => { useEffect(() => {
const clearedProfile: Profile = { if (
name: null, !accountId ||
accountEns: null, accountId === '0x0000000000000000000000000000000000000000' ||
image: null, !isEthAddress
description: null, ) {
links: null
}
if (!accountId || !isEthAddress) {
setProfile(clearedProfile) setProfile(clearedProfile)
return return
} }
const cancelTokenSource = axios.CancelToken.source()
async function getInfo() { async function getInfo() {
setProfile({ name: accountEns || accountTruncate(accountId), accountEns }) const profile = await getEnsProfile(accountId)
setProfile(profile)
const profile3Box = await get3BoxProfile( LoggerInstance.log(`[profile] ENS metadata for ${accountId}:`, profile)
accountId,
cancelTokenSource.token
)
if (profile3Box) {
const { name, emoji, description, image, links } = profile3Box
const newName = `${emoji || ''} ${name || accountTruncate(accountId)}`
const newProfile = {
name: newName,
image,
description,
links
}
setProfile((prevState) => ({
...prevState,
...newProfile
}))
LoggerInstance.log('[profile] Found and set 3box profile.', newProfile)
} else {
// setProfile(clearedProfile)
LoggerInstance.log('[profile] No 3box profile found.')
}
} }
getInfo() getInfo()
}, [accountId, isEthAddress])
return () => {
cancelTokenSource.cancel()
}
}, [accountId, accountEns, isEthAddress])
// //
// PUBLISHED ASSETS // PUBLISHED ASSETS

View File

@ -13,7 +13,7 @@ import { infuraProjectId as infuraId } from '../../app.config'
import WalletConnectProvider from '@walletconnect/web3-provider' import WalletConnectProvider from '@walletconnect/web3-provider'
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import { isBrowser } from '@utils/index' import { isBrowser } from '@utils/index'
import { getEnsName } from '@utils/ens' import { getEnsProfile } from '@utils/ens'
import useNetworkMetadata, { import useNetworkMetadata, {
getNetworkDataById, getNetworkDataById,
getNetworkDisplayName, getNetworkDisplayName,
@ -32,6 +32,7 @@ interface Web3ProviderValue {
web3ProviderInfo: IProviderInfo web3ProviderInfo: IProviderInfo
accountId: string accountId: string
accountEns: string accountEns: string
accountEnsAvatar: string
balance: UserBalance balance: UserBalance
networkId: number networkId: number
chainId: number chainId: number
@ -54,8 +55,6 @@ const web3ModalTheme = {
hover: 'var(--background-highlight)' hover: 'var(--background-highlight)'
} }
// HEADS UP! We inline-require some packages so the SSR build does not break.
// We only need them client-side.
const providerOptions = isBrowser const providerOptions = isBrowser
? { ? {
walletconnect: { walletconnect: {
@ -99,6 +98,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
const [isTestnet, setIsTestnet] = useState<boolean>() const [isTestnet, setIsTestnet] = useState<boolean>()
const [accountId, setAccountId] = useState<string>() const [accountId, setAccountId] = useState<string>()
const [accountEns, setAccountEns] = useState<string>() const [accountEns, setAccountEns] = useState<string>()
const [accountEnsAvatar, setAccountEnsAvatar] = useState<string>()
const [web3Loading, setWeb3Loading] = useState<boolean>(true) const [web3Loading, setWeb3Loading] = useState<boolean>(true)
const [balance, setBalance] = useState<UserBalance>({ const [balance, setBalance] = useState<UserBalance>({
eth: '0' eth: '0'
@ -192,24 +192,35 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
}, [accountId, approvedBaseTokens, networkId, web3, networkData]) }, [accountId, approvedBaseTokens, networkId, web3, networkData])
// ----------------------------------- // -----------------------------------
// Helper: Get user ENS name // Helper: Get user ENS info
// ----------------------------------- // -----------------------------------
const getUserEnsName = useCallback(async () => { const getUserEns = useCallback(async () => {
if (!accountId) return if (!accountId) return
try { try {
// const accountEns = await getEnsNameWithWeb3( const profile = await getEnsProfile(accountId)
// accountId,
// web3Provider, if (!profile) {
// `${networkId}` setAccountEns(null)
// ) setAccountEnsAvatar(null)
const accountEns = await getEnsName(accountId) return
setAccountEns(accountEns) }
accountEns &&
setAccountEns(profile.name)
LoggerInstance.log(
`[web3] ENS name found for ${accountId}:`,
profile.name
)
if (profile.avatar) {
setAccountEnsAvatar(profile.avatar)
LoggerInstance.log( LoggerInstance.log(
`[web3] ENS name found for ${accountId}:`, `[web3] ENS avatar found for ${accountId}:`,
accountEns profile.avatar
) )
} else {
setAccountEnsAvatar(null)
}
} catch (error) { } catch (error) {
LoggerInstance.error('[web3] Error: ', error.message) LoggerInstance.error('[web3] Error: ', error.message)
} }
@ -275,11 +286,11 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
}, [getUserBalance]) }, [getUserBalance])
// ----------------------------------- // -----------------------------------
// Get and set user ENS name // Get and set user ENS info
// ----------------------------------- // -----------------------------------
useEffect(() => { useEffect(() => {
getUserEnsName() getUserEns()
}, [getUserEnsName]) }, [getUserEns])
// ----------------------------------- // -----------------------------------
// Get and set network metadata // Get and set network metadata
@ -337,7 +348,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
// ----------------------------------- // -----------------------------------
async function logout() { async function logout() {
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
if (web3 && web3.currentProvider && (web3.currentProvider as any).close) { if ((web3?.currentProvider as any)?.close) {
await (web3.currentProvider as any).close() await (web3.currentProvider as any).close()
} }
/* eslint-enable @typescript-eslint/no-explicit-any */ /* eslint-enable @typescript-eslint/no-explicit-any */
@ -402,6 +413,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
web3ProviderInfo, web3ProviderInfo,
accountId, accountId,
accountEns, accountEns,
accountEnsAvatar,
balance, balance,
networkId, networkId,
chainId, chainId,

View File

@ -1,36 +1,12 @@
interface ProfileLink { interface ProfileLink {
name: string key: string
value: string value: string
} }
interface Profile { interface Profile {
did?: string name: string
name?: string url?: string
accountEns?: string avatar?: string
description?: string description?: string
emoji?: string
image?: string
links?: ProfileLink[] links?: ProfileLink[]
} }
interface ResponseData3Box {
name: string
description: string
website: string
status?: 'error'
/* eslint-disable camelcase */
proof_did: string
proof_twitter: string
proof_github: string
/* eslint-enable camelcase */
emoji: string
job: string
employer: string
location: string
memberSince: string
image: {
contentUrl: {
[key: string]: string
}
}[]
}

View File

@ -1,4 +0,0 @@
interface AccountTeaserVM {
address: string
nrSales: number
}

View File

@ -9,6 +9,11 @@ import {
} from '../@types/aquarius/SearchQuery' } from '../@types/aquarius/SearchQuery'
import { transformAssetToAssetSelection } from './assetConvertor' import { transformAssetToAssetSelection } from './assetConvertor'
export interface UserSales {
id: string
totalSales: number
}
export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476 export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
export function escapeEsReservedCharacters(value: string): string { export function escapeEsReservedCharacters(value: string): string {
@ -397,6 +402,40 @@ export async function getTopPublishers(
} }
} }
export async function getTopAssetsPublishers(
chainIds: number[],
nrItems = 9
): Promise<UserSales[]> {
const publishers: UserSales[] = []
const result = await getTopPublishers(chainIds, null)
const { topPublishers } = result.aggregations
for (let i = 0; i < topPublishers.buckets.length; i++) {
publishers.push({
id: topPublishers.buckets[i].key,
totalSales: parseInt(topPublishers.buckets[i].totalSales.value)
})
}
publishers.sort((a, b) => b.totalSales - a.totalSales)
return publishers.slice(0, nrItems)
}
export async function getUserSales(
accountId: string,
chainIds: number[]
): Promise<number> {
try {
const result = await getPublishedAssets(accountId, chainIds, null)
const { totalOrders } = result.aggregations
return totalOrders.value
} catch (error) {
LoggerInstance.error('Error getUserSales', error.message)
}
}
export async function getDownloadAssets( export async function getDownloadAssets(
dtList: string[], dtList: string[],
tokenOrders: OrdersData[], tokenOrders: OrdersData[],

64
src/@utils/ens.test.ts Normal file
View File

@ -0,0 +1,64 @@
import { getEnsName, getEnsAddress, getEnsProfile } from './ens'
describe('@utils/ens', () => {
jest.setTimeout(10000)
jest.retryTimes(2)
test('getEnsName', async () => {
const ensName = await getEnsName(
'0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0'
)
expect(ensName).toBe('jellymcjellyfish.eth')
})
test('getEnsName with invalid address', async () => {
const ensName = await getEnsName('0x123')
expect(ensName).toBeUndefined()
})
test('getEnsName with empty address', async () => {
const ensName = await getEnsName('')
expect(ensName).toBeUndefined()
})
test('getEnsName with undefined address', async () => {
const ensName = await getEnsName(undefined)
expect(ensName).toBeUndefined()
})
test('getEnsAddress', async () => {
const ensAddress = await getEnsAddress('jellymcjellyfish.eth')
expect(ensAddress).toBe('0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0')
})
test('getEnsAddress with invalid address', async () => {
const ensAddress = await getEnsAddress('0x123')
expect(ensAddress).toBeUndefined()
})
test('getEnsAddress with empty address', async () => {
const ensAddress = await getEnsAddress('')
expect(ensAddress).toBeUndefined()
})
test('getEnsProfile', async () => {
const ensProfile = await getEnsProfile(
'0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0'
)
expect(ensProfile).toEqual({
avatar:
'https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth',
links: [
{ key: 'url', value: 'https://oceanprotocol.com' },
{ key: 'com.twitter', value: 'oceanprotocol' },
{ key: 'com.github', value: 'oceanprotocol' }
],
name: 'jellymcjellyfish.eth'
})
})
test('getEnsProfile with empty address', async () => {
const ensProfile = await getEnsProfile('')
expect(ensProfile).toBeUndefined()
})
})

View File

@ -1,52 +1,24 @@
import { gql, OperationContext, OperationResult } from 'urql' import { fetchData } from './fetch'
import { fetchData } from './subgraph'
// make sure to only query for domains owned by account, so domains const apiUrl = 'https://ens-proxy.oceanprotocol.com/api'
// solely set by 3rd parties like *.gitcoin.eth won't show up
const UserEnsNames = gql<any>`
query UserEnsDomains($accountId: String!) {
domains(where: { resolvedAddress: $accountId, owner: $accountId }) {
name
}
}
`
const UserEnsAddress = gql<any>`
query UserEnsDomainsAddress($name: String!) {
domains(where: { name: $name }) {
resolvedAddress {
id
}
}
}
`
const ensSubgraphQueryContext: OperationContext = {
url: `https://api.thegraph.com/subgraphs/name/ensdomains/ens`,
requestPolicy: 'cache-and-network'
}
export async function getEnsName(accountId: string): Promise<string> { export async function getEnsName(accountId: string): Promise<string> {
const response: OperationResult<any> = await fetchData( if (!accountId || accountId === '') return
UserEnsNames,
{ accountId: accountId.toLowerCase() },
ensSubgraphQueryContext
)
if (!response?.data?.domains?.length) return
// Default order of response.data.domains seems to be by creation time, from oldest to newest. const data = await fetchData(`${apiUrl}/name?accountId=${accountId}`)
// Pick the last one as that is what direct web3 calls do. return data?.name
const { name } = response.data.domains.slice(-1)[0]
return name
} }
export async function getEnsAddress(ensName: string): Promise<string> { export async function getEnsAddress(accountId: string): Promise<string> {
const response: OperationResult<any> = await fetchData( if (!accountId || accountId === '' || !accountId.includes('.')) return
UserEnsAddress,
{ name: ensName }, const data = await fetchData(`${apiUrl}/address?name=${accountId}`)
ensSubgraphQueryContext return data?.address
) }
if (!response?.data?.domains?.length) return
const { id } = response.data.domains[0].resolvedAddress export async function getEnsProfile(accountId: string): Promise<Profile> {
return id if (!accountId || accountId === '') return
const data = await fetchData(`${apiUrl}/profile?address=${accountId}`)
return data?.profile
} }

View File

@ -1,15 +1,24 @@
import { LoggerInstance } from '@oceanprotocol/lib'
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
export async function fetchData(url: string): Promise<AxiosResponse['data']> { export async function fetchData(url: string): Promise<AxiosResponse['data']> {
try { try {
const response = await axios(url) const response = await axios(url)
return response?.data
if (response.status !== 200) {
return console.error('Non-200 response: ' + response.status)
}
return response.data
} catch (error) { } catch (error) {
console.error('Error parsing json: ' + error.message) if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
LoggerInstance.error(`Non-200 response from ${url}:`, error.response)
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
LoggerInstance.error('No response with:', error.request)
} else {
// Something happened in setting up the request that triggered an Error
LoggerInstance.error('Error in setting up request:', error.message)
}
LoggerInstance.error(error.message)
} }
} }

View File

@ -1,14 +1,5 @@
import { Decimal } from 'decimal.js' import { Decimal } from 'decimal.js'
export function isValidNumber(value: any): boolean {
const isUndefinedValue = typeof value === 'undefined'
const isNullValue = value === null
const isNaNValue = isNaN(Number(value))
const isEmptyString = value === ''
return !isUndefinedValue && !isNullValue && !isNaNValue && !isEmptyString
}
// Run decimal.js comparison // Run decimal.js comparison
// http://mikemcl.github.io/decimal.js/#cmp // http://mikemcl.github.io/decimal.js/#cmp
export function compareAsBN(balance: string, price: string): boolean { export function compareAsBN(balance: string, price: string): boolean {

View File

@ -1,15 +0,0 @@
export function getBuyDTFeedback(dtSymbol: string): { [key: number]: string } {
return {
1: '1/3 Approving OCEAN ...',
2: `2/3 Buying ${dtSymbol} ...`,
3: `3/3 ${dtSymbol} bought.`
}
}
export function getSellDTFeedback(dtSymbol: string): { [key: number]: string } {
return {
1: '1/3 Approving OCEAN ...',
2: `2/3 Selling ${dtSymbol} ...`,
3: `3/3 ${dtSymbol} sold.`
}
}

View File

@ -1,94 +0,0 @@
import axios, { AxiosResponse, CancelToken } from 'axios'
import jwtDecode from 'jwt-decode'
// https://docs.3box.io/api/rest-api
const apiUri = 'https://3box.oceanprotocol.com'
const ipfsUrl = 'https://infura-ipfs.io'
function decodeProof(proofJWT: string) {
if (!proofJWT) return
const proof = jwtDecode(proofJWT) as any
return proof
}
function getLinks(
website: string,
twitterProof: string,
githubProof: string
): ProfileLink[] {
// Conditionally add links if they exist
const links = [
...(website ? [{ name: 'Website', value: website }] : []),
...(twitterProof
? [
{
name: 'Twitter',
value: decodeProof(twitterProof).claim.twitter_handle
}
]
: []),
...(githubProof
? [{ name: 'GitHub', value: githubProof.split('/')[3] }]
: [])
]
return links
}
function transformResponse({
name,
description,
website,
emoji,
image,
/* eslint-disable camelcase */
proof_twitter,
proof_github,
proof_did
}: ResponseData3Box) {
/* eslint-enable camelcase */
const links = getLinks(website, proof_twitter, proof_github)
const profile: Profile = {
did: decodeProof(proof_did).iss,
// Conditionally add profile items if they exist
...(name && { name }),
...(description && { description }),
...(emoji && { emoji }),
...(image && {
image: `${ipfsUrl}/ipfs/${
image.map(
(img: { contentUrl: { [key: string]: string } }) =>
img.contentUrl['/']
)[0]
}`
}),
...(links.length && { links })
}
return profile
}
export default async function get3BoxProfile(
accountId: string,
cancelToken: CancelToken
): Promise<Profile> {
try {
const response = (await axios(`${apiUri}/profile/${accountId}`, {
cancelToken
})) as AxiosResponse<ResponseData3Box>
if (
!response ||
!response.data ||
response.status !== 200 ||
response.data.status === 'error'
)
return
// LoggerInstance.log(`3Box profile found for ${accountId}`, response.data)
const profile = transformResponse(response.data)
return profile
// eslint-disable-next-line no-empty
} catch (error) {}
}

View File

@ -1,4 +1,3 @@
import { Purgatory } from '@oceanprotocol/lib'
import { fetchData } from './fetch' import { fetchData } from './fetch'
const purgatoryUrl = 'https://market-purgatory.oceanprotocol.com/api/' const purgatoryUrl = 'https://market-purgatory.oceanprotocol.com/api/'

View File

@ -6,16 +6,6 @@ import { AssetPreviousOrder } from '../@types/subgraph/AssetPreviousOrder'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData' import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { OpcFeesQuery as OpcFeesData } from '../@types/subgraph/OpcFeesQuery' import { OpcFeesQuery as OpcFeesData } from '../@types/subgraph/OpcFeesQuery'
import { getPublishedAssets, getTopPublishers } from '@utils/aquarius'
export interface UserLiquidity {
price: string
oceanBalance: string
}
export interface PriceList {
[key: string]: string
}
const PreviousOrderQuery = gql` const PreviousOrderQuery = gql`
query AssetPreviousOrder($id: String!, $account: String!) { query AssetPreviousOrder($id: String!, $account: String!) {
orders( orders(
@ -153,29 +143,6 @@ export async function getOpcFees(chainId: number) {
return opcFees return opcFees
} }
export async function getPreviousOrders(
id: string,
account: string,
assetTimeout: string
): Promise<string> {
const variables = { id, account }
const fetchedPreviousOrders: OperationResult<AssetPreviousOrder> =
await fetchData(PreviousOrderQuery, variables, null)
if (fetchedPreviousOrders.data?.orders?.length === 0) return null
if (assetTimeout === '0') {
return fetchedPreviousOrders?.data?.orders[0]?.tx
} else {
const expiry =
fetchedPreviousOrders?.data?.orders[0]?.createdTimestamp * 1000 +
Number(assetTimeout) * 1000
if (Date.now() <= expiry) {
return fetchedPreviousOrders?.data?.orders[0]?.tx
} else {
return null
}
}
}
export async function getUserTokenOrders( export async function getUserTokenOrders(
accountId: string, accountId: string,
chainIds: number[] chainIds: number[]
@ -201,40 +168,6 @@ export async function getUserTokenOrders(
} }
} }
export async function getUserSales(
accountId: string,
chainIds: number[]
): Promise<number> {
try {
const result = await getPublishedAssets(accountId, chainIds, null)
const { totalOrders } = result.aggregations
return totalOrders.value
} catch (error) {
LoggerInstance.error('Error getUserSales', error.message)
}
}
export async function getTopAssetsPublishers(
chainIds: number[],
nrItems = 9
): Promise<AccountTeaserVM[]> {
const publishers: AccountTeaserVM[] = []
const result = await getTopPublishers(chainIds, null)
const { topPublishers } = result.aggregations
for (let i = 0; i < topPublishers.buckets.length; i++) {
publishers.push({
address: topPublishers.buckets[i].key,
nrSales: parseInt(topPublishers.buckets[i].totalSales.value)
})
}
publishers.sort((a, b) => b.nrSales - a.nrSales)
return publishers.slice(0, nrItems)
}
export async function getOpcsApprovedTokens( export async function getOpcsApprovedTokens(
chainId: number chainId: number
): Promise<TokenInfo[]> { ): Promise<TokenInfo[]> {

9
src/@utils/url.test.ts Normal file
View File

@ -0,0 +1,9 @@
import { sanitizeUrl } from './url'
describe('@utils/url', () => {
test('sanitizeUrl', () => {
expect(sanitizeUrl('http://example.com')).toBe('http://example.com')
expect(sanitizeUrl('https://example.com')).toBe('https://example.com')
expect(sanitizeUrl('ftp://example.com')).toBe('about:blank')
})
})

View File

@ -1,10 +1,5 @@
export function sanitizeUrl(url: string) { export function sanitizeUrl(url: string) {
const u = decodeURI(url).trim().toLowerCase() const u = decodeURI(url).trim().toLowerCase()
if ( const isAllowedUrlScheme = u.startsWith('http://') || u.startsWith('https://')
u.startsWith('javascript:') || return isAllowedUrlScheme ? url : 'about:blank'
u.startsWith('data:') ||
u.startsWith('vbscript:')
)
return 'about:blank'
return url
} }

View File

@ -1,57 +0,0 @@
import React, { ReactElement } from 'react'
import styles from './index.module.css'
import classNames from 'classnames/bind'
import Loader from '../atoms/Loader'
import { useUserPreferences } from '@context/UserPreferences'
import AccountTeaser from '@shared/AccountTeaser/AccountTeaser'
const cx = classNames.bind(styles)
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
<Loader />
</div>
)
}
declare type AccountListProps = {
accounts: AccountTeaserVM[]
isLoading: boolean
className?: string
}
export default function AccountList({
accounts,
isLoading,
className
}: AccountListProps): ReactElement {
const { chainIds } = useUserPreferences()
const styleClasses = cx({
accountList: true,
[className]: className
})
return accounts && (isLoading === undefined || isLoading === false) ? (
<>
<div className={styleClasses}>
{accounts.length > 0 ? (
accounts.map((account, index) => (
<AccountTeaser
accountTeaserVM={account}
key={index + 1}
place={index + 1}
/>
))
) : chainIds.length === 0 ? (
<div className={styles.empty}>No network selected.</div>
) : (
<div className={styles.empty}>No results found.</div>
)}
</div>
</>
) : (
<LoaderArea />
)
}

View File

@ -1,58 +0,0 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Dotdotdot from 'react-dotdotdot'
import Link from 'next/link'
import styles from './AccountTeaser.module.css'
import Blockies from '../atoms/Blockies'
import { useCancelToken } from '@hooks/useCancelToken'
import get3BoxProfile from '@utils/profile'
import { accountTruncate } from '@utils/web3'
declare type AccountTeaserProps = {
accountTeaserVM: AccountTeaserVM
place?: number
}
export default function AccountTeaser({
accountTeaserVM,
place
}: AccountTeaserProps): ReactElement {
const [profile, setProfile] = useState<Profile>()
const newCancelToken = useCancelToken()
useEffect(() => {
if (!accountTeaserVM) return
async function getProfileData() {
const profile = await get3BoxProfile(
accountTeaserVM.address,
newCancelToken()
)
if (!profile) return
setProfile(profile)
}
getProfileData()
}, [accountTeaserVM, newCancelToken])
return (
<Link href={`/profile/${accountTeaserVM.address}`}>
<a className={styles.teaser}>
{place && <span className={styles.place}>{place}</span>}
<Blockies
accountId={accountTeaserVM.address}
className={styles.blockies}
image={profile?.image}
/>
<div>
<Dotdotdot tagName="h4" clamp={2} className={styles.name}>
{profile?.name
? profile?.name
: accountTruncate(accountTeaserVM.address)}
</Dotdotdot>
<p className={styles.sales}>
<span>{accountTeaserVM.nrSales}</span>
{`${accountTeaserVM.nrSales === 1 ? ' sale' : ' sales'}`}
</p>
</div>
</a>
</Link>
)
}

View File

@ -37,7 +37,7 @@ export default function AssetComputeSelection({
</Dotdotdot> </Dotdotdot>
</div> </div>
<PriceUnit <PriceUnit
price={asset.price} price={Number(asset.price)}
size="small" size="small"
className={styles.price} className={styles.price}
/> />

View File

@ -29,11 +29,13 @@ export default function AssetType({
{type === 'dataset' ? 'dataset' : 'algorithm'} {type === 'dataset' ? 'dataset' : 'algorithm'}
</div> </div>
{totalSales ? ( {(totalSales || totalSales === 0) && (
<div className={styles.typeLabel}> <div className={styles.typeLabel}>
{`${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`} {totalSales < 0
? 'N/A'
: `${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
</div> </div>
) : null} )}
</div> </div>
) )
} }

View File

@ -108,7 +108,7 @@ export default function AssetSelection({
</label> </label>
<PriceUnit <PriceUnit
price={asset.price} price={Number(asset.price)}
type={asset.price === '0' ? 'free' : undefined} type={asset.price === '0' ? 'free' : undefined}
size="small" size="small"
className={styles.price} className={styles.price}

View File

@ -71,9 +71,7 @@ function checkError(
parsedFieldName: string[], parsedFieldName: string[],
field: FieldInputProps<any> field: FieldInputProps<any>
) { ) {
if (form?.errors === {}) { if (
return false
} else if (
(form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] && (form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] &&
form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]) || form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]) ||
(form?.touched[field.name] && (form?.touched[field.name] &&
@ -140,11 +138,13 @@ export default function Input(props: Partial<InputProps>): ReactElement {
</Label> </Label>
<InputElement size={size} {...field} {...props} /> <InputElement size={size} {...field} {...props} />
{help && prominentHelp && <FormHelp>{help}</FormHelp>} {help && prominentHelp && <FormHelp>{help}</FormHelp>}
{isFormikField && hasFormikError && (
{field?.name !== 'files' && isFormikField && hasFormikError && (
<div className={styles.error}> <div className={styles.error}>
<ErrorMessage name={field.name} /> <ErrorMessage name={field.name} />
</div> </div>
)} )}
{disclaimer && ( {disclaimer && (
<Disclaimer visible={disclaimerVisible}>{disclaimer}</Disclaimer> <Disclaimer visible={disclaimerVisible}>{disclaimer}</Disclaimer>
)} )}

View File

@ -10,7 +10,7 @@ export default function Conversion({
className, className,
hideApproximateSymbol hideApproximateSymbol
}: { }: {
price: string // expects price in OCEAN, not wei price: number // expects price in OCEAN, not wei
symbol: string symbol: string
className?: string className?: string
hideApproximateSymbol?: boolean hideApproximateSymbol?: boolean
@ -28,18 +28,12 @@ export default function Conversion({
const priceTokenId = getCoingeckoTokenId(symbol) const priceTokenId = getCoingeckoTokenId(symbol)
useEffect(() => { useEffect(() => {
if ( if (!prices || !price || !priceTokenId || !prices[priceTokenId]) {
!prices ||
!price ||
price === '0' ||
!priceTokenId ||
!prices[priceTokenId]
) {
return return
} }
const conversionValue = prices[priceTokenId][currency.toLowerCase()] const conversionValue = prices[priceTokenId][currency.toLowerCase()]
const converted = conversionValue * Number(price) const converted = conversionValue * price
const convertedFormatted = formatCurrency( const convertedFormatted = formatCurrency(
converted, converted,
// No passing of `currency` for non-fiat so symbol conversion // No passing of `currency` for non-fiat so symbol conversion
@ -58,7 +52,7 @@ export default function Conversion({
setPriceConverted(convertedFormattedHTMLstring) setPriceConverted(convertedFormattedHTMLstring)
}, [price, prices, currency, locale, isFiat, priceTokenId]) }, [price, prices, currency, locale, isFiat, priceTokenId])
return Number(price) > 0 ? ( return Number(price) >= 0 ? (
<span <span
className={`${styles.conversion} ${className || ''}`} className={`${styles.conversion} ${className || ''}`}
title="Approximation based on the current spot price on Coingecko" title="Approximation based on the current spot price on Coingecko"

View File

@ -3,10 +3,9 @@ import { formatCurrency } from '@coingecko/cryptoformat'
import Conversion from './Conversion' import Conversion from './Conversion'
import styles from './PriceUnit.module.css' import styles from './PriceUnit.module.css'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import Badge from '@shared/atoms/Badge'
export function formatPrice(price: string, locale: string): string { export function formatPrice(price: number, locale: string): string {
return formatCurrency(Number(price), '', locale, false, { return formatCurrency(price, '', locale, false, {
// Not exactly clear what `significant figures` are for this library, // Not exactly clear what `significant figures` are for this library,
// but setting this seems to give us the formatting we want. // but setting this seems to give us the formatting we want.
// See https://github.com/oceanprotocol/market/issues/70 // See https://github.com/oceanprotocol/market/issues/70
@ -22,7 +21,7 @@ export default function PriceUnit({
symbol, symbol,
type type
}: { }: {
price: string price: number
type?: string type?: string
className?: string className?: string
size?: 'small' | 'mini' | 'large' size?: 'small' | 'mini' | 'large'
@ -38,7 +37,7 @@ export default function PriceUnit({
) : ( ) : (
<> <>
<div> <div>
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '} {Number.isNaN(price) ? '-' : formatPrice(price, locale)}{' '}
<span className={styles.symbol}>{symbol}</span> <span className={styles.symbol}>{symbol}</span>
</div> </div>
{conversion && <Conversion price={price} symbol={symbol} />} {conversion && <Conversion price={price} symbol={symbol} />}

View File

@ -16,10 +16,11 @@ export default function Price({
}): ReactElement { }): ReactElement {
const isSupported = const isSupported =
accessDetails?.type === 'fixed' || accessDetails?.type === 'free' accessDetails?.type === 'fixed' || accessDetails?.type === 'free'
const price = `${orderPriceAndFees?.price || accessDetails?.price}`
return isSupported ? ( return isSupported ? (
<PriceUnit <PriceUnit
price={`${orderPriceAndFees?.price || accessDetails?.price}`} price={Number(price)}
symbol={accessDetails.baseToken?.symbol} symbol={accessDetails.baseToken?.symbol}
className={className} className={className}
size={size} size={size}

View File

@ -1,7 +0,0 @@
.add {
color: var(--brand-pink);
}
.linksExternal {
composes: linksExternal from './index.module.css';
}

View File

@ -1,16 +0,0 @@
import React, { ReactElement } from 'react'
import External from '@images/external.svg'
import styles from './Add.module.css'
export default function Add(): ReactElement {
return (
<a
className={styles.add}
href="https://www.3box.io/hub"
target="_blank"
rel="noreferrer"
>
Add profile on 3Box <External className={styles.linksExternal} />
</a>
)
}

View File

@ -7,10 +7,3 @@
display: inline-block; display: inline-block;
} }
} }
.linksExternal {
width: 6px;
height: 6px;
display: inline-block;
fill: var(--color-secondary);
}

View File

@ -0,0 +1,56 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import * as axios from 'axios'
import Publisher from './'
const account = '0x0000000000000000000000000000000000000000'
jest.mock('axios')
describe('Publisher', () => {
test('should return correct markup by default', async () => {
;(axios as any).get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })
)
render(<Publisher account={account} />)
const element = await screen.findByRole('link')
expect(element).toBeInTheDocument()
expect(element).toContainHTML('<a')
expect(element).toHaveAttribute('href', `/profile/${account}`)
})
test('should truncate account by default', async () => {
;(axios as any).get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } })
)
render(<Publisher account={account} />)
const element = await screen.findByText('0x…00000000')
expect(element).toBeInTheDocument()
})
test('should return correct markup in minimal state', async () => {
;(axios as any).get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } })
)
render(<Publisher minimal account={account} />)
const element = await screen.findByText('0x…00000000')
expect(element).not.toHaveAttribute('href')
})
test('should return markup with empty account', async () => {
;(axios as any).get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } })
)
render(<Publisher account={null} />)
const element = await screen.findByRole('link')
expect(element).toBeInTheDocument()
})
})

View File

@ -1,72 +1,47 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
import classNames from 'classnames/bind'
import Link from 'next/link' import Link from 'next/link'
import get3BoxProfile from '@utils/profile'
import { accountTruncate } from '@utils/web3' import { accountTruncate } from '@utils/web3'
import axios from 'axios'
import { getEnsName } from '@utils/ens' import { getEnsName } from '@utils/ens'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
const cx = classNames.bind(styles) export interface PublisherProps {
account: string
minimal?: boolean
className?: string
}
export default function Publisher({ export default function Publisher({
account, account,
minimal, minimal,
className className
}: { }: PublisherProps): ReactElement {
account: string
minimal?: boolean
className?: string
}): ReactElement {
const isMounted = useIsMounted() const isMounted = useIsMounted()
const [profile, setProfile] = useState<Profile>() const [name, setName] = useState(accountTruncate(account))
const [name, setName] = useState('')
const [accountEns, setAccountEns] = useState<string>()
useEffect(() => { useEffect(() => {
if (!account) return if (!account || account === '') return
// set default name on hook // set default name on hook
// to avoid side effect (UI not updating on account's change) // to avoid side effect (UI not updating on account's change)
setName(accountTruncate(account)) setName(accountTruncate(account))
const source = axios.CancelToken.source()
async function getExternalName() { async function getExternalName() {
// ENS
const accountEns = await getEnsName(account) const accountEns = await getEnsName(account)
if (accountEns && isMounted()) { if (accountEns && isMounted()) {
setAccountEns(accountEns)
setName(accountEns) setName(accountEns)
} }
// 3box
const profile = await get3BoxProfile(account, source.token)
if (!profile) return
setProfile(profile)
const { name, emoji } = profile
name && setName(`${emoji || ''} ${name}`)
} }
getExternalName() getExternalName()
return () => {
source.cancel()
}
}, [account, isMounted]) }, [account, isMounted])
const styleClasses = cx({
publisher: true,
[className]: className
})
return ( return (
<div className={styleClasses}> <div className={`${styles.publisher} ${className || ''}`}>
{minimal ? ( {minimal ? (
name name
) : ( ) : (
<> <>
<Link href={`/profile/${accountEns || account}`}> <Link href={`/profile/${account}`}>
<a title="Show profile page.">{name}</a> <a title="Show profile page.">{name}</a>
</Link> </Link>
</> </>

View File

@ -0,0 +1,7 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Alert from '@shared/atoms/Alert'
describe('Alert', () => {
testRender(<Alert text="Alert text" state="info" />)
})

View File

@ -1,4 +1,4 @@
.blockies { .avatar {
width: var(--font-size-large); width: var(--font-size-large);
height: var(--font-size-large); height: var(--font-size-large);
border-radius: 50%; border-radius: 50%;

View File

@ -0,0 +1,31 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Avatar, { AvatarProps } from '@shared/atoms/Avatar'
export default {
title: 'Component/@shared/atoms/Avatar',
component: Avatar
} as ComponentMeta<typeof Avatar>
const Template: ComponentStory<typeof Avatar> = (args) => <Avatar {...args} />
interface Props {
args: AvatarProps
}
export const DefaultWithBlockies: Props = Template.bind({})
DefaultWithBlockies.args = {
accountId: '0x1234567890123456789012345678901234567890'
}
export const CustomSource: Props = Template.bind({})
CustomSource.args = {
accountId: '0x1234567890123456789012345678901234567890',
src: 'http://placekitten.com/g/300/300'
}
export const Empty: Props = Template.bind({})
Empty.args = {
accountId: null
}

View File

@ -0,0 +1,19 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Avatar from '@shared/atoms/Avatar'
import { DefaultWithBlockies, CustomSource, Empty } from './index.stories'
import { render } from '@testing-library/react'
describe('Avatar', () => {
testRender(<Avatar {...DefaultWithBlockies.args} />)
it('renders without crashing with custom source', () => {
const { container } = render(<Avatar {...CustomSource.args} />)
expect(container.firstChild).toBeInTheDocument()
})
it('renders empty without crashing', () => {
const { container } = render(<Avatar {...Empty.args} />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,24 @@
import { toDataUrl } from 'myetherwallet-blockies'
import React, { ReactElement } from 'react'
import styles from './index.module.css'
export interface AvatarProps {
accountId: string
src?: string
className?: string
}
export default function Avatar({
accountId,
src,
className
}: AvatarProps): ReactElement {
return (
<img
className={`${className || ''} ${styles.avatar} `}
src={src || (accountId ? toDataUrl(accountId) : '')}
alt="Avatar"
aria-hidden="true"
/>
)
}

View File

@ -0,0 +1,7 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Badge from '@shared/atoms/Badge'
describe('Badge', () => {
testRender(<Badge label="Badge text" />)
})

View File

@ -1,22 +0,0 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Blockies, { BlockiesProps } from '@shared/atoms/Blockies'
export default {
title: 'Component/@shared/atoms/Blockies',
component: Blockies
} as ComponentMeta<typeof Blockies>
const Template: ComponentStory<typeof Blockies> = (args) => (
<Blockies {...args} />
)
interface Props {
args: BlockiesProps
}
export const Default: Props = Template.bind({})
Default.args = {
accountId: '0x1xxxxxxxxxx3Exxxxxx7xxxxxxxxxxxxF1fd'
}

View File

@ -1,28 +0,0 @@
import { toDataUrl } from 'myetherwallet-blockies'
import React, { ReactElement } from 'react'
import styles from './index.module.css'
export interface BlockiesProps {
accountId: string
className?: string
image?: string
}
export default function Blockies({
accountId,
className,
image
}: BlockiesProps): ReactElement {
if (!accountId) return null
const blockies = toDataUrl(accountId)
return (
<img
className={`${className || ''} ${styles.blockies} `}
src={image || blockies}
alt="Blockies"
aria-hidden="true"
/>
)
}

View File

@ -0,0 +1,8 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Container from '@shared/atoms/Container'
import { Default } from './index.stories'
describe('Container', () => {
testRender(<Container {...Default.args} />)
})

View File

@ -1,12 +1,13 @@
import React from 'react' import React from 'react'
import { render, act, screen, fireEvent } from '@testing-library/react' import { render, act, screen, fireEvent } from '@testing-library/react'
import { Default } from './index.stories' import { Default } from './index.stories'
import Copy from '.'
jest.useFakeTimers() jest.useFakeTimers()
describe('Copy', () => { describe('Copy', () => {
test('should change class on click', () => { test('should change class on click', () => {
render(<Default {...Default.args} />) render(<Copy {...Default.args} />)
const element = screen.getByTitle('Copy to clipboard') const element = screen.getByTitle('Copy to clipboard')
fireEvent.click(element) fireEvent.click(element)
@ -14,7 +15,7 @@ describe('Copy', () => {
}) })
test('should remove class after timer end', () => { test('should remove class after timer end', () => {
render(<Default {...Default.args} />) render(<Copy {...Default.args} />)
const element = screen.getByTitle('Copy to clipboard') const element = screen.getByTitle('Copy to clipboard')
fireEvent.click(element) fireEvent.click(element)

View File

@ -0,0 +1,8 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Loader from '@shared/atoms/Loader'
import { Default } from './index.stories'
describe('Loader', () => {
testRender(<Loader {...Default.args} />)
})

View File

@ -0,0 +1,8 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Logo from '@shared/atoms/Logo'
import { Default } from './index.stories'
describe('Logo', () => {
testRender(<Logo {...Default.args} />)
})

View File

@ -0,0 +1,8 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Status from '@shared/atoms/Status'
import { Default } from './index.stories'
describe('Status', () => {
testRender(<Status {...Default.args} />)
})

View File

@ -0,0 +1,8 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Time from '@shared/atoms/Time'
import { Default } from './index.stories'
describe('Time', () => {
testRender(<Time {...Default.args} />)
})

View File

@ -7,7 +7,9 @@ export default function AssetStats() {
return ( return (
<footer className={styles.stats}> <footer className={styles.stats}>
{!asset || !asset?.stats || asset?.stats?.orders === 0 ? ( {!asset || !asset?.stats || asset?.stats?.orders < 0 ? (
'N/A'
) : asset?.stats?.orders === 0 ? (
'No sales yet' 'No sales yet'
) : ( ) : (
<> <>

View File

@ -45,7 +45,7 @@ function Row({
<div className={styles.type}>{type}</div> <div className={styles.type}>{type}</div>
<div> <div>
<PriceUnit <PriceUnit
price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`} price={hasPreviousOrder || hasDatatoken ? 0 : Number(price)}
symbol={symbol} symbol={symbol}
size="small" size="small"
className={styles.price} className={styles.price}
@ -81,7 +81,7 @@ export default function PriceOutput({
return ( return (
<div className={styles.priceComponent}> <div className={styles.priceComponent}>
You will pay{' '} You will pay{' '}
<PriceUnit price={`${totalPrice}`} symbol={symbol} size="small" /> <PriceUnit price={Number(totalPrice)} symbol={symbol} size="small" />
<Tooltip <Tooltip
content={ content={
<div className={styles.calculation}> <div className={styles.calculation}>

View File

@ -12,10 +12,8 @@ import { useUserPreferences } from '@context/UserPreferences'
import styles from './index.module.css' import styles from './index.module.css'
import Web3Feedback from '@shared/Web3Feedback' import Web3Feedback from '@shared/Web3Feedback'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { import { getComputeSettingsInitialValues } from './_constants'
getComputeSettingsInitialValues, import { computeSettingsValidationSchema } from './_validation'
computeSettingsValidationSchema
} from './_constants'
import content from '../../../../content/pages/editComputeDataset.json' import content from '../../../../content/pages/editComputeDataset.json'
import { getServiceByName } from '@utils/ddo' import { getServiceByName } from '@utils/ddo'
import { setMinterToPublisher, setMinterToDispenser } from '@utils/dispenser' import { setMinterToPublisher, setMinterToDispenser } from '@utils/dispenser'

View File

@ -5,6 +5,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column;
} }
.feedback h3 { .feedback h3 {

View File

@ -7,7 +7,8 @@ import {
Asset, Asset,
Service Service
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import { validationSchema, getInitialValues } from './_constants' import { validationSchema } from './_validation'
import { getInitialValues } from './_constants'
import { MetadataEditForm } from './_types' import { MetadataEditForm } from './_types'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'

View File

@ -1,20 +1,7 @@
import { FileInfo, Metadata, ServiceComputeOptions } from '@oceanprotocol/lib' import { Metadata, ServiceComputeOptions } from '@oceanprotocol/lib'
import { secondsToString } from '@utils/ddo' import { secondsToString } from '@utils/ddo'
import * as Yup from 'yup'
import { ComputeEditForm, MetadataEditForm } from './_types' import { ComputeEditForm, MetadataEditForm } from './_types'
export const validationSchema = Yup.object().shape({
name: Yup.string()
.min(4, (param) => `Title must be at least ${param.min} characters`)
.required('Required'),
description: Yup.string().required('Required').min(10),
price: Yup.number().required('Required'),
links: Yup.array<any[]>().nullable(),
files: Yup.array<FileInfo[]>().nullable(),
timeout: Yup.string().required('Required'),
author: Yup.string().nullable()
})
export function getInitialValues( export function getInitialValues(
metadata: Metadata, metadata: Metadata,
timeout: number, timeout: number,
@ -24,19 +11,13 @@ export function getInitialValues(
name: metadata?.name, name: metadata?.name,
description: metadata?.description, description: metadata?.description,
price, price,
links: metadata?.links, links: metadata?.links as any,
files: '', files: [{ url: '', type: '' }],
timeout: secondsToString(timeout), timeout: secondsToString(timeout),
author: metadata?.author author: metadata?.author
} }
} }
export const computeSettingsValidationSchema = Yup.object().shape({
allowAllPublishedAlgorithms: Yup.boolean().nullable(),
publisherTrustedAlgorithms: Yup.array().nullable(),
publisherTrustedAlgorithmPublishers: Yup.array().nullable()
})
export function getComputeSettingsInitialValues({ export function getComputeSettingsInitialValues({
publisherTrustedAlgorithms, publisherTrustedAlgorithms,
publisherTrustedAlgorithmPublishers publisherTrustedAlgorithmPublishers

View File

@ -1,10 +1,11 @@
import { FileInfo } from '@oceanprotocol/lib'
export interface MetadataEditForm { export interface MetadataEditForm {
name: string name: string
description: string description: string
timeout: string timeout: string
price?: string price?: string
links?: string | any[] files: FileInfo[]
files: string | any[] links?: FileInfo[]
author?: string author?: string
} }

View File

@ -0,0 +1,34 @@
import { FileInfo } from '@oceanprotocol/lib'
import * as Yup from 'yup'
export const validationSchema = Yup.object().shape({
name: Yup.string()
.min(4, (param) => `Title must be at least ${param.min} characters`)
.required('Required'),
description: Yup.string().required('Required').min(10),
price: Yup.number().required('Required'),
files: Yup.array<FileInfo[]>()
.of(
Yup.object().shape({
url: Yup.string().url('Must be a valid URL.'),
valid: Yup.boolean().isTrue()
})
)
.nullable(),
links: Yup.array<FileInfo[]>()
.of(
Yup.object().shape({
url: Yup.string().url('Must be a valid URL.'),
valid: Yup.boolean().isTrue()
})
)
.nullable(),
timeout: Yup.string().required('Required'),
author: Yup.string().nullable()
})
export const computeSettingsValidationSchema = Yup.object().shape({
allowAllPublishedAlgorithms: Yup.boolean().nullable(),
publisherTrustedAlgorithms: Yup.array().nullable(),
publisherTrustedAlgorithmPublishers: Yup.array().nullable()
})

View File

@ -1,5 +1,5 @@
import React, { ReactElement, ChangeEvent } from 'react' import React, { ReactElement, ChangeEvent } from 'react'
import { DarkMode } from 'use-dark-mode' import { DarkMode } from '@oceanprotocol/use-dark-mode'
import FormHelp from '@shared/FormInput/Help' import FormHelp from '@shared/FormInput/Help'
import Label from '@shared/FormInput/Label' import Label from '@shared/FormInput/Label'
import Moon from '@images/moon.svg' import Moon from '@images/moon.svg'

View File

@ -5,7 +5,7 @@ import styles from './index.module.css'
import Currency from './Currency' import Currency from './Currency'
import Debug from './Debug' import Debug from './Debug'
import Caret from '@images/caret.svg' import Caret from '@images/caret.svg'
import useDarkMode from 'use-dark-mode' import useDarkMode from '@oceanprotocol/use-dark-mode'
import Appearance from './Appearance' import Appearance from './Appearance'
import TokenApproval from './TokenApproval' import TokenApproval from './TokenApproval'
import { useMarketMetadata } from '@context/MarketMetadata' import { useMarketMetadata } from '@context/MarketMetadata'

View File

@ -4,12 +4,13 @@ import { accountTruncate } from '@utils/web3'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import styles from './Account.module.css' import styles from './Account.module.css'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import Blockies from '@shared/atoms/Blockies' import Avatar from '@shared/atoms/Avatar'
// Forward ref for Tippy.js // Forward ref for Tippy.js
// eslint-disable-next-line // eslint-disable-next-line
const Account = React.forwardRef((props, ref: any) => { const Account = React.forwardRef((props, ref: any) => {
const { accountId, accountEns, web3Modal, connect } = useWeb3() const { accountId, accountEns, accountEnsAvatar, web3Modal, connect } =
useWeb3()
async function handleActivation(e: FormEvent<HTMLButtonElement>) { async function handleActivation(e: FormEvent<HTMLButtonElement>) {
// prevent accidentially submitting a form the button might be in // prevent accidentially submitting a form the button might be in
@ -30,7 +31,7 @@ const Account = React.forwardRef((props, ref: any) => {
ref={ref} ref={ref}
onClick={(e) => e.preventDefault()} onClick={(e) => e.preventDefault()}
> >
<Blockies accountId={accountId} /> <Avatar accountId={accountId} src={accountEnsAvatar} />
<span className={styles.address} title={accountId}> <span className={styles.address} title={accountId}>
{accountTruncate(accountEns || accountId)} {accountTruncate(accountEns || accountId)}
</span> </span>

View File

@ -56,7 +56,7 @@ export default function Details(): ReactElement {
</span> </span>
<Conversion <Conversion
className={styles.conversion} className={styles.conversion}
price={value} price={Number(value)}
symbol={key} symbol={key}
/> />
</li> </li>

View File

@ -48,7 +48,7 @@ export default function Bookmarks(): ReactElement {
const newCancelToken = useCancelToken() const newCancelToken = useCancelToken()
useEffect(() => { useEffect(() => {
if (!appConfig?.metadataCacheUri || bookmarks === []) return if (!appConfig?.metadataCacheUri || bookmarks?.length === 0) return
async function init() { async function init() {
if (!bookmarks?.length) { if (!bookmarks?.length) {

View File

@ -1,4 +1,4 @@
.blockies { .avatar {
aspect-ratio: 1/1; aspect-ratio: 1/1;
width: calc(var(--font-size-large) * 2) !important; width: calc(var(--font-size-large) * 2) !important;
height: calc(var(--font-size-large) * 2) !important; height: calc(var(--font-size-large) * 2) !important;
@ -8,7 +8,7 @@
} }
.teaser { .teaser {
composes: box from '../atoms/Box.module.css'; composes: box from '@shared/atoms/Box.module.css';
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2); padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2);
color: var(--color-secondary); color: var(--color-secondary);
position: relative; position: relative;

View File

@ -0,0 +1,53 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Dotdotdot from 'react-dotdotdot'
import Link from 'next/link'
import styles from './index.module.css'
import { accountTruncate } from '@utils/web3'
import Avatar from '../../../@shared/atoms/Avatar'
import { getEnsProfile } from '@utils/ens'
import { UserSales } from '@utils/aquarius'
declare type AccountProps = {
account: UserSales
place?: number
}
export default function Account({
account,
place
}: AccountProps): ReactElement {
const [profile, setProfile] = useState<Profile>()
useEffect(() => {
if (!account?.id) return
async function getProfileData() {
const profile = await getEnsProfile(account.id)
if (!profile) return
setProfile(profile)
}
getProfileData()
}, [account?.id])
return (
<Link href={`/profile/${profile?.name || account.id}`}>
<a className={styles.teaser}>
{place && <span className={styles.place}>{place}</span>}
<Avatar
accountId={account.id}
className={styles.avatar}
src={profile?.avatar}
/>
<div>
<Dotdotdot tagName="h4" clamp={2} className={styles.name}>
{profile?.name ? profile?.name : accountTruncate(account.id)}
</Dotdotdot>
<p className={styles.sales}>
<span>{account.totalSales}</span>
{`${account.totalSales === 1 ? ' sale' : ' sales'}`}
</p>
</div>
</a>
</Link>
)
}

View File

@ -0,0 +1,11 @@
.list {
composes: assetList from '@shared/AssetList/index.module.css';
}
.loaderWrap {
composes: loaderWrap from '@shared/AssetList/index.module.css';
}
.empty {
composes: empty from '@shared/AssetList/index.module.css';
}

View File

@ -0,0 +1,43 @@
import React, { ReactElement } from 'react'
import styles from './index.module.css'
import Loader from '../../../@shared/atoms/Loader'
import { useUserPreferences } from '@context/UserPreferences'
import Account from 'src/components/Home/TopSales/Account'
import { UserSales } from '@utils/aquarius'
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
<Loader />
</div>
)
}
declare type AccountListProps = {
accounts: UserSales[]
isLoading: boolean
className?: string
}
export default function AccountList({
accounts,
isLoading
}: AccountListProps): ReactElement {
const { chainIds } = useUserPreferences()
const emptyText =
chainIds.length === 0 ? 'No network selected.' : 'No results found.'
return isLoading ? (
<LoaderArea />
) : (
<div className={styles.list}>
{accounts?.length > 0 ? (
accounts.map((account, index) => (
<Account account={account} key={index} place={index + 1} />
))
) : (
<div className={styles.empty}>{emptyText}</div>
)}
</div>
)
}

View File

@ -0,0 +1,3 @@
.section {
composes: section from '../index.module.css';
}

View File

@ -1,11 +1,11 @@
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import AccountList from '@shared/AccountList/AccountList' import AccountList from 'src/components/Home/TopSales/AccountList'
import { getTopAssetsPublishers } from '@utils/subgraph' import { getTopAssetsPublishers, UserSales } from '@utils/aquarius'
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
export default function PublishersWithMostSales({ export default function TopSales({
title, title,
action action
}: { }: {
@ -13,14 +13,14 @@ export default function PublishersWithMostSales({
action?: ReactElement action?: ReactElement
}): ReactElement { }): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const [result, setResult] = useState<AccountTeaserVM[]>([]) const [result, setResult] = useState<UserSales[]>([])
const [loading, setLoading] = useState<boolean>() const [loading, setLoading] = useState<boolean>()
useEffect(() => { useEffect(() => {
async function init() { async function init() {
setLoading(true) setLoading(true)
if (chainIds.length === 0) { if (chainIds.length === 0) {
const result: AccountTeaserVM[] = [] const result: UserSales[] = []
setResult(result) setResult(result)
setLoading(false) setLoading(false)
} else { } else {

View File

@ -5,11 +5,11 @@ import Bookmarks from './Bookmarks'
import { generateBaseQuery, queryMetadata } from '@utils/aquarius' import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
import { Asset, LoggerInstance } from '@oceanprotocol/lib' import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import styles from './index.module.css'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { SortTermOptions } from '../../@types/aquarius/SearchQuery' import { SortTermOptions } from '../../@types/aquarius/SearchQuery'
import PublishersWithMostSales from './PublishersWithMostSales' import TopSales from './TopSales'
import styles from './index.module.css'
function sortElements(items: Asset[], sorted: string[]) { function sortElements(items: Asset[], sorted: string[]) {
items.sort(function (a, b) { items.sort(function (a, b) {
@ -136,7 +136,7 @@ export default function HomePage(): ReactElement {
} }
/> />
<PublishersWithMostSales title="Publishers With Most Sales" /> <TopSales title="Publishers With Most Sales" />
</> </>
) )
} }

View File

@ -4,9 +4,10 @@ import ExplorerLink from '@shared/ExplorerLink'
import NetworkName from '@shared/NetworkName' import NetworkName from '@shared/NetworkName'
import Jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg' import Jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg'
import Copy from '@shared/atoms/Copy' import Copy from '@shared/atoms/Copy'
import Blockies from '@shared/atoms/Blockies' import Avatar from '@shared/atoms/Avatar'
import styles from './Account.module.css' import styles from './Account.module.css'
import { useProfile } from '@context/Profile' import { useProfile } from '@context/Profile'
import { accountTruncate } from '@utils/web3'
export default function Account({ export default function Account({
accountId accountId
@ -19,28 +20,27 @@ export default function Account({
return ( return (
<div className={styles.account}> <div className={styles.account}>
<figure className={styles.imageWrap}> <figure className={styles.imageWrap}>
{profile?.image ? ( {accountId ? (
<img <Avatar
src={profile?.image} accountId={accountId}
src={profile?.avatar}
className={styles.image} className={styles.image}
width="96"
height="96"
/> />
) : accountId ? (
<Blockies accountId={accountId} className={styles.image} />
) : ( ) : (
<Jellyfish className={styles.image} /> <Jellyfish className={styles.image} />
)} )}
</figure> </figure>
<div> <div>
<h3 className={styles.name}>{profile?.name}</h3> <h3 className={styles.name}>
{profile?.name || accountTruncate(accountId)}
</h3>
{accountId && ( {accountId && (
<code <code
className={styles.accountId} className={styles.accountId}
title={profile?.accountEns ? accountId : null} title={profile?.name ? accountId : null}
> >
{profile?.accountEns || accountId} <Copy text={accountId} /> {accountId} <Copy text={accountId} />
</code> </code>
)} )}
<p> <p>

View File

@ -24,5 +24,8 @@
} }
.linksExternal { .linksExternal {
composes: linksExternal from '@shared/Publisher/index.module.css'; width: 6px;
height: 6px;
display: inline-block;
fill: var(--color-secondary);
} }

View File

@ -6,6 +6,39 @@ import { useProfile } from '@context/Profile'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
function getLinkData(link: ProfileLink): { href: string; label: string } {
let href, label
switch (link.key) {
case 'url':
href = link.value
label = 'Website'
break
case 'com.twitter':
href = `https://twitter.com/${link.value}`
label = 'Twitter'
break
case 'com.github':
href = `https://github.com/${link.value}`
label = 'GitHub'
break
case 'org.telegram':
href = `https://telegram.org/${link.value}`
label = 'Telegram'
break
case 'com.discord':
href = `https://discordapp.com/users/${link.value}`
label = 'Discord'
break
case 'com.reddit':
href = `https://reddit.com/u/${link.value}`
label = 'Reddit'
break
}
return { href, label }
}
export default function PublisherLinks({ export default function PublisherLinks({
className className
}: { }: {
@ -21,22 +54,17 @@ export default function PublisherLinks({
return ( return (
<div className={styleClasses}> <div className={styleClasses}>
{' — '} {' — '}
{profile?.links?.map((link) => { {profile?.links?.map((link) => (
const href = <a
link.name === 'Twitter' href={getLinkData(link).href}
? `https://twitter.com/${link.value}` key={link.key}
: link.name === 'GitHub' target="_blank"
? `https://github.com/${link.value}` rel="noreferrer"
: link.value.includes('http') // safeguard against urls without protocol defined >
? link.value {getLinkData(link).label}{' '}
: `//${link.value}` <External className={styles.linksExternal} />
</a>
return ( ))}
<a href={href} key={link.name} target="_blank" rel="noreferrer">
{link.name} <External className={styles.linksExternal} />
</a>
)
})}
</div> </div>
) )
} }

View File

@ -15,7 +15,7 @@ export default function Stats({
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const { assets, assetsTotal, sales } = useProfile() const { assets, assetsTotal, sales } = useProfile()
const [totalSales, setTotalSales] = useState('0') const [totalSales, setTotalSales] = useState(0)
useEffect(() => { useEffect(() => {
if (!assets || !accountId || !chainIds) return if (!assets || !accountId || !chainIds) return
@ -30,7 +30,7 @@ export default function Stats({
parseInt(priceInfo.accessDetails.price) * priceInfo.stats.orders parseInt(priceInfo.accessDetails.price) * priceInfo.stats.orders
} }
} }
setTotalSales(JSON.stringify(count)) setTotalSales(count)
} catch (error) { } catch (error) {
LoggerInstance.error(error.message) LoggerInstance.error(error.message)
} }
@ -43,14 +43,21 @@ export default function Stats({
<NumberUnit <NumberUnit
label="Total Sales" label="Total Sales"
value={ value={
<Conversion totalSales > 0 ? (
price={totalSales} <Conversion
symbol={'ocean'} price={totalSales}
hideApproximateSymbol symbol={'ocean'}
/> hideApproximateSymbol
/>
) : (
'0'
)
} }
/> />
<NumberUnit label={`Sale${sales === 1 ? '' : 's'}`} value={sales} /> <NumberUnit
label={`Sale${sales === 1 ? '' : 's'}`}
value={sales < 0 ? 0 : sales}
/>
<NumberUnit label="Published" value={assetsTotal} /> <NumberUnit label="Published" value={assetsTotal} />
</div> </div>
) )

View File

@ -43,8 +43,8 @@ export default function AccountHeader({
{isDescriptionTextClamped() ? ( {isDescriptionTextClamped() ? (
<span className={styles.more} onClick={toogleShowMore}> <span className={styles.more} onClick={toogleShowMore}>
<LinkExternal <LinkExternal
url={`https://www.3box.io/${accountId}`} url={`https://app.ens.domains/name/${profile?.name}`}
text="Read more on 3box" text="Read more on ENS"
/> />
</span> </span>
) : ( ) : (
@ -56,18 +56,9 @@ export default function AccountHeader({
</div> </div>
<div className={styles.meta}> <div className={styles.meta}>
Profile data from{' '} Profile data from{' '}
{profile?.accountEns && (
<>
<LinkExternal
url={`https://app.ens.domains/name/${profile.accountEns}`}
text="ENS"
/>{' '}
&{' '}
</>
)}
<LinkExternal <LinkExternal
url={`https://www.3box.io/${accountId}`} url={`https://app.ens.domains/name/${profile?.name}`}
text="3Box Hub" text="ENS"
/> />
</div> </div>
</div> </div>

View File

@ -72,8 +72,8 @@ export const initialValues: FormPublishData = {
}, },
services: [ services: [
{ {
files: [{ url: '' }], files: [{ url: '', type: '' }],
links: [{ url: '' }], links: [{ url: '', type: '' }],
dataTokenOptions: { name: '', symbol: '' }, dataTokenOptions: { name: '', symbol: '' },
timeout: '', timeout: '',
access: 'access', access: 'access',

View File

@ -1,14 +1,6 @@
import { ServiceComputeOptions } from '@oceanprotocol/lib' import { FileInfo, ServiceComputeOptions } from '@oceanprotocol/lib'
import { NftMetadata } from '@utils/nft' import { NftMetadata } from '@utils/nft'
import { ReactElement } from 'react' import { ReactElement } from 'react'
interface FileInfo {
url: string
valid?: boolean
contentLength?: string
contentType?: string
}
export interface FormPublishService { export interface FormPublishService {
files: FileInfo[] files: FileInfo[]
links?: FileInfo[] links?: FileInfo[]

View File

@ -1,6 +1,7 @@
import { MAX_DECIMALS } from '@utils/constants' import { MAX_DECIMALS } from '@utils/constants'
import * as Yup from 'yup' import * as Yup from 'yup'
import { getMaxDecimalsValidation } from '@utils/numbers' import { getMaxDecimalsValidation } from '@utils/numbers'
import { FileInfo } from '@oceanprotocol/lib'
// TODO: conditional validation // TODO: conditional validation
// e.g. when algo is selected, Docker image is required // e.g. when algo is selected, Docker image is required
@ -28,7 +29,7 @@ const validationMetadata = {
} }
const validationService = { const validationService = {
files: Yup.array<{ url: string; valid: boolean }[]>() files: Yup.array<FileInfo[]>()
.of( .of(
Yup.object().shape({ Yup.object().shape({
url: Yup.string().url('Must be a valid URL.').required('Required'), url: Yup.string().url('Must be a valid URL.').required('Required'),
@ -37,7 +38,7 @@ const validationService = {
) )
.min(1, `At least one file is required.`) .min(1, `At least one file is required.`)
.required('Enter a valid URL and click ADD FILE.'), .required('Enter a valid URL and click ADD FILE.'),
links: Yup.array<{ url: string; valid: boolean }[]>() links: Yup.array<FileInfo[]>()
.of( .of(
Yup.object().shape({ Yup.object().shape({
url: Yup.string().url('Must be a valid URL.'), url: Yup.string().url('Must be a valid URL.'),

View File

@ -13,7 +13,7 @@ import Navigation from './Navigation'
import { Steps } from './Steps' import { Steps } from './Steps'
import { FormPublishData } from './_types' import { FormPublishData } from './_types'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import useNftFactory from '@hooks/contracts/useNftFactory' import useNftFactory from '@hooks/useNftFactory'
import { ProviderInstance, LoggerInstance, DDO } from '@oceanprotocol/lib' import { ProviderInstance, LoggerInstance, DDO } from '@oceanprotocol/lib'
import { getOceanConfig } from '@utils/ocean' import { getOceanConfig } from '@utils/ocean'
import { validationSchema } from './_validation' import { validationSchema } from './_validation'

View File

@ -28,7 +28,7 @@ export default function PageProfile(): ReactElement {
const pathAccount = router.query.account as string const pathAccount = router.query.account as string
// Path has ETH addreess // Path has ETH address
if (web3.utils.isAddress(pathAccount)) { if (web3.utils.isAddress(pathAccount)) {
const finalAccountId = pathAccount || accountId const finalAccountId = pathAccount || accountId
setFinalAccountId(finalAccountId) setFinalAccountId(finalAccountId)
@ -40,6 +40,11 @@ export default function PageProfile(): ReactElement {
// Path has ENS name // Path has ENS name
setFinalAccountEns(pathAccount) setFinalAccountEns(pathAccount)
const resolvedAccountId = await getEnsAddress(pathAccount) const resolvedAccountId = await getEnsAddress(pathAccount)
if (
!resolvedAccountId ||
resolvedAccountId === '0x0000000000000000000000000000000000000000'
)
return
setFinalAccountId(resolvedAccountId) setFinalAccountId(resolvedAccountId)
} }
} }

View File

@ -29,5 +29,5 @@
"incremental": true "incremental": true
}, },
"exclude": ["node_modules", ".next", "*.js"], "exclude": ["node_modules", ".next", "*.js"],
"include": ["./src/**/*", "./tests/**/*", "./next-env.d.ts", "./content/**/*"] "include": ["./src/**/*", "./.jest/**/*", "./next-env.d.ts", "./content/**/*"]
} }