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

Merge branch 'main' into fix/issue-1244-edit-ui

This commit is contained in:
Bogdan Fazakas 2022-10-12 17:04:11 +03:00
commit 6489a8d956
142 changed files with 16781 additions and 25826 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)
@ -86,7 +86,7 @@ npm start
To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask. To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask.
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Rinkeby subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31). To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Goerli subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
> Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes` > Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes`
@ -97,18 +97,18 @@ The `app.config.js` file is setup to prioritize environment variables for settin
For local development, you can use a `.env` file: For local development, you can use a `.env` file:
```bash ```bash
# modify env variables, Rinkeby is enabled by default when using those files # modify env variables, Goerli is enabled by default when using those files
cp .env.example .env cp .env.example .env
``` ```
## 🦀 Data Sources ## 🦀 Data Sources
All displayed data in the app is presented around the concept of one data set, which is a combination of: All displayed data in the app is presented around the concept of one asset, which is a combination of:
- metadata about a data set - metadata about an asset
- the actual data set files - the actual asset file
- the NFT which represents the data set - the NFT which represents the asset
- the datatokens representing access rights to the data set files - the datatokens representing access rights to the asset file
- financial data connected to these datatokens, either a fixed rate exchange contract or a dispenser for free assets - financial data connected to these datatokens, either a fixed rate exchange contract or a dispenser for free assets
- calculations and conversions based on financial data - calculations and conversions based on financial data
- metadata about publisher accounts - metadata about publisher accounts
@ -117,7 +117,7 @@ All this data then comes from multiple sources:
### Aquarius ### 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, 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: All initial assets 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) - 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 - hit Aquarius as early as possible without relying on any ocean.js initialization
@ -159,7 +159,7 @@ function Component() {
} }
``` ```
For components within a single data set view the `useAsset()` hook can be used, which in the background gets the respective metadata from Aquarius. For components within a single asset view the `useAsset()` hook can be used, which in the background gets the respective metadata from Aquarius.
```tsx ```tsx
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
@ -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>
) )
} }
@ -232,7 +216,7 @@ function Component() {
### Purgatory ### Purgatory
Based on [list-purgatory](https://github.com/oceanprotocol/list-purgatory) some data sets get additional data. Within most components this can be done with the internal `useAsset()` hook which fetches data from the [market-purgatory](https://github.com/oceanprotocol/market-purgatory) endpoint in the background. Based on [list-purgatory](https://github.com/oceanprotocol/list-purgatory) some assets get additional data. Within most components this can be done with the internal `useAsset()` hook which fetches data from the [market-purgatory](https://github.com/oceanprotocol/market-purgatory) endpoint in the background.
For asset purgatory: For asset purgatory:
@ -332,7 +316,7 @@ npm run jest
A coverage report is automatically shown in console whenever `npm run jest` is called. Generated reports are sent to CodeClimate during CI runs. A coverage report is automatically shown in console whenever `npm run jest` is called. Generated reports are sent to CodeClimate during CI runs.
During local development you can continously get coverage report feedback in your console by running Jest in watch mode: During local development you can continuously get coverage report feedback in your console by running Jest in watch mode:
```bash ```bash
npm run jest:watch npm run jest:watch
@ -395,7 +379,7 @@ We encourage you to fork this repository and create your own data marketplace. W
- The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market. - The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market.
- The name "Ocean Market" is also copyright protected and should be changed to the name of your market. - The name "Ocean Market" is also copyright protected and should be changed to the name of your market.
Additionally, we would also advise that your retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team. Additionally, we would also advise that you retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace! Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
@ -415,7 +399,7 @@ To allow publishers to set pricing as "Fixed" you need to add the following envi
To allow publishers to set pricing as "Free" you need to add the following environmental variable to your .env file: `NEXT_PUBLIC_ALLOW_FREE_PRICING="true"` (default). To allow publishers to set pricing as "Free" you need to add the following environmental variable to your .env file: `NEXT_PUBLIC_ALLOW_FREE_PRICING="true"` (default).
This allocates the datatokens to the [dispenser contract](https://github.com/oceanprotocol/contracts/blob/main/contracts/dispenser/Dispenser.sol) which dispenses data tokens to users for free. Publishers in your market will now be able to offer their datasets to users for free (excluding gas costs). This allocates the datatokens to the [dispenser contract](https://github.com/oceanprotocol/contracts/blob/main/contracts/dispenser/Dispenser.sol) which dispenses data tokens to users for free. Publishers in your market will now be able to offer their assets to users for free (excluding gas costs).
## ✅ GDPR Compliance ## ✅ GDPR Compliance
@ -427,7 +411,7 @@ Feel free to adopt our provided privacy policies to your needs. Per default we c
### Privacy Preference Center ### Privacy Preference Center
Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style propertie to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request. Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style property to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request.
Now your market users will be provided with additional options to toggle the use of your configured cookie consent categories. You can always retrieve the current consent status per category with the provided `useConsent()` hook. See below, how you can set your own custom cookies depending on the market user's consent. Feel free to adjust the provided utility functions for cookie usage provided in the `src/utils/cookies.ts` file to your needs. Now your market users will be provided with additional options to toggle the use of your configured cookie consent categories. You can always retrieve the current consent status per category with the provided `useConsent()` hook. See below, how you can set your own custom cookies depending on the market user's consent. Feel free to adjust the provided utility functions for cookie usage provided in the `src/utils/cookies.ts` file to your needs.

View File

@ -9,20 +9,12 @@ module.exports = {
process.env.NEXT_PUBLIC_METADATACACHE_URI || process.env.NEXT_PUBLIC_METADATACACHE_URI ||
'https://v4.aquarius.oceanprotocol.com', 'https://v4.aquarius.oceanprotocol.com',
v3MetadataCacheUri:
process.env.NEXT_PUBLIC_V3_METADATACACHE_URI ||
'https://aquarius.oceanprotocol.com',
v3MarketUri:
process.env.NEXT_PUBLIC_V3_MARKET_URI ||
'https://v3.market.oceanprotocol.com',
// List of chainIds which metadata cache queries will return by default. // List of chainIds which metadata cache queries will return by default.
// This preselects the Chains user preferences. // This preselects the Chains user preferences.
chainIds: [1, 137, 56, 246, 1285], chainIds: [1, 137, 56, 246, 1285],
// List of all supported chainIds. Used to populate the Chains user preferences list. // List of all supported chainIds. Used to populate the Chains user preferences list.
chainIdsSupported: [1, 137, 56, 246, 1285, 3, 4, 80001, 1287], chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001, 1287],
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx', infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',
@ -66,7 +58,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',

View File

@ -1,14 +1,14 @@
{ {
"form": { "form": {
"title": "Set allowed algorithms", "title": "Set allowed algorithms",
"description": "Only the algorithms selected here will be allowed to run on your data set. Uncheck all to remove any access to your data set.", "description": "Only the algorithms selected here will be allowed to run on your dataset. Uncheck all to remove any access to your dataset.",
"success": "🎉 Successfully updated. 🎉\n\nUpdates might not show up right away on your asset. In this case, wait some seconds and reload your asset details page in your browser.", "success": "🎉 Successfully updated. 🎉\n\nUpdates might not show up right away on your asset. In this case, wait some seconds and reload your asset details page in your browser.",
"error": "Updating DDO failed.", "error": "Updating DDO failed.",
"data": [ "data": [
{ {
"name": "publisherTrustedAlgorithms", "name": "publisherTrustedAlgorithms",
"label": "Selected Algorithms", "label": "Selected Algorithms",
"help": "Choose one or multiple algorithms you trust to allow them to run on this data set.", "help": "Choose one or multiple algorithms you trust to allow them to run on this dataset.",
"type": "assetSelectionMultiple", "type": "assetSelectionMultiple",
"multiple": true, "multiple": true,
"options": [], "options": [],
@ -17,7 +17,7 @@
{ {
"name": "allowAllPublishedAlgorithms", "name": "allowAllPublishedAlgorithms",
"label": "All Algorithms", "label": "All Algorithms",
"help": "Allow any published algorithm to run on this data set.", "help": "Allow any published algorithm to run on this dataset.",
"type": "checkbox", "type": "checkbox",
"options": ["Allow any published algorithm"] "options": ["Allow any published algorithm"]
} }

View File

@ -31,7 +31,7 @@
"name": "files", "name": "files",
"label": "New file", "label": "New file",
"placeholder": "e.g. https://file.com/file.json", "placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. Leaving this field empty will not remove the current value.", "help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. Leaving this field empty will not remove the current value.",
"prominentHelp": true, "prominentHelp": true,
"type": "files" "type": "files"
}, },
@ -39,7 +39,7 @@
"name": "links", "name": "links",
"label": "New sample file", "label": "New sample file",
"placeholder": "e.g. https://file.com/samplefile.json", "placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please provide a URL to a sample of your data set file. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** Leaving this field empty will not remove the current value.", "help": "Please provide a URL to a sample of your dataset file. This file should reveal the data structure of your dataset, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** Leaving this field empty will not remove the current value.",
"prominentHelp": true, "prominentHelp": true,
"type": "files" "type": "files"
}, },
@ -47,7 +47,7 @@
{ {
"name": "timeout", "name": "timeout",
"label": "Timeout", "label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.", "help": "Define how long buyers should be able to download the dataset again after the initial purchase.",
"type": "select", "type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"], "options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false, "sortOptions": false,
@ -57,7 +57,14 @@
"name": "author", "name": "author",
"label": "New Author", "label": "New Author",
"placeholder": "e.g. Mrs McJellyfish", "placeholder": "e.g. Mrs McJellyfish",
"help": "Give proper attribution for your data set.", "help": "Give proper attribution for your dataset.",
"required": false
},
{
"name": "tags",
"label": "New Tags",
"type": "tags",
"placeholder": "e.g. logistics",
"required": false "required": false
} }
] ]

View File

@ -1,4 +1,4 @@
{ {
"title": "Account", "title": "Account",
"description": "Find the data sets and jobs that you previously accessed." "description": "Find the datasets and jobs that you previously accessed."
} }

View File

@ -2,7 +2,7 @@
"create": { "create": {
"fixed": { "fixed": {
"title": "Fixed", "title": "Fixed",
"info": "Set your price for accessing this data set. The datatoken for this data set will be worth the entered amount of the selected base token.", "info": "Set your price for accessing this dataset. The datatoken for this dataset will be worth the entered amount of the selected base token.",
"tooltips": { "tooltips": {
"communityFee": "Goes to Ocean DAO for teams to improve the tools, build apps, do outreach, and more. A small fraction is used to burn OCEAN. This fee is collected when downloading or using an asset in a compute job.", "communityFee": "Goes to Ocean DAO for teams to improve the tools, build apps, do outreach, and more. A small fraction is used to burn OCEAN. This fee is collected when downloading or using an asset in a compute job.",
"marketplaceFee": "Goes to the marketplace owner that is hosting and providing the marketplace and is collected when downloading or using an asset in a compute job. In Ocean Market, it is treated as network revenue that goes to the Ocean community." "marketplaceFee": "Goes to the marketplace owner that is hosting and providing the marketplace and is collected when downloading or using an asset in a compute job. In Ocean Market, it is treated as network revenue that goes to the Ocean community."
@ -10,7 +10,7 @@
}, },
"free": { "free": {
"title": "Free", "title": "Free",
"info": "Set your data set as free. The datatoken for this data set will be given for free via creating a faucet.", "info": "Set your dataset as free. The datatoken for this dataset will be given for free via creating a faucet.",
"fields": [ "fields": [
{ {
"name": "freeAgreement", "name": "freeAgreement",

View File

@ -33,14 +33,14 @@
"name": "author", "name": "author",
"label": "Author", "label": "Author",
"placeholder": "e.g. Jelly McJellyfish", "placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set. You are welcome to use a pseudonym, and you can change your author name at any time. Please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).", "help": "Give proper attribution for your dataset. You are welcome to use a pseudonym, and you can change your author name at any time. Please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"required": true "required": true
}, },
{ {
"name": "tags", "name": "tags",
"label": "Tags", "label": "Tags",
"placeholder": "e.g. logistics, ai", "type": "tags",
"help": "Separate tags with comma." "placeholder": "e.g. logistics"
}, },
{ {
"name": "dockerImage", "name": "dockerImage",
@ -54,21 +54,22 @@
}, },
{ {
"name": "dockerImageCustom", "name": "dockerImageCustom",
"label": "Docker Image URL", "label": "Custom Docker Image",
"placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path", "placeholder": "e.g. oceanprotocol/algo_dockers:node-vibrant or quay.io/startx/mariadb",
"help": "Provide the name of a public Docker image or the full url if you have it hosted in a 3rd party repo", "help": "Provide the name and the tag of a public Docker hub image or the custom image if you have it hosted in a 3rd party repository",
"type": "container",
"required": true "required": true
}, },
{ {
"name": "dockerImageCustomTag", "name": "dockerImageChecksum",
"label": "Docker Image Tag", "label": "Docker Image Checksum",
"placeholder": "e.g. latest", "placeholder": "e.g. sha256:xiXqb7Vet0FbN9q0GFMgUdi5C22wjJT0i2G6lYKC2jl6QxkKzVz7KaPDgqfTMjNF",
"help": "Provide the tag for your Docker image.", "help": "Provide the checksum(DIGEST) of your docker image.",
"required": true "required": true
}, },
{ {
"name": "dockerImageCustomEntrypoint", "name": "dockerImageCustomEntrypoint",
"label": "Docker Entrypoint", "label": "Docker Image Entrypoint",
"placeholder": "e.g. python $ALGO", "placeholder": "e.g. python $ALGO",
"help": "Provide the entrypoint for your algorithm.", "help": "Provide the entrypoint for your algorithm.",
"required": true "required": true
@ -104,7 +105,7 @@
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", "help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true, "prominentHelp": true,
"type": "files", "type": "files",
"required": true "required": true
@ -113,7 +114,7 @@
"name": "links", "name": "links",
"label": "Sample file", "label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json", "placeholder": "e.g. https://file.com/samplefile.json",
"help": "This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**", "help": "This file should reveal the data structure of your dataset, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**",
"prominentHelp": true, "prominentHelp": true,
"type": "files" "type": "files"
}, },
@ -122,7 +123,7 @@
"label": "Algorithm Privacy", "label": "Algorithm Privacy",
"type": "checkbox", "type": "checkbox",
"options": ["Keep my algorithm private"], "options": ["Keep my algorithm private"],
"help": "By default, your algorithm can be downloaded for free or a fixed price, in addition to running in compute jobs. Enabling this option will prevent downloading, so your algorithm can only be run as part of a compute job on a data set.", "help": "By default, your algorithm can be downloaded for free or a fixed price, in addition to running in compute jobs. Enabling this option will prevent downloading, so your algorithm can only be run as part of a compute job on a dataset.",
"required": false "required": false
}, },
{ {
@ -138,7 +139,7 @@
{ {
"name": "timeout", "name": "timeout",
"label": "Timeout", "label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.", "help": "Define how long buyers should be able to download the dataset again after the initial purchase.",
"type": "select", "type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"], "options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false, "sortOptions": false,

View File

@ -1,6 +1,6 @@
{ {
"title": "Publish", "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.", "description": "Highlight the important features of your dataset or algorithm to make it more discoverable and catch the interest of data consumers.",
"warning": "Publishing into a test network 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": "Publishing into a test network 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).",
"tooltipAvailableNetworks": "Assets are published to the network your wallet is connected to. These networks are currently supported:" "tooltipAvailableNetworks": "Assets are published to the network your wallet is connected to. These networks are currently supported:"
} }

View File

@ -1,7 +1,7 @@
{ {
"asset": { "asset": {
"title": "Data Set In Purgatory", "title": "Dataset In Purgatory",
"description": "Except for removing liquidity, no further actions are permitted on this data set and it will not be returned in any search. For more details go to [list-purgatory](https://github.com/oceanprotocol/list-purgatory)." "description": "Except for removing liquidity, no further actions are permitted on this dataset and it will not be returned in any search. For more details go to [list-purgatory](https://github.com/oceanprotocol/list-purgatory)."
}, },
"account": { "account": {
"title": "Account In Purgatory", "title": "Account In Purgatory",

View File

@ -1,6 +1,6 @@
{ {
"siteTitle": "Ocean Market", "siteTitle": "Ocean Market",
"siteTagline": "A marketplace to find, publish and trade data sets in the Ocean Network.", "siteTagline": "A marketplace to find, publish and trade datasets in the Ocean Network.",
"siteUrl": "https://market.oceanprotocol.com", "siteUrl": "https://market.oceanprotocol.com",
"siteImage": "/share.png", "siteImage": "/share.png",
"copyright": "All Rights Reserved. Powered by ", "copyright": "All Rights Reserved. Powered by ",

11
netlify.toml Normal file
View File

@ -0,0 +1,11 @@
[[headers]]
for = "/_next/image/*"
[headers.values]
Content-Security-Policy= "upgrade-insecure-requests"
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
X-XSS-Protection = "1; mode=block"
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy= "accelerometer=(self), ambient-light-sensor=(self), autoplay=(self), battery=(self), camera=(self), cross-origin-isolated=(self), display-capture=(self), document-domain=(self), encrypted-media=(self), execution-while-not-rendered=(self), execution-while-out-of-viewport=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), keyboard-map=(self), magnetometer=(self), microphone=(self), midi=(self), navigation-override=(self), payment=(self), picture-in-picture=(self), publickey-credentials-get=(self), screen-wake-lock=(self), sync-xhr=(self), usb=(self), web-share=(self), xr-spatial-tracking=(self), clipboard-read=(self), clipboard-write=(self), gamepad=(self), speaker-selection=(self), conversion-measurement=(self), focus-without-user-activation=(self), hid=(self), idle-detection=(self), interest-cohort=(self), serial=(self), sync-script=(self), trust-token-redemption=(self), window-placement=(self), vertical-scroll=(self)"

39396
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,8 @@
"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.goerli.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"
}, },
@ -26,27 +26,28 @@
"@coingecko/cryptoformat": "^0.5.4", "@coingecko/cryptoformat": "^0.5.4",
"@loadable/component": "^5.15.2", "@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0", "@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^1.1.6", "@oceanprotocol/lib": "^2.2.1",
"@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": "^10.0.5",
"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",
"match-sorter": "^6.3.1",
"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",
@ -55,6 +56,7 @@
"react-dotdotdot": "^1.3.1", "react-dotdotdot": "^1.3.1",
"react-modal": "^3.15.1", "react-modal": "^3.15.1",
"react-paginate": "^8.1.3", "react-paginate": "^8.1.3",
"react-select": "^5.4.0",
"react-spring": "^9.5.2", "react-spring": "^9.5.2",
"react-tabs": "^5.1.0", "react-tabs": "^5.1.0",
"react-toastify": "^9.0.4", "react-toastify": "^9.0.4",
@ -64,58 +66,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.21",
"@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.1",
"@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.38.1",
"@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.4",
"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.7.0",
"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.1.2",
"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

@ -9,7 +9,7 @@ import React, {
} from 'react' } from 'react'
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib' import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
import { CancelToken } from 'axios' import { CancelToken } from 'axios'
import { checkV3Asset, retrieveAsset } from '@utils/aquarius' import { retrieveAsset } from '@utils/aquarius'
import { useWeb3 } from './Web3' import { useWeb3 } from './Web3'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean' import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
@ -25,7 +25,6 @@ export interface AssetProviderValue {
owner: string owner: string
error?: string error?: string
isAssetNetwork: boolean isAssetNetwork: boolean
isV3Asset: boolean
isOwner: boolean isOwner: boolean
oceanConfig: Config oceanConfig: Config
loading: boolean loading: boolean
@ -53,7 +52,6 @@ function AssetProvider({
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>() const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
const [isV3Asset, setIsV3Asset] = useState<boolean>()
const [oceanConfig, setOceanConfig] = useState<Config>() const [oceanConfig, setOceanConfig] = useState<Config>()
const newCancelToken = useCancelToken() const newCancelToken = useCancelToken()
@ -71,7 +69,6 @@ function AssetProvider({
const asset = await retrieveAsset(did, token) const asset = await retrieveAsset(did, token)
if (!asset) { if (!asset) {
setIsV3Asset(await checkV3Asset(did, token))
setError( setError(
`\`${did}\`` + `\`${did}\`` +
'\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.' '\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.'
@ -96,7 +93,6 @@ function AssetProvider({
} }
setTitle(`This asset has been flagged as "${state}" by the publisher`) setTitle(`This asset has been flagged as "${state}" by the publisher`)
setIsV3Asset(await checkV3Asset(did, token))
setError(`\`${did}\`` + `\n\nPublisher Address: ${asset.nft.owner}`) setError(`\`${did}\`` + `\n\nPublisher Address: ${asset.nft.owner}`)
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset) LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
return return
@ -208,7 +204,6 @@ function AssetProvider({
loading, loading,
fetchAsset, fetchAsset,
isAssetNetwork, isAssetNetwork,
isV3Asset,
isOwner, isOwner,
oceanConfig oceanConfig
} as AssetProviderValue } as AssetProviderValue

View File

@ -26,8 +26,6 @@ export interface AppConfig {
classNameLight: string classNameLight: string
storageKey: string storageKey: string
} }
v3MetadataCacheUri: string
v3MarketUri: string
} }
export interface SiteContent { export interface SiteContent {
siteTitle: string siteTitle: string

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

@ -133,6 +133,14 @@ function UserPreferencesProvider({
setBookmarks(newPinned) setBookmarks(newPinned)
}, [bookmarks]) }, [bookmarks])
// chainIds old data migration
// remove deprecated networks from user-saved chainIds
useEffect(() => {
if (!chainIds.includes(3) && !chainIds.includes(4)) return
const newChainIds = chainIds.filter((id) => id !== 3 && id !== 4)
setChainIds(newChainIds)
}, [chainIds])
return ( return (
<UserPreferencesContext.Provider <UserPreferencesContext.Provider
value={ value={

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( LoggerInstance.log(
`[web3] ENS name found for ${accountId}:`, `[web3] ENS name found for ${accountId}:`,
accountEns profile.name
) )
if (profile.avatar) {
setAccountEnsAvatar(profile.avatar)
LoggerInstance.log(
`[web3] ENS avatar found for ${accountId}:`,
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

@ -10,6 +10,7 @@ export function getNetworkType(network: EthereumListsChain): string {
// .network field, which is innexistent on https://chainid.network/chains.json // .network field, which is innexistent on https://chainid.network/chains.json
// We hack in mainnet detection for moonriver. // We hack in mainnet detection for moonriver.
if ( if (
network &&
!network.name.includes('Testnet') && !network.name.includes('Testnet') &&
!network.title?.includes('Testnet') && !network.title?.includes('Testnet') &&
network.name !== 'Moonbase Alpha' network.name !== 'Moonbase Alpha'

View File

@ -10,7 +10,7 @@ function useNftFactory(): NftFactory {
useEffect(() => { useEffect(() => {
if (!web3 || !chainId) return if (!web3 || !chainId) return
const config = getOceanConfig(chainId) const config = getOceanConfig(chainId)
const factory = new NftFactory(config?.erc721FactoryAddress, web3) const factory = new NftFactory(config?.nftFactoryAddress, web3)
setNftFactory(factory) setNftFactory(factory)
}, [web3, chainId]) }, [web3, 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

@ -6,7 +6,8 @@ export enum SortDirectionOptions {
export enum SortTermOptions { export enum SortTermOptions {
Created = 'nft.created', Created = 'nft.created',
Relevance = '_score', Relevance = '_score',
Stats = 'stats.orders' Orders = 'stats.orders',
Allocated = 'stats.allocated'
} }
// Note: could not figure out how to get `enum` to be ambiant // Note: could not figure out how to get `enum` to be ambiant

4
src/@types/aquarius/TagsList.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
interface AggregatedTag {
doc_count: number
key: string
}

View File

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

View File

@ -23,7 +23,7 @@ import {
const tokensPriceQuery = gql` const tokensPriceQuery = gql`
query TokensPriceQuery($datatokenIds: [ID!], $account: String) { query TokensPriceQuery($datatokenIds: [ID!], $account: String) {
tokens(where: { id_in: $datatokenIds }) { tokens(first: 1000, where: { id_in: $datatokenIds }) {
id id
symbol symbol
name name

View File

@ -2,13 +2,18 @@ import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection' import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios' import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData' import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { metadataCacheUri, v3MetadataCacheUri } from '../../app.config' import { metadataCacheUri } from '../../app.config'
import { import {
SortDirectionOptions, SortDirectionOptions,
SortTermOptions SortTermOptions
} 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 {
@ -39,13 +44,18 @@ export function generateBaseQuery(
): SearchQuery { ): SearchQuery {
const generatedQuery = { const generatedQuery = {
from: baseQueryParams.esPaginationOptions?.from || 0, from: baseQueryParams.esPaginationOptions?.from || 0,
size: baseQueryParams.esPaginationOptions?.size || 1000, size:
baseQueryParams.esPaginationOptions?.size >= 0
? baseQueryParams.esPaginationOptions?.size
: 1000,
query: { query: {
bool: { bool: {
...baseQueryParams.nestedQuery, ...baseQueryParams.nestedQuery,
filter: [ filter: [
...(baseQueryParams.filters || []), ...(baseQueryParams.filters || []),
getFilterTerm('chainId', baseQueryParams.chainIds), baseQueryParams.chainIds
? getFilterTerm('chainId', baseQueryParams.chainIds)
: [],
getFilterTerm('_index', 'aquarius'), getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory ...(baseQueryParams.ignorePurgatory
? [] ? []
@ -140,28 +150,6 @@ export async function retrieveAsset(
} }
} }
export async function checkV3Asset(
did: string,
cancelToken: CancelToken
): Promise<boolean> {
try {
const response: AxiosResponse<Asset> = await axios.get(
`${v3MetadataCacheUri}/api/v1/aquarius/assets/ddo/${did}`,
{ cancelToken }
)
if (!response || response.status !== 200 || !response.data) return false
return true
} catch (error) {
if (axios.isCancel(error)) {
LoggerInstance.log(error.message)
} else {
LoggerInstance.error(error.message)
}
return false
}
}
export async function getAssetsNames( export async function getAssetsNames(
didList: string[], didList: string[],
cancelToken: CancelToken cancelToken: CancelToken
@ -280,7 +268,7 @@ export async function getAlgorithmDatasetsForCompute(
const query = generateBaseQuery(baseQueryParams) const query = generateBaseQuery(baseQueryParams)
const computeDatasets = await queryMetadata(query, cancelToken) const computeDatasets = await queryMetadata(query, cancelToken)
if (computeDatasets.totalResults === 0) return [] if (computeDatasets?.totalResults === 0) return []
const datasets = await transformAssetToAssetSelection( const datasets = await transformAssetToAssetSelection(
datasetProviderUri, datasetProviderUri,
@ -317,7 +305,7 @@ export async function getPublishedAssets(
aggs: { aggs: {
totalOrders: { totalOrders: {
sum: { sum: {
field: SortTermOptions.Stats field: SortTermOptions.Orders
} }
} }
}, },
@ -371,7 +359,7 @@ export async function getTopPublishers(
aggs: { aggs: {
totalSales: { totalSales: {
sum: { sum: {
field: SortTermOptions.Stats field: SortTermOptions.Orders
} }
} }
} }
@ -397,6 +385,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[],
@ -439,3 +461,47 @@ export async function getDownloadAssets(
} }
} }
} }
export async function getTagsList(
chainIds: number[],
cancelToken: CancelToken
): Promise<string[]> {
const baseQueryParams = {
chainIds,
esPaginationOptions: { from: 0, size: 0 }
} as BaseQueryParams
const query = {
...generateBaseQuery(baseQueryParams),
aggs: {
tags: {
terms: {
field: 'metadata.tags.keyword',
size: 1000
}
}
}
}
try {
const response: AxiosResponse<SearchResponse> = await axios.post(
`${metadataCacheUri}/api/aquarius/assets/query`,
{ ...query },
{ cancelToken }
)
if (response?.status !== 200 || !response?.data) return
const { buckets }: { buckets: AggregatedTag[] } =
response.data.aggregations.tags
const tagsList = buckets
.filter((tag) => tag.key !== '')
.map((tag) => tag.key)
return tagsList.sort()
} catch (error) {
if (axios.isCancel(error)) {
LoggerInstance.log(error.message)
} else {
LoggerInstance.error(error.message)
}
}
}

View File

@ -9,7 +9,7 @@ export async function setMinterToPublisher(
accountId: string, accountId: string,
setError: (msg: string) => void setError: (msg: string) => void
): Promise<TransactionReceipt> { ): Promise<TransactionReceipt> {
const dispenserInstance = new Dispenser(web3, dispenserAddress) const dispenserInstance = new Dispenser(dispenserAddress, web3)
const status = await dispenserInstance.status(datatokenAddress) const status = await dispenserInstance.status(datatokenAddress)
if (!status?.active) return if (!status?.active) return

View File

@ -1,16 +1,27 @@
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import axios from 'axios' import axios from 'axios'
import isUrl from 'is-url-superb'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
async function isDockerHubImageValid( export interface dockerContainerInfo {
exists: boolean
checksum: string
}
export async function getContainerChecksum(
image: string, image: string,
tag: string tag: string
): Promise<boolean> { ): Promise<dockerContainerInfo> {
const containerInfo: dockerContainerInfo = {
exists: false,
checksum: null
}
try { try {
const response = await axios.post( const response = await axios.post(
`https://dockerhub-proxy.oceanprotocol.com`, `https://dockerhub-proxy.oceanprotocol.com`,
{ image, tag } {
image,
tag
}
) )
if ( if (
!response || !response ||
@ -18,46 +29,18 @@ async function isDockerHubImageValid(
response.data.status !== 'success' response.data.status !== 'success'
) { ) {
toast.error( toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again' 'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.'
) )
return false return containerInfo
} }
containerInfo.exists = true
return true containerInfo.checksum = response.data.result.checksum
return containerInfo
} catch (error) { } catch (error) {
LoggerInstance.error(error.message) LoggerInstance.error(error.message)
toast.error( toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again' 'Could not fetch docker hub image informations. If you have it hosted in a 3rd party repository please fill in the container checksum manually.'
) )
return false return containerInfo
} }
} }
async function is3rdPartyImageValid(imageURL: string): Promise<boolean> {
try {
const response = await axios.head(imageURL)
if (!response || response.status !== 200) {
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
return true
} catch (error) {
LoggerInstance.error(error.message)
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
}
export async function validateDockerImage(
dockerImage: string,
tag: string
): Promise<boolean> {
const isValid = isUrl(dockerImage)
? await is3rdPartyImageValid(dockerImage)
: await isDockerHubImageValid(dockerImage, tag)
return isValid
}

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

@ -25,8 +25,8 @@ export async function getFixedBuyPrice(
const config = getOceanConfig(chainId) const config = getOceanConfig(chainId)
const fixed = new FixedRateExchange(web3, config.fixedRateExchangeAddress) const fixed = new FixedRateExchange(config.fixedRateExchangeAddress, web3)
const estimatedPrice = await fixed.calcBaseInGivenOutDT( const estimatedPrice = await fixed.calcBaseInGivenDatatokensOut(
accessDetails.addressOrId, accessDetails.addressOrId,
'1', '1',
consumeMarketFixedSwapFee consumeMarketFixedSwapFee

View File

@ -171,15 +171,6 @@ export async function setNFTMetadataAndTokenURI(
metadataProofs: [] metadataProofs: []
} }
const estGasSetMetadataAndTokenURI = await nft.estGasSetMetadataAndTokenURI(
asset.nftAddress,
accountId,
metadataAndTokenURI
)
LoggerInstance.log(
'[setNFTMetadataAndTokenURI] est Gas set metadata and token uri --',
estGasSetMetadataAndTokenURI
)
const setMetadataAndTokenURITx = await nft.setMetadataAndTokenURI( const setMetadataAndTokenURITx = await nft.setMetadataAndTokenURI(
asset.nftAddress, asset.nftAddress,
accountId, accountId,

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

@ -24,7 +24,7 @@ export function getDevelopmentConfig(): Config {
// fixedRateExchangeAddress: contractAddresses.development?.FixedRateExchange, // fixedRateExchangeAddress: contractAddresses.development?.FixedRateExchange,
// metadataContractAddress: contractAddresses.development?.Metadata, // metadataContractAddress: contractAddresses.development?.Metadata,
// oceanTokenAddress: contractAddresses.development?.Ocean, // oceanTokenAddress: contractAddresses.development?.Ocean,
// There is no subgraph in barge so we hardcode the Rinkeby one for now // There is no subgraph in barge so we hardcode the Goerli one for now
subgraphUri: 'https://v4.subgraph.rinkeby.oceanprotocol.com' subgraphUri: 'https://v4.subgraph.goerli.oceanprotocol.com'
} as Config } as Config
} }

View File

@ -68,6 +68,7 @@ export async function order(
// this assumes all fees are in ocean // this assumes all fees are in ocean
const txApprove = await approve( const txApprove = await approve(
web3, web3,
config,
accountId, accountId,
asset.accessDetails.baseToken.address, asset.accessDetails.baseToken.address,
asset.accessDetails.datatoken.address, asset.accessDetails.datatoken.address,
@ -154,19 +155,21 @@ async function approveProviderFee(
accountId: string, accountId: string,
web3: Web3, web3: Web3,
providerFeeAmount: string providerFeeAmount: string
): Promise<string> { ): Promise<TransactionReceipt> {
const config = getOceanConfig(asset.chainId)
const baseToken = const baseToken =
asset?.accessDetails?.type === 'free' asset?.accessDetails?.type === 'free'
? getOceanConfig(asset.chainId).oceanTokenAddress ? getOceanConfig(asset.chainId).oceanTokenAddress
: asset?.accessDetails?.baseToken?.address : asset?.accessDetails?.baseToken?.address
const txApproveWei = await approveWei( const txApproveWei = await approveWei(
web3, web3,
config,
accountId, accountId,
baseToken, baseToken,
asset?.accessDetails?.datatoken?.address, asset?.accessDetails?.datatoken?.address,
providerFeeAmount providerFeeAmount
) )
return txApproveWei as string // thanks ocean.js return txApproveWei
} }
async function startOrder( async function startOrder(

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

@ -0,0 +1,45 @@
import { AllLocked } from 'src/@types/subgraph/AllLocked'
import { gql, OperationResult } from 'urql'
import { fetchData, getQueryContext } from './subgraph'
import axios from 'axios'
const AllLocked = gql`
query AllLocked {
veOCEANs(first: 1000) {
lockedAmount
}
}
`
interface TotalVe {
totalLocked: number
totalAllocated: number
}
export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
const totals = {
totalLocked: 0,
totalAllocated: 0
}
const queryContext = getQueryContext(1)
const response = await axios.post(`https://df-sql.oceandao.org/nftinfo`)
totals.totalAllocated = response.data?.reduce(
(previousValue: number, currentValue: { ve_allocated: any }) =>
previousValue + Number(currentValue.ve_allocated),
0
)
const fetchedLocked: OperationResult<AllLocked, any> = await fetchData(
AllLocked,
null,
queryContext
)
totals.totalLocked = fetchedLocked.data?.veOCEANs.reduce(
(previousValue, currentValue) =>
previousValue + Number(currentValue.lockedAmount),
0
)
return totals
}

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

@ -1,4 +1,4 @@
import AssetTeaser from '@shared/AssetTeaser/AssetTeaser' import AssetTeaser from '@shared/AssetTeaser'
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import Pagination from '@shared/Pagination' import Pagination from '@shared/Pagination'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -1,62 +0,0 @@
import React, { ReactElement } from 'react'
import Link from 'next/link'
import Dotdotdot from 'react-dotdotdot'
import Price from '@shared/Price'
import removeMarkdown from 'remove-markdown'
import Publisher from '@shared/Publisher'
import AssetType from '@shared/AssetType'
import NetworkName from '@shared/NetworkName'
import styles from './AssetTeaser.module.css'
import { getServiceByName } from '@utils/ddo'
declare type AssetTeaserProps = {
asset: AssetExtended
noPublisher?: boolean
}
export default function AssetTeaser({
asset,
noPublisher
}: AssetTeaserProps): ReactElement {
const { name, type, description } = asset.metadata
const { datatokens } = asset
const isCompute = Boolean(getServiceByName(asset, 'compute'))
const accessType = isCompute ? 'compute' : 'access'
const { owner } = asset.nft
const { orders } = asset.stats
return (
<article className={`${styles.teaser} ${styles[type]}`}>
<Link href={`/asset/${asset.id}`}>
<a className={styles.link}>
<header className={styles.header}>
<div className={styles.symbol}>{datatokens[0]?.symbol}</div>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
{name.slice(0, 200)}
</Dotdotdot>
{!noPublisher && (
<Publisher account={owner} minimal className={styles.publisher} />
)}
</header>
<AssetType
type={type}
accessType={accessType}
className={styles.typeDetails}
totalSales={orders}
/>
<div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot>
</div>
<footer className={styles.foot}>
<Price accessDetails={asset.accessDetails} size="small" />
<NetworkName networkId={asset.chainId} className={styles.network} />
</footer>
</a>
</Link>
</article>
)
}

View File

@ -9,6 +9,8 @@
height: 100%; height: 100%;
color: var(--color-secondary); color: var(--color-secondary);
position: relative; position: relative;
padding-top: calc(var(--spacer) / 2);
padding-bottom: calc(var(--spacer) / 2);
/* for sticking footer to bottom */ /* for sticking footer to bottom */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -18,8 +20,12 @@
background-color: var(--background-body); background-color: var(--background-body);
} }
.detailLine {
margin-bottom: calc(var(--spacer) / 2);
}
.content { .content {
margin-top: calc(var(--spacer) / 2); margin-top: calc(var(--spacer) / 3);
overflow-wrap: break-word; overflow-wrap: break-word;
hyphens: auto; hyphens: auto;
/* for sticking footer to bottom */ /* for sticking footer to bottom */
@ -27,7 +33,7 @@
} }
.content p { .content p {
margin-bottom: calc(var(--spacer) / 4); margin-bottom: calc(var(--spacer) / 3);
} }
.title { .title {
@ -37,36 +43,20 @@
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.publisher { .footer {
display: block;
}
.foot {
margin-top: calc(var(--spacer) / 4); margin-top: calc(var(--spacer) / 4);
display: flex;
justify-content: space-between;
align-items: flex-end;
} }
.foot p { .typeLabel {
margin: 0;
}
.symbol {
display: block;
}
.typeDetails {
position: absolute;
top: calc(var(--spacer) / 3);
right: calc(var(--spacer) / 3);
width: auto;
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
display: inline-block;
border-left: 1px solid var(--border-color);
padding-left: calc(var(--spacer) / 3.5);
margin-left: calc(var(--spacer) / 4);
} }
.network { .typeLabel:first-child {
font-size: var(--font-size-mini); border-left: none;
position: absolute; padding-left: 0;
right: calc(var(--spacer) / 3); margin-left: 0;
bottom: calc(var(--spacer) / 3);
} }

View File

@ -0,0 +1,84 @@
import React, { ReactElement } from 'react'
import Link from 'next/link'
import Dotdotdot from 'react-dotdotdot'
import Price from '@shared/Price'
import removeMarkdown from 'remove-markdown'
import Publisher from '@shared/Publisher'
import AssetType from '@shared/AssetType'
import NetworkName from '@shared/NetworkName'
import styles from './index.module.css'
import { getServiceByName } from '@utils/ddo'
import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences'
declare type AssetTeaserProps = {
asset: AssetExtended
noPublisher?: boolean
}
export default function AssetTeaser({
asset,
noPublisher
}: AssetTeaserProps): ReactElement {
const { name, type, description } = asset.metadata
const { datatokens } = asset
const isCompute = Boolean(getServiceByName(asset, 'compute'))
const accessType = isCompute ? 'compute' : 'access'
const { owner } = asset.nft
const { orders, allocated } = asset.stats
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
const { locale } = useUserPreferences()
return (
<article className={`${styles.teaser} ${styles[type]}`}>
<Link href={`/asset/${asset.id}`}>
<a className={styles.link}>
<aside className={styles.detailLine}>
<AssetType
className={styles.typeLabel}
type={type}
accessType={accessType}
/>
<span className={styles.typeLabel}>{datatokens[0]?.symbol}</span>
<NetworkName
networkId={asset.chainId}
className={styles.typeLabel}
/>
</aside>
<header className={styles.header}>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
{name.slice(0, 200)}
</Dotdotdot>
{!noPublisher && <Publisher account={owner} minimal />}
</header>
<div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot>
</div>
{isUnsupportedPricing ? (
<strong>No pricing schema available</strong>
) : (
<Price accessDetails={asset.accessDetails} size="small" />
)}
<footer className={styles.footer}>
{allocated && allocated > 0 ? (
<span className={styles.typeLabel}>
{allocated < 0
? ''
: `${formatPrice(allocated, locale)} veOCEAN`}
</span>
) : null}
{orders && orders > 0 ? (
<span className={styles.typeLabel}>
{orders < 0
? 'N/A'
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
</span>
) : null}
</footer>
</a>
</Link>
</article>
)
}

View File

@ -7,13 +7,11 @@ import Lock from '@images/lock.svg'
export default function AssetType({ export default function AssetType({
type, type,
accessType, accessType,
className, className
totalSales
}: { }: {
type: string type: string
accessType: string accessType: string
className?: string className?: string
totalSales?: number
}): ReactElement { }): ReactElement {
return ( return (
<div className={className || null}> <div className={className || null}>
@ -26,14 +24,8 @@ export default function AssetType({
)} )}
<div className={styles.typeLabel}> <div className={styles.typeLabel}>
{type === 'dataset' ? 'data set' : 'algorithm'} {type === 'dataset' ? 'dataset' : 'algorithm'}
</div> </div>
{totalSales ? (
<div className={styles.typeLabel}>
{`${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
</div>
) : null}
</div> </div>
) )
} }

View File

@ -50,7 +50,7 @@ function getConsumeHelpText(
: hasPreviousOrder : hasPreviousOrder
? `You bought this ${assetType} already allowing you to use it without paying again.` ? `You bought this ${assetType} already allowing you to use it without paying again.`
: hasDatatoken : hasDatatoken
? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying ${btSymbol} again.` ? `You own ${dtBalance} ${dtSymbol} allowing you to use this dataset by spending 1 ${dtSymbol}, but without paying ${btSymbol} again.`
: isBalanceSufficient === false : isBalanceSufficient === false
? `You do not have enough ${btSymbol} in your wallet to purchase this asset.` ? `You do not have enough ${btSymbol} in your wallet to purchase this asset.`
: `For using this ${assetType}, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher.` : `For using this ${assetType}, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher.`

View File

@ -1,13 +1,10 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import filesize from 'filesize' import { filesize } from 'filesize'
import classNames from 'classnames/bind'
import cleanupContentType from '@utils/cleanupContentType' import cleanupContentType from '@utils/cleanupContentType'
import styles from './index.module.css' import styles from './index.module.css'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import { FileInfo } from '@oceanprotocol/lib' import { FileInfo } from '@oceanprotocol/lib'
const cx = classNames.bind(styles)
function LoaderArea() { function LoaderArea() {
return ( return (
<div className={styles.loaderWrap}> <div className={styles.loaderWrap}>
@ -27,11 +24,9 @@ export default function FileIcon({
small?: boolean small?: boolean
isLoading?: boolean isLoading?: boolean
}): ReactElement { }): ReactElement {
const styleClasses = cx({ const styleClasses = `${styles.file} ${small ? styles.small : ''} ${
file: true, className || ''
small, }`
[className]: className
})
return ( return (
<ul className={styleClasses}> <ul className={styleClasses}>
@ -42,7 +37,7 @@ export default function FileIcon({
<li>{cleanupContentType(file.contentType)}</li> <li>{cleanupContentType(file.contentType)}</li>
<li> <li>
{file.contentLength && file.contentLength !== '0' {file.contentLength && file.contentLength !== '0'
? filesize(Number(file.contentLength)) ? filesize(Number(file.contentLength)).toString()
: ''} : ''}
</li> </li>
</> </>

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

@ -0,0 +1,48 @@
.info {
border-radius: var(--border-radius);
padding: calc(var(--spacer) / 2);
border: 1px solid var(--border-color);
background-color: var(--background-highlight);
position: relative;
}
.info ul {
margin: 0;
}
.info li {
display: inline-block;
font-size: var(--font-size-small);
margin-right: calc(var(--spacer) / 2);
color: var(--color-secondary);
}
.info li.success {
color: var(--brand-alert-green);
}
.info li.error {
color: var(--brand-alert-red);
}
.contianer {
margin: 0;
font-size: var(--font-size-base);
line-height: var(--line-height);
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
padding-right: calc(var(--spacer) / 2);
}
.removeButton {
cursor: pointer;
border: none;
position: absolute;
top: -0.2rem;
right: 0;
font-size: var(--font-size-h3);
cursor: pointer;
color: var(--font-color-text);
background-color: transparent;
}

View File

@ -0,0 +1,29 @@
import React, { ReactElement } from 'react'
import styles from './Info.module.css'
export default function ImageInfo({
image,
tag,
valid,
handleClose
}: {
image: string
tag: string
valid: boolean
handleClose(): void
}): ReactElement {
const displayText = valid
? '✓ Image found, container checksum automatically added!'
: 'x Container checksum could not be fetched automatically, please add it manually'
return (
<div className={styles.info}>
<h3 className={styles.contianer}>{`Image: ${image} Tag: ${tag}`}</h3>
<ul>
<li className={valid ? styles.success : styles.error}>{displayText}</li>
</ul>
<button className={styles.removeButton} onClick={handleClose}>
&times;
</button>
</div>
)
}

View File

@ -0,0 +1,83 @@
import React, { ReactElement, useState } from 'react'
import { useField, useFormikContext } from 'formik'
import UrlInput from '../URLInput'
import { InputProps } from '@shared/FormInput'
import { FormPublishData } from 'src/components/Publish/_types'
import { LoggerInstance } from '@oceanprotocol/lib'
import ImageInfo from './Info'
import { getContainerChecksum } from '@utils/docker'
export default function ContainerInput(props: InputProps): ReactElement {
const [field] = useField(props.name)
const [fieldChecksum, metaChecksum, helpersChecksum] = useField(
'metadata.dockerImageCustomChecksum'
)
const { values, setFieldError, setFieldValue } =
useFormikContext<FormPublishData>()
const [isLoading, setIsLoading] = useState(false)
const [isValid, setIsValid] = useState(false)
const [checked, setChecked] = useState(false)
async function handleValidation(e: React.SyntheticEvent, container: string) {
e.preventDefault()
try {
setIsLoading(true)
const parsedContainerValue = container?.split(':')
const imageName =
parsedContainerValue?.length > 1
? parsedContainerValue?.slice(0, -1).join(':')
: parsedContainerValue[0]
const tag =
parsedContainerValue?.length > 1 ? parsedContainerValue?.at(-1) : ''
const containerInfo = await getContainerChecksum(imageName, tag)
setFieldValue('metadata.dockerImageCustom', imageName)
setFieldValue('metadata.dockerImageCustomTag', tag)
setChecked(true)
if (containerInfo.checksum) {
setFieldValue(
'metadata.dockerImageCustomChecksum',
containerInfo.checksum
)
helpersChecksum.setTouched(false)
setIsValid(true)
}
} catch (error) {
setFieldError(`${field.name}[0].url`, error.message)
LoggerInstance.error(error.message)
} finally {
setIsLoading(false)
}
}
function handleClose() {
setFieldValue('metadata.dockerImageCustom', '')
setFieldValue('metadata.dockerImageCustomTag', '')
setFieldValue('metadata.dockerImageCustomChecksum', '')
setChecked(false)
setIsValid(false)
helpersChecksum.setTouched(true)
}
return (
<>
{checked ? (
<ImageInfo
image={values.metadata.dockerImageCustom}
tag={values.metadata.dockerImageCustomTag}
valid={isValid}
handleClose={handleClose}
/>
) : (
<UrlInput
submitText="Use"
{...props}
name={`${field.name}[0].url`}
checkUrl={false}
isLoading={isLoading}
handleButtonClick={handleValidation}
/>
)}
</>
)
}

View File

@ -12,12 +12,14 @@ export default function URLInput({
handleButtonClick, handleButtonClick,
isLoading, isLoading,
name, name,
checkUrl,
...props ...props
}: { }: {
submitText: string submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean isLoading: boolean
name: string name: string
checkUrl?: boolean
}): ReactElement { }): ReactElement {
const [field, meta] = useField(name) const [field, meta] = useField(name)
const [isButtonDisabled, setIsButtonDisabled] = useState(true) const [isButtonDisabled, setIsButtonDisabled] = useState(true)
@ -28,7 +30,7 @@ export default function URLInput({
setIsButtonDisabled( setIsButtonDisabled(
!field?.value || !field?.value ||
field.value === '' || field.value === '' ||
!isUrl(field.value) || (checkUrl && !isUrl(field.value)) ||
field.value.includes('javascript:') || field.value.includes('javascript:') ||
meta?.error meta?.error
) )

View File

@ -11,6 +11,8 @@ import AssetSelection, {
} from '../FormFields/AssetSelection' } from '../FormFields/AssetSelection'
import Nft from '../FormFields/Nft' import Nft from '../FormFields/Nft'
import InputRadio from './InputRadio' import InputRadio from './InputRadio'
import ContainerInput from '@shared/FormFields/ContainerInput'
import TagsAutoComplete from './TagsAutoComplete'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -107,6 +109,8 @@ export default function InputElement({
) )
case 'files': case 'files':
return <FilesInput {...field} {...props} /> return <FilesInput {...field} {...props} />
case 'container':
return <ContainerInput {...field} {...props} />
case 'providerUrl': case 'providerUrl':
return <CustomProvider {...field} {...props} /> return <CustomProvider {...field} {...props} />
case 'nft': case 'nft':
@ -121,6 +125,8 @@ export default function InputElement({
{...props} {...props}
/> />
) )
case 'tags':
return <TagsAutoComplete {...field} {...props} />
default: default:
return prefix || postfix ? ( return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}> <div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>

View File

@ -0,0 +1,101 @@
.select [class$='control'] {
border-color: var(--border-color);
border-radius: var(--border-radius);
box-shadow: none;
background-color: var(--background-content);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
cursor: text;
min-height: 43px;
}
.select [class$='control']:hover {
border-color: var(--border-color);
}
.select [class$='control']:focus-within {
border-color: var(--font-color-text);
}
.select [class$='ValueContainer'] {
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 3);
}
.select [class$='Input'] {
margin: 0;
padding-bottom: 0;
padding-top: 0;
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
}
.select input {
color: var(--font-color-heading) !important;
}
.select [class$='menu'] {
background-color: var(--background-highlight);
border-radius: var(--border-radius);
}
.select [class$='option'] {
color: var(--font-color-heading);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
}
.select [class$='option']:active {
background-color: var(--color-secondary);
}
.select [class$='multiValue'],
.select [class$='multiValue'] > *,
.select [class$='multiValue']:hover > * {
border-radius: var(--border-radius);
background-color: var(--background-highlight);
color: var(--font-color-text);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
padding-top: 0;
padding-bottom: 0;
}
.select [class$='multiValue'] > div[role$='button'],
.select [class$='indicatorContainer'] svg {
cursor: pointer;
}
.select [class$='placeholder'] {
margin-left: 0;
margin-right: 0;
color: var(--color-secondary);
font-weight: var(--font-weight-base);
transition: 0.2s ease-out;
opacity: 0.7;
}
.select [class$='menu'] {
background-color: var(--background-content);
margin-top: -2px;
border: 1px solid var(--font-color-text);
border-top-color: var(--border-color);
border-top-left-radius: 0;
border-top-right-radius: 0;
box-shadow: none;
}
.select [class$='menu'] [class$='option']:hover,
.select [class$='menu'] [class$='option']:focus-within {
background-color: var(--font-color-heading);
color: var(--background-content);
}
.select [class$='NoOptionsMessage'] {
font-size: var(--font-size-small);
color: var(--color-secondary);
text-align: left;
}

View File

@ -0,0 +1,92 @@
import React, { ReactElement, useEffect, useState } from 'react'
import CreatableSelect from 'react-select/creatable'
import { OnChangeValue } from 'react-select'
import { useField } from 'formik'
import { InputProps } from '.'
import { getTagsList } from '@utils/aquarius'
import { chainIds } from 'app.config'
import { useCancelToken } from '@hooks/useCancelToken'
import styles from './TagsAutoComplete.module.css'
import { matchSorter } from 'match-sorter'
interface AutoCompleteOption {
readonly value: string
readonly label: string
}
export default function TagsAutoComplete({
...props
}: InputProps): ReactElement {
const { name, placeholder } = props
const [tagsList, setTagsList] = useState<AutoCompleteOption[]>()
const [matchedTagsList, setMatchedTagsList] = useState<AutoCompleteOption[]>(
[]
)
const [field, meta, helpers] = useField(name)
const [input, setInput] = useState<string>()
const newCancelToken = useCancelToken()
const generateAutocompleteOptions = (
options: string[]
): AutoCompleteOption[] => {
return options?.map((tag) => ({
value: tag,
label: tag
}))
}
const defaultTags = !field.value
? undefined
: generateAutocompleteOptions(field.value)
useEffect(() => {
const generateTagsList = async () => {
const tags = await getTagsList(chainIds, newCancelToken())
const autocompleteOptions = generateAutocompleteOptions(tags)
setTagsList(autocompleteOptions)
}
generateTagsList()
}, [newCancelToken])
const handleChange = (userInput: OnChangeValue<AutoCompleteOption, true>) => {
const normalizedInput = userInput.map((input) => input.value)
helpers.setValue(normalizedInput)
helpers.setTouched(true)
}
const handleOptionsFilter = (
options: AutoCompleteOption[],
input: string
): void => {
setInput(input)
const matchedTagsList = matchSorter(options, input, { keys: ['value'] })
setMatchedTagsList(matchedTagsList)
}
return (
<CreatableSelect
components={{
DropdownIndicator: () => null,
IndicatorSeparator: () => null
}}
className={styles.select}
defaultValue={defaultTags}
hideSelectedOptions
isMulti
isClearable={false}
noOptionsMessage={() =>
'Start typing to get suggestions based on tags from all published assets.'
}
onChange={(value: AutoCompleteOption[]) => handleChange(value)}
onInputChange={(value) => handleOptionsFilter(tagsList, value)}
openMenuOnClick
options={!input || input?.length < 1 ? [] : matchedTagsList}
placeholder={placeholder}
theme={(theme) => ({
...theme,
colors: { ...theme.colors, primary25: 'var(--border-color)' }
})}
/>
)
}

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

@ -20,6 +20,18 @@
} }
} }
.stat {
border-left: 1px solid var(--border-color);
padding-left: calc(var(--spacer) / 3.5);
margin-left: calc(var(--spacer) / 4);
}
.stat:first-child {
border-left: none;
padding-left: 0;
margin-left: 0;
}
.number { .number {
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
color: var(--font-color-heading); color: var(--font-color-heading);

View File

@ -1,19 +1,43 @@
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import React from 'react' import { useUserPreferences } from '@context/UserPreferences'
import { formatPrice } from '@shared/Price/PriceUnit'
import React, { useEffect, useState } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
export default function AssetStats() { export default function AssetStats() {
const { locale } = useUserPreferences()
const { asset } = useAsset() const { asset } = useAsset()
const [orders, setOrders] = useState(0)
const [allocated, setAllocated] = useState(0)
useEffect(() => {
if (!asset) return
const { orders, allocated } = asset.stats
setOrders(orders)
setAllocated(allocated)
}, [asset])
return ( return (
<footer className={styles.stats}> <footer className={styles.stats}>
{!asset || !asset?.stats || asset?.stats?.orders === 0 ? ( {allocated && allocated > 0 ? (
<span className={styles.stat}>
<span className={styles.number}>
{formatPrice(allocated, locale)}
</span>
veOCEAN
</span>
) : null}
{!asset || !asset?.stats || orders < 0 ? (
'N/A'
) : orders === 0 ? (
'No sales yet' 'No sales yet'
) : ( ) : (
<> <span className={styles.stat}>
<span className={styles.number}>{asset.stats.orders}</span> sale <span className={styles.number}>{orders}</span> sale
{asset.stats.orders === 1 ? '' : 's'} {orders === 1 ? '' : 's'}
</> </span>
)} )}
</footer> </footer>
) )

View File

@ -5,14 +5,17 @@ import Caret from '@images/caret.svg'
export default function ComputeHistory({ export default function ComputeHistory({
title, title,
children children,
refetchJobs
}: { }: {
title: string title: string
children: ReactNode children: ReactNode
refetchJobs?: any
}): ReactElement { }): ReactElement {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
function handleClick() { async function handleClick() {
await refetchJobs(true)
setOpen(!open) setOpen(!open)
} }

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

@ -1,4 +1,4 @@
import React, { useState, ReactElement, useEffect } from 'react' import React, { useState, ReactElement, useEffect, useCallback } from 'react'
import { import {
Asset, Asset,
DDO, DDO,
@ -29,7 +29,8 @@ import {
isOrderable, isOrderable,
getAlgorithmAssetSelectionList, getAlgorithmAssetSelectionList,
getAlgorithmsForAsset, getAlgorithmsForAsset,
getComputeEnviroment getComputeEnviroment,
getComputeJobs
} from '@utils/compute' } from '@utils/compute'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection' import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import AlgorithmDatasetsListForCompute from './AlgorithmDatasetsListForCompute' import AlgorithmDatasetsListForCompute from './AlgorithmDatasetsListForCompute'
@ -43,7 +44,9 @@ import { handleComputeOrder } from '@utils/order'
import { getComputeFeedback } from '@utils/feedback' import { getComputeFeedback } from '@utils/feedback'
import { getDummyWeb3 } from '@utils/web3' import { getDummyWeb3 } from '@utils/web3'
import { initializeProviderForCompute } from '@utils/provider' import { initializeProviderForCompute } from '@utils/provider'
import { useUserPreferences } from '@context/UserPreferences'
const refreshInterval = 10000 // 10 sec.
export default function Compute({ export default function Compute({
asset, asset,
dtBalance, dtBalance,
@ -58,6 +61,7 @@ export default function Compute({
consumableFeedback?: string consumableFeedback?: string
}): ReactElement { }): ReactElement {
const { accountId, web3 } = useWeb3() const { accountId, web3 } = useWeb3()
const { chainIds } = useUserPreferences()
const newAbortController = useAbortController() const newAbortController = useAbortController()
const newCancelToken = useCancelToken() const newCancelToken = useCancelToken()
@ -91,6 +95,8 @@ export default function Compute({
const [isRequestingAlgoOrderPrice, setIsRequestingAlgoOrderPrice] = const [isRequestingAlgoOrderPrice, setIsRequestingAlgoOrderPrice] =
useState(false) useState(false)
const [refetchJobs, setRefetchJobs] = useState(false) const [refetchJobs, setRefetchJobs] = useState(false)
const [isLoadingJobs, setIsLoadingJobs] = useState(false)
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
const hasDatatoken = Number(dtBalance) >= 1 const hasDatatoken = Number(dtBalance) >= 1
const isComputeButtonDisabled = const isComputeButtonDisabled =
@ -243,6 +249,44 @@ export default function Compute({
}) })
}, [asset, isUnsupportedPricing]) }, [asset, isUnsupportedPricing])
const fetchJobs = useCallback(
async (type: string) => {
if (!chainIds || chainIds.length === 0 || !accountId) {
return
}
try {
type === 'init' && setIsLoadingJobs(true)
const computeJobs = await getComputeJobs(
[asset?.chainId] || chainIds,
accountId,
asset,
newCancelToken()
)
setJobs(computeJobs.computeJobs)
setIsLoadingJobs(!computeJobs.isLoaded)
} catch (error) {
LoggerInstance.error(error.message)
setIsLoadingJobs(false)
}
},
[accountId, asset, chainIds, isLoadingJobs, newCancelToken]
)
useEffect(() => {
fetchJobs('init')
// init periodic refresh for jobs
const balanceInterval = setInterval(
() => fetchJobs('repeat'),
refreshInterval
)
return () => {
clearInterval(balanceInterval)
}
}, [refetchJobs])
// Output errors in toast UI // Output errors in toast UI
useEffect(() => { useEffect(() => {
const newError = error const newError = error
@ -266,10 +310,10 @@ export default function Compute({
computeAlgorithm, computeAlgorithm,
selectedAlgorithmAsset selectedAlgorithmAsset
) )
LoggerInstance.log('[compute] Is data set orderable?', allowed) LoggerInstance.log('[compute] Is dataset orderable?', allowed)
if (!allowed) if (!allowed)
throw new Error( throw new Error(
'Data set is not orderable in combination with selected algorithm.' 'Dataset is not orderable in combination with selected algorithm.'
) )
await initPriceAndFees() await initPriceAndFees()
@ -376,7 +420,7 @@ export default function Compute({
{asset.services[0].type === 'compute' && ( {asset.services[0].type === 'compute' && (
<Alert <Alert
text={ text={
"This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!" "This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed datasets though!"
} }
state="info" state="info"
/> />
@ -443,11 +487,15 @@ export default function Compute({
)} )}
</footer> </footer>
{accountId && asset?.accessDetails?.datatoken && ( {accountId && asset?.accessDetails?.datatoken && (
<ComputeHistory title="Your Compute Jobs"> <ComputeHistory
title="Your Compute Jobs"
refetchJobs={() => setRefetchJobs(!refetchJobs)}
>
<ComputeJobs <ComputeJobs
minimal minimal
assetChainIds={[asset?.chainId]} jobs={jobs}
refetchJobs={refetchJobs} isLoading={isLoadingJobs}
refetchJobs={() => setRefetchJobs(!refetchJobs)}
/> />
</ComputeHistory> </ComputeHistory>
)} )}

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'
@ -131,7 +129,7 @@ export default function EditComputeDataset({
{({ values, isSubmitting }) => {({ values, isSubmitting }) =>
isSubmitting || hasFeedback ? ( isSubmitting || hasFeedback ? (
<EditFeedback <EditFeedback
loading="Updating data set with new compute settings..." loading="Updating dataset with new compute settings..."
error={error} error={error}
success={success} success={success}
setError={setError} setError={setError}

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'
@ -43,8 +44,8 @@ export default function Edit({
const config = getOceanConfig(asset.chainId) const config = getOceanConfig(asset.chainId)
const fixedRateInstance = new FixedRateExchange( const fixedRateInstance = new FixedRateExchange(
web3, config.fixedRateExchangeAddress,
config.fixedRateExchangeAddress web3
) )
const setPriceResp = await fixedRateInstance.setRate( const setPriceResp = await fixedRateInstance.setRate(
@ -72,7 +73,8 @@ export default function Edit({
name: values.name, name: values.name,
description: values.description, description: values.description,
links: linksTransformed, links: linksTransformed,
author: values.author author: values.author,
tags: values.tags
} }
asset?.accessDetails?.type === 'fixed' && asset?.accessDetails?.type === 'fixed' &&

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,14 @@ 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,
tags: metadata?.tags
} }
} }
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

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