mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge branch 'main' into feature/issue-1418-dark-mode-toggle
This commit is contained in:
commit
4ef734891a
@ -53,7 +53,8 @@
|
||||
"object": true,
|
||||
"array": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"testing-library/no-node-access": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
@ -23,13 +23,13 @@ jobs:
|
||||
node: ['16']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-
|
||||
|
||||
- run: npm ci --legacy-peer-deps
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
test:
|
||||
@ -50,13 +50,13 @@ jobs:
|
||||
node: ['16']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@ -64,11 +64,11 @@ jobs:
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-
|
||||
|
||||
- run: npm ci --legacy-peer-deps
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-${{ runner.os }}
|
||||
path: coverage/
|
||||
@ -79,12 +79,12 @@ jobs:
|
||||
if: ${{ success() && github.actor != 'dependabot[bot]' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@ -92,11 +92,11 @@ jobs:
|
||||
key: ${{ runner.os }}-coverage-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: ${{ runner.os }}-coverage-${{ env.cache-name }}-
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: coverage-${{ runner.os }}
|
||||
|
||||
- run: npm ci --legacy-peer-deps
|
||||
- run: npm ci
|
||||
- run: npm run codegen:apollo
|
||||
|
||||
- uses: paambaati/codeclimate-action@v3.0.0
|
||||
@ -113,13 +113,13 @@ jobs:
|
||||
node: ['16']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@ -127,6 +127,6 @@ jobs:
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
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 storybook:build
|
||||
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@ -35,11 +35,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@ -64,4 +64,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@ -10,9 +10,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- run: npm ci --legacy-peer-deps
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- run: npm ci
|
||||
|
||||
- run: npm run build:static
|
||||
env:
|
||||
|
73
.jest/__mocks__/MarketMetadata.ts
Normal file
73
.jest/__mocks__/MarketMetadata.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import siteContent from '../../content/site.json'
|
||||
import appConfig from '../../app.config'
|
||||
|
||||
export default {
|
||||
getOpcFeeForToken: jest.fn(),
|
||||
siteContent,
|
||||
appConfig,
|
||||
opcFees: [
|
||||
{
|
||||
chainId: 1,
|
||||
approvedTokens: [
|
||||
'0x0642026e7f0b6ccac5925b4e7fa61384250e1701',
|
||||
'0x967da4048cd07ab37855c090aaf366e4ce1b9f48'
|
||||
],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
},
|
||||
{
|
||||
chainId: 137,
|
||||
approvedTokens: [
|
||||
'0x282d8efce846a88b159800bd4130ad77443fa1a1',
|
||||
'0xc5248aa0629c0b2d6a02834a5f172937ac83cbd3'
|
||||
],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
},
|
||||
{
|
||||
chainId: 56,
|
||||
approvedTokens: ['0xdce07662ca8ebc241316a15b611c89711414dd1a'],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
},
|
||||
{
|
||||
chainId: 246,
|
||||
approvedTokens: ['0x593122aae80a6fc3183b2ac0c4ab3336debee528'],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
},
|
||||
{
|
||||
chainId: 1285,
|
||||
approvedTokens: ['0x99c409e5f62e4bd2ac142f17cafb6810b8f0baae'],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
},
|
||||
{
|
||||
chainId: 3,
|
||||
approvedTokens: ['0x5e8dcb2afa23844bcc311b00ad1a0c30025aade9'],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
},
|
||||
{
|
||||
chainId: 4,
|
||||
approvedTokens: [
|
||||
'0x8967bcf84170c91b0d24d4302c2376283b0b3a07',
|
||||
'0xd92e713d051c37ebb2561803a3b5fbabc4962431'
|
||||
],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
},
|
||||
{
|
||||
chainId: 80001,
|
||||
approvedTokens: ['0xd8992ed72c445c35cb4a2be468568ed1079357c8'],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
},
|
||||
{
|
||||
chainId: 1287,
|
||||
approvedTokens: ['0xf6410bf5d773c7a41ebff972f38e7463fa242477'],
|
||||
swapApprovedFee: '0.001',
|
||||
swapNotApprovedFee: '0.002'
|
||||
}
|
||||
]
|
||||
}
|
@ -14,7 +14,7 @@ const customJestConfig = {
|
||||
moduleDirectories: ['node_modules', '<rootDir>/src'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
moduleNameMapper: {
|
||||
'\\.svg': '<rootDir>/.jest/__mocks__/svgrMock.tsx',
|
||||
'^.+\\.(svg)$': '<rootDir>/.jest/__mocks__/svgrMock.tsx',
|
||||
// '^@/components/(.*)$': '<rootDir>/components/$1',
|
||||
'@shared(.*)$': '<rootDir>/src/components/@shared/$1',
|
||||
'@hooks/(.*)$': '<rootDir>/src/@hooks/$1',
|
||||
|
@ -1,2 +1,7 @@
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
import './__mocks__/matchMedia'
|
||||
import marketMetadataMock from './__mocks__/MarketMetadata'
|
||||
|
||||
jest.mock('../../src/@context/MarketMetadata', () => ({
|
||||
useMarketMetadata: () => marketMetadataMock
|
||||
}))
|
||||
|
12
.jest/testRender.ts
Normal file
12
.jest/testRender.ts
Normal 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
|
@ -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())
|
||||
}
|
||||
})
|
54
README.md
54
README.md
@ -16,7 +16,7 @@
|
||||
- [🦀 Data Sources](#-data-sources)
|
||||
- [Aquarius](#aquarius)
|
||||
- [Ocean Protocol Subgraph](#ocean-protocol-subgraph)
|
||||
- [3Box](#3box)
|
||||
- [ENS](#ens)
|
||||
- [Purgatory](#purgatory)
|
||||
- [Network Metadata](#network-metadata)
|
||||
- [👩🎤 Storybook](#-storybook)
|
||||
@ -103,12 +103,12 @@ cp .env.example .env
|
||||
|
||||
## 🦀 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
|
||||
- the actual data set files
|
||||
- the NFT which represents the data set
|
||||
- the datatokens representing access rights to the data set files
|
||||
- metadata about an asset
|
||||
- the actual asset file
|
||||
- the NFT which represents the asset
|
||||
- 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
|
||||
- calculations and conversions based on financial data
|
||||
- metadata about publisher accounts
|
||||
@ -117,7 +117,7 @@ All this data then comes from multiple sources:
|
||||
|
||||
### Aquarius
|
||||
|
||||
All initial data sets and their metadata (DDO) is retrieved client-side on run-time from the [Aquarius](https://github.com/oceanprotocol/aquarius) instance, 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)
|
||||
- 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
|
||||
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
|
||||
import get3BoxProfile from '@utils/profile'
|
||||
import { useProfile } from '@context/Profile'
|
||||
|
||||
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 (
|
||||
<div>
|
||||
{profile.emoji} {profile.name}
|
||||
{profile.avatar} {profile.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -232,7 +216,7 @@ function Component() {
|
||||
|
||||
### 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:
|
||||
|
||||
@ -399,6 +383,12 @@ Additionally, we would also advise that your retain the text saying "Powered by
|
||||
|
||||
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
|
||||
|
||||
If you are looking to fork Ocean Market and create your own marketplace, you will find the following guides useful in our docs:
|
||||
|
||||
- [Forking Ocean Market](https://docs.oceanprotocol.com/building-with-ocean/build-a-marketplace/forking-ocean-market)
|
||||
- [Customising your Market](https://docs.oceanprotocol.com/building-with-ocean/build-a-marketplace/customising-your-market)
|
||||
- [Deploying your Market](https://docs.oceanprotocol.com/building-with-ocean/build-a-marketplace/deploying-market)
|
||||
|
||||
## 💰 Pricing Options
|
||||
|
||||
### Fixed Pricing
|
||||
@ -409,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).
|
||||
|
||||
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
|
||||
|
||||
|
@ -62,7 +62,11 @@ module.exports = {
|
||||
'LINK'
|
||||
],
|
||||
|
||||
// Config for https://github.com/donavon/use-dark-mode
|
||||
// Tokens to fetch the spot prices from coingecko, against above currencies.
|
||||
// Refers to Coingecko API tokenIds.
|
||||
coingeckoTokenIds: ['ocean-protocol', 'h2o', 'ethereum', 'matic-network'],
|
||||
|
||||
// Config for https://github.com/oceanprotocol/use-dark-mode
|
||||
darkModeConfig: {
|
||||
classNameDark: 'dark',
|
||||
classNameLight: 'light',
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"form": {
|
||||
"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.",
|
||||
"error": "Updating DDO failed.",
|
||||
"data": [
|
||||
{
|
||||
"name": "publisherTrustedAlgorithms",
|
||||
"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",
|
||||
"multiple": true,
|
||||
"options": [],
|
||||
@ -17,7 +17,7 @@
|
||||
{
|
||||
"name": "allowAllPublishedAlgorithms",
|
||||
"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",
|
||||
"options": ["Allow any published algorithm"]
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
||||
"name": "files",
|
||||
"label": "New file",
|
||||
"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,
|
||||
"type": "files"
|
||||
},
|
||||
@ -39,7 +39,7 @@
|
||||
"name": "links",
|
||||
"label": "New sample file",
|
||||
"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,
|
||||
"type": "files"
|
||||
},
|
||||
@ -47,7 +47,7 @@
|
||||
{
|
||||
"name": "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",
|
||||
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
||||
"sortOptions": false,
|
||||
@ -57,7 +57,7 @@
|
||||
"name": "author",
|
||||
"label": "New Author",
|
||||
"placeholder": "e.g. Mrs McJellyfish",
|
||||
"help": "Give proper attribution for your data set.",
|
||||
"help": "Give proper attribution for your dataset.",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Account",
|
||||
"description": "Find the data sets and jobs that you previously accessed."
|
||||
"description": "Find the datasets and jobs that you previously accessed."
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"create": {
|
||||
"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": {
|
||||
"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."
|
||||
@ -10,7 +10,7 @@
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"name": "freeAgreement",
|
||||
|
@ -33,7 +33,7 @@
|
||||
"name": "author",
|
||||
"label": "Author",
|
||||
"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
|
||||
},
|
||||
{
|
||||
@ -104,7 +104,7 @@
|
||||
"name": "files",
|
||||
"label": "File",
|
||||
"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,
|
||||
"type": "files",
|
||||
"required": true
|
||||
@ -113,7 +113,7 @@
|
||||
"name": "links",
|
||||
"label": "Sample file",
|
||||
"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,
|
||||
"type": "files"
|
||||
},
|
||||
@ -122,7 +122,7 @@
|
||||
"label": "Algorithm Privacy",
|
||||
"type": "checkbox",
|
||||
"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
|
||||
},
|
||||
{
|
||||
@ -138,7 +138,7 @@
|
||||
{
|
||||
"name": "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",
|
||||
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
||||
"sortOptions": false,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "Publish",
|
||||
"description": "Highlight the important features of your data set or algorithm to make it more discoverable and catch the interest of data consumers.",
|
||||
"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).",
|
||||
"tooltipAvailableNetworks": "Assets are published to the network your wallet is connected to. These networks are currently supported:"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"asset": {
|
||||
"title": "Data Set 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)."
|
||||
"title": "Dataset In 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": {
|
||||
"title": "Account In Purgatory",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"siteImage": "/share.png",
|
||||
"copyright": "All Rights Reserved. Powered by ",
|
||||
|
11
netlify.toml
Normal file
11
netlify.toml
Normal 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)"
|
61084
package-lock.json
generated
61084
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
@ -17,7 +17,7 @@
|
||||
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
|
||||
"type-check": "tsc --noEmit",
|
||||
"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/",
|
||||
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
|
||||
"storybook:build": "cross-env NODE_ENV=test build-storybook"
|
||||
@ -26,28 +26,27 @@
|
||||
"@coingecko/cryptoformat": "^0.5.4",
|
||||
"@loadable/component": "^5.15.2",
|
||||
"@oceanprotocol/art": "^3.2.0",
|
||||
"@oceanprotocol/lib": "^1.1.6",
|
||||
"@oceanprotocol/lib": "^2.0.2",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@storybook/theming": "^6.5.9",
|
||||
"@oceanprotocol/use-dark-mode": "^2.4.3",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@urql/exchange-refocus": "^0.2.5",
|
||||
"@walletconnect/web3-provider": "^1.7.8",
|
||||
"@urql/exchange-refocus": "^1.0.0",
|
||||
"@walletconnect/web3-provider": "^1.8.0",
|
||||
"axios": "^0.27.2",
|
||||
"classnames": "^2.3.1",
|
||||
"date-fns": "^2.29.1",
|
||||
"classnames": "^2.3.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"decimal.js": "^10.3.1",
|
||||
"dom-confetti": "^0.2.2",
|
||||
"dotenv": "^16.0.1",
|
||||
"filesize": "^9.0.1",
|
||||
"filesize": "^9.0.11",
|
||||
"formik": "^2.2.9",
|
||||
"gray-matter": "^4.0.3",
|
||||
"is-url-superb": "^6.1.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"myetherwallet-blockies": "^0.1.1",
|
||||
"next": "^12.1.6",
|
||||
"next": "12.3.1",
|
||||
"query-string": "^7.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-clipboard.js": "^2.0.16",
|
||||
@ -65,58 +64,53 @@
|
||||
"remove-markdown": "^0.5.0",
|
||||
"slugify": "^1.6.5",
|
||||
"swr": "^1.3.0",
|
||||
"urql": "^2.2.1",
|
||||
"use-dark-mode": "^2.3.1",
|
||||
"web3": "^1.7.4",
|
||||
"web3modal": "^1.9.8",
|
||||
"urql": "^3.0.3",
|
||||
"web3": "^1.8.0",
|
||||
"web3modal": "^1.9.9",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^6.5.7",
|
||||
"@storybook/addon-storyshots": "^6.5.9",
|
||||
"@storybook/builder-webpack5": "^6.5.9",
|
||||
"@storybook/manager-webpack5": "^6.5.7",
|
||||
"@storybook/react": "^6.5.7",
|
||||
"@storybook/testing-library": "^0.0.11",
|
||||
"@storybook/testing-react": "^1.3.0",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@storybook/addon-essentials": "^6.5.12",
|
||||
"@storybook/builder-webpack5": "^6.5.12",
|
||||
"@storybook/manager-webpack5": "^6.5.12",
|
||||
"@storybook/react": "^6.5.12",
|
||||
"@storybook/theming": "^6.5.9",
|
||||
"@svgr/webpack": "^6.3.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/loadable__component": "^5.13.4",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.omit": "^4.5.7",
|
||||
"@types/node": "^17.0.41",
|
||||
"@types/node": "^18.7.18",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-paginate": "^7.1.1",
|
||||
"@types/remove-markdown": "^0.3.1",
|
||||
"@types/yup": "^0.29.14",
|
||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||
"@typescript-eslint/parser": "^5.27.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||
"@typescript-eslint/parser": "^5.38.0",
|
||||
"apollo": "^2.34.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-oceanprotocol": "^2.0.3",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest-dom": "^4.0.2",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.30.0",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"eslint-plugin-testing-library": "^5.5.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-testing-library": "^5.6.4",
|
||||
"https-browserify": "^1.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"jest": "^28.1.2",
|
||||
"jest-environment-jsdom": "^28.1.2",
|
||||
"prettier": "^2.6.2",
|
||||
"jest": "^29.0.3",
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"process": "^0.11.10",
|
||||
"serve": "^13.0.2",
|
||||
"serve": "^14.0.1",
|
||||
"stream-http": "^3.2.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.7.3"
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^4.8.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -16,6 +16,7 @@ export interface AppConfig {
|
||||
consumeMarketOrderFee: string
|
||||
consumeMarketFixedSwapFee: string
|
||||
currencies: string[]
|
||||
coingeckoTokenIds: string[]
|
||||
allowFixedPricing: string
|
||||
allowFreePricing: string
|
||||
defaultPrivacyPolicySlug: string
|
||||
|
@ -14,6 +14,7 @@ import { MarketMetadataProviderValue, OpcFee } from './_types'
|
||||
import siteContent from '../../../content/site.json'
|
||||
import appConfig from '../../../app.config'
|
||||
import { fetchData, getQueryContext } from '@utils/subgraph'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
|
||||
const MarketMetadataContext = createContext({} as MarketMetadataProviderValue)
|
||||
|
||||
@ -43,6 +44,11 @@ function MarketMetadataProvider({
|
||||
swapNotApprovedFee: response.data?.opc.swapNonOceanFee
|
||||
} as OpcFee)
|
||||
}
|
||||
LoggerInstance.log('[MarketMetadata] Got new data.', {
|
||||
opcFees: opcData,
|
||||
siteContent,
|
||||
appConfig
|
||||
})
|
||||
setOpcFees(opcData)
|
||||
}
|
||||
getOpcData()
|
||||
|
13
src/@context/Prices/_constants.ts
Normal file
13
src/@context/Prices/_constants.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Prices } from './_types'
|
||||
import { coingeckoTokenIds } from '../../../app.config'
|
||||
|
||||
export const initialData: Prices = coingeckoTokenIds.map((tokenId) => ({
|
||||
[tokenId]: {
|
||||
eur: 0.0,
|
||||
usd: 0.0,
|
||||
eth: 0.0,
|
||||
btc: 0.0
|
||||
}
|
||||
}))[0]
|
||||
|
||||
export const refreshInterval = 120000 // 120 sec.
|
9
src/@context/Prices/_types.ts
Normal file
9
src/@context/Prices/_types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface Prices {
|
||||
[key: string]: {
|
||||
[key: string]: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface PricesValue {
|
||||
prices: Prices
|
||||
}
|
23
src/@context/Prices/_utils.ts
Normal file
23
src/@context/Prices/_utils.ts
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// Deal with differences between token symbol & Coingecko API IDs
|
||||
//
|
||||
export function getCoingeckoTokenId(symbol: string) {
|
||||
// can be OCEAN or mOCEAN
|
||||
const isOcean = symbol?.toLowerCase().includes('ocean')
|
||||
// can be H2O or H20
|
||||
const isH2o = symbol?.toLowerCase().includes('h2')
|
||||
const isEth = symbol?.toLowerCase() === 'eth'
|
||||
const isMatic = symbol?.toLowerCase() === 'matic'
|
||||
|
||||
const priceTokenId = isOcean
|
||||
? 'ocean-protocol'
|
||||
: isH2o
|
||||
? 'h2o'
|
||||
: isEth
|
||||
? 'ethereum'
|
||||
: isMatic
|
||||
? 'matic-network'
|
||||
: symbol?.toLowerCase()
|
||||
|
||||
return priceTokenId
|
||||
}
|
36
src/@context/Prices/index.test.tsx
Normal file
36
src/@context/Prices/index.test.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import * as SWR from 'swr'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { PricesProvider, usePrices, getCoingeckoTokenId } from '.'
|
||||
|
||||
jest.spyOn(SWR, 'default').mockImplementation(() => ({
|
||||
useSWR: { data: { 'ocean-protocol': { eur: '2' } } },
|
||||
isValidating: false,
|
||||
mutate: jest.fn()
|
||||
}))
|
||||
|
||||
const wrapper = ({ children }: { children: ReactElement }) => (
|
||||
<PricesProvider>{children}</PricesProvider>
|
||||
)
|
||||
|
||||
test('should correctly initialize data', async () => {
|
||||
const { result } = renderHook(() => usePrices(), { wrapper })
|
||||
|
||||
expect(result.current.prices['ocean-protocol'].eur).toBeDefined()
|
||||
})
|
||||
|
||||
test('useSWR is called', async () => {
|
||||
const { result } = renderHook(() => usePrices(), { wrapper })
|
||||
expect(SWR.default).toHaveBeenCalled()
|
||||
|
||||
// somehow the above spy seems to not fully work, but this assertion is the goal
|
||||
// expect(result.current.prices['ocean-protocol'].eur).toBe('2')
|
||||
})
|
||||
|
||||
test('should get correct Coingecko API ID for OCEAN', async () => {
|
||||
const id1 = getCoingeckoTokenId('OCEAN')
|
||||
expect(id1).toBe('ocean-protocol')
|
||||
|
||||
const id2 = getCoingeckoTokenId('mOCEAN')
|
||||
expect(id2).toBe('ocean-protocol')
|
||||
})
|
@ -9,24 +9,10 @@ import React, {
|
||||
import { fetchData } from '@utils/fetch'
|
||||
import useSWR from 'swr'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { useMarketMetadata } from './MarketMetadata'
|
||||
|
||||
interface Prices {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
interface PricesValue {
|
||||
prices: Prices
|
||||
}
|
||||
|
||||
const initialData: Prices = {
|
||||
eur: 0.0,
|
||||
usd: 0.0,
|
||||
eth: 0.0,
|
||||
btc: 0.0
|
||||
}
|
||||
|
||||
const refreshInterval = 120000 // 120 sec.
|
||||
import { useMarketMetadata } from '../MarketMetadata'
|
||||
import { Prices, PricesValue } from './_types'
|
||||
import { initialData, refreshInterval } from './_constants'
|
||||
import { getCoingeckoTokenId } from './_utils'
|
||||
|
||||
const PricesContext = createContext(null)
|
||||
|
||||
@ -36,23 +22,23 @@ export default function PricesProvider({
|
||||
children: ReactNode
|
||||
}): ReactElement {
|
||||
const { appConfig } = useMarketMetadata()
|
||||
const tokenId = 'ocean-protocol'
|
||||
|
||||
const [prices, setPrices] = useState(initialData)
|
||||
const [url, setUrl] = useState('')
|
||||
const [url, setUrl] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!appConfig) return
|
||||
// comma-separated list
|
||||
|
||||
const currencies = appConfig.currencies.join(',')
|
||||
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=${currencies}`
|
||||
const tokenIds = appConfig.coingeckoTokenIds.join(',')
|
||||
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenIds}&vs_currencies=${currencies}`
|
||||
setUrl(url)
|
||||
}, [appConfig])
|
||||
|
||||
const onSuccess = async (data: { [tokenId]: Prices }) => {
|
||||
const onSuccess = async (data: Prices) => {
|
||||
if (!data) return
|
||||
LoggerInstance.log('[prices] Got new OCEAN spot prices.', data[tokenId])
|
||||
setPrices(data[tokenId])
|
||||
LoggerInstance.log('[prices] Got new spot prices.', data)
|
||||
setPrices(data)
|
||||
}
|
||||
|
||||
// Fetch new prices periodically with swr
|
||||
@ -71,4 +57,4 @@ export default function PricesProvider({
|
||||
// Helper hook to access the provider values
|
||||
const usePrices = (): PricesValue => useContext(PricesContext)
|
||||
|
||||
export { PricesProvider, usePrices }
|
||||
export { PricesProvider, usePrices, getCoingeckoTokenId }
|
@ -7,15 +7,18 @@ import React, {
|
||||
useCallback,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
import { getUserSales, getUserTokenOrders } from '@utils/subgraph'
|
||||
import { useUserPreferences } from './UserPreferences'
|
||||
import { getUserTokenOrders } from '@utils/subgraph'
|
||||
import { useUserPreferences } from '../UserPreferences'
|
||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { getDownloadAssets, getPublishedAssets } from '@utils/aquarius'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import {
|
||||
getDownloadAssets,
|
||||
getPublishedAssets,
|
||||
getUserSales
|
||||
} from '@utils/aquarius'
|
||||
import axios, { CancelToken } from 'axios'
|
||||
import get3BoxProfile from '@utils/profile'
|
||||
import web3 from 'web3'
|
||||
import { useMarketMetadata } from './MarketMetadata'
|
||||
import { useMarketMetadata } from '../MarketMetadata'
|
||||
import { getEnsProfile } from '@utils/ens'
|
||||
|
||||
interface ProfileProviderValue {
|
||||
profile: Profile
|
||||
@ -32,6 +35,14 @@ const ProfileContext = createContext({} as ProfileProviderValue)
|
||||
|
||||
const refreshInterval = 10000 // 10 sec.
|
||||
|
||||
const clearedProfile: Profile = {
|
||||
name: null,
|
||||
avatar: null,
|
||||
url: null,
|
||||
description: null,
|
||||
links: null
|
||||
}
|
||||
|
||||
function ProfileProvider({
|
||||
accountId,
|
||||
accountEns,
|
||||
@ -56,9 +67,9 @@ function ProfileProvider({
|
||||
}, [accountId])
|
||||
|
||||
//
|
||||
// User profile: ENS + 3Box
|
||||
// User profile: ENS
|
||||
//
|
||||
const [profile, setProfile] = useState<Profile>()
|
||||
const [profile, setProfile] = useState<Profile>({ name: accountEns })
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountEns) return
|
||||
@ -66,53 +77,22 @@ function ProfileProvider({
|
||||
}, [accountId, accountEns])
|
||||
|
||||
useEffect(() => {
|
||||
const clearedProfile: Profile = {
|
||||
name: null,
|
||||
accountEns: null,
|
||||
image: null,
|
||||
description: null,
|
||||
links: null
|
||||
}
|
||||
|
||||
if (!accountId || !isEthAddress) {
|
||||
if (
|
||||
!accountId ||
|
||||
accountId === '0x0000000000000000000000000000000000000000' ||
|
||||
!isEthAddress
|
||||
) {
|
||||
setProfile(clearedProfile)
|
||||
return
|
||||
}
|
||||
|
||||
const cancelTokenSource = axios.CancelToken.source()
|
||||
|
||||
async function getInfo() {
|
||||
setProfile({ name: accountEns || accountTruncate(accountId), accountEns })
|
||||
|
||||
const profile3Box = await get3BoxProfile(
|
||||
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.')
|
||||
}
|
||||
const profile = await getEnsProfile(accountId)
|
||||
setProfile(profile)
|
||||
LoggerInstance.log(`[profile] ENS metadata for ${accountId}:`, profile)
|
||||
}
|
||||
getInfo()
|
||||
|
||||
return () => {
|
||||
cancelTokenSource.cancel()
|
||||
}
|
||||
}, [accountId, accountEns, isEthAddress])
|
||||
}, [accountId, isEthAddress])
|
||||
|
||||
//
|
||||
// PUBLISHED ASSETS
|
@ -13,7 +13,7 @@ import { infuraProjectId as infuraId } from '../../app.config'
|
||||
import WalletConnectProvider from '@walletconnect/web3-provider'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { isBrowser } from '@utils/index'
|
||||
import { getEnsName } from '@utils/ens'
|
||||
import { getEnsProfile } from '@utils/ens'
|
||||
import useNetworkMetadata, {
|
||||
getNetworkDataById,
|
||||
getNetworkDisplayName,
|
||||
@ -32,6 +32,7 @@ interface Web3ProviderValue {
|
||||
web3ProviderInfo: IProviderInfo
|
||||
accountId: string
|
||||
accountEns: string
|
||||
accountEnsAvatar: string
|
||||
balance: UserBalance
|
||||
networkId: number
|
||||
chainId: number
|
||||
@ -54,8 +55,6 @@ const web3ModalTheme = {
|
||||
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
|
||||
? {
|
||||
walletconnect: {
|
||||
@ -99,6 +98,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
const [isTestnet, setIsTestnet] = useState<boolean>()
|
||||
const [accountId, setAccountId] = useState<string>()
|
||||
const [accountEns, setAccountEns] = useState<string>()
|
||||
const [accountEnsAvatar, setAccountEnsAvatar] = useState<string>()
|
||||
const [web3Loading, setWeb3Loading] = useState<boolean>(true)
|
||||
const [balance, setBalance] = useState<UserBalance>({
|
||||
eth: '0'
|
||||
@ -160,12 +160,15 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
// Helper: Get user balance
|
||||
// -----------------------------------
|
||||
const getUserBalance = useCallback(async () => {
|
||||
if (!accountId || !networkId || !web3) return
|
||||
if (!accountId || !networkId || !web3 || !networkData) return
|
||||
|
||||
try {
|
||||
const balance: UserBalance = {
|
||||
eth: web3.utils.fromWei(await web3.eth.getBalance(accountId, 'latest'))
|
||||
}
|
||||
const userBalance = web3.utils.fromWei(
|
||||
await web3.eth.getBalance(accountId, 'latest')
|
||||
)
|
||||
const key = networkData.nativeCurrency.symbol.toLowerCase()
|
||||
const balance: UserBalance = { [key]: userBalance }
|
||||
|
||||
if (approvedBaseTokens?.length > 0) {
|
||||
await Promise.all(
|
||||
approvedBaseTokens.map(async (token) => {
|
||||
@ -186,27 +189,38 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
} catch (error) {
|
||||
LoggerInstance.error('[web3] Error: ', error.message)
|
||||
}
|
||||
}, [accountId, approvedBaseTokens, networkId, web3])
|
||||
}, [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
|
||||
|
||||
try {
|
||||
// const accountEns = await getEnsNameWithWeb3(
|
||||
// accountId,
|
||||
// web3Provider,
|
||||
// `${networkId}`
|
||||
// )
|
||||
const accountEns = await getEnsName(accountId)
|
||||
setAccountEns(accountEns)
|
||||
accountEns &&
|
||||
const profile = await getEnsProfile(accountId)
|
||||
|
||||
if (!profile) {
|
||||
setAccountEns(null)
|
||||
setAccountEnsAvatar(null)
|
||||
return
|
||||
}
|
||||
|
||||
setAccountEns(profile.name)
|
||||
LoggerInstance.log(
|
||||
`[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) {
|
||||
LoggerInstance.error('[web3] Error: ', error.message)
|
||||
}
|
||||
@ -272,11 +286,11 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
}, [getUserBalance])
|
||||
|
||||
// -----------------------------------
|
||||
// Get and set user ENS name
|
||||
// Get and set user ENS info
|
||||
// -----------------------------------
|
||||
useEffect(() => {
|
||||
getUserEnsName()
|
||||
}, [getUserEnsName])
|
||||
getUserEns()
|
||||
}, [getUserEns])
|
||||
|
||||
// -----------------------------------
|
||||
// Get and set network metadata
|
||||
@ -334,7 +348,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
// -----------------------------------
|
||||
async function logout() {
|
||||
/* 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()
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
@ -399,6 +413,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
web3ProviderInfo,
|
||||
accountId,
|
||||
accountEns,
|
||||
accountEnsAvatar,
|
||||
balance,
|
||||
networkId,
|
||||
chainId,
|
||||
|
@ -10,6 +10,7 @@ export function getNetworkType(network: EthereumListsChain): string {
|
||||
// .network field, which is innexistent on https://chainid.network/chains.json
|
||||
// We hack in mainnet detection for moonriver.
|
||||
if (
|
||||
network &&
|
||||
!network.name.includes('Testnet') &&
|
||||
!network.title?.includes('Testnet') &&
|
||||
network.name !== 'Moonbase Alpha'
|
||||
|
@ -10,7 +10,7 @@ function useNftFactory(): NftFactory {
|
||||
useEffect(() => {
|
||||
if (!web3 || !chainId) return
|
||||
const config = getOceanConfig(chainId)
|
||||
const factory = new NftFactory(config?.erc721FactoryAddress, web3)
|
||||
const factory = new NftFactory(config?.nftFactoryAddress, web3)
|
||||
setNftFactory(factory)
|
||||
}, [web3, chainId])
|
||||
|
32
src/@types/Profile.d.ts
vendored
32
src/@types/Profile.d.ts
vendored
@ -1,36 +1,12 @@
|
||||
interface ProfileLink {
|
||||
name: string
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
did?: string
|
||||
name?: string
|
||||
accountEns?: string
|
||||
name: string
|
||||
url?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
emoji?: string
|
||||
image?: string
|
||||
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
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
1
src/@types/TokenBalance.d.ts
vendored
1
src/@types/TokenBalance.d.ts
vendored
@ -1,4 +1,3 @@
|
||||
interface UserBalance {
|
||||
eth: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
4
src/@types/viewModels/AccountTeaserVM.d.ts
vendored
4
src/@types/viewModels/AccountTeaserVM.d.ts
vendored
@ -1,4 +0,0 @@
|
||||
interface AccountTeaserVM {
|
||||
address: string
|
||||
nrSales: number
|
||||
}
|
@ -23,7 +23,7 @@ import {
|
||||
|
||||
const tokensPriceQuery = gql`
|
||||
query TokensPriceQuery($datatokenIds: [ID!], $account: String) {
|
||||
tokens(where: { id_in: $datatokenIds }) {
|
||||
tokens(first: 1000, where: { id_in: $datatokenIds }) {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
|
@ -9,6 +9,11 @@ import {
|
||||
} from '../@types/aquarius/SearchQuery'
|
||||
import { transformAssetToAssetSelection } from './assetConvertor'
|
||||
|
||||
export interface UserSales {
|
||||
id: string
|
||||
totalSales: number
|
||||
}
|
||||
|
||||
export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
|
||||
|
||||
export function escapeEsReservedCharacters(value: string): string {
|
||||
@ -397,6 +402,40 @@ export async function getTopPublishers(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTopAssetsPublishers(
|
||||
chainIds: number[],
|
||||
nrItems = 9
|
||||
): Promise<UserSales[]> {
|
||||
const publishers: UserSales[] = []
|
||||
|
||||
const result = await getTopPublishers(chainIds, null)
|
||||
const { topPublishers } = result.aggregations
|
||||
|
||||
for (let i = 0; i < topPublishers.buckets.length; i++) {
|
||||
publishers.push({
|
||||
id: topPublishers.buckets[i].key,
|
||||
totalSales: parseInt(topPublishers.buckets[i].totalSales.value)
|
||||
})
|
||||
}
|
||||
|
||||
publishers.sort((a, b) => b.totalSales - a.totalSales)
|
||||
|
||||
return publishers.slice(0, nrItems)
|
||||
}
|
||||
|
||||
export async function getUserSales(
|
||||
accountId: string,
|
||||
chainIds: number[]
|
||||
): Promise<number> {
|
||||
try {
|
||||
const result = await getPublishedAssets(accountId, chainIds, null)
|
||||
const { totalOrders } = result.aggregations
|
||||
return totalOrders.value
|
||||
} catch (error) {
|
||||
LoggerInstance.error('Error getUserSales', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDownloadAssets(
|
||||
dtList: string[],
|
||||
tokenOrders: OrdersData[],
|
||||
|
@ -9,7 +9,7 @@ export async function setMinterToPublisher(
|
||||
accountId: string,
|
||||
setError: (msg: string) => void
|
||||
): Promise<TransactionReceipt> {
|
||||
const dispenserInstance = new Dispenser(web3, dispenserAddress)
|
||||
const dispenserInstance = new Dispenser(dispenserAddress, web3)
|
||||
const status = await dispenserInstance.status(datatokenAddress)
|
||||
if (!status?.active) return
|
||||
|
||||
|
64
src/@utils/ens.test.ts
Normal file
64
src/@utils/ens.test.ts
Normal 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()
|
||||
})
|
||||
})
|
@ -1,52 +1,24 @@
|
||||
import { gql, OperationContext, OperationResult } from 'urql'
|
||||
import { fetchData } from './subgraph'
|
||||
import { fetchData } from './fetch'
|
||||
|
||||
// make sure to only query for domains owned by account, so domains
|
||||
// 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'
|
||||
}
|
||||
const apiUrl = 'https://ens-proxy.oceanprotocol.com/api'
|
||||
|
||||
export async function getEnsName(accountId: string): Promise<string> {
|
||||
const response: OperationResult<any> = await fetchData(
|
||||
UserEnsNames,
|
||||
{ accountId: accountId.toLowerCase() },
|
||||
ensSubgraphQueryContext
|
||||
)
|
||||
if (!response?.data?.domains?.length) return
|
||||
if (!accountId || accountId === '') return
|
||||
|
||||
// Default order of response.data.domains seems to be by creation time, from oldest to newest.
|
||||
// Pick the last one as that is what direct web3 calls do.
|
||||
const { name } = response.data.domains.slice(-1)[0]
|
||||
return name
|
||||
const data = await fetchData(`${apiUrl}/name?accountId=${accountId}`)
|
||||
return data?.name
|
||||
}
|
||||
|
||||
export async function getEnsAddress(ensName: string): Promise<string> {
|
||||
const response: OperationResult<any> = await fetchData(
|
||||
UserEnsAddress,
|
||||
{ name: ensName },
|
||||
ensSubgraphQueryContext
|
||||
)
|
||||
if (!response?.data?.domains?.length) return
|
||||
const { id } = response.data.domains[0].resolvedAddress
|
||||
return id
|
||||
export async function getEnsAddress(accountId: string): Promise<string> {
|
||||
if (!accountId || accountId === '' || !accountId.includes('.')) return
|
||||
|
||||
const data = await fetchData(`${apiUrl}/address?name=${accountId}`)
|
||||
return data?.address
|
||||
}
|
||||
|
||||
export async function getEnsProfile(accountId: string): Promise<Profile> {
|
||||
if (!accountId || accountId === '') return
|
||||
|
||||
const data = await fetchData(`${apiUrl}/profile?address=${accountId}`)
|
||||
return data?.profile
|
||||
}
|
||||
|
@ -1,15 +1,24 @@
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
|
||||
export async function fetchData(url: string): Promise<AxiosResponse['data']> {
|
||||
try {
|
||||
const response = await axios(url)
|
||||
|
||||
if (response.status !== 200) {
|
||||
return console.error('Non-200 response: ' + response.status)
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response?.data
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,8 @@ export async function getFixedBuyPrice(
|
||||
|
||||
const config = getOceanConfig(chainId)
|
||||
|
||||
const fixed = new FixedRateExchange(web3, config.fixedRateExchangeAddress)
|
||||
const estimatedPrice = await fixed.calcBaseInGivenOutDT(
|
||||
const fixed = new FixedRateExchange(config.fixedRateExchangeAddress, web3)
|
||||
const estimatedPrice = await fixed.calcBaseInGivenDatatokensOut(
|
||||
accessDetails.addressOrId,
|
||||
'1',
|
||||
consumeMarketFixedSwapFee
|
||||
|
@ -171,15 +171,6 @@ export async function setNFTMetadataAndTokenURI(
|
||||
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(
|
||||
asset.nftAddress,
|
||||
accountId,
|
||||
|
@ -1,14 +1,5 @@
|
||||
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
|
||||
// http://mikemcl.github.io/decimal.js/#cmp
|
||||
export function compareAsBN(balance: string, price: string): boolean {
|
||||
|
@ -57,7 +57,9 @@ export async function order(
|
||||
_consumeMarketFee: {
|
||||
consumeMarketFeeAddress: marketFeeAddress,
|
||||
consumeMarketFeeAmount: consumeMarketOrderFee,
|
||||
consumeMarketFeeToken: asset.accessDetails.baseToken.address
|
||||
consumeMarketFeeToken:
|
||||
asset?.accessDetails?.baseToken?.address ||
|
||||
'0x0000000000000000000000000000000000000000'
|
||||
}
|
||||
} as OrderParams
|
||||
|
||||
@ -66,6 +68,7 @@ export async function order(
|
||||
// this assumes all fees are in ocean
|
||||
const txApprove = await approve(
|
||||
web3,
|
||||
config,
|
||||
accountId,
|
||||
asset.accessDetails.baseToken.address,
|
||||
asset.accessDetails.datatoken.address,
|
||||
@ -152,19 +155,21 @@ async function approveProviderFee(
|
||||
accountId: string,
|
||||
web3: Web3,
|
||||
providerFeeAmount: string
|
||||
): Promise<string> {
|
||||
): Promise<TransactionReceipt> {
|
||||
const config = getOceanConfig(asset.chainId)
|
||||
const baseToken =
|
||||
asset?.accessDetails?.type === 'free'
|
||||
? getOceanConfig(asset.chainId).oceanTokenAddress
|
||||
: asset?.accessDetails?.baseToken?.address
|
||||
const txApproveWei = await approveWei(
|
||||
web3,
|
||||
config,
|
||||
accountId,
|
||||
baseToken,
|
||||
asset?.accessDetails?.datatoken?.address,
|
||||
providerFeeAmount
|
||||
)
|
||||
return txApproveWei as string // thanks ocean.js
|
||||
return txApproveWei
|
||||
}
|
||||
|
||||
async function startOrder(
|
||||
|
@ -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.`
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { Purgatory } from '@oceanprotocol/lib'
|
||||
import { fetchData } from './fetch'
|
||||
|
||||
const purgatoryUrl = 'https://market-purgatory.oceanprotocol.com/api/'
|
||||
|
@ -6,16 +6,6 @@ import { AssetPreviousOrder } from '../@types/subgraph/AssetPreviousOrder'
|
||||
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
|
||||
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`
|
||||
query AssetPreviousOrder($id: String!, $account: String!) {
|
||||
orders(
|
||||
@ -153,29 +143,6 @@ export async function getOpcFees(chainId: number) {
|
||||
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(
|
||||
accountId: string,
|
||||
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(
|
||||
chainId: number
|
||||
): Promise<TokenInfo[]> {
|
||||
|
9
src/@utils/url.test.ts
Normal file
9
src/@utils/url.test.ts
Normal 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')
|
||||
})
|
||||
})
|
@ -1,10 +1,5 @@
|
||||
export function sanitizeUrl(url: string) {
|
||||
const u = decodeURI(url).trim().toLowerCase()
|
||||
if (
|
||||
u.startsWith('javascript:') ||
|
||||
u.startsWith('data:') ||
|
||||
u.startsWith('vbscript:')
|
||||
)
|
||||
return 'about:blank'
|
||||
return url
|
||||
const isAllowedUrlScheme = u.startsWith('http://') || u.startsWith('https://')
|
||||
return isAllowedUrlScheme ? url : 'about:blank'
|
||||
}
|
||||
|
@ -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 />
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -37,7 +37,7 @@ export default function AssetComputeSelection({
|
||||
</Dotdotdot>
|
||||
</div>
|
||||
<PriceUnit
|
||||
price={asset.price}
|
||||
price={Number(asset.price)}
|
||||
size="small"
|
||||
className={styles.price}
|
||||
/>
|
||||
|
@ -26,14 +26,16 @@ export default function AssetType({
|
||||
)}
|
||||
|
||||
<div className={styles.typeLabel}>
|
||||
{type === 'dataset' ? 'data set' : 'algorithm'}
|
||||
{type === 'dataset' ? 'dataset' : 'algorithm'}
|
||||
</div>
|
||||
|
||||
{totalSales ? (
|
||||
{(totalSales || totalSales === 0) && (
|
||||
<div className={styles.typeLabel}>
|
||||
{`${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
|
||||
{totalSales < 0
|
||||
? 'N/A'
|
||||
: `${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ function getConsumeHelpText(
|
||||
: hasPreviousOrder
|
||||
? `You bought this ${assetType} already allowing you to use it without paying again.`
|
||||
: 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
|
||||
? `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.`
|
||||
|
@ -108,7 +108,7 @@ export default function AssetSelection({
|
||||
</label>
|
||||
|
||||
<PriceUnit
|
||||
price={asset.price}
|
||||
price={Number(asset.price)}
|
||||
type={asset.price === '0' ? 'free' : undefined}
|
||||
size="small"
|
||||
className={styles.price}
|
||||
|
@ -1,68 +0,0 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { usePrices } from '@context/Prices'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import Web3 from 'web3'
|
||||
import useNftFactory from '@hooks/contracts/useNftFactory'
|
||||
import { NftFactory } from '@oceanprotocol/lib'
|
||||
import Conversion from '@shared/Price/Conversion'
|
||||
import { generateNftCreateData, NftMetadata } from '@utils/nft'
|
||||
|
||||
const getEstGasFee = async (
|
||||
address: string,
|
||||
nftFactory: NftFactory,
|
||||
nftMetadata: NftMetadata,
|
||||
ethToOceanConversionRate: number
|
||||
): Promise<string> => {
|
||||
if (!address || !nftFactory || !nftMetadata || !ethToOceanConversionRate)
|
||||
return
|
||||
|
||||
const { web3 } = nftFactory
|
||||
const nft = generateNftCreateData(nftMetadata, address)
|
||||
|
||||
const gasPrice = await web3.eth.getGasPrice()
|
||||
const gasLimit = await nftFactory?.estGasCreateNFT(address, nft)
|
||||
const gasFeeEth = Web3.utils.fromWei(
|
||||
(+gasPrice * +gasLimit).toString(),
|
||||
'ether'
|
||||
)
|
||||
const gasFeeOcean = (+gasFeeEth / +ethToOceanConversionRate).toString()
|
||||
return gasFeeOcean
|
||||
}
|
||||
|
||||
export default function TxFee({
|
||||
nftMetadata
|
||||
}: {
|
||||
nftMetadata: NftMetadata
|
||||
}): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { prices } = usePrices()
|
||||
const nftFactory = useNftFactory()
|
||||
const [gasFee, setGasFee] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const calculateGasFee = async () =>
|
||||
setGasFee(
|
||||
await getEstGasFee(
|
||||
accountId,
|
||||
nftFactory,
|
||||
nftMetadata,
|
||||
(prices as any)?.eth
|
||||
)
|
||||
)
|
||||
calculateGasFee()
|
||||
}, [accountId, nftFactory, nftMetadata, prices])
|
||||
|
||||
return gasFee ? (
|
||||
<p>
|
||||
Gas fee estimation for this artwork
|
||||
<Conversion price={gasFee} />
|
||||
</p>
|
||||
) : accountId ? (
|
||||
<p>
|
||||
An error occurred while estimating the gas fee for this artwork, please
|
||||
try again.
|
||||
</p>
|
||||
) : (
|
||||
<p>Please connect your wallet to get a gas fee estimate for this artwork</p>
|
||||
)
|
||||
}
|
@ -27,7 +27,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 0 calc(var(--spacer) / 4);
|
||||
padding: calc(var(--spacer) / 4);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
@ -5,8 +5,6 @@ import { useField } from 'formik'
|
||||
import React, { ReactElement, useEffect } from 'react'
|
||||
import Refresh from '@images/refresh.svg'
|
||||
import styles from './index.module.css'
|
||||
import Tooltip from '@shared/atoms/Tooltip'
|
||||
import TxFee from './TxFee'
|
||||
|
||||
export default function Nft(props: InputProps): ReactElement {
|
||||
const [field, meta, helpers] = useField(props.name)
|
||||
@ -28,7 +26,6 @@ export default function Nft(props: InputProps): ReactElement {
|
||||
<figure className={styles.image}>
|
||||
<img src={field?.value?.image_data} width="128" height="128" />
|
||||
<div className={styles.actions}>
|
||||
<Tooltip content={<TxFee nftMetadata={field.value} />} />
|
||||
<Button
|
||||
style="text"
|
||||
size="small"
|
||||
|
@ -71,9 +71,7 @@ function checkError(
|
||||
parsedFieldName: string[],
|
||||
field: FieldInputProps<any>
|
||||
) {
|
||||
if (form?.errors === {}) {
|
||||
return false
|
||||
} else if (
|
||||
if (
|
||||
(form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] &&
|
||||
form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]) ||
|
||||
(form?.touched[field.name] &&
|
||||
@ -140,11 +138,13 @@ export default function Input(props: Partial<InputProps>): ReactElement {
|
||||
</Label>
|
||||
<InputElement size={size} {...field} {...props} />
|
||||
{help && prominentHelp && <FormHelp>{help}</FormHelp>}
|
||||
{isFormikField && hasFormikError && (
|
||||
|
||||
{field?.name !== 'files' && isFormikField && hasFormikError && (
|
||||
<div className={styles.error}>
|
||||
<ErrorMessage name={field.name} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{disclaimer && (
|
||||
<Disclaimer visible={disclaimerVisible}>{disclaimer}</Disclaimer>
|
||||
)}
|
||||
|
@ -1,18 +1,17 @@
|
||||
import React, { useEffect, useState, ReactElement } from 'react'
|
||||
import styles from './Conversion.module.css'
|
||||
import classNames from 'classnames/bind'
|
||||
import { formatCurrency, isCrypto } from '@coingecko/cryptoformat'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { usePrices } from '@context/Prices'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
import { usePrices, getCoingeckoTokenId } from '@context/Prices'
|
||||
|
||||
export default function Conversion({
|
||||
price,
|
||||
symbol,
|
||||
className,
|
||||
hideApproximateSymbol
|
||||
}: {
|
||||
price: string // expects price in OCEAN, not wei
|
||||
price: number // expects price in OCEAN, not wei
|
||||
symbol: string
|
||||
className?: string
|
||||
hideApproximateSymbol?: boolean
|
||||
}): ReactElement {
|
||||
@ -25,19 +24,16 @@ export default function Conversion({
|
||||
// isCrypto() only checks for BTC & ETH & unknown but seems sufficient for now
|
||||
// const isFiat = /(EUR|USD|CAD|SGD|HKD|CNY|JPY|GBP|INR|RUB)/g.test(currency)
|
||||
|
||||
const styleClasses = cx({
|
||||
conversion: true,
|
||||
[className]: className
|
||||
})
|
||||
// referring to Coingecko tokenId in Prices context provider
|
||||
const priceTokenId = getCoingeckoTokenId(symbol)
|
||||
|
||||
useEffect(() => {
|
||||
if (!prices || !price || price === '0') {
|
||||
setPriceConverted('0.00')
|
||||
if (!prices || !price || !priceTokenId || !prices[priceTokenId]) {
|
||||
return
|
||||
}
|
||||
|
||||
const conversionValue = prices[currency.toLowerCase()]
|
||||
const converted = conversionValue * Number(price)
|
||||
const conversionValue = prices[priceTokenId][currency.toLowerCase()]
|
||||
const converted = conversionValue * price
|
||||
const convertedFormatted = formatCurrency(
|
||||
converted,
|
||||
// No passing of `currency` for non-fiat so symbol conversion
|
||||
@ -54,16 +50,16 @@ export default function Conversion({
|
||||
(match) => `<span>${match}</span>`
|
||||
)
|
||||
setPriceConverted(convertedFormattedHTMLstring)
|
||||
}, [price, prices, currency, locale, isFiat])
|
||||
}, [price, prices, currency, locale, isFiat, priceTokenId])
|
||||
|
||||
return (
|
||||
return Number(price) >= 0 ? (
|
||||
<span
|
||||
className={styleClasses}
|
||||
title="Approximation based on the current selected base token spot price on Coingecko"
|
||||
className={`${styles.conversion} ${className || ''}`}
|
||||
title="Approximation based on the current spot price on Coingecko"
|
||||
>
|
||||
{!hideApproximateSymbol && '≈ '}
|
||||
<strong dangerouslySetInnerHTML={{ __html: priceConverted }} />{' '}
|
||||
{!isFiat && currency}
|
||||
</span>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
@ -3,10 +3,9 @@ import { formatCurrency } from '@coingecko/cryptoformat'
|
||||
import Conversion from './Conversion'
|
||||
import styles from './PriceUnit.module.css'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import Badge from '@shared/atoms/Badge'
|
||||
|
||||
export function formatPrice(price: string, locale: string): string {
|
||||
return formatCurrency(Number(price), '', locale, false, {
|
||||
export function formatPrice(price: number, locale: string): string {
|
||||
return formatCurrency(price, '', locale, false, {
|
||||
// Not exactly clear what `significant figures` are for this library,
|
||||
// but setting this seems to give us the formatting we want.
|
||||
// See https://github.com/oceanprotocol/market/issues/70
|
||||
@ -22,7 +21,7 @@ export default function PriceUnit({
|
||||
symbol,
|
||||
type
|
||||
}: {
|
||||
price: string
|
||||
price: number
|
||||
type?: string
|
||||
className?: string
|
||||
size?: 'small' | 'mini' | 'large'
|
||||
@ -38,10 +37,10 @@ export default function PriceUnit({
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
|
||||
{Number.isNaN(price) ? '-' : formatPrice(price, locale)}{' '}
|
||||
<span className={styles.symbol}>{symbol}</span>
|
||||
</div>
|
||||
{conversion && <Conversion price={price} />}
|
||||
{conversion && <Conversion price={price} symbol={symbol} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -16,10 +16,11 @@ export default function Price({
|
||||
}): ReactElement {
|
||||
const isSupported =
|
||||
accessDetails?.type === 'fixed' || accessDetails?.type === 'free'
|
||||
const price = `${orderPriceAndFees?.price || accessDetails?.price}`
|
||||
|
||||
return isSupported ? (
|
||||
<PriceUnit
|
||||
price={`${orderPriceAndFees?.price || accessDetails?.price}`}
|
||||
price={Number(price)}
|
||||
symbol={accessDetails.baseToken?.symbol}
|
||||
className={className}
|
||||
size={size}
|
||||
|
@ -1,7 +0,0 @@
|
||||
.add {
|
||||
color: var(--brand-pink);
|
||||
}
|
||||
|
||||
.linksExternal {
|
||||
composes: linksExternal from './index.module.css';
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -7,10 +7,3 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.linksExternal {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
display: inline-block;
|
||||
fill: var(--color-secondary);
|
||||
}
|
||||
|
56
src/components/@shared/Publisher/index.test.tsx
Normal file
56
src/components/@shared/Publisher/index.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
@ -1,72 +1,47 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import styles from './index.module.css'
|
||||
import classNames from 'classnames/bind'
|
||||
import Link from 'next/link'
|
||||
import get3BoxProfile from '@utils/profile'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import axios from 'axios'
|
||||
import { getEnsName } from '@utils/ens'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
export interface PublisherProps {
|
||||
account: string
|
||||
minimal?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Publisher({
|
||||
account,
|
||||
minimal,
|
||||
className
|
||||
}: {
|
||||
account: string
|
||||
minimal?: boolean
|
||||
className?: string
|
||||
}): ReactElement {
|
||||
}: PublisherProps): ReactElement {
|
||||
const isMounted = useIsMounted()
|
||||
const [profile, setProfile] = useState<Profile>()
|
||||
const [name, setName] = useState('')
|
||||
const [accountEns, setAccountEns] = useState<string>()
|
||||
const [name, setName] = useState(accountTruncate(account))
|
||||
|
||||
useEffect(() => {
|
||||
if (!account) return
|
||||
if (!account || account === '') return
|
||||
|
||||
// set default name on hook
|
||||
// to avoid side effect (UI not updating on account's change)
|
||||
setName(accountTruncate(account))
|
||||
|
||||
const source = axios.CancelToken.source()
|
||||
|
||||
async function getExternalName() {
|
||||
// ENS
|
||||
const accountEns = await getEnsName(account)
|
||||
if (accountEns && isMounted()) {
|
||||
setAccountEns(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()
|
||||
|
||||
return () => {
|
||||
source.cancel()
|
||||
}
|
||||
}, [account, isMounted])
|
||||
|
||||
const styleClasses = cx({
|
||||
publisher: true,
|
||||
[className]: className
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styleClasses}>
|
||||
<div className={`${styles.publisher} ${className || ''}`}>
|
||||
{minimal ? (
|
||||
name
|
||||
) : (
|
||||
<>
|
||||
<Link href={`/profile/${accountEns || account}`}>
|
||||
<Link href={`/profile/${account}`}>
|
||||
<a title="Show profile page.">{name}</a>
|
||||
</Link>
|
||||
</>
|
||||
|
7
src/components/@shared/atoms/Alert/index.test.tsx
Normal file
7
src/components/@shared/atoms/Alert/index.test.tsx
Normal 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" />)
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
.blockies {
|
||||
.avatar {
|
||||
width: var(--font-size-large);
|
||||
height: var(--font-size-large);
|
||||
border-radius: 50%;
|
31
src/components/@shared/atoms/Avatar/index.stories.tsx
Normal file
31
src/components/@shared/atoms/Avatar/index.stories.tsx
Normal 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
|
||||
}
|
19
src/components/@shared/atoms/Avatar/index.test.tsx
Normal file
19
src/components/@shared/atoms/Avatar/index.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
24
src/components/@shared/atoms/Avatar/index.tsx
Normal file
24
src/components/@shared/atoms/Avatar/index.tsx
Normal 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"
|
||||
/>
|
||||
)
|
||||
}
|
7
src/components/@shared/atoms/Badge/index.test.tsx
Normal file
7
src/components/@shared/atoms/Badge/index.test.tsx
Normal 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" />)
|
||||
})
|
@ -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'
|
||||
}
|
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
8
src/components/@shared/atoms/Container/index.test.tsx
Normal file
8
src/components/@shared/atoms/Container/index.test.tsx
Normal 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} />)
|
||||
})
|
@ -1,12 +1,13 @@
|
||||
import React from 'react'
|
||||
import { render, act, screen, fireEvent } from '@testing-library/react'
|
||||
import { Default } from './index.stories'
|
||||
import Copy from '.'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
describe('Copy', () => {
|
||||
test('should change class on click', () => {
|
||||
render(<Default {...Default.args} />)
|
||||
render(<Copy {...Default.args} />)
|
||||
|
||||
const element = screen.getByTitle('Copy to clipboard')
|
||||
fireEvent.click(element)
|
||||
@ -14,7 +15,7 @@ describe('Copy', () => {
|
||||
})
|
||||
|
||||
test('should remove class after timer end', () => {
|
||||
render(<Default {...Default.args} />)
|
||||
render(<Copy {...Default.args} />)
|
||||
|
||||
const element = screen.getByTitle('Copy to clipboard')
|
||||
fireEvent.click(element)
|
||||
|
8
src/components/@shared/atoms/Loader/index.test.tsx
Normal file
8
src/components/@shared/atoms/Loader/index.test.tsx
Normal 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} />)
|
||||
})
|
8
src/components/@shared/atoms/Logo/index.test.tsx
Normal file
8
src/components/@shared/atoms/Logo/index.test.tsx
Normal 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} />)
|
||||
})
|
8
src/components/@shared/atoms/Status/index.test.tsx
Normal file
8
src/components/@shared/atoms/Status/index.test.tsx
Normal 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} />)
|
||||
})
|
@ -15,9 +15,7 @@ const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => {
|
||||
return noLinks ? (
|
||||
<span className={styles.tag}>{tag}</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/search?tags=${urlEncodedTag}&sort=metadata.created&sortOrder=desc`}
|
||||
>
|
||||
<Link href={`/search?tags=${urlEncodedTag}&sort=_score&sortOrder=desc`}>
|
||||
<a className={styles.tag} title={tag}>
|
||||
{tag}
|
||||
</a>
|
||||
|
8
src/components/@shared/atoms/Time/index.test.tsx
Normal file
8
src/components/@shared/atoms/Time/index.test.tsx
Normal 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} />)
|
||||
})
|
@ -7,7 +7,9 @@ export default function AssetStats() {
|
||||
|
||||
return (
|
||||
<footer className={styles.stats}>
|
||||
{!asset || !asset?.stats || asset?.stats?.orders === 0 ? (
|
||||
{!asset || !asset?.stats || asset?.stats?.orders < 0 ? (
|
||||
'N/A'
|
||||
) : asset?.stats?.orders === 0 ? (
|
||||
'No sales yet'
|
||||
) : (
|
||||
<>
|
||||
|
@ -45,7 +45,7 @@ function Row({
|
||||
<div className={styles.type}>{type}</div>
|
||||
<div>
|
||||
<PriceUnit
|
||||
price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`}
|
||||
price={hasPreviousOrder || hasDatatoken ? 0 : Number(price)}
|
||||
symbol={symbol}
|
||||
size="small"
|
||||
className={styles.price}
|
||||
@ -81,7 +81,7 @@ export default function PriceOutput({
|
||||
return (
|
||||
<div className={styles.priceComponent}>
|
||||
You will pay{' '}
|
||||
<PriceUnit price={`${totalPrice}`} symbol={symbol} size="small" />
|
||||
<PriceUnit price={Number(totalPrice)} symbol={symbol} size="small" />
|
||||
<Tooltip
|
||||
content={
|
||||
<div className={styles.calculation}>
|
||||
|
@ -266,10 +266,10 @@ export default function Compute({
|
||||
computeAlgorithm,
|
||||
selectedAlgorithmAsset
|
||||
)
|
||||
LoggerInstance.log('[compute] Is data set orderable?', allowed)
|
||||
LoggerInstance.log('[compute] Is dataset orderable?', allowed)
|
||||
if (!allowed)
|
||||
throw new Error(
|
||||
'Data set is not orderable in combination with selected algorithm.'
|
||||
'Dataset is not orderable in combination with selected algorithm.'
|
||||
)
|
||||
|
||||
await initPriceAndFees()
|
||||
@ -376,7 +376,7 @@ export default function Compute({
|
||||
{asset.services[0].type === 'compute' && (
|
||||
<Alert
|
||||
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"
|
||||
/>
|
||||
|
@ -16,6 +16,7 @@ import { toast } from 'react-toastify'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
import Alert from '@shared/atoms/Alert'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
|
||||
export default function Download({
|
||||
asset,
|
||||
@ -41,6 +42,7 @@ export default function Download({
|
||||
const [hasDatatoken, setHasDatatoken] = useState(false)
|
||||
const [statusText, setStatusText] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isPriceLoading, setIsPriceLoading] = useState(false)
|
||||
const [isOwned, setIsOwned] = useState(false)
|
||||
const [validOrderTx, setValidOrderTx] = useState('')
|
||||
const [orderPriceAndFees, setOrderPriceAndFees] =
|
||||
@ -64,8 +66,11 @@ export default function Download({
|
||||
)
|
||||
return
|
||||
|
||||
!orderPriceAndFees && setIsPriceLoading(true)
|
||||
|
||||
const _orderPriceAndFees = await getOrderPriceAndFees(asset, ZERO_ADDRESS)
|
||||
setOrderPriceAndFees(_orderPriceAndFees)
|
||||
!orderPriceAndFees && setIsPriceLoading(false)
|
||||
}
|
||||
|
||||
init()
|
||||
@ -84,6 +89,7 @@ export default function Download({
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(asset?.accessDetails?.type === 'fixed' && !orderPriceAndFees) ||
|
||||
!isMounted ||
|
||||
!accountId ||
|
||||
!asset?.accessDetails ||
|
||||
@ -112,7 +118,8 @@ export default function Download({
|
||||
hasDatatoken,
|
||||
accountId,
|
||||
isOwned,
|
||||
isUnsupportedPricing
|
||||
isUnsupportedPricing,
|
||||
orderPriceAndFees
|
||||
])
|
||||
|
||||
async function handleOrderOrDownload() {
|
||||
@ -184,12 +191,17 @@ export default function Download({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isPriceLoading ? (
|
||||
<Loader message="Calculating full price (including fees)" />
|
||||
) : (
|
||||
<Price
|
||||
accessDetails={asset.accessDetails}
|
||||
orderPriceAndFees={orderPriceAndFees}
|
||||
conversion
|
||||
size="large"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isInPurgatory && <PurchaseButton />}
|
||||
</>
|
||||
)}
|
||||
|
@ -12,10 +12,8 @@ import { useUserPreferences } from '@context/UserPreferences'
|
||||
import styles from './index.module.css'
|
||||
import Web3Feedback from '@shared/Web3Feedback'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import {
|
||||
getComputeSettingsInitialValues,
|
||||
computeSettingsValidationSchema
|
||||
} from './_constants'
|
||||
import { getComputeSettingsInitialValues } from './_constants'
|
||||
import { computeSettingsValidationSchema } from './_validation'
|
||||
import content from '../../../../content/pages/editComputeDataset.json'
|
||||
import { getServiceByName } from '@utils/ddo'
|
||||
import { setMinterToPublisher, setMinterToDispenser } from '@utils/dispenser'
|
||||
@ -131,7 +129,7 @@ export default function EditComputeDataset({
|
||||
{({ values, isSubmitting }) =>
|
||||
isSubmitting || hasFeedback ? (
|
||||
<EditFeedback
|
||||
loading="Updating data set with new compute settings..."
|
||||
loading="Updating dataset with new compute settings..."
|
||||
error={error}
|
||||
success={success}
|
||||
setError={setError}
|
||||
|
@ -5,6 +5,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.feedback h3 {
|
||||
|
@ -7,7 +7,8 @@ import {
|
||||
Asset,
|
||||
Service
|
||||
} from '@oceanprotocol/lib'
|
||||
import { validationSchema, getInitialValues } from './_constants'
|
||||
import { validationSchema } from './_validation'
|
||||
import { getInitialValues } from './_constants'
|
||||
import { MetadataEditForm } from './_types'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
@ -43,8 +44,8 @@ export default function Edit({
|
||||
const config = getOceanConfig(asset.chainId)
|
||||
|
||||
const fixedRateInstance = new FixedRateExchange(
|
||||
web3,
|
||||
config.fixedRateExchangeAddress
|
||||
config.fixedRateExchangeAddress,
|
||||
web3
|
||||
)
|
||||
|
||||
const setPriceResp = await fixedRateInstance.setRate(
|
||||
|
@ -1,20 +1,7 @@
|
||||
import { FileInfo, Metadata, ServiceComputeOptions } from '@oceanprotocol/lib'
|
||||
import { Metadata, ServiceComputeOptions } from '@oceanprotocol/lib'
|
||||
import { secondsToString } from '@utils/ddo'
|
||||
import * as Yup from 'yup'
|
||||
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(
|
||||
metadata: Metadata,
|
||||
timeout: number,
|
||||
@ -24,19 +11,13 @@ export function getInitialValues(
|
||||
name: metadata?.name,
|
||||
description: metadata?.description,
|
||||
price,
|
||||
links: metadata?.links,
|
||||
files: '',
|
||||
links: metadata?.links as any,
|
||||
files: [{ url: '', type: '' }],
|
||||
timeout: secondsToString(timeout),
|
||||
author: metadata?.author
|
||||
}
|
||||
}
|
||||
|
||||
export const computeSettingsValidationSchema = Yup.object().shape({
|
||||
allowAllPublishedAlgorithms: Yup.boolean().nullable(),
|
||||
publisherTrustedAlgorithms: Yup.array().nullable(),
|
||||
publisherTrustedAlgorithmPublishers: Yup.array().nullable()
|
||||
})
|
||||
|
||||
export function getComputeSettingsInitialValues({
|
||||
publisherTrustedAlgorithms,
|
||||
publisherTrustedAlgorithmPublishers
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FileInfo } from '@oceanprotocol/lib'
|
||||
export interface MetadataEditForm {
|
||||
name: string
|
||||
description: string
|
||||
timeout: string
|
||||
price?: string
|
||||
links?: string | any[]
|
||||
files: string | any[]
|
||||
files: FileInfo[]
|
||||
links?: FileInfo[]
|
||||
author?: string
|
||||
}
|
||||
|
||||
|
34
src/components/Asset/Edit/_validation.ts
Normal file
34
src/components/Asset/Edit/_validation.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { FileInfo } from '@oceanprotocol/lib'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
export const validationSchema = Yup.object().shape({
|
||||
name: Yup.string()
|
||||
.min(4, (param) => `Title must be at least ${param.min} characters`)
|
||||
.required('Required'),
|
||||
description: Yup.string().required('Required').min(10),
|
||||
price: Yup.number().required('Required'),
|
||||
files: Yup.array<FileInfo[]>()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
url: Yup.string().url('Must be a valid URL.'),
|
||||
valid: Yup.boolean().isTrue()
|
||||
})
|
||||
)
|
||||
.nullable(),
|
||||
links: Yup.array<FileInfo[]>()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
url: Yup.string().url('Must be a valid URL.'),
|
||||
valid: Yup.boolean().isTrue()
|
||||
})
|
||||
)
|
||||
.nullable(),
|
||||
timeout: Yup.string().required('Required'),
|
||||
author: Yup.string().nullable()
|
||||
})
|
||||
|
||||
export const computeSettingsValidationSchema = Yup.object().shape({
|
||||
allowAllPublishedAlgorithms: Yup.boolean().nullable(),
|
||||
publisherTrustedAlgorithms: Yup.array().nullable(),
|
||||
publisherTrustedAlgorithmPublishers: Yup.array().nullable()
|
||||
})
|
@ -1,7 +1,6 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Link from 'next/link'
|
||||
import loadable from '@loadable/component'
|
||||
import Badge from '@shared/atoms/Badge'
|
||||
import Logo from '@shared/atoms/Logo'
|
||||
import UserPreferences from './UserPreferences'
|
||||
import Networks from './UserPreferences/Networks'
|
||||
@ -9,8 +8,6 @@ import SearchBar from './SearchBar'
|
||||
import styles from './Menu.module.css'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
import Tooltip from '@shared/atoms/Tooltip'
|
||||
import Caret from '@images/caret.svg'
|
||||
const Wallet = loadable(() => import('./Wallet'))
|
||||
|
||||
declare type MenuItem = {
|
||||
@ -34,7 +31,7 @@ function MenuLink({ item }: { item: MenuItem }) {
|
||||
}
|
||||
|
||||
export default function Menu(): ReactElement {
|
||||
const { appConfig, siteContent } = useMarketMetadata()
|
||||
const { siteContent } = useMarketMetadata()
|
||||
|
||||
return (
|
||||
<nav className={styles.menu}>
|
||||
@ -45,31 +42,6 @@ export default function Menu(): ReactElement {
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Tooltip
|
||||
className={styles.badgeWrap}
|
||||
content={
|
||||
<div className={styles.versions}>
|
||||
<a className={styles.link} href={appConfig.v3MarketUri}>
|
||||
v3
|
||||
</a>
|
||||
<a className={styles.link} href="" aria-current aria-disabled>
|
||||
v4
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
trigger="click focus"
|
||||
placement="bottom"
|
||||
>
|
||||
<Badge
|
||||
className={styles.badge}
|
||||
label={
|
||||
<>
|
||||
v4 <Caret aria-hidden="true" className={styles.caret} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<ul className={styles.navigation}>
|
||||
{siteContent?.menu.map((item: MenuItem) => (
|
||||
<li key={item.name}>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { ReactElement, ChangeEvent } from 'react'
|
||||
import { DarkMode } from 'use-dark-mode'
|
||||
import { DarkMode } from '@oceanprotocol/use-dark-mode'
|
||||
import FormHelp from '@shared/FormInput/Help'
|
||||
import Label from '@shared/FormInput/Label'
|
||||
import Moon from '@images/moon.svg'
|
||||
|
@ -5,7 +5,7 @@ import styles from './index.module.css'
|
||||
import Currency from './Currency'
|
||||
import Debug from './Debug'
|
||||
import Caret from '@images/caret.svg'
|
||||
import useDarkMode from 'use-dark-mode'
|
||||
import useDarkMode from '@oceanprotocol/use-dark-mode'
|
||||
import Appearance from './Appearance'
|
||||
import TokenApproval from './TokenApproval'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
|
@ -4,12 +4,13 @@ import { accountTruncate } from '@utils/web3'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import styles from './Account.module.css'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import Blockies from '@shared/atoms/Blockies'
|
||||
import Avatar from '@shared/atoms/Avatar'
|
||||
|
||||
// Forward ref for Tippy.js
|
||||
// eslint-disable-next-line
|
||||
const Account = React.forwardRef((props, ref: any) => {
|
||||
const { accountId, accountEns, web3Modal, connect } = useWeb3()
|
||||
const { accountId, accountEns, accountEnsAvatar, web3Modal, connect } =
|
||||
useWeb3()
|
||||
|
||||
async function handleActivation(e: FormEvent<HTMLButtonElement>) {
|
||||
// prevent accidentially submitting a form the button might be in
|
||||
@ -30,7 +31,7 @@ const Account = React.forwardRef((props, ref: any) => {
|
||||
ref={ref}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<Blockies accountId={accountId} />
|
||||
<Avatar accountId={accountId} src={accountEnsAvatar} />
|
||||
<span className={styles.address} title={accountId}>
|
||||
{accountTruncate(accountEns || accountId)}
|
||||
</span>
|
||||
|
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-size: var(--font-size-base);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-secondary);
|
||||
white-space: nowrap;
|
||||
@ -32,6 +32,14 @@
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--font-color-text);
|
||||
}
|
||||
|
||||
.conversion strong {
|
||||
font-weight: var(--font-weight-base);
|
||||
}
|
||||
|
||||
.actions {
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user