mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge branch 'main' into fix/issue-1069-c2d-unsupported-networks
This commit is contained in:
commit
3ad213c444
@ -1,10 +1,8 @@
|
||||
#NEXT_PUBLIC_INFURA_PROJECT_ID="xxx"
|
||||
#NEXT_PUBLIC_MARKET_FEE_ADDRESS="0xxx"
|
||||
#NEXT_PUBLIC_PUBLISHER_MARKET_ORDER_FEE="1"
|
||||
#NEXT_PUBLIC_PUBLISHER_MARKET_POOL_SWAP_FEE="1"
|
||||
#NEXT_PUBLIC_PUBLISHER_MARKET_FIXED_SWAP_FEE="1"
|
||||
#NEXT_PUBLIC_CONSUME_MARKET_ORDER_FEE="1"
|
||||
#NEXT_PUBLIC_CONSUME_MARKET_POOL_SWAP_FEE="1"
|
||||
#NEXT_PUBLIC_CONSUME_MARKET_FIXED_SWAP_FEE="1"
|
||||
|
||||
#
|
||||
@ -13,7 +11,6 @@
|
||||
|
||||
# Toggle pricing options presented during price creation
|
||||
#NEXT_PUBLIC_ALLOW_FIXED_PRICING="true"
|
||||
#NEXT_PUBLIC_ALLOW_DYNAMIC_PRICING="true"
|
||||
#NEXT_PUBLIC_ALLOW_FREE_PRICING="true"
|
||||
|
||||
# Privacy Preference Center
|
||||
|
@ -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
|
||||
|
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
@ -10,11 +10,11 @@ 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
|
||||
- run: npm run build:static
|
||||
env:
|
||||
NEXT_PUBLIC_INFURA_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_INFURA_PROJECT_ID }}
|
||||
|
||||
|
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())
|
||||
}
|
||||
})
|
79
README.md
79
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)
|
||||
@ -27,7 +27,6 @@
|
||||
- [💖 Contributing](#-contributing)
|
||||
- [🍴 Forking](#-forking)
|
||||
- [💰 Pricing Options](#-pricing-options)
|
||||
- [Dynamic Pricing](#dynamic-pricing)
|
||||
- [Fixed Pricing](#fixed-pricing)
|
||||
- [Free Pricing](#free-pricing)
|
||||
- [✅ GDPR Compliance](#-gdpr-compliance)
|
||||
@ -50,6 +49,8 @@ cd market
|
||||
nvm use
|
||||
|
||||
npm install
|
||||
# in case of dependency errors, rather use:
|
||||
# npm install --legacy-peer-deps
|
||||
npm start
|
||||
```
|
||||
|
||||
@ -85,7 +86,7 @@ npm start
|
||||
|
||||
To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask.
|
||||
|
||||
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Rinkeby subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
|
||||
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Goerli subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
|
||||
|
||||
> Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes`
|
||||
|
||||
@ -96,19 +97,19 @@ The `app.config.js` file is setup to prioritize environment variables for settin
|
||||
For local development, you can use a `.env` file:
|
||||
|
||||
```bash
|
||||
# modify env variables, Rinkeby is enabled by default when using those files
|
||||
# modify env variables, Goerli is enabled by default when using those files
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## 🦀 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
|
||||
- financial data connected to these datatokens, either a pool or a fixed rate exchange contract
|
||||
- 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
|
||||
|
||||
@ -116,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
|
||||
@ -158,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'
|
||||
@ -179,10 +180,10 @@ The app has [Urql Client](https://formidable.com/open-source/urql/docs/basics/re
|
||||
import { gql, useQuery } from 'urql'
|
||||
|
||||
const query = gql`
|
||||
query PoolLiquidity($id: ID!, $shareId: ID) {
|
||||
pool(id: $id) {
|
||||
query TopSalesQuery {
|
||||
users(first: 20, orderBy: totalSales, orderDirection: desc) {
|
||||
id
|
||||
totalShares
|
||||
totalSales
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -193,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>
|
||||
)
|
||||
}
|
||||
@ -231,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:
|
||||
|
||||
@ -331,7 +316,7 @@ npm run jest
|
||||
|
||||
A coverage report is automatically shown in console whenever `npm run jest` is called. Generated reports are sent to CodeClimate during CI runs.
|
||||
|
||||
During local development you can continously get coverage report feedback in your console by running Jest in watch mode:
|
||||
During local development you can continuously get coverage report feedback in your console by running Jest in watch mode:
|
||||
|
||||
```bash
|
||||
npm run jest:watch
|
||||
@ -394,16 +379,18 @@ We encourage you to fork this repository and create your own data marketplace. W
|
||||
- The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market.
|
||||
- The name "Ocean Market" is also copyright protected and should be changed to the name of your market.
|
||||
|
||||
Additionally, we would also advise that your retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
|
||||
Additionally, we would also advise that you retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
|
||||
|
||||
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
|
||||
|
||||
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
|
||||
|
||||
### Dynamic Pricing
|
||||
|
||||
To allow publishers to set pricing as "Dynamic" you need to add the following environmental variable to your .env file: `NEXT_PUBLIC_ALLOW_DYNAMIC_PRICING="true"` (default).
|
||||
|
||||
### Fixed Pricing
|
||||
|
||||
To allow publishers to set pricing as "Fixed" you need to add the following environmental variable to your .env file: `NEXT_PUBLIC_ALLOW_FIXED_PRICING="true"` (default).
|
||||
@ -412,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
|
||||
|
||||
@ -424,7 +411,7 @@ Feel free to adopt our provided privacy policies to your needs. Per default we c
|
||||
|
||||
### Privacy Preference Center
|
||||
|
||||
Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style propertie to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request.
|
||||
Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style property to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request.
|
||||
|
||||
Now your market users will be provided with additional options to toggle the use of your configured cookie consent categories. You can always retrieve the current consent status per category with the provided `useConsent()` hook. See below, how you can set your own custom cookies depending on the market user's consent. Feel free to adjust the provided utility functions for cookie usage provided in the `src/utils/cookies.ts` file to your needs.
|
||||
|
||||
|
@ -9,20 +9,12 @@ module.exports = {
|
||||
process.env.NEXT_PUBLIC_METADATACACHE_URI ||
|
||||
'https://v4.aquarius.oceanprotocol.com',
|
||||
|
||||
v3MetadataCacheUri:
|
||||
process.env.NEXT_PUBLIC_V3_METADATACACHE_URI ||
|
||||
'https://aquarius.oceanprotocol.com',
|
||||
|
||||
v3MarketUri:
|
||||
process.env.NEXT_PUBLIC_V3_MARKET_URI ||
|
||||
'https://v3.market.oceanprotocol.com',
|
||||
|
||||
// List of chainIds which metadata cache queries will return by default.
|
||||
// This preselects the Chains user preferences.
|
||||
chainIds: [1, 137, 56, 246, 1285],
|
||||
|
||||
// List of all supported chainIds. Used to populate the Chains user preferences list.
|
||||
chainIdsSupported: [1, 137, 56, 246, 1285, 3, 4, 80001, 1287],
|
||||
chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001, 1287],
|
||||
|
||||
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',
|
||||
|
||||
@ -33,9 +25,6 @@ module.exports = {
|
||||
// publisher market fee that is taken upon ordering an asset, it is an absolute value, it is declared on erc20 creation
|
||||
publisherMarketOrderFee:
|
||||
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_ORDER_FEE || '0',
|
||||
// fee recieved by the publisher market when a dt is swaped from a pool, percent
|
||||
publisherMarketPoolSwapFee:
|
||||
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_POOL_SWAP_FEE || '0',
|
||||
// fee recieved by the publisher market when a dt is bought from a fixed rate exchange, percent
|
||||
publisherMarketFixedSwapFee:
|
||||
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_FIXED_SWAP_FEE || '0',
|
||||
@ -43,9 +32,6 @@ module.exports = {
|
||||
// consume market fee that is taken upon ordering an asset, it is an absolute value, it is specified on order
|
||||
consumeMarketOrderFee:
|
||||
process.env.NEXT_PUBLIC_CONSUME_MARKET_ORDER_FEE || '0',
|
||||
// fee recieved by the consume market when a dt is swaped from a pool, percent
|
||||
consumeMarketPoolSwapFee:
|
||||
process.env.NEXT_PUBLIC_CONSUME_MARKET_POOL_SWAP_FEE || '0',
|
||||
// fee recieved by the consume market when a dt is bought from a fixed rate exchange, percent
|
||||
consumeMarketFixedSwapFee:
|
||||
process.env.NEXT_PUBLIC_CONSUME_MARKET_FIXED_SWAP_FEE || '0',
|
||||
@ -68,17 +54,20 @@ 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',
|
||||
storageKey: 'oceanDarkMode'
|
||||
},
|
||||
|
||||
// Used to show or hide the fixed, dynamic or free price options
|
||||
// Used to show or hide the fixed or free price options
|
||||
// tab to publishers during the price creation.
|
||||
allowFixedPricing: process.env.NEXT_PUBLIC_ALLOW_FIXED_PRICING || 'true',
|
||||
allowDynamicPricing: process.env.NEXT_PUBLIC_ALLOW_DYNAMIC_PRICING || 'true',
|
||||
allowFreePricing: process.env.NEXT_PUBLIC_ALLOW_FREE_PRICING || 'true',
|
||||
|
||||
// Set the default privacy policy to initially display
|
||||
|
@ -14,6 +14,6 @@
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"note": "Counted on-chain from our NFT and pool factories. Includes assets in all Ocean Market forks and [purgatory](https://github.com/oceanprotocol/list-purgatory)."
|
||||
"note": "Counted on-chain from our NFT factories. Includes assets in all Ocean Market forks and [purgatory](https://github.com/oceanprotocol/list-purgatory)."
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +1,3 @@
|
||||
{
|
||||
"description": "Update selected metadata of this data set. Updating metadata will create an on-chain transaction you have to approve in your wallet.",
|
||||
"form": {
|
||||
"success": "🎉 Successfully updated. 🎉",
|
||||
"successAction": "Close",
|
||||
"error": "Updating DDO failed.",
|
||||
"data": [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "New Title",
|
||||
"placeholder": "e.g. Shapes of Desert Plants",
|
||||
"help": "Enter a concise title.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"label": "New Description",
|
||||
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics).",
|
||||
"type": "textarea",
|
||||
"rows": 10,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"label": "New Price",
|
||||
"type": "number",
|
||||
"min": "1",
|
||||
"placeholder": "0",
|
||||
"help": "Enter a new price.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"prominentHelp": true,
|
||||
"type": "files"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"prominentHelp": true,
|
||||
"type": "files"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "timeout",
|
||||
"label": "Timeout",
|
||||
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
|
||||
"type": "select",
|
||||
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
|
||||
"sortOptions": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"label": "New Author",
|
||||
"placeholder": "e.g. Mrs McJellyfish",
|
||||
"help": "Give proper attribution for your data set.",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
"description": "Updating metadata or updating compute settings will create an on-chain transaction you have to approve in your wallet."
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
{
|
||||
"description": "Only selected algorithms are allowed to run on this data set. Updating these settings will create an on-chain transaction you have to approve in your wallet.",
|
||||
"form": {
|
||||
"title": "Set allowed algorithms",
|
||||
"success": "🎉 Successfully updated. 🎉",
|
||||
"successAction": "Close",
|
||||
"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": [
|
||||
{
|
||||
|
72
content/pages/editMetadata.json
Normal file
72
content/pages/editMetadata.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"form": {
|
||||
"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": "name",
|
||||
"label": "New Title",
|
||||
"placeholder": "e.g. Shapes of Desert Plants",
|
||||
"help": "Enter a concise title.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"label": "New Description",
|
||||
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics).",
|
||||
"type": "textarea",
|
||||
"rows": 10,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"label": "New Price",
|
||||
"type": "number",
|
||||
"min": "1",
|
||||
"placeholder": "0",
|
||||
"help": "Enter a new price.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"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 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"
|
||||
},
|
||||
{
|
||||
"name": "links",
|
||||
"label": "New sample file",
|
||||
"placeholder": "e.g. https://file.com/samplefile.json",
|
||||
"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"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "timeout",
|
||||
"label": "Timeout",
|
||||
"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,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"label": "New Author",
|
||||
"placeholder": "e.g. Mrs McJellyfish",
|
||||
"help": "Give proper attribution for your dataset.",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"label": "New Tags",
|
||||
"type": "tags",
|
||||
"placeholder": "e.g. logistics",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -1,31 +1,13 @@
|
||||
{
|
||||
"create": {
|
||||
"empty": {
|
||||
"title": "No Price Created",
|
||||
"info": "This data set has no price yet. As the publisher you can create a fixed price, or a dynamic price for it. Onwards!",
|
||||
"action": {
|
||||
"name": "Create Pricing",
|
||||
"help": "Create Pricing will mint your datatokens, approve spending, and create either a pool or a fixed rate exchange in one process. You will need to approve those multiple steps in your wallet."
|
||||
}
|
||||
},
|
||||
"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 OCEAN.",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"dynamic": {
|
||||
"title": "Dynamic",
|
||||
"info": "Let's create a decentralized, automated market for your data set. The datatoken for this data set will be worth the entered amount of OCEAN. Additionally, you will provide liquidity into a Datatoken/OCEAN liquidity pool with Balancer.",
|
||||
"tooltips": {
|
||||
"poolInfo": "The liquidity pool provides the funds for traders to trade against. The price of the asset is determined by the ratio of OCEAN to datatokens.",
|
||||
"swapFee": "Liquidity providers earn this fee on all pool trades, proportionally to their share of the pool. The fee is set by the creator of the pool and is used to incentivize liquidity providers to join the pool.",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"free": {
|
||||
"title": "Free",
|
||||
"info": "Set your dataset as free. The datatoken for this dataset will be given for free via creating a faucet.",
|
||||
@ -40,33 +22,10 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"pool": {
|
||||
"approval": {
|
||||
"tooltips": {
|
||||
"price": "The price is determined by an automated market maker, which is a type of decentralized exchange protocol that relies on a mathematical formula. It is an alternative to a traditional order book.",
|
||||
"liquidity": "Providing liquidity will earn you SWAPFEE% on every transaction in this pool, proportionally to your share of the pool.",
|
||||
"approveSpecific": "Give the smart contract permission to spend your COIN which has to be done for each transaction. You can optionally set this to infinite in your user preferences.",
|
||||
"approveInfinite": "Give the smart contract permission to spend infinte amounts of your COIN so you have to do this only once. You can disable allowing infinite amounts in your user preferences."
|
||||
},
|
||||
"add": {
|
||||
"title": "Add Liquidity",
|
||||
"output": {
|
||||
"help": "Providing liquidity will earn you SWAPFEE% on every transaction in this pool, proportionally to your share of the pool.",
|
||||
"titleIn": "You will receive",
|
||||
"titleOut": "Pool conversion"
|
||||
},
|
||||
"action": "Supply"
|
||||
},
|
||||
"remove": {
|
||||
"title": "Remove Liquidity",
|
||||
"simple": "Set the amount of your pool shares to spend. You will get the equivalent value in OCEAN, limited to maximum amount for pool protection.",
|
||||
"output": {
|
||||
"titleOutExpected": "Expected output",
|
||||
"titleOutMinimum": "Minimum received"
|
||||
},
|
||||
"action": "Remove"
|
||||
}
|
||||
},
|
||||
"trade": {
|
||||
"action": "Swap"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,8 +39,8 @@
|
||||
{
|
||||
"name": "tags",
|
||||
"label": "Tags",
|
||||
"placeholder": "e.g. logistics, ai",
|
||||
"help": "Separate tags with comma."
|
||||
"type": "tags",
|
||||
"placeholder": "e.g. logistics"
|
||||
},
|
||||
{
|
||||
"name": "dockerImage",
|
||||
@ -122,7 +122,7 @@
|
||||
"label": "Algorithm Privacy",
|
||||
"type": "checkbox",
|
||||
"options": ["Keep my algorithm private"],
|
||||
"help": "By default, your algorithm can be downloaded for a fixed or dynamic 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
|
||||
},
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"asset": {
|
||||
"title": "Data Set In 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": {
|
||||
|
@ -14,7 +14,7 @@
|
||||
"link": "/profile"
|
||||
}
|
||||
],
|
||||
"announcement": "Data NFTs, One-Sided Staking and more.",
|
||||
"announcement": "Explore [OceanONDA V4](https://blog.oceanprotocol.com/how-to-publish-a-data-nft-f58ad2a622a9).",
|
||||
"warning": {
|
||||
"ctd": "Compute-to-Data is still in a testing phase, please use it only on test networks."
|
||||
}
|
||||
|
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)"
|
42012
package-lock.json
generated
42012
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
90
package.json
90
package.json
@ -7,6 +7,7 @@
|
||||
"scripts": {
|
||||
"start": "npm run pregenerate && next dev -p 8000",
|
||||
"build": "npm run pregenerate && next build",
|
||||
"build:static": "npm run build && next export",
|
||||
"serve": "serve -s public/",
|
||||
"pregenerate": "bash scripts/pregenerate.sh",
|
||||
"test": "npm run pregenerate && npm run lint && npm run type-check && npm run jest",
|
||||
@ -16,8 +17,8 @@
|
||||
"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",
|
||||
"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/",
|
||||
"postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql",
|
||||
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
|
||||
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
|
||||
"storybook:build": "cross-env NODE_ENV=test build-storybook"
|
||||
},
|
||||
@ -25,38 +26,38 @@
|
||||
"@coingecko/cryptoformat": "^0.5.4",
|
||||
"@loadable/component": "^5.15.2",
|
||||
"@oceanprotocol/art": "^3.2.0",
|
||||
"@oceanprotocol/lib": "^1.1.2",
|
||||
"@oceanprotocol/lib": "^2.1.1",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@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",
|
||||
"chart.js": "^3.8.0",
|
||||
"classnames": "^2.3.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"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": "^10.0.5",
|
||||
"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",
|
||||
"match-sorter": "^6.3.1",
|
||||
"myetherwallet-blockies": "^0.1.1",
|
||||
"next": "^12.1.6",
|
||||
"next": "12.3.1",
|
||||
"query-string": "^7.1.1",
|
||||
"react": "^18.1.0",
|
||||
"react-chartjs-2": "^4.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-clipboard.js": "^2.0.16",
|
||||
"react-data-table-component": "^6.11.7",
|
||||
"react-data-table-component": "^7.5.2",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-dotdotdot": "^1.3.1",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-paginate": "^8.1.3",
|
||||
"react-spring": "^9.4.5",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spring": "^9.5.2",
|
||||
"react-tabs": "^5.1.0",
|
||||
"react-toastify": "^9.0.4",
|
||||
"remark": "^13.0.0",
|
||||
@ -65,59 +66,52 @@
|
||||
"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.3",
|
||||
"web3modal": "^1.9.7",
|
||||
"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.7",
|
||||
"@storybook/builder-webpack5": "^6.5.7",
|
||||
"@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",
|
||||
"@types/chart.js": "^2.9.37",
|
||||
"@storybook/addon-essentials": "^6.5.12",
|
||||
"@storybook/builder-webpack5": "^6.5.12",
|
||||
"@storybook/manager-webpack5": "^6.5.12",
|
||||
"@storybook/react": "^6.5.12",
|
||||
"@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/react": "^18.0.12",
|
||||
"@types/node": "^18.7.18",
|
||||
"@types/react": "^18.0.21",
|
||||
"@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.27.1",
|
||||
"@typescript-eslint/parser": "^5.27.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"apollo": "^2.34.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-oceanprotocol": "^2.0.1",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-oceanprotocol": "^2.0.4",
|
||||
"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.7.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"jest": "^28.1.1",
|
||||
"jest-environment-jsdom": "^28.1.1",
|
||||
"prettier": "^2.6.2",
|
||||
"jest": "^29.1.2",
|
||||
"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",
|
||||
|
@ -10,7 +10,7 @@ AWS_S3_BUCKET="www-market"
|
||||
set -e;
|
||||
|
||||
function s3sync {
|
||||
aws s3 sync ./public s3://"$1" \
|
||||
aws s3 sync ./out s3://"$1" \
|
||||
--include "*" \
|
||||
--exclude "*.html" \
|
||||
--exclude "sw.js" \
|
||||
@ -24,7 +24,7 @@ function s3sync {
|
||||
--delete \
|
||||
--acl public-read
|
||||
|
||||
aws s3 sync ./public s3://"$1" \
|
||||
aws s3 sync ./out s3://"$1" \
|
||||
--exclude "*" \
|
||||
--include "*.html" \
|
||||
--include "sw.js" \
|
||||
|
@ -9,11 +9,10 @@ import React, {
|
||||
} from 'react'
|
||||
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
|
||||
import { CancelToken } from 'axios'
|
||||
import { checkV3Asset, retrieveAsset } from '@utils/aquarius'
|
||||
import { retrieveAsset } from '@utils/aquarius'
|
||||
import { useWeb3 } from './Web3'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import { getAccessDetails } from '@utils/accessDetailsAndPricing'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { useMarketMetadata } from './MarketMetadata'
|
||||
@ -26,7 +25,6 @@ export interface AssetProviderValue {
|
||||
owner: string
|
||||
error?: string
|
||||
isAssetNetwork: boolean
|
||||
isV3Asset: boolean
|
||||
isOwner: boolean
|
||||
oceanConfig: Config
|
||||
loading: boolean
|
||||
@ -54,7 +52,6 @@ function AssetProvider({
|
||||
const [error, setError] = useState<string>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
|
||||
const [isV3Asset, setIsV3Asset] = useState<boolean>()
|
||||
const [oceanConfig, setOceanConfig] = useState<Config>()
|
||||
|
||||
const newCancelToken = useCancelToken()
|
||||
@ -72,13 +69,36 @@ function AssetProvider({
|
||||
const asset = await retrieveAsset(did, token)
|
||||
|
||||
if (!asset) {
|
||||
setIsV3Asset(await checkV3Asset(did, token))
|
||||
setError(
|
||||
`\`${did}\`` +
|
||||
'\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.'
|
||||
)
|
||||
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if (asset.nft.state) {
|
||||
// handle nft states as documented in https://docs.oceanprotocol.com/concepts/did-ddo/#state
|
||||
let state
|
||||
switch (asset.nft.state) {
|
||||
case 1:
|
||||
state = 'end-of-life'
|
||||
break
|
||||
case 2:
|
||||
state = 'deprecated'
|
||||
break
|
||||
case 3:
|
||||
state = 'revoked'
|
||||
break
|
||||
}
|
||||
|
||||
setTitle(`This asset has been flagged as "${state}" by the publisher`)
|
||||
setError(`\`${did}\`` + `\n\nPublisher Address: ${asset.nft.owner}`)
|
||||
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
|
||||
return
|
||||
}
|
||||
|
||||
if (asset) {
|
||||
setError(undefined)
|
||||
setAsset((prevState) => ({
|
||||
...prevState,
|
||||
@ -101,6 +121,7 @@ function AssetProvider({
|
||||
// -----------------------------------
|
||||
const fetchAccessDetails = useCallback(async (): Promise<void> => {
|
||||
if (!asset?.chainId || !asset?.services) return
|
||||
|
||||
const accessDetails = await getAccessDetails(
|
||||
asset.chainId,
|
||||
asset.services[0].datatokenAddress,
|
||||
@ -183,7 +204,6 @@ function AssetProvider({
|
||||
loading,
|
||||
fetchAsset,
|
||||
isAssetNetwork,
|
||||
isV3Asset,
|
||||
isOwner,
|
||||
oceanConfig
|
||||
} as AssetProviderValue
|
||||
|
@ -6,7 +6,10 @@ export const opcQuery = gql`
|
||||
swapOceanFee
|
||||
swapNonOceanFee
|
||||
approvedTokens {
|
||||
id
|
||||
address: id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
}
|
||||
id
|
||||
}
|
||||
|
@ -12,14 +12,12 @@ export interface AppConfig {
|
||||
chainIdsSupported: number[]
|
||||
marketFeeAddress: string
|
||||
publisherMarketOrderFee: string
|
||||
publisherMarketPoolSwapFee: string
|
||||
publisherMarketFixedSwapFee: string
|
||||
consumeMarketOrderFee: string
|
||||
consumeMarketPoolSwapFee: string
|
||||
consumeMarketFixedSwapFee: string
|
||||
currencies: string[]
|
||||
coingeckoTokenIds: string[]
|
||||
allowFixedPricing: string
|
||||
allowDynamicPricing: string
|
||||
allowFreePricing: string
|
||||
defaultPrivacyPolicySlug: string
|
||||
privacyPreferenceCenter: string
|
||||
@ -28,8 +26,6 @@ export interface AppConfig {
|
||||
classNameLight: string
|
||||
storageKey: string
|
||||
}
|
||||
v3MetadataCacheUri: string
|
||||
v3MarketUri: string
|
||||
}
|
||||
export interface SiteContent {
|
||||
siteTitle: 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)
|
||||
|
||||
@ -36,11 +37,18 @@ function MarketMetadataProvider({
|
||||
|
||||
opcData.push({
|
||||
chainId: appConfig.chainIdsSupported[i],
|
||||
approvedTokens: response.data?.opc.approvedTokens?.map((x) => x.id),
|
||||
approvedTokens: response.data?.opc.approvedTokens.map(
|
||||
(token) => token.address
|
||||
),
|
||||
swapApprovedFee: response.data?.opc.swapOceanFee,
|
||||
swapNotApprovedFee: response.data?.opc.swapNonOceanFee
|
||||
} as unknown as OpcFee)
|
||||
}
|
||||
LoggerInstance.log('[MarketMetadata] Got new data.', {
|
||||
opcFees: opcData,
|
||||
siteContent,
|
||||
appConfig
|
||||
})
|
||||
setOpcFees(opcData)
|
||||
}
|
||||
getOpcData()
|
||||
|
@ -1,57 +0,0 @@
|
||||
import { gql } from 'urql'
|
||||
|
||||
export const poolDataQuery = gql`
|
||||
query PoolData(
|
||||
$pool: ID!
|
||||
$poolAsString: String!
|
||||
$owner: String!
|
||||
$user: String
|
||||
) {
|
||||
poolData: pool(id: $pool) {
|
||||
id
|
||||
totalShares
|
||||
liquidityProviderSwapFee
|
||||
publishMarketSwapFee
|
||||
spotPrice
|
||||
baseToken {
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
baseTokenWeight
|
||||
baseTokenLiquidity
|
||||
datatoken {
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
datatokenWeight
|
||||
datatokenLiquidity
|
||||
shares(where: { user: $owner }) {
|
||||
shares
|
||||
}
|
||||
}
|
||||
poolDataUser: pool(id: $pool) {
|
||||
shares(where: { user: $user }) {
|
||||
shares
|
||||
}
|
||||
}
|
||||
poolSnapshots(first: 1000, where: { pool: $poolAsString }, orderBy: date) {
|
||||
date
|
||||
spotPrice
|
||||
baseTokenLiquidity
|
||||
datatokenLiquidity
|
||||
swapVolume
|
||||
baseToken {
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
datatoken {
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
@ -1,35 +0,0 @@
|
||||
import {
|
||||
PoolData_poolSnapshots as PoolDataPoolSnapshots,
|
||||
PoolData_poolData as PoolDataPoolData
|
||||
} from 'src/@types/subgraph/PoolData'
|
||||
|
||||
export interface PoolInfo {
|
||||
liquidityProviderSwapFee: string
|
||||
publishMarketSwapFee: string
|
||||
weightBaseToken: string
|
||||
weightDt: string
|
||||
datatokenSymbol: string
|
||||
datatokenAddress: string
|
||||
datatokenDecimals: number
|
||||
baseTokenSymbol: string
|
||||
baseTokenAddress: string
|
||||
baseTokenDecimals: number
|
||||
totalPoolTokens: string
|
||||
}
|
||||
|
||||
export interface PoolInfoUser {
|
||||
liquidity: string
|
||||
poolShares: string
|
||||
poolSharePercentage: string
|
||||
}
|
||||
|
||||
export interface PoolProviderValue {
|
||||
poolData: PoolDataPoolData
|
||||
poolInfo: PoolInfo
|
||||
poolInfoOwner: PoolInfoUser
|
||||
poolInfoUser: PoolInfoUser
|
||||
poolSnapshots: PoolDataPoolSnapshots[]
|
||||
hasUserAddedLiquidity: boolean
|
||||
refreshInterval: number
|
||||
fetchAllData: () => void
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { isValidNumber } from '@utils/numbers'
|
||||
import { getQueryContext, fetchData } from '@utils/subgraph'
|
||||
import Decimal from 'decimal.js'
|
||||
import { PoolData } from 'src/@types/subgraph/PoolData'
|
||||
import { OperationResult } from 'urql'
|
||||
import { poolDataQuery } from './_queries'
|
||||
|
||||
export async function getPoolData(
|
||||
chainId: number,
|
||||
pool: string,
|
||||
owner: string,
|
||||
user: string
|
||||
) {
|
||||
const queryVariables = {
|
||||
// Using `pool` & `poolAsString` is a workaround to make the mega query work.
|
||||
// See https://github.com/oceanprotocol/ocean-subgraph/issues/301
|
||||
pool: pool.toLowerCase(),
|
||||
poolAsString: pool.toLowerCase(),
|
||||
owner: owner.toLowerCase(),
|
||||
user: user.toLowerCase()
|
||||
}
|
||||
|
||||
const response: OperationResult<PoolData> = await fetchData(
|
||||
poolDataQuery,
|
||||
queryVariables,
|
||||
getQueryContext(chainId)
|
||||
)
|
||||
return response?.data
|
||||
}
|
||||
|
||||
export function getWeight(weight: string) {
|
||||
return isValidNumber(weight) ? new Decimal(weight).mul(10).toString() : '0'
|
||||
}
|
||||
|
||||
export function getFee(fee: string) {
|
||||
// fees are tricky: to get 0.1% you need to convert from 0.001
|
||||
return isValidNumber(fee) ? new Decimal(fee).mul(100).toString() : '0'
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import Decimal from 'decimal.js'
|
||||
import React, {
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
createContext,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
import {
|
||||
PoolData_poolSnapshots as PoolDataPoolSnapshots,
|
||||
PoolData_poolData as PoolDataPoolData
|
||||
} from 'src/@types/subgraph/PoolData'
|
||||
import { useAsset } from '../Asset'
|
||||
import { useWeb3 } from '../Web3'
|
||||
import { calcSingleOutGivenPoolIn } from '@utils/pool'
|
||||
import { PoolProviderValue, PoolInfo, PoolInfoUser } from './_types'
|
||||
import { getFee, getPoolData, getWeight } from './_utils'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
|
||||
const PoolContext = createContext({} as PoolProviderValue)
|
||||
|
||||
const refreshInterval = 10000 // 10 sec.
|
||||
|
||||
const initialPoolInfoUser: Partial<PoolInfoUser> = {
|
||||
liquidity: '0',
|
||||
poolShares: '0'
|
||||
}
|
||||
|
||||
const initialPoolInfoCreator: Partial<PoolInfoUser> = initialPoolInfoUser
|
||||
|
||||
function PoolProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
const { accountId, web3, chainId } = useWeb3()
|
||||
const { asset, owner } = useAsset()
|
||||
const { getOpcFeeForToken } = useMarketMetadata()
|
||||
const [poolData, setPoolData] = useState<PoolDataPoolData>()
|
||||
const [poolInfo, setPoolInfo] = useState<PoolInfo>()
|
||||
const [poolInfoOwner, setPoolInfoOwner] = useState<PoolInfoUser>(
|
||||
initialPoolInfoCreator as PoolInfoUser
|
||||
)
|
||||
const [poolInfoUser, setPoolInfoUser] = useState<PoolInfoUser>(
|
||||
initialPoolInfoUser as PoolInfoUser
|
||||
)
|
||||
const [poolSnapshots, setPoolSnapshots] = useState<PoolDataPoolSnapshots[]>()
|
||||
const [hasUserAddedLiquidity, setUserHasAddedLiquidity] = useState(false)
|
||||
|
||||
const fetchAllData = useCallback(async () => {
|
||||
if (!asset?.chainId || !asset?.accessDetails?.addressOrId || !owner) return
|
||||
|
||||
const response = await getPoolData(
|
||||
asset.chainId,
|
||||
asset.accessDetails.addressOrId,
|
||||
owner,
|
||||
accountId || ''
|
||||
)
|
||||
if (!response) return
|
||||
|
||||
setPoolData(response.poolData)
|
||||
|
||||
// calculate pool info user
|
||||
const poolInfoShares = response.poolDataUser?.shares[0]?.shares || '0'
|
||||
const userLiquidity = calcSingleOutGivenPoolIn(
|
||||
response.poolData.baseTokenLiquidity,
|
||||
response.poolData.totalShares,
|
||||
poolInfoShares
|
||||
)
|
||||
|
||||
// Pool share in %. We double it to compensate for ss bot
|
||||
const poolSharePercentage = new Decimal(poolInfoShares)
|
||||
.dividedBy(new Decimal(response.poolData.totalShares))
|
||||
.mul(200)
|
||||
.toFixed(2)
|
||||
|
||||
setUserHasAddedLiquidity(Number(poolSharePercentage) > 0)
|
||||
|
||||
const newPoolInfoUser: PoolInfoUser = {
|
||||
liquidity: userLiquidity,
|
||||
poolShares: poolInfoShares,
|
||||
poolSharePercentage
|
||||
}
|
||||
setPoolInfoUser((prevState: PoolInfoUser) => ({
|
||||
...prevState,
|
||||
...newPoolInfoUser
|
||||
}))
|
||||
|
||||
setPoolSnapshots(response.poolSnapshots)
|
||||
LoggerInstance.log('[pool] Fetched pool data:', response.poolData)
|
||||
LoggerInstance.log('[pool] Fetched user data:', response.poolDataUser)
|
||||
LoggerInstance.log('[pool] Fetched pool snapshots:', response.poolSnapshots)
|
||||
}, [asset?.chainId, asset?.accessDetails?.addressOrId, owner, accountId])
|
||||
|
||||
// 0 Fetch all the data on mount if we are on a pool.
|
||||
// All further effects depend on the fetched data
|
||||
// and only do further data checking and manipulation.
|
||||
//
|
||||
useEffect(() => {
|
||||
if (asset?.accessDetails?.type !== 'dynamic') return
|
||||
fetchAllData()
|
||||
const interval = setInterval(() => {
|
||||
fetchAllData()
|
||||
}, refreshInterval)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchAllData, asset?.accessDetails?.type])
|
||||
|
||||
//
|
||||
// 1 General Pool Info
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!poolData) return
|
||||
|
||||
const newPoolInfo = {
|
||||
liquidityProviderSwapFee: getFee(poolData.liquidityProviderSwapFee),
|
||||
publishMarketSwapFee: getFee(poolData.publishMarketSwapFee),
|
||||
opcFee: getFee(
|
||||
getOpcFeeForToken(poolData.baseToken.address, asset?.chainId)
|
||||
),
|
||||
weightBaseToken: getWeight(poolData.baseTokenWeight),
|
||||
weightDt: getWeight(poolData.datatokenWeight),
|
||||
datatokenSymbol: poolData.datatoken.symbol,
|
||||
datatokenAddress: poolData.datatoken.address,
|
||||
datatokenDecimals: poolData.datatoken.decimals,
|
||||
baseTokenSymbol: poolData.baseToken.symbol,
|
||||
baseTokenAddress: poolData.baseToken.address,
|
||||
baseTokenDecimals: poolData.baseToken.decimals,
|
||||
totalPoolTokens: poolData.totalShares
|
||||
}
|
||||
|
||||
setPoolInfo(newPoolInfo)
|
||||
LoggerInstance.log('[pool] Created new pool info:', newPoolInfo)
|
||||
}, [asset?.chainId, chainId, getOpcFeeForToken, poolData, web3])
|
||||
|
||||
//
|
||||
// 2 Pool Creator Info
|
||||
//
|
||||
useEffect(() => {
|
||||
if (
|
||||
!poolData ||
|
||||
!poolInfo?.totalPoolTokens ||
|
||||
!poolData.shares[0]?.shares ||
|
||||
poolData.shares[0]?.shares === '0'
|
||||
)
|
||||
return
|
||||
|
||||
// Pool share tokens. We double it to compensate for ss bot
|
||||
const poolSharePercentage = new Decimal(poolData.shares[0]?.shares)
|
||||
.dividedBy(poolInfo.totalPoolTokens)
|
||||
.mul(200)
|
||||
.toFixed(2)
|
||||
|
||||
const ownerLiquidity = calcSingleOutGivenPoolIn(
|
||||
poolData.baseTokenLiquidity,
|
||||
poolData.totalShares,
|
||||
poolData?.shares[0]?.shares
|
||||
)
|
||||
|
||||
const newPoolOwnerInfo = {
|
||||
liquidity: ownerLiquidity,
|
||||
poolShares: poolData.shares[0]?.shares,
|
||||
poolSharePercentage
|
||||
}
|
||||
setPoolInfoOwner(newPoolOwnerInfo)
|
||||
LoggerInstance.log('[pool] Created new pool creatorinfo:', newPoolOwnerInfo)
|
||||
}, [
|
||||
asset?.chainId,
|
||||
poolData,
|
||||
poolInfo?.baseTokenAddress,
|
||||
poolInfo?.totalPoolTokens
|
||||
])
|
||||
|
||||
return (
|
||||
<PoolContext.Provider
|
||||
value={
|
||||
{
|
||||
poolData,
|
||||
poolInfo,
|
||||
poolInfoOwner,
|
||||
poolInfoUser,
|
||||
poolSnapshots,
|
||||
hasUserAddedLiquidity,
|
||||
refreshInterval,
|
||||
fetchAllData
|
||||
} as PoolProviderValue
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</PoolContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper hook to access the provider values
|
||||
const usePool = (): PoolProviderValue => useContext(PoolContext)
|
||||
|
||||
export { PoolProvider, usePool, PoolContext }
|
||||
export default PoolProvider
|
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,25 +7,21 @@ import React, {
|
||||
useCallback,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
import {
|
||||
getPoolSharesData,
|
||||
getUserSales,
|
||||
getUserTokenOrders
|
||||
} from '@utils/subgraph'
|
||||
import { useUserPreferences } from './UserPreferences'
|
||||
import { PoolShares_poolShares as PoolShare } from '../@types/subgraph/PoolShares'
|
||||
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
|
||||
poolShares: PoolShare[]
|
||||
isPoolSharesLoading: boolean
|
||||
assets: Asset[]
|
||||
assetsTotal: number
|
||||
isEthAddress: boolean
|
||||
@ -39,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,
|
||||
@ -63,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
|
||||
@ -73,102 +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])
|
||||
|
||||
//
|
||||
// POOL SHARES
|
||||
//
|
||||
const [poolShares, setPoolShares] = useState<PoolShare[]>()
|
||||
const [isPoolSharesLoading, setIsPoolSharesLoading] = useState<boolean>(false)
|
||||
const [poolSharesInterval, setPoolSharesInterval] = useState<NodeJS.Timeout>()
|
||||
|
||||
const fetchPoolShares = useCallback(
|
||||
async (accountId: string, chainIds: number[], isEthAddress: boolean) => {
|
||||
if (!accountId || !chainIds || !isEthAddress) return
|
||||
|
||||
try {
|
||||
setIsPoolSharesLoading(true)
|
||||
const poolShares = await getPoolSharesData(accountId, chainIds)
|
||||
setPoolShares(poolShares)
|
||||
LoggerInstance.log(
|
||||
`[profile] Fetched ${poolShares.length} pool shares.`,
|
||||
poolShares
|
||||
)
|
||||
} catch (error) {
|
||||
LoggerInstance.error('Error fetching pool shares: ', error.message)
|
||||
} finally {
|
||||
setIsPoolSharesLoading(false)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
await fetchPoolShares(accountId, chainIds, isEthAddress)
|
||||
|
||||
if (poolSharesInterval) return
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
LoggerInstance.log(
|
||||
`[profile] Re-fetching pool shares after ${refreshInterval / 1000}s.`
|
||||
)
|
||||
await fetchPoolShares(accountId, chainIds, isEthAddress)
|
||||
}, refreshInterval)
|
||||
setPoolSharesInterval(interval)
|
||||
}
|
||||
init()
|
||||
|
||||
return () => {
|
||||
clearInterval(poolSharesInterval)
|
||||
}
|
||||
}, [poolSharesInterval, fetchPoolShares, accountId, chainIds, isEthAddress])
|
||||
}, [accountId, isEthAddress])
|
||||
|
||||
//
|
||||
// PUBLISHED ASSETS
|
||||
@ -300,8 +224,6 @@ function ProfileProvider({
|
||||
<ProfileContext.Provider
|
||||
value={{
|
||||
profile,
|
||||
poolShares,
|
||||
isPoolSharesLoading,
|
||||
assets,
|
||||
assetsTotal,
|
||||
isEthAddress,
|
@ -133,6 +133,14 @@ function UserPreferencesProvider({
|
||||
setBookmarks(newPinned)
|
||||
}, [bookmarks])
|
||||
|
||||
// chainIds old data migration
|
||||
// remove deprecated networks from user-saved chainIds
|
||||
useEffect(() => {
|
||||
if (!chainIds.includes(3) && !chainIds.includes(4)) return
|
||||
const newChainIds = chainIds.filter((id) => id !== 3 && id !== 4)
|
||||
setChainIds(newChainIds)
|
||||
}, [chainIds])
|
||||
|
||||
return (
|
||||
<UserPreferencesContext.Provider
|
||||
value={
|
||||
|
@ -13,8 +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 { getOceanBalance } from '@utils/ocean'
|
||||
import { getEnsProfile } from '@utils/ens'
|
||||
import useNetworkMetadata, {
|
||||
getNetworkDataById,
|
||||
getNetworkDisplayName,
|
||||
@ -22,14 +21,18 @@ import useNetworkMetadata, {
|
||||
NetworkType
|
||||
} from '../@hooks/useNetworkMetadata'
|
||||
import { useMarketMetadata } from './MarketMetadata'
|
||||
import { getTokenBalance } from '@utils/web3'
|
||||
import { getOpcsApprovedTokens } from '@utils/subgraph'
|
||||
|
||||
interface Web3ProviderValue {
|
||||
web3: Web3
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
web3Provider: any
|
||||
web3Modal: Web3Modal
|
||||
web3ProviderInfo: IProviderInfo
|
||||
accountId: string
|
||||
accountEns: string
|
||||
accountEnsAvatar: string
|
||||
balance: UserBalance
|
||||
networkId: number
|
||||
chainId: number
|
||||
@ -39,6 +42,7 @@ interface Web3ProviderValue {
|
||||
isTestnet: boolean
|
||||
web3Loading: boolean
|
||||
isSupportedOceanNetwork: boolean
|
||||
approvedBaseTokens: TokenInfo[]
|
||||
connect: () => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
}
|
||||
@ -51,24 +55,18 @@ 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: {
|
||||
package: WalletConnectProvider,
|
||||
options: { infuraId }
|
||||
options: {
|
||||
infuraId,
|
||||
rpc: {
|
||||
137: 'https://polygon-rpc.com',
|
||||
80001: 'https://rpc-mumbai.matic.today'
|
||||
}
|
||||
}
|
||||
}
|
||||
// torus: {
|
||||
// package: require('@toruslabs/torus-embed')
|
||||
// // options: {
|
||||
// // networkParams: {
|
||||
// // host: oceanConfig.url, // optional
|
||||
// // chainId: 1337, // optional
|
||||
// // networkId: 1337 // optional
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
}
|
||||
: {}
|
||||
|
||||
@ -87,7 +85,9 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
const { appConfig } = useMarketMetadata()
|
||||
|
||||
const [web3, setWeb3] = useState<Web3>()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [web3Provider, setWeb3Provider] = useState<any>()
|
||||
|
||||
const [web3Modal, setWeb3Modal] = useState<Web3Modal>()
|
||||
const [web3ProviderInfo, setWeb3ProviderInfo] = useState<IProviderInfo>()
|
||||
const [networkId, setNetworkId] = useState<number>()
|
||||
@ -98,12 +98,13 @@ 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',
|
||||
ocean: '0'
|
||||
eth: '0'
|
||||
})
|
||||
const [isSupportedOceanNetwork, setIsSupportedOceanNetwork] = useState(true)
|
||||
const [approvedBaseTokens, setApprovedBaseTokens] = useState<TokenInfo[]>()
|
||||
|
||||
// -----------------------------------
|
||||
// Helper: connect to web3
|
||||
@ -142,43 +143,84 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
}
|
||||
}, [web3Modal])
|
||||
|
||||
// -----------------------------------
|
||||
// Helper: Get approved base tokens list
|
||||
// -----------------------------------
|
||||
const getApprovedBaseTokens = useCallback(async (chainId: number) => {
|
||||
try {
|
||||
const approvedTokensList = await getOpcsApprovedTokens(chainId)
|
||||
setApprovedBaseTokens(approvedTokensList)
|
||||
LoggerInstance.log('[web3] Approved baseTokens', approvedTokensList)
|
||||
} catch (error) {
|
||||
LoggerInstance.error('[web3] Error: ', error.message)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// -----------------------------------
|
||||
// Helper: Get user balance
|
||||
// -----------------------------------
|
||||
const getUserBalance = useCallback(async () => {
|
||||
if (!accountId || !networkId || !web3) return
|
||||
if (!accountId || !networkId || !web3 || !networkData) return
|
||||
|
||||
try {
|
||||
const balance = {
|
||||
eth: web3.utils.fromWei(await web3.eth.getBalance(accountId, 'latest')),
|
||||
ocean: await getOceanBalance(accountId, networkId, web3)
|
||||
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) => {
|
||||
const { address, decimals, symbol } = token
|
||||
const tokenBalance = await getTokenBalance(
|
||||
accountId,
|
||||
decimals,
|
||||
address,
|
||||
web3
|
||||
)
|
||||
balance[symbol.toLocaleLowerCase()] = tokenBalance
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
setBalance(balance)
|
||||
LoggerInstance.log('[web3] Balance: ', balance)
|
||||
} catch (error) {
|
||||
LoggerInstance.error('[web3] Error: ', error.message)
|
||||
}
|
||||
}, [accountId, 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)
|
||||
}
|
||||
@ -221,6 +263,14 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
connectCached()
|
||||
}, [connect, web3Modal])
|
||||
|
||||
// -----------------------------------
|
||||
// Get and set approved base tokens list
|
||||
// -----------------------------------
|
||||
useEffect(() => {
|
||||
if (web3Loading) return
|
||||
getApprovedBaseTokens(chainId || 1)
|
||||
}, [chainId, getApprovedBaseTokens, web3Loading])
|
||||
|
||||
// -----------------------------------
|
||||
// Get and set user balance
|
||||
// -----------------------------------
|
||||
@ -236,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
|
||||
@ -297,9 +347,12 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
// Logout helper
|
||||
// -----------------------------------
|
||||
async function logout() {
|
||||
if (web3 && web3.currentProvider && (web3.currentProvider as any).close) {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
if ((web3?.currentProvider as any)?.close) {
|
||||
await (web3.currentProvider as any).close()
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
await web3Modal.clearCachedProvider()
|
||||
}
|
||||
// -----------------------------------
|
||||
@ -348,6 +401,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
web3Provider.removeListener('networkChanged', handleNetworkChanged)
|
||||
web3Provider.removeListener('accountsChanged', handleAccountsChanged)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [web3Provider, web3])
|
||||
|
||||
return (
|
||||
@ -359,6 +413,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
web3ProviderInfo,
|
||||
accountId,
|
||||
accountEns,
|
||||
accountEnsAvatar,
|
||||
balance,
|
||||
networkId,
|
||||
chainId,
|
||||
@ -368,6 +423,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
isTestnet,
|
||||
web3Loading,
|
||||
isSupportedOceanNetwork,
|
||||
approvedBaseTokens,
|
||||
connect,
|
||||
logout
|
||||
}}
|
||||
|
@ -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])
|
||||
|
4
src/@types/AssetExtended.d.ts
vendored
4
src/@types/AssetExtended.d.ts
vendored
@ -1,5 +1,9 @@
|
||||
import { Asset } from '@oceanprotocol/lib'
|
||||
|
||||
// declaring into global scope to be able to use this as
|
||||
// ambiant types despite the above imports
|
||||
declare global {
|
||||
interface AssetExtended extends Asset {
|
||||
accessDetails?: AccessDetails
|
||||
}
|
||||
}
|
||||
|
5
src/@types/Compute.d.ts
vendored
5
src/@types/Compute.d.ts
vendored
@ -15,11 +15,6 @@ declare global {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface ComputePrivacyForm {
|
||||
allowAllPublishedAlgorithms: boolean
|
||||
publisherTrustedAlgorithms: string[]
|
||||
}
|
||||
|
||||
interface TokenOrder {
|
||||
id: string
|
||||
serviceIndex: number
|
||||
|
33
src/@types/Price.d.ts
vendored
33
src/@types/Price.d.ts
vendored
@ -1,46 +1,43 @@
|
||||
import { ProviderFees } from '@oceanprotocol/lib'
|
||||
|
||||
// declaring into global scope to be able to use this as
|
||||
// ambiant types despite the above imports
|
||||
declare global {
|
||||
/**
|
||||
* @interface OrderPriceAndFee
|
||||
* @prop {string} price total price including fees
|
||||
* @prop {string} publisherMarketOrderFee fee received by the market where the asset was published. It is set on erc20 creation. It is a absolute value
|
||||
* @prop {string} publisherMarketPoolSwapFee fee received by the market where the asset was published on any swap (pool or fre). Absolute value based on the configured percentage
|
||||
* @prop {string} publisherMarketFixedSwapFee fee received by the market where the asset was published on any swap (pool or fre). Absolute value based on the configured percentage
|
||||
* @prop {string} publisherMarketFixedSwapFee fee received by the market where the asset was published on any swap (fre). Absolute value based on the configured percentage
|
||||
* @prop {string} consumeMarketOrderFee fee received by the market where the asset is ordered. It is set on erc20 creation. It is a absolute value
|
||||
* @prop {string} consumeMarketPoolSwapFee fee received by the market where the asset is ordered on any swap (pool or fre). Absolute value based on the configured percentage
|
||||
* @prop {string} consumeMarketFixedSwapFee fee received by the market where the asset is ordered on any swap (pool or fre). Absolute value based on the configured percentage
|
||||
* @prop {string} liquidityProviderSwapFee fee received by the liquidity providers of the pool. It is a percentage ( ex 50% means liquidityProviderSwapFee=0.5)
|
||||
* @prop {string} consumeMarketFixedSwapFee fee received by the market where the asset is ordered on any swap (fre). Absolute value based on the configured percentage
|
||||
* @prop {ProviderFees} providerFee received from provider
|
||||
* @prop {string} opcFee ocean protocol community fee, Absolute value based on the configured percentage
|
||||
*/
|
||||
interface OrderPriceAndFees {
|
||||
price: string
|
||||
publisherMarketOrderFee: string
|
||||
publisherMarketPoolSwapFee: string
|
||||
publisherMarketFixedSwapFee: string
|
||||
consumeMarketOrderFee: string
|
||||
consumeMarketPoolSwapFee: string
|
||||
consumeMarketFixedSwapFee: string
|
||||
liquidityProviderSwapFee: string
|
||||
providerFee: ProviderFees
|
||||
opcFee: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface AccessDetails
|
||||
* @prop {'dynamic' | 'fixed' | 'free' | ''} type
|
||||
* @prop {'fixed' | 'free' | 'NOT_SUPPORTED'} type
|
||||
* @prop {string} price can be either spotPrice/rate
|
||||
* @prop {string} addressOrId if type is dynamic this is the pool address, for fixed/free this is an id.
|
||||
* @prop {string} addressOrId for fixed/free this is an id.
|
||||
* @prop {TokenInfo} baseToken
|
||||
* @prop {TokenInfo} datatoken
|
||||
* @prop {bool} isPurchasable checks if you can buy a datatoken from fixed rate exchange/pool/dispenser. For pool it also checks if there is enough dt liquidity
|
||||
* @prop {bool} isPurchasable checks if you can buy a datatoken from fixed rate exchange/dispenser.
|
||||
* @prop {bool} isOwned checks if there are valid orders for this, it also takes in consideration timeout
|
||||
* @prop {string} validOrderTx the latest valid order tx, it also takes in consideration timeout
|
||||
* @prop {string} publisherMarketOrderFee this is here just because it's more efficient, it's allready in the query
|
||||
* @prop {FeeInfo} feeInfo values of the relevant fees
|
||||
*/
|
||||
interface AccessDetails {
|
||||
type: 'dynamic' | 'fixed' | 'free' | ''
|
||||
type: 'fixed' | 'free' | 'NOT_SUPPORTED'
|
||||
price: string
|
||||
addressOrId: string
|
||||
baseToken: TokenInfo
|
||||
@ -51,14 +48,10 @@ interface AccessDetails {
|
||||
publisherMarketOrderFee: string
|
||||
}
|
||||
|
||||
interface PriceOptions {
|
||||
interface PricePublishOptions {
|
||||
price: number
|
||||
amountDataToken: number
|
||||
amountOcean: number
|
||||
type: 'dynamic' | 'fixed' | 'free' | ''
|
||||
weightOnDataToken: string
|
||||
weightOnOcean: string
|
||||
// easier to keep this as number for Yup input validation
|
||||
swapFee: number
|
||||
baseToken: TokenInfo
|
||||
type: 'fixed' | 'free'
|
||||
freeAgreement: boolean
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
8
src/@types/TokenBalance.d.ts
vendored
8
src/@types/TokenBalance.d.ts
vendored
@ -1,9 +1,3 @@
|
||||
interface PoolBalance {
|
||||
baseToken: string
|
||||
datatoken: string
|
||||
}
|
||||
|
||||
interface UserBalance {
|
||||
eth: string
|
||||
ocean: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
9
src/@types/Utils.d.ts
vendored
9
src/@types/Utils.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
interface CalcInGivenOutParams {
|
||||
tokenInLiquidity: string
|
||||
tokenOutLiquidity: string
|
||||
tokenOutAmount: string
|
||||
opcFee: string
|
||||
lpSwapFee: string
|
||||
publishMarketSwapFee: string
|
||||
consumeMarketSwapFee: string
|
||||
}
|
1
src/@types/aquarius/BaseQueryParams.d.ts
vendored
1
src/@types/aquarius/BaseQueryParams.d.ts
vendored
@ -9,6 +9,7 @@ interface BaseQueryParams {
|
||||
nestedQuery?: any
|
||||
esPaginationOptions?: EsPaginationOptions
|
||||
sortOptions?: SortOptions
|
||||
aggs?: any
|
||||
filters?: FilterTerm[]
|
||||
ignorePurgatory?: boolean
|
||||
}
|
||||
|
1
src/@types/aquarius/PagedAssets.d.ts
vendored
1
src/@types/aquarius/PagedAssets.d.ts
vendored
@ -3,4 +3,5 @@ interface PagedAssets {
|
||||
page: number
|
||||
totalPages: number
|
||||
totalResults: number
|
||||
aggregations: any
|
||||
}
|
||||
|
@ -4,8 +4,9 @@ export enum SortDirectionOptions {
|
||||
}
|
||||
|
||||
export enum SortTermOptions {
|
||||
Created = 'metadata.created',
|
||||
Relevance = '_score'
|
||||
Created = 'nft.created',
|
||||
Relevance = '_score',
|
||||
Stats = 'stats.orders'
|
||||
}
|
||||
|
||||
// Note: could not figure out how to get `enum` to be ambiant
|
||||
@ -43,5 +44,6 @@ declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
query: any
|
||||
sort?: { [jsonPath: string]: SortDirectionOptions }
|
||||
aggs?: any
|
||||
}
|
||||
}
|
||||
|
4
src/@types/aquarius/TagsList.d.ts
vendored
Normal file
4
src/@types/aquarius/TagsList.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
interface AggregatedTag {
|
||||
doc_count: number
|
||||
key: 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
|
||||
}
|
@ -14,17 +14,16 @@ import {
|
||||
ProviderFees,
|
||||
ProviderInstance
|
||||
} from '@oceanprotocol/lib'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import { calcInGivenOut } from './pool'
|
||||
import { getFixedBuyPrice } from './fixedRateExchange'
|
||||
import { AccessDetails, OrderPriceAndFees } from 'src/@types/Price'
|
||||
import Decimal from 'decimal.js'
|
||||
import { consumeMarketOrderFee } from '../../app.config'
|
||||
import Web3 from 'web3'
|
||||
import {
|
||||
consumeMarketOrderFee,
|
||||
publisherMarketOrderFee
|
||||
} from '../../app.config'
|
||||
|
||||
const tokensPriceQuery = gql`
|
||||
query TokensPriceQuery($datatokenIds: [ID!], $account: String) {
|
||||
tokens(where: { id_in: $datatokenIds }) {
|
||||
tokens(first: 1000, where: { id_in: $datatokenIds }) {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
@ -67,6 +66,7 @@ const tokensPriceQuery = gql`
|
||||
symbol
|
||||
name
|
||||
address
|
||||
decimals
|
||||
}
|
||||
datatoken {
|
||||
symbol
|
||||
@ -75,22 +75,6 @@ const tokensPriceQuery = gql`
|
||||
}
|
||||
active
|
||||
}
|
||||
pools {
|
||||
id
|
||||
spotPrice
|
||||
isFinalized
|
||||
datatokenLiquidity
|
||||
baseToken {
|
||||
symbol
|
||||
name
|
||||
address
|
||||
}
|
||||
datatoken {
|
||||
symbol
|
||||
name
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -139,6 +123,7 @@ const tokenPriceQuery = gql`
|
||||
symbol
|
||||
name
|
||||
address
|
||||
decimals
|
||||
}
|
||||
datatoken {
|
||||
symbol
|
||||
@ -147,22 +132,6 @@ const tokenPriceQuery = gql`
|
||||
}
|
||||
active
|
||||
}
|
||||
pools {
|
||||
id
|
||||
spotPrice
|
||||
isFinalized
|
||||
datatokenLiquidity
|
||||
baseToken {
|
||||
symbol
|
||||
name
|
||||
address
|
||||
}
|
||||
datatoken {
|
||||
symbol
|
||||
name
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -173,6 +142,15 @@ function getAccessDetailsFromTokenPrice(
|
||||
): AccessDetails {
|
||||
const accessDetails = {} as AccessDetails
|
||||
|
||||
// Return early when no supported pricing schema found.
|
||||
if (
|
||||
tokenPrice?.dispensers?.length === 0 &&
|
||||
tokenPrice?.fixedRateExchanges?.length === 0
|
||||
) {
|
||||
accessDetails.type = 'NOT_SUPPORTED'
|
||||
return accessDetails
|
||||
}
|
||||
|
||||
if (tokenPrice?.orders?.length > 0) {
|
||||
const order = tokenPrice.orders[0]
|
||||
const reusedOrder = order?.reuses?.length > 0 ? order.reuses[0] : null
|
||||
@ -198,7 +176,6 @@ function getAccessDetailsFromTokenPrice(
|
||||
name: dispenser.token.name,
|
||||
symbol: dispenser.token.symbol
|
||||
}
|
||||
return accessDetails
|
||||
}
|
||||
|
||||
// checking for fixed price
|
||||
@ -212,37 +189,16 @@ function getAccessDetailsFromTokenPrice(
|
||||
accessDetails.baseToken = {
|
||||
address: fixed.baseToken.address,
|
||||
name: fixed.baseToken.name,
|
||||
symbol: fixed.baseToken.symbol
|
||||
symbol: fixed.baseToken.symbol,
|
||||
decimals: fixed.baseToken.decimals
|
||||
}
|
||||
accessDetails.datatoken = {
|
||||
address: fixed.datatoken.address,
|
||||
name: fixed.datatoken.name,
|
||||
symbol: fixed.datatoken.symbol
|
||||
}
|
||||
return accessDetails
|
||||
}
|
||||
|
||||
// checking for pools
|
||||
if (tokenPrice?.pools?.length > 0) {
|
||||
const pool = tokenPrice.pools[0]
|
||||
accessDetails.type = 'dynamic'
|
||||
accessDetails.addressOrId = pool.id
|
||||
accessDetails.price = pool.spotPrice
|
||||
// TODO: pool.datatokenLiquidity > 3 is kinda random here, we shouldn't run into this anymore now , needs more thinking/testing
|
||||
accessDetails.isPurchasable =
|
||||
pool.isFinalized && pool.datatokenLiquidity > 3
|
||||
accessDetails.baseToken = {
|
||||
address: pool.baseToken.address,
|
||||
name: pool.baseToken.name,
|
||||
symbol: pool.baseToken.symbol
|
||||
}
|
||||
accessDetails.datatoken = {
|
||||
address: pool.datatoken.address,
|
||||
name: pool.datatoken.name,
|
||||
symbol: pool.datatoken.symbol
|
||||
}
|
||||
return accessDetails
|
||||
}
|
||||
return accessDetails
|
||||
}
|
||||
|
||||
@ -254,17 +210,13 @@ function getAccessDetailsFromTokenPrice(
|
||||
export async function getOrderPriceAndFees(
|
||||
asset: AssetExtended,
|
||||
accountId?: string,
|
||||
paramsForPool?: CalcInGivenOutParams,
|
||||
providerFees?: ProviderFees
|
||||
): Promise<OrderPriceAndFees> {
|
||||
const orderPriceAndFee = {
|
||||
price: '0',
|
||||
publisherMarketOrderFee:
|
||||
asset?.accessDetails?.publisherMarketOrderFee || '0',
|
||||
publisherMarketPoolSwapFee: '0',
|
||||
publisherMarketOrderFee: publisherMarketOrderFee || '0',
|
||||
publisherMarketFixedSwapFee: '0',
|
||||
consumeMarketOrderFee: consumeMarketOrderFee || '0',
|
||||
consumeMarketPoolSwapFee: '0',
|
||||
consumeMarketFixedSwapFee: '0',
|
||||
providerFee: {
|
||||
providerFeeAmount: '0'
|
||||
@ -273,7 +225,6 @@ export async function getOrderPriceAndFees(
|
||||
} as OrderPriceAndFees
|
||||
|
||||
// fetch provider fee
|
||||
|
||||
const initializeData =
|
||||
!providerFees &&
|
||||
(await ProviderInstance.initialize(
|
||||
@ -286,27 +237,12 @@ export async function getOrderPriceAndFees(
|
||||
orderPriceAndFee.providerFee = providerFees || initializeData.providerFee
|
||||
|
||||
// fetch price and swap fees
|
||||
switch (asset?.accessDetails?.type) {
|
||||
case 'dynamic': {
|
||||
const poolPrice = calcInGivenOut(paramsForPool)
|
||||
orderPriceAndFee.price = poolPrice.tokenAmount
|
||||
orderPriceAndFee.liquidityProviderSwapFee =
|
||||
poolPrice.liquidityProviderSwapFeeAmount
|
||||
orderPriceAndFee.publisherMarketPoolSwapFee =
|
||||
poolPrice.publishMarketSwapFeeAmount
|
||||
orderPriceAndFee.consumeMarketPoolSwapFee =
|
||||
poolPrice.consumeMarketSwapFeeAmount
|
||||
break
|
||||
}
|
||||
case 'fixed': {
|
||||
if (asset?.accessDetails?.type === 'fixed') {
|
||||
const fixed = await getFixedBuyPrice(asset?.accessDetails, asset?.chainId)
|
||||
orderPriceAndFee.price = fixed.baseTokenAmount
|
||||
orderPriceAndFee.opcFee = fixed.oceanFeeAmount
|
||||
orderPriceAndFee.publisherMarketFixedSwapFee = fixed.marketFeeAmount
|
||||
orderPriceAndFee.consumeMarketFixedSwapFee = fixed.consumeMarketFeeAmount
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// calculate full price, we assume that all the values are in ocean, otherwise this will be incorrect
|
||||
@ -314,6 +250,7 @@ export async function getOrderPriceAndFees(
|
||||
.add(new Decimal(+orderPriceAndFee?.consumeMarketOrderFee || 0))
|
||||
.add(new Decimal(+orderPriceAndFee?.publisherMarketOrderFee || 0))
|
||||
.toString()
|
||||
|
||||
return orderPriceAndFee
|
||||
}
|
||||
|
||||
|
@ -2,13 +2,18 @@ import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
|
||||
import axios, { CancelToken, AxiosResponse } from 'axios'
|
||||
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
|
||||
import { metadataCacheUri, v3MetadataCacheUri } from '../../app.config'
|
||||
import { metadataCacheUri } from '../../app.config'
|
||||
import {
|
||||
SortDirectionOptions,
|
||||
SortTermOptions
|
||||
} 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 {
|
||||
@ -39,7 +44,10 @@ export function generateBaseQuery(
|
||||
): SearchQuery {
|
||||
const generatedQuery = {
|
||||
from: baseQueryParams.esPaginationOptions?.from || 0,
|
||||
size: baseQueryParams.esPaginationOptions?.size || 1000,
|
||||
size:
|
||||
baseQueryParams.esPaginationOptions?.size >= 0
|
||||
? baseQueryParams.esPaginationOptions?.size
|
||||
: 1000,
|
||||
query: {
|
||||
bool: {
|
||||
...baseQueryParams.nestedQuery,
|
||||
@ -55,6 +63,10 @@ export function generateBaseQuery(
|
||||
}
|
||||
} as SearchQuery
|
||||
|
||||
if (baseQueryParams.aggs !== undefined) {
|
||||
generatedQuery.aggs = baseQueryParams.aggs
|
||||
}
|
||||
|
||||
if (baseQueryParams.sortOptions !== undefined)
|
||||
generatedQuery.sort = {
|
||||
[baseQueryParams.sortOptions.sortBy]:
|
||||
@ -74,12 +86,15 @@ export function transformQueryResult(
|
||||
results: [],
|
||||
page: 0,
|
||||
totalPages: 0,
|
||||
totalResults: 0
|
||||
totalResults: 0,
|
||||
aggregations: []
|
||||
}
|
||||
|
||||
result.results = (queryResult.hits.hits || []).map(
|
||||
(hit) => hit._source as Asset
|
||||
)
|
||||
|
||||
result.aggregations = queryResult.aggregations
|
||||
result.totalResults = queryResult.hits.total.value
|
||||
result.totalPages =
|
||||
result.totalResults / size < 1
|
||||
@ -133,28 +148,6 @@ export async function retrieveAsset(
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkV3Asset(
|
||||
did: string,
|
||||
cancelToken: CancelToken
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const response: AxiosResponse<Asset> = await axios.get(
|
||||
`${v3MetadataCacheUri}/api/v1/aquarius/assets/ddo/${did}`,
|
||||
{ cancelToken }
|
||||
)
|
||||
if (!response || response.status !== 200 || !response.data) return false
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
LoggerInstance.log(error.message)
|
||||
} else {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAssetsNames(
|
||||
didList: string[],
|
||||
cancelToken: CancelToken
|
||||
@ -307,6 +300,14 @@ export async function getPublishedAssets(
|
||||
sortBy: SortTermOptions.Created,
|
||||
sortDirection: SortDirectionOptions.Descending
|
||||
},
|
||||
aggs: {
|
||||
totalOrders: {
|
||||
sum: {
|
||||
field: SortTermOptions.Stats
|
||||
}
|
||||
}
|
||||
},
|
||||
ignorePurgatory: true,
|
||||
esPaginationOptions: {
|
||||
from: (Number(page) - 1 || 0) * 9,
|
||||
size: 9
|
||||
@ -314,6 +315,7 @@ export async function getPublishedAssets(
|
||||
} as BaseQueryParams
|
||||
|
||||
const query = generateBaseQuery(baseQueryParams)
|
||||
|
||||
try {
|
||||
const result = await queryMetadata(query, cancelToken)
|
||||
return result
|
||||
@ -326,6 +328,95 @@ export async function getPublishedAssets(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTopPublishers(
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken,
|
||||
page?: number,
|
||||
type?: string,
|
||||
accesType?: string
|
||||
): Promise<PagedAssets> {
|
||||
const filters: FilterTerm[] = []
|
||||
|
||||
accesType !== undefined &&
|
||||
filters.push(getFilterTerm('services.type', accesType))
|
||||
type !== undefined && filters.push(getFilterTerm('metadata.type', type))
|
||||
|
||||
const baseQueryParams = {
|
||||
chainIds,
|
||||
filters,
|
||||
sortOptions: {
|
||||
sortBy: SortTermOptions.Created,
|
||||
sortDirection: SortDirectionOptions.Descending
|
||||
},
|
||||
aggs: {
|
||||
topPublishers: {
|
||||
terms: {
|
||||
field: 'nft.owner.keyword',
|
||||
order: { totalSales: 'desc' }
|
||||
},
|
||||
aggs: {
|
||||
totalSales: {
|
||||
sum: {
|
||||
field: SortTermOptions.Stats
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
esPaginationOptions: {
|
||||
from: (Number(page) - 1 || 0) * 9,
|
||||
size: 9
|
||||
}
|
||||
} as BaseQueryParams
|
||||
|
||||
const query = generateBaseQuery(baseQueryParams)
|
||||
|
||||
try {
|
||||
const result = await queryMetadata(query, cancelToken)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
LoggerInstance.log(error.message)
|
||||
} else {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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[],
|
||||
@ -368,3 +459,47 @@ export async function getDownloadAssets(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTagsList(
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken
|
||||
): Promise<string[]> {
|
||||
const baseQueryParams = {
|
||||
chainIds,
|
||||
esPaginationOptions: { from: 0, size: 0 }
|
||||
} as BaseQueryParams
|
||||
const query = {
|
||||
...generateBaseQuery(baseQueryParams),
|
||||
aggs: {
|
||||
tags: {
|
||||
terms: {
|
||||
field: 'metadata.tags.keyword',
|
||||
size: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response: AxiosResponse<SearchResponse> = await axios.post(
|
||||
`${metadataCacheUri}/api/aquarius/assets/query`,
|
||||
{ ...query },
|
||||
{ cancelToken }
|
||||
)
|
||||
if (response?.status !== 200 || !response?.data) return
|
||||
const { buckets }: { buckets: AggregatedTag[] } =
|
||||
response.data.aggregations.tags
|
||||
|
||||
const tagsList = buckets
|
||||
.filter((tag) => tag.key !== '')
|
||||
.map((tag) => tag.key)
|
||||
|
||||
return tagsList.sort()
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
LoggerInstance.log(error.message)
|
||||
} else {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { getAccessDetailsForAssets } from './accessDetailsAndPricing'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import { PublisherTrustedAlgorithm, Asset } from '@oceanprotocol/lib'
|
||||
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
|
||||
import { getServiceByName } from './ddo'
|
||||
|
@ -24,7 +24,8 @@ import { getServiceById, getServiceByName } from './ddo'
|
||||
import { SortTermOptions } from 'src/@types/aquarius/SearchQuery'
|
||||
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
|
||||
import { transformAssetToAssetSelection } from './assetConvertor'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import { ComputeEditForm } from 'src/components/Asset/Edit/_types'
|
||||
import { getFileDidInfo } from './provider'
|
||||
|
||||
const getComputeOrders = gql`
|
||||
query ComputeOrders($user: String!) {
|
||||
@ -260,6 +261,7 @@ async function getJobs(
|
||||
// }
|
||||
return computeJobs
|
||||
}
|
||||
|
||||
export async function getComputeJobs(
|
||||
chainIds: number[],
|
||||
accountId: string,
|
||||
@ -331,25 +333,33 @@ export async function createTrustedAlgorithmList(
|
||||
): Promise<PublisherTrustedAlgorithm[]> {
|
||||
const trustedAlgorithms: PublisherTrustedAlgorithm[] = []
|
||||
|
||||
// Condition to prevent app from hitting Aquarius with empty DID list
|
||||
// when nothing is selected in the UI.
|
||||
if (!selectedAlgorithms || selectedAlgorithms.length === 0)
|
||||
return trustedAlgorithms
|
||||
|
||||
const selectedAssets = await retrieveDDOListByDIDs(
|
||||
selectedAlgorithms,
|
||||
[assetChainId],
|
||||
cancelToken
|
||||
)
|
||||
|
||||
if (!selectedAssets || selectedAssets.length === 0) return []
|
||||
|
||||
for (const selectedAlgorithm of selectedAssets) {
|
||||
const sanitizedAlgorithmContainer = {
|
||||
entrypoint: selectedAlgorithm.metadata.algorithm.container.entrypoint,
|
||||
image: selectedAlgorithm.metadata.algorithm.container.image,
|
||||
tag: selectedAlgorithm.metadata.algorithm.container.tag,
|
||||
checksum: selectedAlgorithm.metadata.algorithm.container.checksum
|
||||
}
|
||||
const filesChecksum = await getFileDidInfo(
|
||||
selectedAlgorithm?.id,
|
||||
selectedAlgorithm?.services?.[0].id,
|
||||
selectedAlgorithm?.services?.[0]?.serviceEndpoint,
|
||||
true
|
||||
)
|
||||
const containerChecksum =
|
||||
selectedAlgorithm.metadata.algorithm.container.entrypoint +
|
||||
selectedAlgorithm.metadata.algorithm.container.checksum
|
||||
const trustedAlgorithm = {
|
||||
did: selectedAlgorithm.id,
|
||||
containerSectionChecksum: getHash(
|
||||
JSON.stringify(sanitizedAlgorithmContainer)
|
||||
),
|
||||
filesChecksum: getHash(selectedAlgorithm.services[0].files)
|
||||
containerSectionChecksum: getHash(containerChecksum),
|
||||
filesChecksum: filesChecksum?.[0]?.checksum
|
||||
}
|
||||
trustedAlgorithms.push(trustedAlgorithm)
|
||||
}
|
||||
@ -357,22 +367,28 @@ export async function createTrustedAlgorithmList(
|
||||
}
|
||||
|
||||
export async function transformComputeFormToServiceComputeOptions(
|
||||
values: ComputePrivacyForm,
|
||||
values: ComputeEditForm,
|
||||
currentOptions: ServiceComputeOptions,
|
||||
assetChainId: number,
|
||||
cancelToken: CancelToken
|
||||
): Promise<ServiceComputeOptions> {
|
||||
const publisherTrustedAlgorithms = values.allowAllPublishedAlgorithms
|
||||
? []
|
||||
? null
|
||||
: await createTrustedAlgorithmList(
|
||||
values.publisherTrustedAlgorithms,
|
||||
assetChainId,
|
||||
cancelToken
|
||||
)
|
||||
|
||||
// TODO: add support for selecting trusted publishers and transforming here.
|
||||
// This only deals with basics so we don't accidentially allow all accounts
|
||||
// to be trusted.
|
||||
const publisherTrustedAlgorithmPublishers: string[] = []
|
||||
|
||||
const privacy: ServiceComputeOptions = {
|
||||
...currentOptions,
|
||||
publisherTrustedAlgorithms
|
||||
publisherTrustedAlgorithms,
|
||||
publisherTrustedAlgorithmPublishers
|
||||
}
|
||||
|
||||
return privacy
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ export function getOrderFeedback(
|
||||
datatokenSymbol: string
|
||||
): { [key in number]: string } {
|
||||
return {
|
||||
0: `Approving and buying one ${datatokenSymbol} from pool`,
|
||||
0: `Approving and buying one ${datatokenSymbol}`,
|
||||
1: `Ordering asset`,
|
||||
2: `Approving ${baseTokenSymbol} and ordering asset`,
|
||||
3: 'Generating signature to access download url'
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { FixedRateExchange, PriceAndFees } from '@oceanprotocol/lib'
|
||||
import { AccessDetails } from 'src/@types/Price'
|
||||
import { consumeMarketFixedSwapFee } from '../../app.config'
|
||||
import Web3 from 'web3'
|
||||
import { getOceanConfig } from './ocean'
|
||||
import { consumeMarketPoolSwapFee } from '../../app.config'
|
||||
import { getDummyWeb3 } from './web3'
|
||||
|
||||
/**
|
||||
@ -26,11 +25,11 @@ 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',
|
||||
consumeMarketPoolSwapFee
|
||||
consumeMarketFixedSwapFee
|
||||
)
|
||||
return estimatedPrice
|
||||
}
|
||||
|
6
src/@utils/form.ts
Normal file
6
src/@utils/form.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export function getFieldContent(
|
||||
fieldName: string,
|
||||
fields: FormFieldContent[]
|
||||
): FormFieldContent {
|
||||
return fields.filter((field: FormFieldContent) => field.name === fieldName)[0]
|
||||
}
|
@ -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 {
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { ConfigHelper, LoggerInstance, Config } from '@oceanprotocol/lib'
|
||||
import { ConfigHelper, Config } from '@oceanprotocol/lib'
|
||||
// import contractAddresses from '@oceanprotocol/contracts/artifacts/address.json'
|
||||
import { AbiItem } from 'web3-utils/types'
|
||||
import Web3 from 'web3'
|
||||
|
||||
export function getOceanConfig(network: string | number): Config {
|
||||
const config = new ConfigHelper().getConfig(
|
||||
@ -26,49 +24,7 @@ export function getDevelopmentConfig(): Config {
|
||||
// fixedRateExchangeAddress: contractAddresses.development?.FixedRateExchange,
|
||||
// metadataContractAddress: contractAddresses.development?.Metadata,
|
||||
// oceanTokenAddress: contractAddresses.development?.Ocean,
|
||||
// There is no subgraph in barge so we hardcode the Rinkeby one for now
|
||||
subgraphUri: 'https://v4.subgraph.rinkeby.oceanprotocol.com'
|
||||
// There is no subgraph in barge so we hardcode the Goerli one for now
|
||||
subgraphUri: 'https://v4.subgraph.goerli.oceanprotocol.com'
|
||||
} as Config
|
||||
}
|
||||
|
||||
export async function getOceanBalance(
|
||||
accountId: string,
|
||||
networkId: number,
|
||||
web3: Web3
|
||||
): Promise<string> {
|
||||
const minABI = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_owner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'balanceOf',
|
||||
outputs: [
|
||||
{
|
||||
name: 'balance',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
] as AbiItem[]
|
||||
|
||||
try {
|
||||
const token = new web3.eth.Contract(
|
||||
minABI,
|
||||
getOceanConfig(networkId).oceanTokenAddress,
|
||||
{ from: accountId }
|
||||
)
|
||||
const result = web3.utils.fromWei(
|
||||
await token.methods.balanceOf(accountId).call()
|
||||
)
|
||||
return result
|
||||
} catch (e) {
|
||||
LoggerInstance.error(`ERROR: Failed to get the balance: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
amountToUnits,
|
||||
approve,
|
||||
approveWei,
|
||||
Datatoken,
|
||||
@ -9,21 +10,17 @@ import {
|
||||
ProviderFees,
|
||||
ProviderInstance
|
||||
} from '@oceanprotocol/lib'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import Web3 from 'web3'
|
||||
import { getOceanConfig } from './ocean'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { OrderPriceAndFees } from 'src/@types/Price'
|
||||
import {
|
||||
marketFeeAddress,
|
||||
consumeMarketOrderFee,
|
||||
consumeMarketFixedSwapFee
|
||||
} from '../../app.config'
|
||||
import { buyDtFromPool } from './pool'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
/**
|
||||
* For pool you need to buy the datatoken beforehand, this always assumes you want to order the first service
|
||||
* @param web3
|
||||
* @param asset
|
||||
* @param orderPriceAndFees
|
||||
@ -60,7 +57,9 @@ export async function order(
|
||||
_consumeMarketFee: {
|
||||
consumeMarketFeeAddress: marketFeeAddress,
|
||||
consumeMarketFeeAmount: consumeMarketOrderFee,
|
||||
consumeMarketFeeToken: config.oceanTokenAddress
|
||||
consumeMarketFeeToken:
|
||||
asset?.accessDetails?.baseToken?.address ||
|
||||
'0x0000000000000000000000000000000000000000'
|
||||
}
|
||||
} as OrderParams
|
||||
|
||||
@ -69,10 +68,15 @@ 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,
|
||||
orderPriceAndFees.price,
|
||||
await amountToUnits(
|
||||
web3,
|
||||
asset?.accessDetails?.baseToken?.address,
|
||||
orderPriceAndFees.price
|
||||
),
|
||||
false
|
||||
)
|
||||
if (!txApprove) {
|
||||
@ -83,6 +87,8 @@ export async function order(
|
||||
exchangeContract: config.fixedRateExchangeAddress,
|
||||
exchangeId: asset.accessDetails.addressOrId,
|
||||
maxBaseTokenAmount: orderPriceAndFees.price,
|
||||
baseTokenAddress: asset?.accessDetails?.baseToken?.address,
|
||||
baseTokenDecimals: asset?.accessDetails?.baseToken?.decimals || 18,
|
||||
swapMarketFee: consumeMarketFixedSwapFee,
|
||||
marketFeeAddress
|
||||
} as FreOrderParams
|
||||
@ -95,17 +101,6 @@ export async function order(
|
||||
|
||||
return tx
|
||||
}
|
||||
case 'dynamic': {
|
||||
const tx = await datatoken.startOrder(
|
||||
asset.accessDetails.datatoken.address,
|
||||
accountId,
|
||||
computeConsumerAddress || accountId,
|
||||
0,
|
||||
providerFees || initializeData.providerFee
|
||||
)
|
||||
return tx
|
||||
}
|
||||
|
||||
case 'free': {
|
||||
const tx = await datatoken.buyFromDispenserAndOrder(
|
||||
asset.services[0].datatokenAddress,
|
||||
@ -160,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(
|
||||
@ -184,14 +181,6 @@ async function startOrder(
|
||||
initializeData: ProviderComputeInitialize,
|
||||
computeConsumerAddress?: string
|
||||
): Promise<TransactionReceipt> {
|
||||
if (!hasDatatoken && asset?.accessDetails.type === 'dynamic') {
|
||||
const poolTx = await buyDtFromPool(asset?.accessDetails, accountId, web3)
|
||||
LoggerInstance.log('[compute] Bought datatoken from pool: ', poolTx)
|
||||
if (!poolTx) {
|
||||
toast.error('Failed to buy datatoken from pool!')
|
||||
return
|
||||
}
|
||||
}
|
||||
const tx = await order(
|
||||
web3,
|
||||
asset,
|
||||
|
@ -1,185 +0,0 @@
|
||||
import { approve, Pool, PoolPriceAndFees } from '@oceanprotocol/lib'
|
||||
import Web3 from 'web3'
|
||||
import { getDummyWeb3 } from './web3'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import Decimal from 'decimal.js'
|
||||
import { AccessDetails } from 'src/@types/Price'
|
||||
import { consumeMarketPoolSwapFee, marketFeeAddress } from '../../app.config'
|
||||
|
||||
/**
|
||||
* This is used to calculate the price to buy one datatoken from a pool, that is different from spot price. You need to pass either a web3 object or a chainId. If you pass a chainId a dummy web3 object will be created
|
||||
* @param {AccessDetails} accessDetails
|
||||
* @param {Web3?} [web3]
|
||||
* @param {number?} [chainId]
|
||||
* @return {Promise<PoolPriceAndFees>}
|
||||
*/
|
||||
export async function calculateBuyPrice(
|
||||
accessDetails: AccessDetails,
|
||||
chainId?: number,
|
||||
web3?: Web3
|
||||
): Promise<PoolPriceAndFees> {
|
||||
if (!web3 && !chainId)
|
||||
throw new Error("web3 and chainId can't be undefined at the same time!")
|
||||
|
||||
if (!web3) {
|
||||
web3 = await getDummyWeb3(chainId)
|
||||
}
|
||||
|
||||
const pool = new Pool(web3)
|
||||
|
||||
const estimatedPrice = await pool.getAmountInExactOut(
|
||||
accessDetails.addressOrId,
|
||||
accessDetails.baseToken.address,
|
||||
accessDetails.datatoken.address,
|
||||
'1',
|
||||
consumeMarketPoolSwapFee,
|
||||
accessDetails.baseToken.decimals,
|
||||
accessDetails.datatoken.decimals
|
||||
)
|
||||
|
||||
return estimatedPrice
|
||||
}
|
||||
|
||||
export async function buyDtFromPool(
|
||||
accessDetails: AccessDetails,
|
||||
accountId: string,
|
||||
web3: Web3
|
||||
): Promise<TransactionReceipt> {
|
||||
const pool = new Pool(web3)
|
||||
// we need to calculate the actual price to buy one datatoken
|
||||
const dtPrice = await calculateBuyPrice(accessDetails, null, web3)
|
||||
const approveTx = await approve(
|
||||
web3,
|
||||
accountId,
|
||||
accessDetails.baseToken.address,
|
||||
accessDetails.addressOrId,
|
||||
dtPrice.tokenAmount,
|
||||
false,
|
||||
accessDetails.baseToken.decimals
|
||||
)
|
||||
if (!approveTx) {
|
||||
return
|
||||
}
|
||||
const result = await pool.swapExactAmountOut(
|
||||
accountId,
|
||||
accessDetails.addressOrId,
|
||||
{
|
||||
marketFeeAddress,
|
||||
tokenIn: accessDetails.baseToken.address,
|
||||
tokenOut: accessDetails.datatoken.address,
|
||||
tokenInDecimals: accessDetails.baseToken.decimals,
|
||||
tokenOutDecimals: accessDetails.datatoken.decimals
|
||||
},
|
||||
{
|
||||
// this is just to be safe
|
||||
maxAmountIn: new Decimal(dtPrice.tokenAmount).mul(10).toString(),
|
||||
swapMarketFee: consumeMarketPoolSwapFee,
|
||||
tokenAmountOut: '1'
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to calculate the actual price of buying a datatoken, it's a copy of the math in the contracts.
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function calcInGivenOut(params: CalcInGivenOutParams): PoolPriceAndFees {
|
||||
const result = {
|
||||
tokenAmount: '0',
|
||||
liquidityProviderSwapFeeAmount: '0',
|
||||
oceanFeeAmount: '0',
|
||||
publishMarketSwapFeeAmount: '0',
|
||||
consumeMarketSwapFeeAmount: '0'
|
||||
} as PoolPriceAndFees
|
||||
const one = new Decimal(1)
|
||||
const tokenOutLiqudity = new Decimal(params.tokenOutLiquidity)
|
||||
const tokenInLiquidity = new Decimal(params.tokenInLiquidity)
|
||||
const tokenOutAmount = new Decimal(params.tokenOutAmount)
|
||||
const opcFee = new Decimal(params.opcFee)
|
||||
const lpFee = new Decimal(params.lpSwapFee)
|
||||
const publishMarketSwapFee = new Decimal(params.publishMarketSwapFee)
|
||||
const consumeMarketSwapFee = new Decimal(params.consumeMarketSwapFee)
|
||||
|
||||
const diff = tokenOutLiqudity.minus(tokenOutAmount)
|
||||
const y = tokenOutLiqudity.div(diff)
|
||||
let foo = y.pow(one)
|
||||
foo = foo.minus(one)
|
||||
const totalFee = lpFee
|
||||
.plus(opcFee)
|
||||
.plus(publishMarketSwapFee)
|
||||
.plus(consumeMarketSwapFee)
|
||||
|
||||
const tokenAmountIn = tokenInLiquidity.mul(foo).div(one.sub(totalFee))
|
||||
result.tokenAmount = tokenAmountIn.toString()
|
||||
result.oceanFeeAmount = tokenAmountIn
|
||||
.sub(tokenAmountIn.mul(one.sub(opcFee)))
|
||||
.toString()
|
||||
result.publishMarketSwapFeeAmount = tokenAmountIn
|
||||
.sub(tokenAmountIn.mul(one.sub(publishMarketSwapFee)))
|
||||
.toString()
|
||||
result.consumeMarketSwapFeeAmount = tokenAmountIn
|
||||
.sub(tokenAmountIn.mul(one.sub(consumeMarketSwapFee)))
|
||||
.toString()
|
||||
result.liquidityProviderSwapFeeAmount = tokenAmountIn
|
||||
.sub(tokenAmountIn.mul(one.sub(lpFee)))
|
||||
.toString()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to calculate swap values, it's a copy of the math in the contracts.
|
||||
* @param tokenLiquidity
|
||||
* @param poolSupply
|
||||
* @param poolShareAmount
|
||||
* @returns
|
||||
*/
|
||||
export function calcSingleOutGivenPoolIn(
|
||||
tokenLiquidity: string,
|
||||
poolSupply: string,
|
||||
poolShareAmount: string
|
||||
): string {
|
||||
const tokenLiquidityD = new Decimal(tokenLiquidity)
|
||||
const poolSupplyD = new Decimal(poolSupply)
|
||||
const poolShareAmountD = new Decimal(poolShareAmount).mul(2)
|
||||
const newPoolSupply = poolSupplyD.sub(poolShareAmountD)
|
||||
const poolRatio = newPoolSupply.div(poolSupplyD)
|
||||
|
||||
const tokenOutRatio = new Decimal(1).sub(poolRatio)
|
||||
const newTokenBalanceOut = tokenLiquidityD.mul(tokenOutRatio)
|
||||
return newTokenBalanceOut.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of tokens (based on tokenAddress) that can be withdrawn from the pool
|
||||
* @param {string} poolAddress
|
||||
* @param {string} tokenAddress
|
||||
* @param {string} shares
|
||||
* @param {number} chainId
|
||||
* @returns
|
||||
*/
|
||||
export async function getLiquidityByShares(
|
||||
pool: string,
|
||||
tokenAddress: string,
|
||||
tokenDecimals: number,
|
||||
shares: string,
|
||||
chainId: number
|
||||
): Promise<string> {
|
||||
// we only use the dummyWeb3 connection here
|
||||
const web3 = await getDummyWeb3(chainId)
|
||||
|
||||
const poolInstance = new Pool(web3)
|
||||
// get shares VL in ocean
|
||||
const amountBaseToken = await poolInstance.calcSingleOutGivenPoolIn(
|
||||
pool,
|
||||
tokenAddress,
|
||||
shares,
|
||||
18,
|
||||
tokenDecimals
|
||||
)
|
||||
|
||||
return amountBaseToken
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -8,7 +8,6 @@ import {
|
||||
ProviderComputeInitializeResults,
|
||||
ProviderInstance
|
||||
} from '@oceanprotocol/lib'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import Web3 from 'web3'
|
||||
import { getValidUntilTime } from './compute'
|
||||
|
||||
@ -67,13 +66,15 @@ export async function getEncryptedFiles(
|
||||
export async function getFileDidInfo(
|
||||
did: string,
|
||||
serviceId: string,
|
||||
providerUrl: string
|
||||
providerUrl: string,
|
||||
withChecksum = false
|
||||
): Promise<FileInfo[]> {
|
||||
try {
|
||||
const response = await ProviderInstance.checkDidFiles(
|
||||
did,
|
||||
serviceId as any, // TODO: why does ocean.js want a number here?
|
||||
providerUrl
|
||||
serviceId,
|
||||
providerUrl,
|
||||
withChecksum
|
||||
)
|
||||
return response
|
||||
} catch (error) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Purgatory } from '@oceanprotocol/lib'
|
||||
import { fetchData } from './fetch'
|
||||
|
||||
const purgatoryUrl = 'https://market-purgatory.oceanprotocol.com/api/'
|
||||
|
@ -1,50 +1,10 @@
|
||||
import { gql, OperationResult, TypedDocumentNode, OperationContext } from 'urql'
|
||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { getUrqlClientInstance } from '@context/UrqlProvider'
|
||||
import { getOceanConfig } from './ocean'
|
||||
import { AssetPoolPrice } from '../@types/subgraph/AssetPoolPrice'
|
||||
import { AssetPreviousOrder } from '../@types/subgraph/AssetPreviousOrder'
|
||||
import {
|
||||
HighestLiquidityAssets_pools as HighestLiquidityAssetsPool,
|
||||
HighestLiquidityAssets as HighestLiquidityGraphAssets
|
||||
} from '../@types/subgraph/HighestLiquidityAssets'
|
||||
import {
|
||||
PoolShares as PoolSharesList,
|
||||
PoolShares_poolShares as PoolShare
|
||||
} from '../@types/subgraph/PoolShares'
|
||||
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
|
||||
import { UserSalesQuery as UsersSalesList } from '../@types/subgraph/UserSalesQuery'
|
||||
import { OpcFeesQuery as OpcFeesData } from '../@types/subgraph/OpcFeesQuery'
|
||||
import { calcSingleOutGivenPoolIn } from './pool'
|
||||
import Decimal from 'decimal.js'
|
||||
import { MAX_DECIMALS } from './constants'
|
||||
|
||||
export interface UserLiquidity {
|
||||
price: string
|
||||
oceanBalance: string
|
||||
}
|
||||
|
||||
export interface PriceList {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const AssetPoolPriceQuery = gql`
|
||||
query AssetPoolPrice($datatokenAddress: String) {
|
||||
pools(where: { datatoken: $datatokenAddress }) {
|
||||
id
|
||||
spotPrice
|
||||
datatoken {
|
||||
address
|
||||
symbol
|
||||
}
|
||||
baseToken {
|
||||
symbol
|
||||
}
|
||||
datatokenLiquidity
|
||||
baseTokenLiquidity
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const PreviousOrderQuery = gql`
|
||||
query AssetPreviousOrder($id: String!, $account: String!) {
|
||||
@ -59,84 +19,6 @@ const PreviousOrderQuery = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
const HighestLiquidityAssets = gql`
|
||||
query HighestLiquidityAssets {
|
||||
pools(
|
||||
where: { datatokenLiquidity_gte: 1 }
|
||||
orderBy: baseTokenLiquidity
|
||||
orderDirection: desc
|
||||
first: 15
|
||||
) {
|
||||
id
|
||||
datatoken {
|
||||
address
|
||||
}
|
||||
baseToken {
|
||||
symbol
|
||||
}
|
||||
baseTokenLiquidity
|
||||
datatokenLiquidity
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const UserSharesQuery = gql`
|
||||
query UserSharesQuery($user: String, $pools: [String!]) {
|
||||
poolShares(where: { user: $user, pool_in: $pools }) {
|
||||
id
|
||||
shares
|
||||
user {
|
||||
id
|
||||
}
|
||||
pool {
|
||||
id
|
||||
datatoken {
|
||||
address
|
||||
symbol
|
||||
}
|
||||
baseToken {
|
||||
address
|
||||
symbol
|
||||
}
|
||||
datatokenLiquidity
|
||||
baseTokenLiquidity
|
||||
totalShares
|
||||
spotPrice
|
||||
createdTimestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const userPoolSharesQuery = gql`
|
||||
query PoolShares($user: String) {
|
||||
poolShares(where: { user: $user, shares_gt: 0.001 }, first: 1000) {
|
||||
id
|
||||
shares
|
||||
user {
|
||||
id
|
||||
}
|
||||
pool {
|
||||
id
|
||||
datatoken {
|
||||
id
|
||||
address
|
||||
symbol
|
||||
}
|
||||
baseToken {
|
||||
id
|
||||
address
|
||||
symbol
|
||||
}
|
||||
baseTokenLiquidity
|
||||
datatokenLiquidity
|
||||
totalShares
|
||||
spotPrice
|
||||
createdTimestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const UserTokenOrders = gql`
|
||||
query OrdersData($user: String!) {
|
||||
@ -163,32 +45,6 @@ const UserTokenOrders = gql`
|
||||
}
|
||||
`
|
||||
|
||||
const UserSalesQuery = gql`
|
||||
query UserSalesQuery($user: ID!) {
|
||||
users(where: { id: $user }) {
|
||||
id
|
||||
totalSales
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// TODO: figure out some way to get this
|
||||
const TopSalesQuery = gql`
|
||||
query TopSalesQuery {
|
||||
users(
|
||||
first: 20
|
||||
orderBy: sharesOwned
|
||||
orderDirection: desc
|
||||
where: { tokenBalancesOwned_not: "0" }
|
||||
) {
|
||||
id
|
||||
tokenBalancesOwned {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const OpcFeesQuery = gql`
|
||||
query OpcFeesQuery($id: ID!) {
|
||||
opc(id: $id) {
|
||||
@ -200,6 +56,19 @@ const OpcFeesQuery = gql`
|
||||
}
|
||||
`
|
||||
|
||||
const OpcsApprovedTokensQuery = gql`
|
||||
query OpcsApprovedTokensQuery {
|
||||
opcs {
|
||||
approvedTokens {
|
||||
address: id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function getSubgraphUri(chainId: number): string {
|
||||
const config = getOceanConfig(chainId)
|
||||
return config.subgraphUri
|
||||
@ -274,122 +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 getSpotPrice(asset: Asset): Promise<number> {
|
||||
const poolVariables = {
|
||||
datatokenAddress: asset?.services[0].datatokenAddress.toLowerCase()
|
||||
}
|
||||
const queryContext = getQueryContext(Number(asset.chainId))
|
||||
|
||||
const poolPriceResponse: OperationResult<AssetPoolPrice> = await fetchData(
|
||||
AssetPoolPriceQuery,
|
||||
poolVariables,
|
||||
queryContext
|
||||
)
|
||||
|
||||
return poolPriceResponse.data.pools[0].spotPrice
|
||||
}
|
||||
|
||||
export async function getHighestLiquidityDatatokens(
|
||||
chainIds: number[]
|
||||
): Promise<string[]> {
|
||||
const dtList: string[] = []
|
||||
let highestLiquidityAssets: HighestLiquidityAssetsPool[] = []
|
||||
|
||||
for (const chain of chainIds) {
|
||||
const queryContext = getQueryContext(Number(chain))
|
||||
const fetchedPools: OperationResult<HighestLiquidityGraphAssets, any> =
|
||||
await fetchData(HighestLiquidityAssets, null, queryContext)
|
||||
highestLiquidityAssets = highestLiquidityAssets.concat(
|
||||
fetchedPools?.data?.pools
|
||||
)
|
||||
}
|
||||
highestLiquidityAssets.sort(
|
||||
(a, b) => b.baseTokenLiquidity - a.baseTokenLiquidity
|
||||
)
|
||||
|
||||
for (let i = 0; i < highestLiquidityAssets.length; i++) {
|
||||
if (!highestLiquidityAssets[i]?.datatoken?.address) continue
|
||||
dtList.push(highestLiquidityAssets[i].datatoken.address)
|
||||
}
|
||||
return dtList
|
||||
}
|
||||
|
||||
export async function getAccountLiquidityInOwnAssets(
|
||||
accountId: string,
|
||||
chainIds: number[],
|
||||
pools: string[]
|
||||
): Promise<string> {
|
||||
const queryVariables = {
|
||||
user: accountId.toLowerCase(),
|
||||
pools
|
||||
}
|
||||
const results: PoolSharesList[] = await fetchDataForMultipleChains(
|
||||
UserSharesQuery,
|
||||
queryVariables,
|
||||
chainIds
|
||||
)
|
||||
let totalLiquidity = new Decimal(0)
|
||||
|
||||
for (const result of results) {
|
||||
for (const poolShare of result.poolShares) {
|
||||
const poolUserLiquidity = calcSingleOutGivenPoolIn(
|
||||
poolShare.pool.baseTokenLiquidity,
|
||||
poolShare.pool.totalShares,
|
||||
poolShare.shares
|
||||
)
|
||||
|
||||
totalLiquidity = totalLiquidity.add(new Decimal(poolUserLiquidity))
|
||||
}
|
||||
}
|
||||
return totalLiquidity.toDecimalPlaces(MAX_DECIMALS).toString()
|
||||
}
|
||||
|
||||
export async function getPoolSharesData(
|
||||
accountId: string,
|
||||
chainIds: number[]
|
||||
): Promise<PoolShare[]> {
|
||||
const variables = { user: accountId?.toLowerCase() }
|
||||
const data: PoolShare[] = []
|
||||
try {
|
||||
const result = await fetchDataForMultipleChains(
|
||||
userPoolSharesQuery,
|
||||
variables,
|
||||
chainIds
|
||||
)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
result[i].poolShares.forEach((poolShare: PoolShare) => {
|
||||
data.push(poolShare)
|
||||
})
|
||||
}
|
||||
return data
|
||||
} catch (error) {
|
||||
LoggerInstance.error('Error getPoolSharesData: ', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserTokenOrders(
|
||||
accountId: string,
|
||||
chainIds: number[]
|
||||
@ -415,60 +168,16 @@ export async function getUserTokenOrders(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserSales(
|
||||
accountId: string,
|
||||
chainIds: number[]
|
||||
): Promise<number> {
|
||||
const variables = { user: accountId?.toLowerCase() }
|
||||
export async function getOpcsApprovedTokens(
|
||||
chainId: number
|
||||
): Promise<TokenInfo[]> {
|
||||
const context = getQueryContext(chainId)
|
||||
|
||||
try {
|
||||
const userSales = await fetchDataForMultipleChains(
|
||||
UserSalesQuery,
|
||||
variables,
|
||||
chainIds
|
||||
)
|
||||
let salesSum = 0
|
||||
for (let i = 0; i < userSales.length; i++) {
|
||||
if (userSales[i].users.length > 0) {
|
||||
salesSum += parseInt(userSales[i].users[0].totalSales)
|
||||
}
|
||||
}
|
||||
return salesSum
|
||||
const response = await fetchData(OpcsApprovedTokensQuery, null, context)
|
||||
return response?.data?.opcs[0].approvedTokens
|
||||
} catch (error) {
|
||||
LoggerInstance.error('Error getUserSales', error.message)
|
||||
LoggerInstance.error('Error getOpcsApprovedTokens: ', error.message)
|
||||
throw Error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTopAssetsPublishers(
|
||||
chainIds: number[],
|
||||
nrItems = 9
|
||||
): Promise<AccountTeaserVM[]> {
|
||||
const publisherSales: AccountTeaserVM[] = []
|
||||
|
||||
for (const chain of chainIds) {
|
||||
const queryContext = getQueryContext(Number(chain))
|
||||
const fetchedUsers: OperationResult<UsersSalesList> = await fetchData(
|
||||
TopSalesQuery,
|
||||
null,
|
||||
queryContext
|
||||
)
|
||||
for (let i = 0; i < fetchedUsers.data.users.length; i++) {
|
||||
const publishersIndex = publisherSales.findIndex(
|
||||
(user) => fetchedUsers.data.users[i].id === user.address
|
||||
)
|
||||
if (publishersIndex === -1) {
|
||||
const publisher: AccountTeaserVM = {
|
||||
address: fetchedUsers.data.users[i].id,
|
||||
nrSales: fetchedUsers.data.users[i].totalSales
|
||||
}
|
||||
publisherSales.push(publisher)
|
||||
} else {
|
||||
publisherSales[publishersIndex].nrSales +=
|
||||
publisherSales[publishersIndex].nrSales
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publisherSales.sort((a, b) => b.nrSales - a.nrSales)
|
||||
|
||||
return publisherSales.slice(0, nrItems)
|
||||
}
|
||||
|
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'
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { getNetworkDisplayName } from '@hooks/useNetworkMetadata'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import Web3 from 'web3'
|
||||
import { getOceanConfig } from './ocean'
|
||||
import { AbiItem } from 'web3-utils/types'
|
||||
|
||||
export function accountTruncate(account: string): string {
|
||||
if (!account || account === '') return
|
||||
@ -110,3 +111,52 @@ export async function addTokenToWallet(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function getTokenBalance(
|
||||
accountId: string,
|
||||
decimals: number,
|
||||
tokenAddress: string,
|
||||
web3: Web3
|
||||
): Promise<string> {
|
||||
const minABI = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_owner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'balanceOf',
|
||||
outputs: [
|
||||
{
|
||||
name: 'balance',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
] as AbiItem[]
|
||||
|
||||
try {
|
||||
const token = new web3.eth.Contract(minABI, tokenAddress, {
|
||||
from: accountId
|
||||
})
|
||||
const balance = await token.methods.balanceOf(accountId).call()
|
||||
const adjustedDecimalsBalance = `${balance}${'0'.repeat(18 - decimals)}`
|
||||
return web3.utils.fromWei(adjustedDecimalsBalance)
|
||||
} catch (e) {
|
||||
LoggerInstance.error(`ERROR: Failed to get the balance: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getTokenBalanceFromSymbol(
|
||||
balance: UserBalance,
|
||||
symbol: string
|
||||
): string {
|
||||
if (!symbol) return
|
||||
const baseTokenBalance = balance?.[symbol.toLocaleLowerCase()]
|
||||
return baseTokenBalance || '0'
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './AssetList.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({
|
||||
assetList: 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 />
|
||||
)
|
||||
}
|
24
src/components/@shared/AccountList/index.module.css
Normal file
24
src/components/@shared/AccountList/index.module.css
Normal file
@ -0,0 +1,24 @@
|
||||
.accountList {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 25rem) {
|
||||
.accountList {
|
||||
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||
gap: var(--spacer);
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loaderWrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
.blockies {
|
||||
aspect-ratio: 1/1;
|
||||
width: 13%;
|
||||
height: 13%;
|
||||
border-radius: 50%;
|
||||
margin-left: 0;
|
||||
margin-right: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.teaser {
|
||||
max-width: 40rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: box from '../atoms/Box.module.css';
|
||||
padding: calc(var(--spacer) / 2) !important;
|
||||
font-size: var(--font-size-mini);
|
||||
height: 90%;
|
||||
color: var(--color-secondary);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link span {
|
||||
font-size: var(--font-size-large);
|
||||
margin-right: calc(var(--spacer) / 3);
|
||||
}
|
||||
.name {
|
||||
margin-bottom: 0;
|
||||
font-size: var(--font-size-base) !important;
|
||||
padding-top: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sales {
|
||||
font-size: small;
|
||||
margin-top: -5px !important;
|
||||
margin-bottom: clac(var(--spacer) / 2);
|
||||
}
|
@ -1,66 +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 (
|
||||
<article className={styles.teaser}>
|
||||
<Link href={`/profile/${accountTeaserVM.address}`}>
|
||||
<header className={styles.header}>
|
||||
{place && <span>{place}</span>}
|
||||
{profile?.image ? (
|
||||
<img src={profile.image} className={styles.blockies} />
|
||||
) : (
|
||||
<Blockies
|
||||
accountId={accountTeaserVM.address}
|
||||
className={styles.blockies}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Dotdotdot clamp={3}>
|
||||
<h3 className={styles.name}>
|
||||
{profile?.name
|
||||
? profile?.name
|
||||
: accountTruncate(accountTeaserVM.address)}
|
||||
</h3>
|
||||
</Dotdotdot>
|
||||
<p className={styles.sales}>
|
||||
{`${accountTeaserVM.nrSales} ${
|
||||
accountTeaserVM.nrSales === 1 ? 'sale' : 'sales'
|
||||
}`}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
}
|
@ -38,6 +38,10 @@
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.button:hover .logo,
|
||||
.button:focus .logo {
|
||||
border-color: var(--color-primary);
|
||||
|
@ -3,6 +3,7 @@ import classNames from 'classnames/bind'
|
||||
import { addTokenToWallet } from '@utils/web3'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import OceanLogo from '@images/logo.svg'
|
||||
import styles from './index.module.css'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
@ -10,14 +11,12 @@ const cx = classNames.bind(styles)
|
||||
export default function AddToken({
|
||||
address,
|
||||
symbol,
|
||||
logo,
|
||||
text,
|
||||
className,
|
||||
minimal
|
||||
}: {
|
||||
address: string
|
||||
symbol: string
|
||||
logo: string // needs to be a remote image
|
||||
text?: string
|
||||
className?: string
|
||||
minimal?: boolean
|
||||
@ -33,7 +32,7 @@ export default function AddToken({
|
||||
async function handleAddToken() {
|
||||
if (!web3Provider) return
|
||||
|
||||
await addTokenToWallet(web3Provider, address, symbol, logo)
|
||||
await addTokenToWallet(web3Provider, address, symbol)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -44,7 +43,9 @@ export default function AddToken({
|
||||
onClick={handleAddToken}
|
||||
>
|
||||
<span className={styles.logoWrap}>
|
||||
<img src={logo} className={styles.logo} width="16" height="16" />
|
||||
<div className={styles.logo}>
|
||||
<OceanLogo />
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span className={styles.text}>{text || `Add ${symbol}`}</span>
|
||||
|
@ -37,7 +37,7 @@ export default function AssetComputeSelection({
|
||||
</Dotdotdot>
|
||||
</div>
|
||||
<PriceUnit
|
||||
price={asset.price}
|
||||
price={Number(asset.price)}
|
||||
size="small"
|
||||
className={styles.price}
|
||||
/>
|
||||
|
@ -6,9 +6,6 @@ import classNames from 'classnames/bind'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
// not sure why this import is required
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import { Asset } from '@oceanprotocol/lib'
|
||||
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
|
||||
@ -23,7 +20,7 @@ function LoaderArea() {
|
||||
}
|
||||
|
||||
declare type AssetListProps = {
|
||||
assets: Asset[]
|
||||
assets: AssetExtended[]
|
||||
showPagination: boolean
|
||||
page?: number
|
||||
totalPages?: number
|
||||
@ -51,6 +48,7 @@ export default function AssetList({
|
||||
|
||||
useEffect(() => {
|
||||
if (!assets) return
|
||||
|
||||
setAssetsWithPrices(assets as AssetExtended[])
|
||||
setLoading(false)
|
||||
async function fetchPrices() {
|
||||
@ -58,7 +56,7 @@ export default function AssetList({
|
||||
assets,
|
||||
accountId || ''
|
||||
)
|
||||
if (!isMounted()) return
|
||||
if (!isMounted() || !assetsWithPrices) return
|
||||
setAssetsWithPrices([...assetsWithPrices])
|
||||
}
|
||||
fetchPrices()
|
||||
|
@ -8,7 +8,6 @@ import AssetType from '@shared/AssetType'
|
||||
import NetworkName from '@shared/NetworkName'
|
||||
import styles from './AssetTeaser.module.css'
|
||||
import { getServiceByName } from '@utils/ddo'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
|
||||
declare type AssetTeaserProps = {
|
||||
asset: AssetExtended
|
||||
@ -24,14 +23,15 @@ export default function AssetTeaser({
|
||||
const isCompute = Boolean(getServiceByName(asset, 'compute'))
|
||||
const accessType = isCompute ? 'compute' : 'access'
|
||||
const { owner } = asset.nft
|
||||
const { orders } = asset.stats
|
||||
return (
|
||||
<article className={`${styles.teaser} ${styles[type]}`}>
|
||||
<Link href={`/asset/${asset.id}`}>
|
||||
<a className={styles.link}>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.symbol}>{datatokens[0]?.symbol}</div>
|
||||
<Dotdotdot clamp={3}>
|
||||
<h1 className={styles.title}>{name}</h1>
|
||||
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
|
||||
{name.slice(0, 200)}
|
||||
</Dotdotdot>
|
||||
{!noPublisher && (
|
||||
<Publisher account={owner} minimal className={styles.publisher} />
|
||||
@ -42,6 +42,7 @@ export default function AssetTeaser({
|
||||
type={type}
|
||||
accessType={accessType}
|
||||
className={styles.typeDetails}
|
||||
totalSales={orders}
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
|
@ -1,26 +1,22 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './index.module.css'
|
||||
import classNames from 'classnames/bind'
|
||||
import Compute from '@images/compute.svg'
|
||||
import Download from '@images/download.svg'
|
||||
import Lock from '@images/lock.svg'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
export default function AssetType({
|
||||
type,
|
||||
accessType,
|
||||
className
|
||||
className,
|
||||
totalSales
|
||||
}: {
|
||||
type: string
|
||||
accessType: string
|
||||
className?: string
|
||||
totalSales?: number
|
||||
}): ReactElement {
|
||||
const styleClasses = cx({
|
||||
[className]: className
|
||||
})
|
||||
return (
|
||||
<div className={styleClasses}>
|
||||
<div className={className || null}>
|
||||
{accessType === 'access' ? (
|
||||
<Download role="img" aria-label="Download" className={styles.icon} />
|
||||
) : accessType === 'compute' && type === 'algorithm' ? (
|
||||
@ -32,6 +28,14 @@ export default function AssetType({
|
||||
<div className={styles.typeLabel}>
|
||||
{type === 'dataset' ? 'dataset' : 'algorithm'}
|
||||
</div>
|
||||
|
||||
{(totalSales || totalSales === 0) && (
|
||||
<div className={styles.typeLabel}>
|
||||
{totalSales < 0
|
||||
? 'N/A'
|
||||
: `${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -9,9 +9,9 @@ interface ButtonBuyProps {
|
||||
disabled: boolean
|
||||
hasPreviousOrder: boolean
|
||||
hasDatatoken: boolean
|
||||
btSymbol: string
|
||||
dtSymbol: string
|
||||
dtBalance: string
|
||||
datasetLowPoolLiquidity: boolean
|
||||
assetType: string
|
||||
assetTimeout: string
|
||||
isConsumable: boolean
|
||||
@ -20,7 +20,6 @@ interface ButtonBuyProps {
|
||||
hasDatatokenSelectedComputeAsset?: boolean
|
||||
dtSymbolSelectedComputeAsset?: string
|
||||
dtBalanceSelectedComputeAsset?: string
|
||||
selectedComputeAssetLowPoolLiquidity?: boolean
|
||||
selectedComputeAssetType?: string
|
||||
isBalanceSufficient: boolean
|
||||
isLoading?: boolean
|
||||
@ -36,11 +35,11 @@ interface ButtonBuyProps {
|
||||
|
||||
// TODO: we need to take a look at these messages
|
||||
function getConsumeHelpText(
|
||||
btSymbol: string,
|
||||
dtBalance: string,
|
||||
dtSymbol: string,
|
||||
hasDatatoken: boolean,
|
||||
hasPreviousOrder: boolean,
|
||||
lowPoolLiquidity: boolean,
|
||||
assetType: string,
|
||||
isConsumable: boolean,
|
||||
isBalanceSufficient: boolean,
|
||||
@ -54,14 +53,10 @@ function getConsumeHelpText(
|
||||
: hasPreviousOrder && web3 && isSupportedOceanNetwork
|
||||
? `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 OCEAN again.`
|
||||
: lowPoolLiquidity
|
||||
? `There are not enought ${dtSymbol} available in the pool for the transaction to take place`
|
||||
: web3 && !isSupportedOceanNetwork
|
||||
? `You are connected to an unsupported network.`
|
||||
? `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 OCEAN in your wallet to purchase this asset.'
|
||||
: `For using this ${assetType}, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher and pool.`
|
||||
? `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.`
|
||||
return text
|
||||
}
|
||||
|
||||
@ -100,18 +95,17 @@ function getAlgoHelpText(
|
||||
function getComputeAssetHelpText(
|
||||
hasPreviousOrder: boolean,
|
||||
hasDatatoken: boolean,
|
||||
btSymbol: string,
|
||||
dtSymbol: string,
|
||||
dtBalance: string,
|
||||
lowPoolLiquidity: boolean,
|
||||
assetType: string,
|
||||
isConsumable: boolean,
|
||||
consumableFeedback: string,
|
||||
isBalanceSufficient: boolean,
|
||||
hasPreviousOrderSelectedComputeAsset?: boolean,
|
||||
hasDatatokenSelectedComputeAsset?: boolean,
|
||||
assetType?: string,
|
||||
dtSymbolSelectedComputeAsset?: string,
|
||||
dtBalanceSelectedComputeAsset?: string,
|
||||
selectedComputeAssettLowPoolLiquidity?: boolean,
|
||||
selectedComputeAssetType?: string,
|
||||
isAlgorithmConsumable?: boolean,
|
||||
isSupportedOceanNetwork?: boolean,
|
||||
@ -119,11 +113,11 @@ function getComputeAssetHelpText(
|
||||
hasProviderFee?: boolean
|
||||
) {
|
||||
const computeAssetHelpText = getConsumeHelpText(
|
||||
btSymbol,
|
||||
dtBalance,
|
||||
dtSymbol,
|
||||
hasDatatoken,
|
||||
hasPreviousOrder,
|
||||
lowPoolLiquidity,
|
||||
assetType,
|
||||
isConsumable,
|
||||
isBalanceSufficient,
|
||||
@ -132,31 +126,22 @@ function getComputeAssetHelpText(
|
||||
web3
|
||||
)
|
||||
|
||||
const computeAlgoHelpText = getAlgoHelpText(
|
||||
dtSymbolSelectedComputeAsset,
|
||||
dtBalanceSelectedComputeAsset,
|
||||
isConsumable,
|
||||
isAlgorithmConsumable,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
selectedComputeAssetType,
|
||||
hasDatatokenSelectedComputeAsset,
|
||||
selectedComputeAssettLowPoolLiquidity,
|
||||
isBalanceSufficient,
|
||||
isSupportedOceanNetwork,
|
||||
web3
|
||||
)
|
||||
|
||||
const providerFeeHelpText = !isSupportedOceanNetwork
|
||||
const computeAlgoHelpText =
|
||||
(!dtSymbolSelectedComputeAsset && !dtBalanceSelectedComputeAsset) ||
|
||||
isConsumable === false ||
|
||||
isAlgorithmConsumable === false
|
||||
? ''
|
||||
: hasProviderFee
|
||||
: hasPreviousOrderSelectedComputeAsset
|
||||
? `You already bought the selected ${selectedComputeAssetType}, allowing you to use it without paying again.`
|
||||
: hasDatatokenSelectedComputeAsset
|
||||
? `You own ${dtBalanceSelectedComputeAsset} ${dtSymbolSelectedComputeAsset} allowing you to use the selected ${selectedComputeAssetType} by spending 1 ${dtSymbolSelectedComputeAsset}, but without paying ${btSymbol} again.`
|
||||
: isBalanceSufficient === false
|
||||
? ''
|
||||
: `Additionally, you will buy 1 ${dtSymbolSelectedComputeAsset} for the ${selectedComputeAssetType} and spend it back to its publisher.`
|
||||
const providerFeeHelpText = hasProviderFee
|
||||
? 'In order to start the job you also need to pay the fees for renting the c2d resources.'
|
||||
: 'C2D resources required to start the job are available, no payment required for those fees.'
|
||||
|
||||
const computeHelpText = selectedComputeAssettLowPoolLiquidity
|
||||
? computeAlgoHelpText
|
||||
: lowPoolLiquidity
|
||||
? computeAssetHelpText
|
||||
: `${computeAssetHelpText} ${computeAlgoHelpText} ${providerFeeHelpText}`
|
||||
const computeHelpText = `${computeAssetHelpText} ${computeAlgoHelpText} ${providerFeeHelpText}`
|
||||
return computeHelpText
|
||||
}
|
||||
|
||||
@ -165,9 +150,9 @@ export default function ButtonBuy({
|
||||
disabled,
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
btSymbol,
|
||||
dtSymbol,
|
||||
dtBalance,
|
||||
datasetLowPoolLiquidity,
|
||||
assetType,
|
||||
assetTimeout,
|
||||
isConsumable,
|
||||
@ -177,7 +162,6 @@ export default function ButtonBuy({
|
||||
hasDatatokenSelectedComputeAsset,
|
||||
dtSymbolSelectedComputeAsset,
|
||||
dtBalanceSelectedComputeAsset,
|
||||
selectedComputeAssetLowPoolLiquidity,
|
||||
selectedComputeAssetType,
|
||||
onClick,
|
||||
stepText,
|
||||
@ -223,11 +207,11 @@ export default function ButtonBuy({
|
||||
<div className={styles.help}>
|
||||
{action === 'download'
|
||||
? getConsumeHelpText(
|
||||
btSymbol,
|
||||
dtBalance,
|
||||
dtSymbol,
|
||||
hasDatatoken,
|
||||
hasPreviousOrder,
|
||||
datasetLowPoolLiquidity,
|
||||
assetType,
|
||||
isConsumable,
|
||||
isBalanceSufficient,
|
||||
@ -238,18 +222,17 @@ export default function ButtonBuy({
|
||||
: getComputeAssetHelpText(
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
btSymbol,
|
||||
dtSymbol,
|
||||
dtBalance,
|
||||
datasetLowPoolLiquidity,
|
||||
assetType,
|
||||
isConsumable,
|
||||
consumableFeedback,
|
||||
isBalanceSufficient,
|
||||
hasPreviousOrderSelectedComputeAsset,
|
||||
hasDatatokenSelectedComputeAsset,
|
||||
assetType,
|
||||
dtSymbolSelectedComputeAsset,
|
||||
dtBalanceSelectedComputeAsset,
|
||||
selectedComputeAssetLowPoolLiquidity,
|
||||
selectedComputeAssetType,
|
||||
isAlgorithmConsumable,
|
||||
isSupportedOceanNetwork,
|
||||
|
@ -1,13 +1,10 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import filesize from 'filesize'
|
||||
import classNames from 'classnames/bind'
|
||||
import { filesize } from 'filesize'
|
||||
import cleanupContentType from '@utils/cleanupContentType'
|
||||
import styles from './index.module.css'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import { FileInfo } from '@oceanprotocol/lib'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
function LoaderArea() {
|
||||
return (
|
||||
<div className={styles.loaderWrap}>
|
||||
@ -27,11 +24,9 @@ export default function FileIcon({
|
||||
small?: boolean
|
||||
isLoading?: boolean
|
||||
}): ReactElement {
|
||||
const styleClasses = cx({
|
||||
file: true,
|
||||
small,
|
||||
[className]: className
|
||||
})
|
||||
const styleClasses = `${styles.file} ${small ? styles.small : ''} ${
|
||||
className || ''
|
||||
}`
|
||||
|
||||
return (
|
||||
<ul className={styleClasses}>
|
||||
@ -42,7 +37,7 @@ export default function FileIcon({
|
||||
<li>{cleanupContentType(file.contentType)}</li>
|
||||
<li>
|
||||
{file.contentLength && file.contentLength !== '0'
|
||||
? filesize(Number(file.contentLength))
|
||||
? filesize(Number(file.contentLength)).toString()
|
||||
: ''}
|
||||
</li>
|
||||
</>
|
||||
|
@ -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"
|
||||
|
@ -11,6 +11,7 @@ import AssetSelection, {
|
||||
} from '../FormFields/AssetSelection'
|
||||
import Nft from '../FormFields/Nft'
|
||||
import InputRadio from './InputRadio'
|
||||
import TagsAutoComplete from './TagsAutoComplete'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
@ -121,6 +122,8 @@ export default function InputElement({
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
case 'tags':
|
||||
return <TagsAutoComplete {...field} {...props} />
|
||||
default:
|
||||
return prefix || postfix ? (
|
||||
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>
|
||||
|
101
src/components/@shared/FormInput/TagsAutoComplete.module.css
Normal file
101
src/components/@shared/FormInput/TagsAutoComplete.module.css
Normal file
@ -0,0 +1,101 @@
|
||||
.select [class$='control'] {
|
||||
border-color: var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: none;
|
||||
background-color: var(--background-content);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
cursor: text;
|
||||
min-height: 43px;
|
||||
}
|
||||
|
||||
.select [class$='control']:hover {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.select [class$='control']:focus-within {
|
||||
border-color: var(--font-color-text);
|
||||
}
|
||||
|
||||
.select [class$='ValueContainer'] {
|
||||
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 3);
|
||||
}
|
||||
|
||||
.select [class$='Input'] {
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.select input {
|
||||
color: var(--font-color-heading) !important;
|
||||
}
|
||||
|
||||
.select [class$='menu'] {
|
||||
background-color: var(--background-highlight);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.select [class$='option'] {
|
||||
color: var(--font-color-heading);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.select [class$='option']:active {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.select [class$='multiValue'],
|
||||
.select [class$='multiValue'] > *,
|
||||
.select [class$='multiValue']:hover > * {
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background-highlight);
|
||||
color: var(--font-color-text);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.select [class$='multiValue'] > div[role$='button'],
|
||||
.select [class$='indicatorContainer'] svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select [class$='placeholder'] {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-base);
|
||||
transition: 0.2s ease-out;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.select [class$='menu'] {
|
||||
background-color: var(--background-content);
|
||||
margin-top: -2px;
|
||||
border: 1px solid var(--font-color-text);
|
||||
border-top-color: var(--border-color);
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.select [class$='menu'] [class$='option']:hover,
|
||||
.select [class$='menu'] [class$='option']:focus-within {
|
||||
background-color: var(--font-color-heading);
|
||||
color: var(--background-content);
|
||||
}
|
||||
|
||||
.select [class$='NoOptionsMessage'] {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-secondary);
|
||||
text-align: left;
|
||||
}
|
92
src/components/@shared/FormInput/TagsAutoComplete.tsx
Normal file
92
src/components/@shared/FormInput/TagsAutoComplete.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import CreatableSelect from 'react-select/creatable'
|
||||
import { OnChangeValue } from 'react-select'
|
||||
import { useField } from 'formik'
|
||||
import { InputProps } from '.'
|
||||
import { getTagsList } from '@utils/aquarius'
|
||||
import { chainIds } from 'app.config'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import styles from './TagsAutoComplete.module.css'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
|
||||
interface AutoCompleteOption {
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
}
|
||||
|
||||
export default function TagsAutoComplete({
|
||||
...props
|
||||
}: InputProps): ReactElement {
|
||||
const { name, placeholder } = props
|
||||
const [tagsList, setTagsList] = useState<AutoCompleteOption[]>()
|
||||
const [matchedTagsList, setMatchedTagsList] = useState<AutoCompleteOption[]>(
|
||||
[]
|
||||
)
|
||||
const [field, meta, helpers] = useField(name)
|
||||
const [input, setInput] = useState<string>()
|
||||
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
const generateAutocompleteOptions = (
|
||||
options: string[]
|
||||
): AutoCompleteOption[] => {
|
||||
return options?.map((tag) => ({
|
||||
value: tag,
|
||||
label: tag
|
||||
}))
|
||||
}
|
||||
|
||||
const defaultTags = !field.value
|
||||
? undefined
|
||||
: generateAutocompleteOptions(field.value)
|
||||
|
||||
useEffect(() => {
|
||||
const generateTagsList = async () => {
|
||||
const tags = await getTagsList(chainIds, newCancelToken())
|
||||
const autocompleteOptions = generateAutocompleteOptions(tags)
|
||||
setTagsList(autocompleteOptions)
|
||||
}
|
||||
generateTagsList()
|
||||
}, [newCancelToken])
|
||||
|
||||
const handleChange = (userInput: OnChangeValue<AutoCompleteOption, true>) => {
|
||||
const normalizedInput = userInput.map((input) => input.value)
|
||||
helpers.setValue(normalizedInput)
|
||||
helpers.setTouched(true)
|
||||
}
|
||||
|
||||
const handleOptionsFilter = (
|
||||
options: AutoCompleteOption[],
|
||||
input: string
|
||||
): void => {
|
||||
setInput(input)
|
||||
const matchedTagsList = matchSorter(options, input, { keys: ['value'] })
|
||||
setMatchedTagsList(matchedTagsList)
|
||||
}
|
||||
|
||||
return (
|
||||
<CreatableSelect
|
||||
components={{
|
||||
DropdownIndicator: () => null,
|
||||
IndicatorSeparator: () => null
|
||||
}}
|
||||
className={styles.select}
|
||||
defaultValue={defaultTags}
|
||||
hideSelectedOptions
|
||||
isMulti
|
||||
isClearable={false}
|
||||
noOptionsMessage={() =>
|
||||
'Start typing to get suggestions based on tags from all published assets.'
|
||||
}
|
||||
onChange={(value: AutoCompleteOption[]) => handleChange(value)}
|
||||
onInputChange={(value) => handleOptionsFilter(tagsList, value)}
|
||||
openMenuOnClick
|
||||
options={!input || input?.length < 1 ? [] : matchedTagsList}
|
||||
placeholder={placeholder}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
colors: { ...theme.colors, primary25: 'var(--border-color)' }
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)}
|
||||
|
@ -22,7 +22,9 @@
|
||||
width: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 42rem) {
|
||||
.minimal:hover .name {
|
||||
width: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,11 @@
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
display: inline-flex;
|
||||
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
|
77
src/components/@shared/Page/Seo/DatasetSchema.tsx
Normal file
77
src/components/@shared/Page/Seo/DatasetSchema.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { useAsset } from '@context/Asset'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
import useNetworkMetadata, {
|
||||
filterNetworksByType
|
||||
} from '@hooks/useNetworkMetadata'
|
||||
import removeMarkdown from 'remove-markdown'
|
||||
|
||||
const DatasetSchema = (): object => {
|
||||
const { asset, isInPurgatory } = useAsset()
|
||||
const { networksList } = useNetworkMetadata()
|
||||
const { appConfig } = useMarketMetadata()
|
||||
|
||||
const networksMain = filterNetworksByType(
|
||||
'mainnet',
|
||||
appConfig.chainIdsSupported,
|
||||
networksList
|
||||
)
|
||||
|
||||
// only show schema on main nets
|
||||
const isMainNetwork = networksMain.includes(asset?.chainId)
|
||||
|
||||
const isDataset = asset?.metadata?.type === 'dataset'
|
||||
|
||||
if (!asset || !isMainNetwork || !isDataset || isInPurgatory) return null
|
||||
|
||||
let isDownloadable = false
|
||||
if (asset?.services && Array.isArray(asset?.services)) {
|
||||
for (const service of asset.services) {
|
||||
if (service?.type === 'access') {
|
||||
isDownloadable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://developers.google.com/search/docs/advanced/structured-data/dataset
|
||||
const datasetSchema = {
|
||||
'@context': 'https://schema.org/',
|
||||
'@type': 'Dataset',
|
||||
name: asset?.metadata?.name,
|
||||
description: removeMarkdown(
|
||||
asset?.metadata?.description?.substring(0, 5000) || ''
|
||||
),
|
||||
keywords: asset?.metadata?.tags,
|
||||
datePublished: asset?.metadata?.created,
|
||||
dateModified: asset?.metadata?.updated,
|
||||
license: asset?.metadata?.license,
|
||||
...(asset?.accessDetails?.type === 'free'
|
||||
? { isAccessibleForFree: true }
|
||||
: {
|
||||
isAccessibleForFree: false,
|
||||
paymentAccepted: 'Cryptocurrency',
|
||||
currenciesAccepted: asset?.accessDetails?.baseToken?.symbol,
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: asset?.accessDetails?.price,
|
||||
priceCurrency: asset?.accessDetails?.baseToken?.symbol
|
||||
}
|
||||
}),
|
||||
creator: {
|
||||
'@type': 'Organization',
|
||||
name: asset?.metadata?.author
|
||||
},
|
||||
...(isDownloadable && {
|
||||
distribution: [
|
||||
{
|
||||
'@type': 'DataDownload',
|
||||
encodingFormat: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return datasetSchema
|
||||
}
|
||||
|
||||
export { DatasetSchema }
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user