mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge pull request #1431 from oceanprotocol/use-computeInitialize
Use compute initialize and refactors
This commit is contained in:
commit
07f3e6747a
11
.eslintrc
11
.eslintrc
@ -30,9 +30,16 @@
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:testing-library/react",
|
||||
"plugin:jest-dom/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"prettier",
|
||||
"testing-library",
|
||||
"jest-dom"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"react/prop-types": "off",
|
||||
"react/no-unused-prop-types": "off",
|
||||
|
79
.github/workflows/ci.yml
vendored
79
.github/workflows/ci.yml
vendored
@ -12,14 +12,14 @@ on:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: ['16', '14']
|
||||
node: ['16']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -37,19 +37,80 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run codegen:apollo
|
||||
- run: npm run lint
|
||||
# - run: npm test
|
||||
- run: npm run build
|
||||
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: ['16']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
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
|
||||
- run: npm test
|
||||
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: coverage-${{ runner.os }}
|
||||
path: coverage/
|
||||
|
||||
# coverage:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [test]
|
||||
# if: ${{ success() && github.actor != 'dependabot[bot]' }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions/setup-node@v2
|
||||
# - run: npm ci
|
||||
# - uses: paambaati/codeclimate-action@v2.7.5
|
||||
# - uses: actions/download-artifact@v2
|
||||
# with:
|
||||
# name: coverage-${{ runner.os }}
|
||||
|
||||
# - uses: paambaati/codeclimate-action@v3.0.0
|
||||
# env:
|
||||
# CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
|
||||
# with:
|
||||
# coverageCommand: npm test
|
||||
# debug: true
|
||||
|
||||
storybook:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: ['16']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
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
|
||||
- run: npm run storybook:build
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,3 +15,5 @@ src/@types/apollo/
|
||||
graphql.schema.json
|
||||
src/@types/graph.types.ts
|
||||
tsconfig.tsbuildinfo
|
||||
__snapshots__
|
||||
storybook-static
|
34
.jest/jest.config.js
Normal file
34
.jest/jest.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
const nextJest = require('next/jest')
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './'
|
||||
})
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const customJestConfig = {
|
||||
rootDir: '../',
|
||||
// Add more setup options before each test is run
|
||||
setupFilesAfterEnv: ['<rootDir>/.jest/jest.setup.js'],
|
||||
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
|
||||
moduleDirectories: ['node_modules', '<rootDir>/src'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
moduleNameMapper: {
|
||||
// '^@/components/(.*)$': '<rootDir>/components/$1',
|
||||
'@shared(.*)$': '<rootDir>/src/components/@shared/$1',
|
||||
'@hooks/(.*)$': '<rootDir>/src/@hooks/$1',
|
||||
'@context/(.*)$': '<rootDir>/src/@context/$1',
|
||||
'@images/(.*)$': '<rootDir>/src/@images/$1',
|
||||
'@utils/(.*)$': '<rootDir>/src/@utils/$1',
|
||||
'@content/(.*)$': '<rootDir>/@content/$1'
|
||||
},
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/**/*.{stories,test}.{ts,tsx}'
|
||||
],
|
||||
testPathIgnorePatterns: ['node_modules', '\\.cache', '.next', 'coverage']
|
||||
}
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig)
|
1
.jest/jest.setup.js
Normal file
1
.jest/jest.setup.js
Normal file
@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/extend-expect'
|
47
.storybook/main.js
Normal file
47
.storybook/main.js
Normal file
@ -0,0 +1,47 @@
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
core: { builder: 'webpack5' },
|
||||
stories: ['../src/**/*.stories.tsx'],
|
||||
addons: ['@storybook/addon-essentials'],
|
||||
framework: '@storybook/react',
|
||||
webpackFinal: async (config) => {
|
||||
config.resolve.plugins = [
|
||||
...(config.resolve.plugins || []),
|
||||
new TsconfigPathsPlugin({
|
||||
extensions: config.resolve.extensions
|
||||
})
|
||||
]
|
||||
|
||||
// Mimic next.config.js webpack config
|
||||
config.module.rules.push(
|
||||
{
|
||||
test: /\.svg$/,
|
||||
issuer: /\.(tsx|ts)$/,
|
||||
use: [
|
||||
{ loader: require.resolve('@svgr/webpack'), options: { icon: true } }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.gif$/,
|
||||
// yay for webpack 5
|
||||
// https://webpack.js.org/guides/asset-management/#loading-images
|
||||
type: 'asset/resource'
|
||||
}
|
||||
)
|
||||
|
||||
const fallback = config.resolve.fallback || {}
|
||||
Object.assign(fallback, {
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
fs: false,
|
||||
crypto: false,
|
||||
os: false,
|
||||
stream: false,
|
||||
assert: false
|
||||
})
|
||||
config.resolve.fallback = fallback
|
||||
|
||||
return config
|
||||
}
|
||||
}
|
19
.storybook/preview.js
Normal file
19
.storybook/preview.js
Normal file
@ -0,0 +1,19 @@
|
||||
import '@oceanprotocol/typographies/css/ocean-typo.css'
|
||||
import '../src/stylesGlobal/styles.css'
|
||||
|
||||
export const parameters = {
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
values: [
|
||||
{ name: 'dark', value: 'rgb(10, 10, 10)' },
|
||||
{ name: 'light', value: '#fcfcfc' }
|
||||
]
|
||||
},
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/
|
||||
}
|
||||
}
|
||||
}
|
14
.storybook/storyshots.test.tsx
Normal file
14
.storybook/storyshots.test.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
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())
|
||||
}
|
||||
})
|
66
README.md
66
README.md
@ -19,6 +19,8 @@
|
||||
- [3Box](#3box)
|
||||
- [Purgatory](#purgatory)
|
||||
- [Network Metadata](#network-metadata)
|
||||
- [👩🎤 Storybook](#-storybook)
|
||||
- [🤖 Testing](#-testing)
|
||||
- [✨ Code Style](#-code-style)
|
||||
- [🛳 Production](#-production)
|
||||
- [⬆️ Deployment](#️-deployment)
|
||||
@ -276,18 +278,77 @@ export default function NetworkName(): ReactElement {
|
||||
}
|
||||
```
|
||||
|
||||
## 👩🎤 Storybook
|
||||
|
||||
Storybook helps us build UI components in isolation from our app's business logic, data, and context. That makes it easy to develop hard-to-reach states and save these UI states as stories to revisit during development, testing, or QA.
|
||||
|
||||
To start adding stories, create a `index.stories.tsx` inside the component's folder:
|
||||
|
||||
<pre>
|
||||
src
|
||||
└─── components
|
||||
│ └─── @shared
|
||||
│ └─── <your component>
|
||||
│ │ index.tsx
|
||||
│ │ index.module.css
|
||||
│ │ <b>index.stories.tsx</b>
|
||||
│ │ index.test.tsx
|
||||
</pre>
|
||||
|
||||
Starting up the Storybook server with this command will make it accessible under `http://localhost:6006`:
|
||||
|
||||
```bash
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
If you want to build a portable static version under `storybook-static/`:
|
||||
|
||||
```bash
|
||||
npm run storybook:build
|
||||
```
|
||||
|
||||
## 🤖 Testing
|
||||
|
||||
Test runs utilize [Jest](https://jestjs.io/) as test runner and [Testing Library](https://testing-library.com/docs/react-testing-library/intro) for writing tests.
|
||||
|
||||
All created Storybook stories will automatically run as individual tests by using the [StoryShots Addon](https://storybook.js.org/addons/@storybook/addon-storyshots).
|
||||
|
||||
Creating Storybook stories for a component will provide good coverage of a component in many cases. Additional tests for dedicated component functionality which can't be done with Storybook are created as usual [Testing Library](https://testing-library.com/docs/react-testing-library/intro) tests, but you can also [import exisiting Storybook stories](https://storybook.js.org/docs/react/writing-tests/importing-stories-in-tests#example-with-testing-library) into those tests.
|
||||
|
||||
Executing linting, type checking, and full test run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Which is a combination of multiple scripts which can also be run individually:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run type-check
|
||||
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:
|
||||
|
||||
```bash
|
||||
npm run jest:watch
|
||||
```
|
||||
|
||||
## ✨ Code Style
|
||||
|
||||
Code style is automatically enforced through [ESLint](https://eslint.org) & [Prettier](https://prettier.io) rules:
|
||||
|
||||
- Git pre-commit hook runs `prettier` on staged files, setup with [Husky](https://typicode.github.io/husky)
|
||||
- VS Code suggested extensions and settings for auto-formatting on file save
|
||||
- CI runs a linting & TypeScript typings check with `npm run lint`, and fails if errors are found
|
||||
- CI runs a linting & TypeScript typings check as part of `npm test`, and fails if errors are found
|
||||
|
||||
For running linting and auto-formatting manually, you can use from the root of the project:
|
||||
|
||||
```bash
|
||||
# linting check, also runs Typescript typings check
|
||||
# linting check
|
||||
npm run lint
|
||||
|
||||
# auto format all files in the project with prettier, taking all configs into account
|
||||
@ -300,6 +361,7 @@ To create a production build, run from the root of the project:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
|
||||
# serve production build
|
||||
npm run serve
|
||||
```
|
||||
|
@ -54,8 +54,7 @@
|
||||
"titleIn": "You will receive",
|
||||
"titleOut": "Pool conversion"
|
||||
},
|
||||
"action": "Supply",
|
||||
"warning": "Use at your own risk. Please familiarize yourself [with the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13) and the [Terms of Use](/terms)."
|
||||
"action": "Supply"
|
||||
},
|
||||
"remove": {
|
||||
"title": "Remove Liquidity",
|
||||
@ -68,7 +67,6 @@
|
||||
}
|
||||
},
|
||||
"trade": {
|
||||
"action": "Swap",
|
||||
"warning": "Use at your own risk. Please familiarize yourself [with the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13) and the [Terms of Use](/terms)."
|
||||
"action": "Swap"
|
||||
}
|
||||
}
|
||||
|
46875
package-lock.json
generated
46875
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
67
package.json
67
package.json
@ -9,31 +9,35 @@
|
||||
"build": "npm run pregenerate && next build",
|
||||
"serve": "serve -s public/",
|
||||
"pregenerate": "bash scripts/pregenerate.sh",
|
||||
"test": "npm run pregenerate && npm run lint && npm run type-check",
|
||||
"test": "npm run pregenerate && npm run lint && npm run type-check && npm run jest",
|
||||
"jest": "jest -c .jest/jest.config.js",
|
||||
"jest:watch": "jest -c .jest/jest.config.js --watch",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js --ext .ts --ext .tsx .",
|
||||
"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.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/"
|
||||
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
|
||||
"storybook": "start-storybook -p 6006 --quiet",
|
||||
"storybook:build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coingecko/cryptoformat": "^0.4.4",
|
||||
"@loadable/component": "^5.15.2",
|
||||
"@oceanprotocol/art": "^3.2.0",
|
||||
"@oceanprotocol/lib": "^1.0.0-next.37",
|
||||
"@oceanprotocol/lib": "^1.0.0-next.42",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@portis/web3": "^4.0.7",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@urql/exchange-refocus": "^0.2.5",
|
||||
"@walletconnect/web3-provider": "^1.7.7",
|
||||
"axios": "^0.26.1",
|
||||
"@walletconnect/web3-provider": "^1.7.8",
|
||||
"axios": "^0.27.2",
|
||||
"chart.js": "^3.7.1",
|
||||
"classnames": "^2.3.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"decimal.js": "^10.3.1",
|
||||
"dom-confetti": "^0.2.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"filesize": "^8.0.7",
|
||||
"formik": "^2.2.9",
|
||||
"gray-matter": "^4.0.3",
|
||||
@ -43,17 +47,17 @@
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"myetherwallet-blockies": "^0.1.1",
|
||||
"next": "^12.1.5",
|
||||
"next": "^12.1.6",
|
||||
"query-string": "^7.1.1",
|
||||
"react": "^17.0.2",
|
||||
"react": "^18.1.0",
|
||||
"react-chartjs-2": "^4.1.0",
|
||||
"react-clipboard.js": "^2.0.16",
|
||||
"react-data-table-component": "^6.11.7",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-dotdotdot": "^1.3.1",
|
||||
"react-modal": "^3.14.4",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-paginate": "^8.1.3",
|
||||
"react-spring": "^9.4.4",
|
||||
"react-spring": "^9.4.5",
|
||||
"react-tabs": "^3.2.3",
|
||||
"react-toastify": "^8.2.0",
|
||||
"remark": "^13.0.0",
|
||||
@ -69,7 +73,16 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^6.4.22",
|
||||
"@storybook/addon-storyshots": "^6.4.22",
|
||||
"@storybook/builder-webpack5": "^6.4.22",
|
||||
"@storybook/manager-webpack5": "^6.4.22",
|
||||
"@storybook/react": "^6.4.22",
|
||||
"@storybook/testing-library": "^0.0.11",
|
||||
"@storybook/testing-react": "^1.2.4",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.2.0",
|
||||
"@types/chart.js": "^2.9.37",
|
||||
"@types/d3": "^7.1.0",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
@ -77,31 +90,37 @@
|
||||
"@types/lodash.debounce": "^4.0.3",
|
||||
"@types/lodash.omit": "^4.5.6",
|
||||
"@types/node": "^17.0.13",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react": "^18.0.9",
|
||||
"@types/react-dom": "^18.0.3",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-paginate": "^7.1.1",
|
||||
"@types/react-tabs": "^2.3.4",
|
||||
"@types/remove-markdown": "^0.3.1",
|
||||
"@types/yup": "^0.29.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.15.0",
|
||||
"@typescript-eslint/parser": "^5.15.0",
|
||||
"@types/yup": "^0.29.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
||||
"@typescript-eslint/parser": "^5.23.0",
|
||||
"apollo": "^2.33.9",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-oceanprotocol": "^1.5.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint": "^8.15.0",
|
||||
"eslint-config-oceanprotocol": "^2.0.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest-dom": "^4.0.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"eslint-plugin-testing-library": "^5.4.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"prettier": "^2.6.0",
|
||||
"husky": "^8.0.1",
|
||||
"jest": "^28.1.0",
|
||||
"jest-environment-jsdom": "^28.1.0",
|
||||
"prettier": "^2.6.2",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"process": "^0.11.10",
|
||||
"serve": "^13.0.2",
|
||||
"stream-http": "^3.2.0",
|
||||
"typescript": "^4.6.3"
|
||||
"ts-jest": "^28.0.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -41,6 +41,7 @@ function ConsentProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
cookies.optionalCookies?.map((cookie) => {
|
||||
deleteCookie(cookie.cookieName)
|
||||
resetCookieConsent[cookie.cookieName] = status
|
||||
return status
|
||||
})
|
||||
setConsentStatus(resetCookieConsent)
|
||||
}
|
||||
@ -97,12 +98,15 @@ function ConsentProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
initialValues[cookie.cookieName] = CookieConsentStatus.NOT_AVAILABLE
|
||||
break
|
||||
}
|
||||
|
||||
return initialValues
|
||||
})
|
||||
|
||||
setConsentStatus(initialValues)
|
||||
}, [cookies.optionalCookies, appConfig])
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line array-callback-return
|
||||
Object.keys(consentStatus).map((cookieName) => {
|
||||
switch (consentStatus[cookieName]) {
|
||||
case CookieConsentStatus.APPROVED:
|
||||
|
@ -16,12 +16,14 @@ export const poolDataQuery = gql`
|
||||
baseToken {
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
baseTokenWeight
|
||||
baseTokenLiquidity
|
||||
datatoken {
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
datatokenWeight
|
||||
datatokenLiquidity
|
||||
@ -43,10 +45,12 @@ export const poolDataQuery = gql`
|
||||
baseToken {
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
datatoken {
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,10 @@ export interface PoolInfo {
|
||||
weightDt: string
|
||||
datatokenSymbol: string
|
||||
datatokenAddress: string
|
||||
datatokenDecimals: number
|
||||
baseTokenSymbol: string
|
||||
baseTokenAddress: string
|
||||
baseTokenDecimals: number
|
||||
totalPoolTokens: string
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,6 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
)
|
||||
const [poolSnapshots, setPoolSnapshots] = useState<PoolDataPoolSnapshots[]>()
|
||||
const [hasUserAddedLiquidity, setUserHasAddedLiquidity] = useState(false)
|
||||
// const [fetchInterval, setFetchInterval] = useState<NodeJS.Timeout>()
|
||||
|
||||
const fetchAllData = useCallback(async () => {
|
||||
if (!asset?.chainId || !asset?.accessDetails?.addressOrId || !owner) return
|
||||
@ -56,14 +55,36 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
owner,
|
||||
accountId || ''
|
||||
)
|
||||
|
||||
if (!response) return
|
||||
|
||||
setPoolData(response.poolData)
|
||||
setPoolInfoUser((prevState) => ({
|
||||
|
||||
// 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 %.
|
||||
const poolSharePercentage = new Decimal(poolInfoShares)
|
||||
.dividedBy(new Decimal(response.poolData.totalShares))
|
||||
.mul(100)
|
||||
.toFixed(2)
|
||||
|
||||
setUserHasAddedLiquidity(Number(poolSharePercentage) > 0)
|
||||
|
||||
const newPoolInfoUser: PoolInfoUser = {
|
||||
liquidity: userLiquidity,
|
||||
poolShares: poolInfoShares,
|
||||
poolSharePercentage
|
||||
}
|
||||
setPoolInfoUser((prevState: PoolInfoUser) => ({
|
||||
...prevState,
|
||||
poolShares: response.poolDataUser?.shares[0]?.shares || '0'
|
||||
...newPoolInfoUser
|
||||
}))
|
||||
|
||||
setPoolSnapshots(response.poolSnapshots)
|
||||
LoggerInstance.log('[pool] Fetched pool data:', response.poolData)
|
||||
LoggerInstance.log('[pool] Fetched user data:', response.poolDataUser)
|
||||
@ -99,8 +120,10 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
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
|
||||
}
|
||||
|
||||
@ -145,54 +168,6 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
poolInfo?.totalPoolTokens
|
||||
])
|
||||
|
||||
//
|
||||
// 3 User Pool Info
|
||||
//
|
||||
useEffect(() => {
|
||||
if (
|
||||
!poolData ||
|
||||
!poolInfo?.totalPoolTokens ||
|
||||
!poolInfoUser?.poolShares ||
|
||||
!poolData?.baseTokenLiquidity ||
|
||||
!asset?.chainId
|
||||
)
|
||||
return
|
||||
|
||||
const userLiquidity = calcSingleOutGivenPoolIn(
|
||||
poolData.baseTokenLiquidity,
|
||||
poolData.totalShares,
|
||||
poolInfoUser.poolShares
|
||||
)
|
||||
|
||||
// Pool share in %.
|
||||
const poolSharePercentage = new Decimal(poolInfoUser.poolShares)
|
||||
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
|
||||
.mul(100)
|
||||
.toFixed(2)
|
||||
|
||||
setUserHasAddedLiquidity(Number(poolSharePercentage) > 0)
|
||||
|
||||
const newPoolInfoUser: PoolInfoUser = {
|
||||
liquidity: userLiquidity,
|
||||
poolShares: poolInfoUser.poolShares,
|
||||
poolSharePercentage
|
||||
}
|
||||
setPoolInfoUser((prevState: PoolInfoUser) => ({
|
||||
...prevState,
|
||||
...newPoolInfoUser
|
||||
}))
|
||||
|
||||
LoggerInstance.log('[pool] Created new user pool info:', {
|
||||
...newPoolInfoUser
|
||||
})
|
||||
}, [
|
||||
poolData,
|
||||
poolInfoUser?.poolShares,
|
||||
asset?.chainId,
|
||||
owner,
|
||||
poolInfo?.totalPoolTokens
|
||||
])
|
||||
|
||||
return (
|
||||
<PoolContext.Provider
|
||||
value={
|
||||
|
@ -129,7 +129,7 @@ function ProfileProvider({
|
||||
const [poolSharesInterval, setPoolSharesInterval] = useState<NodeJS.Timeout>()
|
||||
|
||||
const fetchPoolShares = useCallback(
|
||||
async (accountId, chainIds, isEthAddress) => {
|
||||
async (accountId: string, chainIds: number[], isEthAddress: boolean) => {
|
||||
if (!accountId || !chainIds || !isEthAddress) return
|
||||
|
||||
try {
|
||||
|
@ -8,7 +8,12 @@ import {
|
||||
TokensPriceQuery,
|
||||
TokensPriceQuery_tokens as TokensPrice
|
||||
} from '../@types/subgraph/TokensPriceQuery'
|
||||
import { Asset, LoggerInstance, ProviderInstance } from '@oceanprotocol/lib'
|
||||
import {
|
||||
Asset,
|
||||
LoggerInstance,
|
||||
ProviderFees,
|
||||
ProviderInstance
|
||||
} from '@oceanprotocol/lib'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import { calcInGivenOut } from './pool'
|
||||
import { getFixedBuyPrice } from './fixedRateExchange'
|
||||
@ -238,8 +243,7 @@ export async function getOrderPriceAndFees(
|
||||
asset: AssetExtended,
|
||||
accountId?: string,
|
||||
paramsForPool?: CalcInGivenOutParams,
|
||||
computeEnv: string = null,
|
||||
computeValidUntil: number = null
|
||||
providerFees?: ProviderFees
|
||||
): Promise<OrderPriceAndFees> {
|
||||
const orderPriceAndFee = {
|
||||
price: '0',
|
||||
@ -258,18 +262,16 @@ export async function getOrderPriceAndFees(
|
||||
|
||||
// fetch provider fee
|
||||
|
||||
const initializeData = await ProviderInstance.initialize(
|
||||
asset?.id,
|
||||
asset.services[0].id,
|
||||
0,
|
||||
accountId,
|
||||
asset.services[0].serviceEndpoint,
|
||||
null,
|
||||
null,
|
||||
computeEnv,
|
||||
computeValidUntil
|
||||
)
|
||||
orderPriceAndFee.providerFee = initializeData.providerFee
|
||||
const initializeData =
|
||||
!providerFees &&
|
||||
(await ProviderInstance.initialize(
|
||||
asset?.id,
|
||||
asset.services[0].id,
|
||||
0,
|
||||
accountId,
|
||||
asset.services[0].serviceEndpoint
|
||||
))
|
||||
orderPriceAndFee.providerFee = providerFees || initializeData.providerFee
|
||||
|
||||
// fetch price and swap fees
|
||||
switch (asset?.accessDetails?.type) {
|
||||
|
@ -163,7 +163,7 @@ export async function getAssetsFromDidList(
|
||||
if (!(didList.length > 0)) return
|
||||
|
||||
const baseParams = {
|
||||
chainIds: chainIds,
|
||||
chainIds,
|
||||
filters: [getFilterTerm('_id', didList)],
|
||||
ignorePurgatory: true
|
||||
} as BaseQueryParams
|
||||
@ -185,7 +185,7 @@ export async function getAssetsFromDtList(
|
||||
if (!(dtList.length > 0)) return
|
||||
|
||||
const baseParams = {
|
||||
chainIds: chainIds,
|
||||
chainIds,
|
||||
filters: [getFilterTerm('services.datatokenAddress', dtList)],
|
||||
ignorePurgatory: true
|
||||
} as BaseQueryParams
|
||||
|
@ -281,11 +281,11 @@ export async function getComputeJobs(
|
||||
)
|
||||
|
||||
let tokenOrders: TokenOrder[] = []
|
||||
results.map((result) => {
|
||||
results.map((result) =>
|
||||
result.orders.forEach((tokenOrder: TokenOrder) =>
|
||||
tokenOrders.push(tokenOrder)
|
||||
)
|
||||
})
|
||||
)
|
||||
if (tokenOrders.length === 0) {
|
||||
computeResult.isLoaded = true
|
||||
return computeResult
|
||||
|
@ -18,9 +18,8 @@ export function getComputeFeedback(
|
||||
): { [key in number]: string } {
|
||||
return {
|
||||
0: `Setting price and fees for ${assetType}`,
|
||||
1: `Approving and buying one ${datatokenSymbol} from pool`,
|
||||
2: `Ordering ${assetType} and transfering datatoken ...`,
|
||||
3: `Approving ${baseTokenSymbol} and ordering ${assetType}`,
|
||||
4: 'Generating signature. Starting compute job ...'
|
||||
1: `Approving ${datatokenSymbol} and ordering ${assetType} `,
|
||||
2: `Approving ${baseTokenSymbol} and ordering ${assetType}`,
|
||||
3: 'Generating signature. Starting compute job ...'
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,10 @@ import {
|
||||
approve,
|
||||
Datatoken,
|
||||
FreOrderParams,
|
||||
LoggerInstance,
|
||||
OrderParams,
|
||||
ProviderComputeInitialize,
|
||||
ProviderFees,
|
||||
ProviderInstance
|
||||
} from '@oceanprotocol/lib'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
@ -15,6 +18,8 @@ import {
|
||||
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
|
||||
@ -31,29 +36,26 @@ export async function order(
|
||||
asset: AssetExtended,
|
||||
orderPriceAndFees: OrderPriceAndFees,
|
||||
accountId: string,
|
||||
computeEnv: string = null,
|
||||
computeValidUntil: number = null,
|
||||
providerFees?: ProviderFees,
|
||||
computeConsumerAddress?: string
|
||||
): Promise<TransactionReceipt> {
|
||||
const datatoken = new Datatoken(web3)
|
||||
const config = getOceanConfig(asset.chainId)
|
||||
|
||||
const initializeData = await ProviderInstance.initialize(
|
||||
asset.id,
|
||||
asset.services[0].id,
|
||||
0,
|
||||
accountId,
|
||||
asset.services[0].serviceEndpoint,
|
||||
null,
|
||||
null,
|
||||
computeEnv,
|
||||
computeValidUntil
|
||||
)
|
||||
const initializeData =
|
||||
!providerFees &&
|
||||
(await ProviderInstance.initialize(
|
||||
asset.id,
|
||||
asset.services[0].id,
|
||||
0,
|
||||
accountId,
|
||||
asset.services[0].serviceEndpoint
|
||||
))
|
||||
|
||||
const orderParams = {
|
||||
consumer: computeConsumerAddress || accountId,
|
||||
serviceIndex: 0,
|
||||
_providerFee: initializeData.providerFee,
|
||||
_providerFee: providerFees || initializeData.providerFee,
|
||||
_consumeMarketFee: {
|
||||
consumeMarketFeeAddress: marketFeeAddress,
|
||||
consumeMarketFeeAmount: consumeMarketOrderFee,
|
||||
@ -99,7 +101,7 @@ export async function order(
|
||||
accountId,
|
||||
computeConsumerAddress || accountId,
|
||||
0,
|
||||
initializeData.providerFee
|
||||
providerFees || initializeData.providerFee
|
||||
)
|
||||
return tx
|
||||
}
|
||||
@ -121,10 +123,8 @@ export async function order(
|
||||
* @param web3
|
||||
* @param asset
|
||||
* @param accountId
|
||||
* @param accountId validOrderTx
|
||||
* @param computeEnv
|
||||
* @param computeValidUntil
|
||||
* @param computeConsumerAddress
|
||||
* @param validOrderTx
|
||||
* @param providerFees
|
||||
* @returns {TransactionReceipt} receipt of the order
|
||||
*/
|
||||
export async function reuseOrder(
|
||||
@ -132,40 +132,107 @@ export async function reuseOrder(
|
||||
asset: AssetExtended,
|
||||
accountId: string,
|
||||
validOrderTx: string,
|
||||
computeEnv: string = null,
|
||||
computeValidUntil: number = null,
|
||||
computeConsumerAddress?: string
|
||||
) {
|
||||
providerFees?: ProviderFees
|
||||
): Promise<TransactionReceipt> {
|
||||
const datatoken = new Datatoken(web3)
|
||||
const initializeData = await ProviderInstance.initialize(
|
||||
asset.id,
|
||||
asset.services[0].id,
|
||||
0,
|
||||
accountId,
|
||||
asset.services[0].serviceEndpoint,
|
||||
null,
|
||||
null,
|
||||
computeEnv,
|
||||
computeValidUntil
|
||||
)
|
||||
const txApprove = await approve(
|
||||
web3,
|
||||
accountId,
|
||||
initializeData.providerFee.providerFeeToken,
|
||||
asset.accessDetails.datatoken.address,
|
||||
initializeData.providerFee.providerFeeAmount,
|
||||
false
|
||||
)
|
||||
if (!txApprove) {
|
||||
return
|
||||
const initializeData =
|
||||
!providerFees &&
|
||||
(await ProviderInstance.initialize(
|
||||
asset.id,
|
||||
asset.services[0].id,
|
||||
0,
|
||||
accountId,
|
||||
asset.services[0].serviceEndpoint
|
||||
))
|
||||
|
||||
if (
|
||||
providerFees?.providerFeeAmount ||
|
||||
initializeData?.providerFee?.providerFeeAmount
|
||||
) {
|
||||
const txApprove = await approve(
|
||||
web3,
|
||||
accountId,
|
||||
providerFees.providerFeeToken ||
|
||||
initializeData.providerFee.providerFeeAmount,
|
||||
asset.accessDetails.datatoken.address,
|
||||
providerFees.providerFeeAmount ||
|
||||
initializeData.providerFee.providerFeeToken,
|
||||
false
|
||||
)
|
||||
if (!txApprove) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const tx = await datatoken.reuseOrder(
|
||||
asset.accessDetails.datatoken.address,
|
||||
accountId,
|
||||
validOrderTx,
|
||||
initializeData.providerFee
|
||||
providerFees || initializeData.providerFee
|
||||
)
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles order for compute assets for the following scenarios:
|
||||
* - have validOrder and no providerFees -> then order is valid, providerFees are valid, it returns the valid order value
|
||||
* - have validOrder and providerFees -> then order is valid but providerFees are not valid, we need to call reuseOrder and pay only providerFees
|
||||
* - no validOrder -> we need to call order, to pay 1 DT & providerFees
|
||||
* @param web3
|
||||
* @param asset
|
||||
* @param accountId
|
||||
* @param computeEnv
|
||||
* @param computeValidUntil
|
||||
* @param computeConsumerAddress
|
||||
* @returns {Promise<string>} tx id
|
||||
*/
|
||||
export async function handleComputeOrder(
|
||||
web3: Web3,
|
||||
asset: AssetExtended,
|
||||
orderPriceAndFees: OrderPriceAndFees,
|
||||
accountId: string,
|
||||
hasDatatoken: boolean,
|
||||
initializeData: ProviderComputeInitialize,
|
||||
computeConsumerAddress?: string
|
||||
): Promise<string> {
|
||||
LoggerInstance.log(
|
||||
'[compute] Handle compute order for asset type: ',
|
||||
asset.metadata.type
|
||||
)
|
||||
if (initializeData.validOrder && !initializeData.providerFee) {
|
||||
LoggerInstance.log('[compute] Has valid order: ', initializeData.validOrder)
|
||||
return initializeData.validOrder
|
||||
} else if (initializeData.validOrder) {
|
||||
LoggerInstance.log('[compute] Calling reuseOrder ...', initializeData)
|
||||
const tx = await reuseOrder(
|
||||
web3,
|
||||
asset,
|
||||
accountId,
|
||||
initializeData.validOrder,
|
||||
initializeData.providerFee
|
||||
)
|
||||
LoggerInstance.log('[compute] Reused order:', tx.transactionHash)
|
||||
return tx.transactionHash
|
||||
} else {
|
||||
LoggerInstance.log('[compute] Calling order ...', initializeData)
|
||||
if (!hasDatatoken && asset?.accessDetails.type === 'dynamic') {
|
||||
const poolTx = await buyDtFromPool(asset?.accessDetails, accountId, web3)
|
||||
LoggerInstance.log('[compute] Buoght dt from pool: ', poolTx)
|
||||
if (!poolTx) {
|
||||
toast.error('Failed to buy datatoken from pool!')
|
||||
return
|
||||
}
|
||||
}
|
||||
const tx = await order(
|
||||
web3,
|
||||
asset,
|
||||
orderPriceAndFees,
|
||||
accountId,
|
||||
initializeData.providerFee,
|
||||
computeConsumerAddress
|
||||
)
|
||||
LoggerInstance.log('[compute] Asset ordered:', tx.transactionHash)
|
||||
return tx.transactionHash
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,9 @@ export async function calculateBuyPrice(
|
||||
accessDetails.baseToken.address,
|
||||
accessDetails.datatoken.address,
|
||||
'1',
|
||||
consumeMarketPoolSwapFee
|
||||
consumeMarketPoolSwapFee,
|
||||
accessDetails.baseToken.decimals,
|
||||
accessDetails.datatoken.decimals
|
||||
)
|
||||
|
||||
return estimatedPrice
|
||||
@ -52,7 +54,8 @@ export async function buyDtFromPool(
|
||||
accessDetails.baseToken.address,
|
||||
accessDetails.addressOrId,
|
||||
dtPrice.tokenAmount,
|
||||
false
|
||||
false,
|
||||
accessDetails.baseToken.decimals
|
||||
)
|
||||
if (!approveTx) {
|
||||
return
|
||||
@ -63,7 +66,9 @@ export async function buyDtFromPool(
|
||||
{
|
||||
marketFeeAddress,
|
||||
tokenIn: accessDetails.baseToken.address,
|
||||
tokenOut: accessDetails.datatoken.address
|
||||
tokenOut: accessDetails.datatoken.address,
|
||||
tokenInDecimals: accessDetails.baseToken.decimals,
|
||||
tokenOutDecimals: accessDetails.datatoken.decimals
|
||||
},
|
||||
{
|
||||
// this is just to be safe
|
||||
@ -163,6 +168,7 @@ export function calcSingleOutGivenPoolIn(
|
||||
export async function getLiquidityByShares(
|
||||
pool: string,
|
||||
tokenAddress: string,
|
||||
tokenDecimals: number,
|
||||
shares: string,
|
||||
chainId: number
|
||||
): Promise<string> {
|
||||
@ -174,7 +180,9 @@ export async function getLiquidityByShares(
|
||||
const amountBaseToken = await poolInstance.calcSingleOutGivenPoolIn(
|
||||
pool,
|
||||
tokenAddress,
|
||||
shares
|
||||
shares,
|
||||
18,
|
||||
tokenDecimals
|
||||
)
|
||||
|
||||
return amountBaseToken
|
||||
|
@ -1,11 +1,49 @@
|
||||
import {
|
||||
ComputeAlgorithm,
|
||||
ComputeAsset,
|
||||
ComputeEnvironment,
|
||||
downloadFileBrowser,
|
||||
FileMetadata,
|
||||
LoggerInstance,
|
||||
ProviderComputeInitializeResults,
|
||||
ProviderInstance
|
||||
} from '@oceanprotocol/lib'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import Web3 from 'web3'
|
||||
import { getValidUntilTime } from './compute'
|
||||
|
||||
export async function initializeProviderForCompute(
|
||||
dataset: AssetExtended,
|
||||
algorithm: AssetExtended,
|
||||
accountId: string,
|
||||
computeEnv: ComputeEnvironment = null
|
||||
): Promise<ProviderComputeInitializeResults> {
|
||||
const computeAsset: ComputeAsset = {
|
||||
documentId: dataset.id,
|
||||
serviceId: dataset.services[0].id,
|
||||
transferTxId: dataset.accessDetails.validOrderTx
|
||||
}
|
||||
const computeAlgo: ComputeAlgorithm = {
|
||||
documentId: algorithm.id,
|
||||
serviceId: algorithm.services[0].id,
|
||||
transferTxId: algorithm.accessDetails.validOrderTx
|
||||
}
|
||||
|
||||
const validUntil = getValidUntilTime(
|
||||
computeEnv?.maxJobDuration,
|
||||
dataset.services[0].timeout,
|
||||
algorithm.services[0].timeout
|
||||
)
|
||||
|
||||
return await ProviderInstance.initializeCompute(
|
||||
[computeAsset],
|
||||
computeAlgo,
|
||||
computeEnv?.id,
|
||||
validUntil,
|
||||
dataset.services[0].serviceEndpoint,
|
||||
accountId
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Why do we have these one line functions ?!?!?!
|
||||
export async function getEncryptedFiles(
|
||||
|
@ -279,10 +279,7 @@ export async function getPreviousOrders(
|
||||
account: string,
|
||||
assetTimeout: string
|
||||
): Promise<string> {
|
||||
const variables = {
|
||||
id: id,
|
||||
account: account
|
||||
}
|
||||
const variables = { id, account }
|
||||
const fetchedPreviousOrders: OperationResult<AssetPreviousOrder> =
|
||||
await fetchData(PreviousOrderQuery, variables, null)
|
||||
if (fetchedPreviousOrders.data?.orders?.length === 0) return null
|
||||
@ -347,7 +344,7 @@ export async function getAccountLiquidityInOwnAssets(
|
||||
): Promise<string> {
|
||||
const queryVariables = {
|
||||
user: accountId.toLowerCase(),
|
||||
pools: pools
|
||||
pools
|
||||
}
|
||||
const results: PoolSharesList[] = await fetchDataForMultipleChains(
|
||||
UserSharesQuery,
|
||||
|
10
src/@utils/url.ts
Normal file
10
src/@utils/url.ts
Normal file
@ -0,0 +1,10 @@
|
||||
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
|
||||
}
|
@ -26,7 +26,7 @@ export default function AddToken({
|
||||
|
||||
const styleClasses = cx({
|
||||
button: true,
|
||||
minimal: minimal,
|
||||
minimal,
|
||||
[className]: className
|
||||
})
|
||||
|
||||
|
@ -29,7 +29,7 @@ export default function FileIcon({
|
||||
}): ReactElement {
|
||||
const styleClasses = cx({
|
||||
file: true,
|
||||
small: small,
|
||||
small,
|
||||
[className]: className
|
||||
})
|
||||
|
||||
|
@ -16,7 +16,7 @@ export default function PageHeader({
|
||||
}): ReactElement {
|
||||
const styleClasses = cx({
|
||||
header: true,
|
||||
center: center
|
||||
center
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,5 @@
|
||||
.alert {
|
||||
composes: box from './Box.module.css';
|
||||
composes: box from '../Box.module.css';
|
||||
max-width: 40rem;
|
||||
margin: auto;
|
||||
border-width: 0;
|
37
src/components/@shared/atoms/Alert/index.stories.tsx
Normal file
37
src/components/@shared/atoms/Alert/index.stories.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
|
||||
import Alert, { AlertProps } from '@shared/atoms/Alert'
|
||||
|
||||
export default {
|
||||
title: 'Component/@shared/atoms/Alert',
|
||||
component: Alert
|
||||
} as ComponentMeta<typeof Alert>
|
||||
|
||||
const Template: ComponentStory<typeof Alert> = (args) => <Alert {...args} />
|
||||
|
||||
interface Props {
|
||||
args: AlertProps
|
||||
}
|
||||
|
||||
export const Default: Props = Template.bind({})
|
||||
Default.args = {
|
||||
text: 'Alert text',
|
||||
state: 'info',
|
||||
onDismiss: () => console.log('Alert closed!')
|
||||
}
|
||||
|
||||
export const Full: Props = Template.bind({})
|
||||
Full.args = {
|
||||
title: 'Alert',
|
||||
text: 'Alert text',
|
||||
state: 'info',
|
||||
action: {
|
||||
name: 'Action',
|
||||
handleAction: () => null as any
|
||||
},
|
||||
badge: 'Hello',
|
||||
onDismiss: () => {
|
||||
console.log('Alert closed!')
|
||||
}
|
||||
}
|
@ -1,21 +1,13 @@
|
||||
import React, { ReactElement, FormEvent } from 'react'
|
||||
import classNames from 'classnames/bind'
|
||||
import styles from './Alert.module.css'
|
||||
import Button from './Button'
|
||||
import Markdown from '../Markdown'
|
||||
import Badge from './Badge'
|
||||
import styles from './index.module.css'
|
||||
import Button from '../Button'
|
||||
import Markdown from '../../Markdown'
|
||||
import Badge from '../Badge'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
export default function Alert({
|
||||
title,
|
||||
badge,
|
||||
text,
|
||||
state,
|
||||
action,
|
||||
onDismiss,
|
||||
className
|
||||
}: {
|
||||
export interface AlertProps {
|
||||
title?: string
|
||||
badge?: string
|
||||
text: string
|
||||
@ -28,7 +20,17 @@ export default function Alert({
|
||||
}
|
||||
onDismiss?: () => void
|
||||
className?: string
|
||||
}): ReactElement {
|
||||
}
|
||||
|
||||
export default function Alert({
|
||||
title,
|
||||
badge,
|
||||
text,
|
||||
state,
|
||||
action,
|
||||
onDismiss,
|
||||
className
|
||||
}: AlertProps): ReactElement {
|
||||
const styleClasses = cx({
|
||||
alert: true,
|
||||
[state]: state,
|
47
src/components/@shared/atoms/Button/index.stories.tsx
Normal file
47
src/components/@shared/atoms/Button/index.stories.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
import Button, { ButtonProps } from '@shared/atoms/Button'
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: 'Component/@shared/atoms/Button',
|
||||
component: Button
|
||||
} as ComponentMeta<typeof Button>
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template: ComponentStory<typeof Button> = (args: ButtonProps) => (
|
||||
<Button {...args} />
|
||||
)
|
||||
|
||||
interface Props {
|
||||
args: ButtonProps
|
||||
}
|
||||
|
||||
export const Default: Props = Template.bind({})
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Default.args = {
|
||||
children: 'Button',
|
||||
onClick: () => {
|
||||
console.log('Button pressed!')
|
||||
}
|
||||
}
|
||||
|
||||
export const Primary: Props = Template.bind({})
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Primary.args = {
|
||||
children: 'Button',
|
||||
style: 'primary',
|
||||
onClick: () => {
|
||||
console.log('Button pressed!')
|
||||
}
|
||||
}
|
||||
|
||||
export const Small: Props = Template.bind({})
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Small.args = {
|
||||
children: 'Button',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
console.log('Button pressed!')
|
||||
}
|
||||
}
|
25
src/components/@shared/atoms/Button/index.test.tsx
Normal file
25
src/components/@shared/atoms/Button/index.test.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Button from './'
|
||||
|
||||
test('returns correct markup when href or to is passed', async () => {
|
||||
const { rerender } = render(
|
||||
<Button href="https://oceanprotocol.com">Hello Button</Button>
|
||||
)
|
||||
|
||||
let button = screen.getByText('Hello Button')
|
||||
expect(button).toHaveAttribute('href', 'https://oceanprotocol.com')
|
||||
expect(button).toContainHTML('<a')
|
||||
|
||||
rerender(<Button to="/publish">Hello Button</Button>)
|
||||
button = screen.getByText('Hello Button')
|
||||
expect(button).toHaveAttribute('href', '/publish')
|
||||
expect(button).toContainHTML('<a')
|
||||
})
|
||||
|
||||
test('returns correct markup when no href or to is passed', async () => {
|
||||
render(<Button>Hello Button</Button>)
|
||||
|
||||
const button = screen.getByText('Hello Button')
|
||||
expect(button).toContainHTML('<button')
|
||||
})
|
@ -1,7 +1,7 @@
|
||||
import React, { ReactNode, FormEvent, ReactElement } from 'react'
|
||||
import Link from 'next/link'
|
||||
import classNames from 'classnames/bind'
|
||||
import styles from './Button.module.css'
|
||||
import styles from './index.module.css'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
@ -15,7 +15,7 @@ export default function Container({
|
||||
}): ReactElement {
|
||||
const styleClasses = cx({
|
||||
container: true,
|
||||
narrow: narrow,
|
||||
narrow,
|
||||
[className]: className
|
||||
})
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import loadable from '@loadable/component'
|
||||
import styles from './Copy.module.css'
|
||||
import IconCopy from '@images/copy.svg'
|
||||
|
||||
// lazy load when needed only, as library is a bit big
|
||||
const Clipboard = loadable(() => import('react-clipboard.js'))
|
||||
import Clipboard from 'react-clipboard.js'
|
||||
|
||||
export default function Copy({ text }: { text: string }): ReactElement {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import DataTable, { IDataTableProps } from 'react-data-table-component'
|
||||
import Loader from './Loader'
|
||||
import Pagination from '@shared/Pagination'
|
||||
@ -47,7 +47,7 @@ export default function Table({
|
||||
noDataComponent={<Empty message={emptyMessage} />}
|
||||
progressPending={isLoading}
|
||||
progressComponent={<Loader />}
|
||||
paginationComponent={Pagination}
|
||||
paginationComponent={Pagination as unknown as ReactNode}
|
||||
defaultSortField={sortField}
|
||||
defaultSortAsc={sortAsc}
|
||||
{...props}
|
||||
|
@ -10,7 +10,8 @@ import {
|
||||
ComputeEnvironment,
|
||||
LoggerInstance,
|
||||
ComputeAlgorithm,
|
||||
ComputeOutput
|
||||
ComputeOutput,
|
||||
ProviderComputeInitializeResults
|
||||
} from '@oceanprotocol/lib'
|
||||
import { toast } from 'react-toastify'
|
||||
import Price from '@shared/Price'
|
||||
@ -27,28 +28,26 @@ import {
|
||||
isOrderable,
|
||||
getAlgorithmAssetSelectionList,
|
||||
getAlgorithmsForAsset,
|
||||
getValidUntilTime,
|
||||
getComputeEnviroment,
|
||||
checkComputeResourcesValidity
|
||||
getComputeEnviroment
|
||||
} from '@utils/compute'
|
||||
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
|
||||
import AlgorithmDatasetsListForCompute from './AlgorithmDatasetsListForCompute'
|
||||
import AssetActionHistoryTable from '../AssetActionHistoryTable'
|
||||
import ComputeJobs from '../../../Profile/History/ComputeJobs'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
// import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { Decimal } from 'decimal.js'
|
||||
import { useAbortController } from '@hooks/useAbortController'
|
||||
import { getOrderPriceAndFees } from '@utils/accessDetailsAndPricing'
|
||||
import { OrderPriceAndFees } from 'src/@types/Price'
|
||||
import { buyDtFromPool } from '@utils/pool'
|
||||
import { order, reuseOrder } from '@utils/order'
|
||||
import { handleComputeOrder } from '@utils/order'
|
||||
import { AssetExtended } from 'src/@types/AssetExtended'
|
||||
import { getComputeFeedback } from '@utils/feedback'
|
||||
import { usePool } from '@context/Pool'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
import { getPoolData } from '@context/Pool/_utils'
|
||||
import { getDummyWeb3 } from '@utils/web3'
|
||||
import { initializeProviderForCompute } from '@utils/provider'
|
||||
|
||||
export default function Compute({
|
||||
asset,
|
||||
@ -78,13 +77,13 @@ export default function Compute({
|
||||
const [isPublished, setIsPublished] = useState(false)
|
||||
const [algorithmDTBalance, setAlgorithmDTBalance] = useState<string>()
|
||||
|
||||
const [isOwned, setIsOwned] = useState(false)
|
||||
// const [isOwned, setIsOwned] = useState(false)
|
||||
const [validOrderTx, setValidOrderTx] = useState('')
|
||||
const [isAlgorithmOwned, setIsAlgorithmOwned] = useState(false)
|
||||
// const [isAlgorithmOwned, setIsAlgorithmOwned] = useState(false)
|
||||
const [validAlgorithmOrderTx, setValidAlgorithmOrderTx] = useState('')
|
||||
|
||||
const hasDatatoken = Number(dtBalance) >= 1
|
||||
const isMounted = useIsMounted()
|
||||
// const isMounted = useIsMounted()
|
||||
const { getOpcFeeForToken } = useMarketMetadata()
|
||||
const { poolData } = usePool()
|
||||
const newCancelToken = useCancelToken()
|
||||
@ -92,7 +91,9 @@ export default function Compute({
|
||||
const [isAlgoConsumablePrice, setIsAlgoConsumablePrice] = useState(true)
|
||||
const [computeStatusText, setComputeStatusText] = useState('')
|
||||
const [computeEnv, setComputeEnv] = useState<ComputeEnvironment>()
|
||||
const [computeValidUntil, setComputeValidUntil] = useState<number>()
|
||||
const [initializedProviderResponse, setInitializedProviderResponse] =
|
||||
useState<ProviderComputeInitializeResults>()
|
||||
// const [computeValidUntil, setComputeValidUntil] = useState<number>()
|
||||
const [datasetOrderPriceAndFees, setDatasetOrderPriceAndFees] =
|
||||
useState<OrderPriceAndFees>()
|
||||
const [isRequestingDataseOrderPrice, setIsRequestingDataseOrderPrice] =
|
||||
@ -101,7 +102,7 @@ export default function Compute({
|
||||
useState<OrderPriceAndFees>()
|
||||
const [isRequestingAlgoOrderPrice, setIsRequestingAlgoOrderPrice] =
|
||||
useState(false)
|
||||
const [isProviderFeeValid, setIsProviderFeeValid] = useState(false)
|
||||
// const [isProviderFeeValid, setIsProviderFeeValid] = useState(false)
|
||||
const isComputeButtonDisabled =
|
||||
isJobStarting === true ||
|
||||
file === null ||
|
||||
@ -125,24 +126,18 @@ export default function Compute({
|
||||
async function initPriceAndFees() {
|
||||
const computeEnv = await getComputeEnviroment(asset)
|
||||
setComputeEnv(computeEnv)
|
||||
setIsProviderFeeValid(
|
||||
await checkComputeResourcesValidity(
|
||||
asset,
|
||||
accountId,
|
||||
computeEnv?.maxJobDuration,
|
||||
asset.services[0].timeout,
|
||||
selectedAlgorithmAsset.services[0].timeout
|
||||
)
|
||||
const initializedProvider = await initializeProviderForCompute(
|
||||
asset,
|
||||
selectedAlgorithmAsset,
|
||||
accountId,
|
||||
computeEnv
|
||||
)
|
||||
const validUntil = getValidUntilTime(
|
||||
computeEnv?.maxJobDuration,
|
||||
asset.services[0].timeout,
|
||||
selectedAlgorithmAsset.services[0].timeout
|
||||
)
|
||||
setComputeValidUntil(validUntil)
|
||||
console.log('initializedProvider == ', initializedProvider)
|
||||
setInitializedProviderResponse(initializedProvider)
|
||||
if (
|
||||
asset?.accessDetails?.addressOrId !== ZERO_ADDRESS ||
|
||||
asset?.accessDetails?.type !== 'free'
|
||||
asset?.accessDetails?.addressOrId !== ZERO_ADDRESS &&
|
||||
asset?.accessDetails?.type !== 'free' &&
|
||||
initializedProvider.datasets[0].providerFee
|
||||
) {
|
||||
setIsRequestingDataseOrderPrice(true)
|
||||
setComputeStatusText(
|
||||
@ -172,10 +167,9 @@ export default function Compute({
|
||||
asset,
|
||||
ZERO_ADDRESS,
|
||||
poolParams,
|
||||
computeEnv?.id,
|
||||
validUntil
|
||||
initializedProvider.datasets[0].providerFee
|
||||
)
|
||||
console.log('datasetPriceAndFees price and fees', datasetPriceAndFees)
|
||||
console.log('datasetPriceAndFees', datasetPriceAndFees)
|
||||
if (!datasetPriceAndFees) {
|
||||
setError('Error setting dataset price and fees!')
|
||||
toast.error('Error setting dataset price and fees!')
|
||||
@ -186,8 +180,9 @@ export default function Compute({
|
||||
}
|
||||
|
||||
if (
|
||||
selectedAlgorithmAsset?.accessDetails?.addressOrId !== ZERO_ADDRESS ||
|
||||
selectedAlgorithmAsset?.accessDetails?.type !== 'free'
|
||||
selectedAlgorithmAsset?.accessDetails?.addressOrId !== ZERO_ADDRESS &&
|
||||
selectedAlgorithmAsset?.accessDetails?.type !== 'free' &&
|
||||
initializedProvider.algorithm.providerFee
|
||||
) {
|
||||
setIsRequestingAlgoOrderPrice(true)
|
||||
setComputeStatusText(
|
||||
@ -223,15 +218,14 @@ export default function Compute({
|
||||
selectedAlgorithmAsset,
|
||||
ZERO_ADDRESS,
|
||||
algoPoolParams,
|
||||
computeEnv?.id,
|
||||
validUntil
|
||||
initializedProvider.algorithm.providerFee
|
||||
)
|
||||
console.log('algorithmOrderPriceAndFees', algorithmOrderPriceAndFees)
|
||||
if (!algorithmOrderPriceAndFees) {
|
||||
setError('Error setting algorithm price and fees!')
|
||||
toast.error('Error setting algorithm price and fees!')
|
||||
return
|
||||
}
|
||||
console.log('algo price and fees', algorithmOrderPriceAndFees)
|
||||
setAlgoOrderPriceAndFees(algorithmOrderPriceAndFees)
|
||||
setIsRequestingAlgoOrderPrice(false)
|
||||
}
|
||||
@ -241,16 +235,15 @@ export default function Compute({
|
||||
if (!asset?.accessDetails || !accountId) return
|
||||
|
||||
setIsConsumablePrice(asset?.accessDetails?.isPurchasable)
|
||||
setIsOwned(asset?.accessDetails?.isOwned)
|
||||
// setIsOwned(asset?.accessDetails?.isOwned)
|
||||
setValidOrderTx(asset?.accessDetails?.validOrderTx)
|
||||
}, [asset?.accessDetails])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('selcted algo', selectedAlgorithmAsset)
|
||||
if (!selectedAlgorithmAsset?.accessDetails || !accountId) return
|
||||
|
||||
setIsConsumablePrice(selectedAlgorithmAsset?.accessDetails?.isPurchasable)
|
||||
setIsAlgorithmOwned(selectedAlgorithmAsset?.accessDetails?.isOwned)
|
||||
// setIsAlgorithmOwned(selectedAlgorithmAsset?.accessDetails?.isOwned)
|
||||
setValidAlgorithmOrderTx(
|
||||
selectedAlgorithmAsset?.accessDetails?.validOrderTx
|
||||
)
|
||||
@ -311,154 +304,39 @@ export default function Compute({
|
||||
return
|
||||
}
|
||||
|
||||
let datasetOrderTx
|
||||
if (isOwned) {
|
||||
datasetOrderTx = validOrderTx
|
||||
LoggerInstance.log('[compute] Dataset owned txId:', validOrderTx)
|
||||
} else {
|
||||
try {
|
||||
if (!hasDatatoken && asset?.accessDetails.type === 'dynamic') {
|
||||
setComputeStatusText(
|
||||
getComputeFeedback(
|
||||
asset.accessDetails.baseToken?.symbol,
|
||||
asset.accessDetails.datatoken?.symbol,
|
||||
asset.metadata.type
|
||||
)[1]
|
||||
)
|
||||
const tx = await buyDtFromPool(
|
||||
asset?.accessDetails,
|
||||
accountId,
|
||||
web3
|
||||
)
|
||||
LoggerInstance.log('[compute] Buy dataset dt from pool: ', tx)
|
||||
if (!tx) {
|
||||
toast.error('Failed to buy datatoken from pool!')
|
||||
return
|
||||
}
|
||||
}
|
||||
LoggerInstance.log(
|
||||
'dataset orderPriceAndFees: ',
|
||||
datasetOrderPriceAndFees
|
||||
)
|
||||
setComputeStatusText(
|
||||
getComputeFeedback(
|
||||
asset.accessDetails.baseToken?.symbol,
|
||||
asset.accessDetails.datatoken?.symbol,
|
||||
asset.metadata.type
|
||||
)[asset.accessDetails?.type === 'fixed' ? 3 : 2]
|
||||
)
|
||||
const orderTx = await order(
|
||||
web3,
|
||||
asset,
|
||||
datasetOrderPriceAndFees,
|
||||
accountId,
|
||||
computeEnv?.id,
|
||||
computeValidUntil,
|
||||
computeEnv?.consumerAddress
|
||||
)
|
||||
if (!orderTx) {
|
||||
toast.error('Failed to order dataset asset!')
|
||||
return
|
||||
}
|
||||
LoggerInstance.log(
|
||||
'[compute] Order dataset: ',
|
||||
orderTx.transactionHash
|
||||
)
|
||||
setIsOwned(true)
|
||||
setValidOrderTx(orderTx.transactionHash)
|
||||
datasetOrderTx = orderTx.transactionHash
|
||||
} catch (e) {
|
||||
LoggerInstance.log(e.message)
|
||||
toast.error('Failed to order dataset asset!')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let algorithmOrderTx
|
||||
if (isAlgorithmOwned) {
|
||||
algorithmOrderTx = validAlgorithmOrderTx
|
||||
LoggerInstance.log(
|
||||
'[compute] Algorithm owned txId:',
|
||||
validAlgorithmOrderTx
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
if (
|
||||
!hasAlgoAssetDatatoken &&
|
||||
selectedAlgorithmAsset?.accessDetails?.type === 'dynamic'
|
||||
) {
|
||||
setComputeStatusText(
|
||||
getComputeFeedback(
|
||||
selectedAlgorithmAsset.accessDetails.baseToken?.symbol,
|
||||
selectedAlgorithmAsset.accessDetails.datatoken?.symbol,
|
||||
selectedAlgorithmAsset.metadata.type
|
||||
)[1]
|
||||
)
|
||||
const tx = await buyDtFromPool(
|
||||
selectedAlgorithmAsset?.accessDetails,
|
||||
accountId,
|
||||
web3
|
||||
)
|
||||
LoggerInstance.log('[compute] Buy algorithm dt from pool: ', tx)
|
||||
if (!tx) {
|
||||
toast.error('Failed to buy datatoken from pool!')
|
||||
return
|
||||
}
|
||||
}
|
||||
setComputeStatusText(
|
||||
getComputeFeedback(
|
||||
selectedAlgorithmAsset.accessDetails.baseToken?.symbol,
|
||||
selectedAlgorithmAsset.accessDetails.datatoken?.symbol,
|
||||
selectedAlgorithmAsset.metadata.type
|
||||
)[selectedAlgorithmAsset.accessDetails?.type === 'fixed' ? 3 : 2]
|
||||
)
|
||||
const orderTx = await order(
|
||||
web3,
|
||||
selectedAlgorithmAsset,
|
||||
algoOrderPriceAndFees,
|
||||
accountId,
|
||||
computeEnv?.id,
|
||||
computeValidUntil,
|
||||
computeEnv?.consumerAddress
|
||||
)
|
||||
if (!orderTx) {
|
||||
toast.error('Failed to order algorithm asset!')
|
||||
return
|
||||
}
|
||||
LoggerInstance.log(
|
||||
'[compute] Order algorithm: ',
|
||||
orderTx.transactionHash
|
||||
)
|
||||
setIsAlgorithmOwned(true)
|
||||
setValidAlgorithmOrderTx(orderTx.transactionHash)
|
||||
algorithmOrderTx = orderTx.transactionHash
|
||||
} catch (e) {
|
||||
LoggerInstance.log(e.message)
|
||||
toast.error('Failed to order algorithm asset!')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isOwned && !isProviderFeeValid) {
|
||||
const reuseOrderTx = await reuseOrder(
|
||||
web3,
|
||||
asset,
|
||||
accountId,
|
||||
validOrderTx,
|
||||
computeEnv?.id,
|
||||
computeValidUntil
|
||||
)
|
||||
if (!reuseOrderTx) {
|
||||
toast.error('Failed to pay provider fees!')
|
||||
return
|
||||
}
|
||||
LoggerInstance.log(
|
||||
'[compute] Reused order: ',
|
||||
reuseOrderTx.transactionHash
|
||||
)
|
||||
datasetOrderTx = reuseOrderTx.transactionHash
|
||||
}
|
||||
setComputeStatusText(
|
||||
getComputeFeedback(
|
||||
asset.accessDetails.baseToken?.symbol,
|
||||
asset.accessDetails.datatoken?.symbol,
|
||||
asset.metadata.type
|
||||
)[asset.accessDetails?.type === 'fixed' ? 2 : 1]
|
||||
)
|
||||
const datasetOrderTx = await handleComputeOrder(
|
||||
web3,
|
||||
asset,
|
||||
datasetOrderPriceAndFees,
|
||||
accountId,
|
||||
hasDatatoken,
|
||||
initializedProviderResponse.datasets[0],
|
||||
computeEnv.consumerAddress
|
||||
)
|
||||
|
||||
setComputeStatusText(
|
||||
getComputeFeedback(
|
||||
selectedAlgorithmAsset.accessDetails.baseToken?.symbol,
|
||||
selectedAlgorithmAsset.accessDetails.datatoken?.symbol,
|
||||
selectedAlgorithmAsset.metadata.type
|
||||
)[selectedAlgorithmAsset.accessDetails?.type === 'fixed' ? 2 : 1]
|
||||
)
|
||||
const algorithmOrderTx = await handleComputeOrder(
|
||||
web3,
|
||||
selectedAlgorithmAsset,
|
||||
algoOrderPriceAndFees,
|
||||
accountId,
|
||||
hasAlgoAssetDatatoken,
|
||||
initializedProviderResponse.algorithm,
|
||||
computeEnv.consumerAddress
|
||||
)
|
||||
LoggerInstance.log('[compute] Starting compute job.')
|
||||
const computeAsset: ComputeAsset = {
|
||||
documentId: asset.id,
|
||||
@ -470,7 +348,7 @@ export default function Compute({
|
||||
publishAlgorithmLog: true,
|
||||
publishOutput: true
|
||||
}
|
||||
setComputeStatusText(getComputeFeedback()[4])
|
||||
setComputeStatusText(getComputeFeedback()[3])
|
||||
const response = await ProviderInstance.computeStart(
|
||||
asset.services[0].serviceEndpoint,
|
||||
web3,
|
||||
@ -488,6 +366,7 @@ export default function Compute({
|
||||
}
|
||||
LoggerInstance.log('[compute] Starting compute job response: ', response)
|
||||
setIsPublished(true)
|
||||
initPriceAndFees()
|
||||
} catch (error) {
|
||||
setError('Failed to start job!')
|
||||
LoggerInstance.error('[compute] Failed to start job: ', error.message)
|
||||
|
@ -49,7 +49,9 @@ export default function FormAdd({
|
||||
const poolTokens = await poolInstance.calcPoolOutGivenSingleIn(
|
||||
poolData.id,
|
||||
poolInfo.baseTokenAddress,
|
||||
values.amount.toString()
|
||||
values.amount.toString(),
|
||||
18,
|
||||
poolInfo.baseTokenDecimals
|
||||
)
|
||||
setNewPoolTokens(poolTokens)
|
||||
const newPoolShareDecimal =
|
||||
@ -68,6 +70,7 @@ export default function FormAdd({
|
||||
calculatePoolShares()
|
||||
}, [
|
||||
poolInfo?.baseTokenAddress,
|
||||
poolInfo?.baseTokenDecimals,
|
||||
web3,
|
||||
values.amount,
|
||||
poolInfo?.totalPoolTokens,
|
||||
|
@ -40,7 +40,6 @@ export default function Add({
|
||||
const [amountMax, setAmountMax] = useState<string>()
|
||||
const [newPoolTokens, setNewPoolTokens] = useState('0')
|
||||
const [newPoolShare, setNewPoolShare] = useState('0')
|
||||
const [isWarningAccepted, setIsWarningAccepted] = useState(false)
|
||||
|
||||
// Live validation rules
|
||||
// https://github.com/jquense/yup#number
|
||||
@ -77,7 +76,8 @@ export default function Add({
|
||||
|
||||
const poolReserve = await poolInstance.getReserve(
|
||||
poolData.id,
|
||||
poolInfo.baseTokenAddress
|
||||
poolInfo.baseTokenAddress,
|
||||
poolInfo.baseTokenDecimals
|
||||
)
|
||||
|
||||
const amountMaxPool = calcMaxExactIn(poolReserve)
|
||||
@ -97,6 +97,7 @@ export default function Add({
|
||||
isAssetNetwork,
|
||||
poolData?.id,
|
||||
poolInfo?.baseTokenAddress,
|
||||
poolInfo?.baseTokenDecimals,
|
||||
balance?.ocean
|
||||
])
|
||||
|
||||
@ -141,37 +142,17 @@ export default function Add({
|
||||
{({ isSubmitting, setSubmitting, submitForm, values, isValid }) => (
|
||||
<>
|
||||
<div className={styles.addInput}>
|
||||
{isWarningAccepted ? (
|
||||
<FormAdd
|
||||
amountMax={amountMax}
|
||||
setNewPoolTokens={setNewPoolTokens}
|
||||
setNewPoolShare={setNewPoolShare}
|
||||
/>
|
||||
) : (
|
||||
content.pool.add.warning && (
|
||||
<Alert
|
||||
className={styles.warning}
|
||||
text={content.pool.add.warning.toString()}
|
||||
state="info"
|
||||
action={{
|
||||
name: 'I understand',
|
||||
style: 'text',
|
||||
handleAction: () => setIsWarningAccepted(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<FormAdd
|
||||
amountMax={amountMax}
|
||||
setNewPoolTokens={setNewPoolTokens}
|
||||
setNewPoolShare={setNewPoolShare}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Output newPoolTokens={newPoolTokens} newPoolShare={newPoolShare} />
|
||||
|
||||
<Actions
|
||||
isDisabled={
|
||||
!isValid ||
|
||||
!isWarningAccepted ||
|
||||
!values.amount ||
|
||||
values.amount === 0
|
||||
}
|
||||
isDisabled={!isValid || !values.amount || values.amount === 0}
|
||||
isLoading={isSubmitting}
|
||||
loaderMessage="Adding Liquidity..."
|
||||
successMessage="Successfully added liquidity."
|
||||
|
@ -40,10 +40,14 @@ export default function Remove({
|
||||
const [amountOcean, setAmountOcean] = useState('0')
|
||||
const [isLoading, setIsLoading] = useState<boolean>()
|
||||
const [txId, setTxId] = useState<string>()
|
||||
const [slippage, setSlippage] = useState<string>('5')
|
||||
const [slippage, setSlippage] = useState(slippagePresets[0])
|
||||
const [minOceanAmount, setMinOceanAmount] = useState<string>('0')
|
||||
const [poolInstance, setPoolInstance] = useState<Pool>()
|
||||
|
||||
const poolInstance = new Pool(web3)
|
||||
useEffect(() => {
|
||||
if (!web3) return
|
||||
setPoolInstance(new Pool(web3))
|
||||
}, [web3])
|
||||
|
||||
async function handleRemoveLiquidity() {
|
||||
setIsLoading(true)
|
||||
@ -74,28 +78,21 @@ export default function Remove({
|
||||
// Calculate and set maximum shares user is able to remove
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!accountId || !poolInfoUser?.poolShares || !poolInfo?.totalPoolTokens)
|
||||
return
|
||||
if (!accountId || !poolInfoUser || !poolInfo || !poolInstance) return
|
||||
|
||||
getMax(poolInstance, poolInfo, poolInfoUser, poolData).then((max) =>
|
||||
setAmountMaxPercent(max)
|
||||
)
|
||||
}, [
|
||||
accountId,
|
||||
poolInfoUser?.poolShares,
|
||||
poolInfo?.totalPoolTokens,
|
||||
poolInfoUser,
|
||||
poolInfo,
|
||||
poolInstance,
|
||||
poolData
|
||||
])
|
||||
}, [accountId, poolInfoUser, poolInfo, poolInstance, poolData])
|
||||
|
||||
const getValues = useRef(
|
||||
debounce(async (newAmountPoolShares) => {
|
||||
debounce(async (poolInstance, id, poolInfo, newAmountPoolShares) => {
|
||||
const newAmountOcean = await poolInstance.calcSingleOutGivenPoolIn(
|
||||
poolData?.id,
|
||||
poolInfo?.baseTokenAddress,
|
||||
newAmountPoolShares
|
||||
id,
|
||||
poolInfo.baseTokenAddress,
|
||||
newAmountPoolShares,
|
||||
18,
|
||||
poolInfo.baseTokenDecimals
|
||||
)
|
||||
setAmountOcean(newAmountOcean)
|
||||
}, 150)
|
||||
@ -103,21 +100,9 @@ export default function Remove({
|
||||
|
||||
// Check and set outputs when amountPoolShares changes
|
||||
useEffect(() => {
|
||||
if (
|
||||
!accountId ||
|
||||
!poolInfoUser?.poolShares ||
|
||||
!poolInfo?.totalPoolTokens ||
|
||||
!poolData?.id
|
||||
)
|
||||
return
|
||||
getValues.current(amountPoolShares)
|
||||
}, [
|
||||
amountPoolShares,
|
||||
accountId,
|
||||
poolInfoUser?.poolShares,
|
||||
poolData?.id,
|
||||
poolInfo?.totalPoolTokens
|
||||
])
|
||||
if (!accountId || !poolInfo || !poolData?.id || !poolInstance) return
|
||||
getValues.current(poolInstance, poolData?.id, poolInfo, amountPoolShares)
|
||||
}, [amountPoolShares, accountId, poolInfo, poolData?.id, poolInstance])
|
||||
|
||||
useEffect(() => {
|
||||
if (!amountOcean || amountPercent === '0') {
|
||||
|
@ -41,7 +41,6 @@ export default function FormTrade({
|
||||
const [coinFrom, setCoinFrom] = useState<string>('OCEAN')
|
||||
const [maximumBaseToken, setMaximumBaseToken] = useState('0')
|
||||
const [maximumDt, setMaximumDt] = useState('0')
|
||||
const [isWarningAccepted, setIsWarningAccepted] = useState(false)
|
||||
|
||||
const validationSchema: Yup.SchemaOf<FormTradeData> = Yup.object()
|
||||
.shape({
|
||||
@ -82,7 +81,15 @@ export default function FormTrade({
|
||||
values.type === 'sell'
|
||||
? poolInfo.baseTokenAddress
|
||||
: poolInfo.datatokenAddress,
|
||||
marketFeeAddress: appConfig.marketFeeAddress
|
||||
marketFeeAddress: appConfig.marketFeeAddress,
|
||||
tokenInDecimals:
|
||||
values.type === 'sell'
|
||||
? poolInfo.datatokenDecimals
|
||||
: poolInfo.baseTokenDecimals,
|
||||
tokenOutDecimals:
|
||||
values.type === 'sell'
|
||||
? poolInfo.baseTokenDecimals
|
||||
: poolInfo.datatokenDecimals
|
||||
}
|
||||
|
||||
const amountsInOutMaxFee: AmountsInMaxFee = {
|
||||
@ -120,7 +127,15 @@ export default function FormTrade({
|
||||
values.type === 'sell'
|
||||
? poolInfo.baseTokenAddress
|
||||
: poolInfo.datatokenAddress,
|
||||
marketFeeAddress: appConfig.marketFeeAddress
|
||||
marketFeeAddress: appConfig.marketFeeAddress,
|
||||
tokenInDecimals:
|
||||
values.type === 'sell'
|
||||
? poolInfo.datatokenDecimals
|
||||
: poolInfo.baseTokenDecimals,
|
||||
tokenOutDecimals:
|
||||
values.type === 'sell'
|
||||
? poolInfo.baseTokenDecimals
|
||||
: poolInfo.datatokenDecimals
|
||||
}
|
||||
|
||||
const amountsOutMaxFee: AmountsOutMaxFee = {
|
||||
@ -169,32 +184,17 @@ export default function FormTrade({
|
||||
>
|
||||
{({ isSubmitting, setSubmitting, submitForm, values, isValid }) => (
|
||||
<>
|
||||
{isWarningAccepted ? (
|
||||
<Swap
|
||||
asset={asset}
|
||||
balance={balance}
|
||||
setCoin={setCoinFrom}
|
||||
setMaximumBaseToken={setMaximumBaseToken}
|
||||
setMaximumDt={setMaximumDt}
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.alertWrap}>
|
||||
<Alert
|
||||
text={content.trade.warning}
|
||||
state="info"
|
||||
action={{
|
||||
name: 'I understand',
|
||||
style: 'text',
|
||||
handleAction: () => setIsWarningAccepted(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Swap
|
||||
asset={asset}
|
||||
balance={balance}
|
||||
setCoin={setCoinFrom}
|
||||
setMaximumBaseToken={setMaximumBaseToken}
|
||||
setMaximumDt={setMaximumDt}
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
<Actions
|
||||
isDisabled={
|
||||
!isValid ||
|
||||
!isWarningAccepted ||
|
||||
!isAssetNetwork ||
|
||||
values.datatoken === undefined ||
|
||||
values.baseToken === undefined
|
||||
|
@ -78,11 +78,13 @@ export default function Swap({
|
||||
async function calculateMaximum() {
|
||||
const datatokenLiquidity = await poolInstance.getReserve(
|
||||
poolData.id,
|
||||
poolData.datatoken.address
|
||||
poolData.datatoken.address,
|
||||
poolData.datatoken.decimals
|
||||
)
|
||||
const baseTokenLiquidity = await poolInstance.getReserve(
|
||||
poolData.id,
|
||||
poolData.baseToken.address
|
||||
poolData.baseToken.address,
|
||||
poolData.baseToken.decimals
|
||||
)
|
||||
if (values.type === 'buy') {
|
||||
const maxBaseTokenFromPool = calcMaxExactIn(baseTokenLiquidity)
|
||||
@ -98,7 +100,9 @@ export default function Swap({
|
||||
poolInfo.baseTokenAddress,
|
||||
poolInfo.datatokenAddress,
|
||||
maxBaseTokens.toString(),
|
||||
appConfig.consumeMarketPoolSwapFee
|
||||
appConfig.consumeMarketPoolSwapFee,
|
||||
poolInfo.baseTokenDecimals,
|
||||
poolInfo.datatokenDecimals
|
||||
)
|
||||
const maximumDt = new Decimal(maxDt.tokenAmount)
|
||||
.toDecimalPlaces(MAX_DECIMALS)
|
||||
@ -130,7 +134,9 @@ export default function Swap({
|
||||
poolInfo?.datatokenAddress,
|
||||
poolInfo?.baseTokenAddress,
|
||||
maxDatatokens.toString(),
|
||||
appConfig.consumeMarketPoolSwapFee
|
||||
appConfig.consumeMarketPoolSwapFee,
|
||||
poolInfo.datatokenDecimals,
|
||||
poolInfo.baseTokenDecimals
|
||||
)
|
||||
const maximumBasetokens = new Decimal(maxBaseTokens.tokenAmount)
|
||||
.toDecimalPlaces(MAX_DECIMALS)
|
||||
|
@ -9,7 +9,8 @@ export default function MetaFull({ ddo }: { ddo: Asset }): ReactElement {
|
||||
const { isInPurgatory } = useAsset()
|
||||
|
||||
function DockerImage() {
|
||||
const { image, tag } = ddo?.metadata?.algorithm?.container
|
||||
const containerInfo = ddo?.metadata?.algorithm?.container
|
||||
const { image, tag } = containerInfo
|
||||
return <span>{`${image}:${tag}`}</span>
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import React, { ReactElement } from 'react'
|
||||
import DebugOutput from '@shared/DebugOutput'
|
||||
import { MetadataEditForm } from './_types'
|
||||
import { mapTimeoutStringToSeconds } from '@utils/ddo'
|
||||
import { sanitizeUrl } from '@utils/url'
|
||||
|
||||
export default function DebugEditMetadata({
|
||||
values,
|
||||
@ -12,7 +13,8 @@ export default function DebugEditMetadata({
|
||||
asset: Asset
|
||||
}): ReactElement {
|
||||
const linksTransformed = values.links?.length &&
|
||||
values.links[0].valid && [values.links[0].url.replace('javascript:', '')]
|
||||
values.links[0].valid && [sanitizeUrl(values.links[0].url)]
|
||||
|
||||
const newMetadata: Metadata = {
|
||||
...asset?.metadata,
|
||||
name: values.name,
|
||||
|
@ -23,6 +23,7 @@ import { getOceanConfig } from '@utils/ocean'
|
||||
import EditFeedback from './EditFeedback'
|
||||
import { useAsset } from '@context/Asset'
|
||||
import { setNftMetadata } from '@utils/nft'
|
||||
import { sanitizeUrl } from '@utils/url'
|
||||
|
||||
export default function Edit({
|
||||
asset
|
||||
@ -64,9 +65,7 @@ export default function Edit({
|
||||
) {
|
||||
try {
|
||||
const linksTransformed = values.links?.length &&
|
||||
values.links[0].valid && [
|
||||
values.links[0].url.replace('javascript:', '')
|
||||
]
|
||||
values.links[0].valid && [sanitizeUrl(values.links[0].url)]
|
||||
const updatedMetadata: Metadata = {
|
||||
...asset.metadata,
|
||||
name: values.name,
|
||||
|
@ -33,7 +33,7 @@ export default function FormEditComputeDataset({
|
||||
const { publisherTrustedAlgorithms } = getServiceByName(
|
||||
asset,
|
||||
'compute'
|
||||
)?.compute
|
||||
).compute
|
||||
|
||||
async function getAlgorithmList(
|
||||
publisherTrustedAlgorithms: PublisherTrustedAlgorithm[]
|
||||
|
@ -117,7 +117,7 @@ export default function HomePage(): ReactElement {
|
||||
})
|
||||
|
||||
const baseParams = {
|
||||
chainIds: chainIds,
|
||||
chainIds,
|
||||
esPaginationOptions: {
|
||||
size: 9
|
||||
},
|
||||
|
@ -21,8 +21,10 @@ export default function NumberUnit({
|
||||
return (
|
||||
<div className={styles.unit}>
|
||||
<div className={`${styles.number} ${small && styles.small}`}>
|
||||
{icon && icon}
|
||||
{value}
|
||||
<>
|
||||
{icon && icon}
|
||||
{value}
|
||||
</>
|
||||
</div>
|
||||
<span className={styles.label}>
|
||||
{label}{' '}
|
||||
|
@ -7,6 +7,7 @@ import styles from './PublishedList.module.css'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import Filters from '../../Search/Filters'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
import { CancelToken } from 'axios'
|
||||
|
||||
export default function PublishedList({
|
||||
accountId
|
||||
@ -19,12 +20,19 @@ export default function PublishedList({
|
||||
const [queryResult, setQueryResult] = useState<PagedAssets>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const [service, setServiceType] = useState()
|
||||
const [access, setAccsesType] = useState()
|
||||
const [service, setServiceType] = useState<string>()
|
||||
const [access, setAccessType] = useState<string>()
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
const getPublished = useCallback(
|
||||
async (accountId, chainIds, page, service, access, cancelToken) => {
|
||||
async (
|
||||
accountId: string,
|
||||
chainIds: number[],
|
||||
page: number,
|
||||
service: string,
|
||||
access: string,
|
||||
cancelToken: CancelToken
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const result = await getPublishedAssets(
|
||||
@ -70,7 +78,7 @@ export default function PublishedList({
|
||||
serviceType={service}
|
||||
setServiceType={setServiceType}
|
||||
accessType={access}
|
||||
setAccessType={setAccsesType}
|
||||
setAccessType={setAccessType}
|
||||
className={styles.filters}
|
||||
/>
|
||||
<AssetList
|
||||
|
@ -49,7 +49,9 @@ export default function Coin({
|
||||
{datatokenOptions?.symbol === 'OCEAN' && (
|
||||
<Conversion price={field.value} />
|
||||
)}
|
||||
<Error meta={meta} />
|
||||
<div>
|
||||
<Error meta={meta} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -8,6 +8,7 @@ import { getOpcFees } from '../../../@utils/subgraph'
|
||||
import { OpcFeesQuery_opc as OpcFeesData } from '../../../@types/subgraph/OpcFeesQuery'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||
import Decimal from 'decimal.js'
|
||||
|
||||
const Default = ({
|
||||
title,
|
||||
@ -43,13 +44,17 @@ export default function Fees({
|
||||
pricingType: 'dynamic' | 'fixed'
|
||||
}): ReactElement {
|
||||
const [field, meta] = useField('pricing.swapFee')
|
||||
const [opcFees, setOpcFees] = useState<OpcFeesData>(undefined)
|
||||
const [oceanCommunitySwapFee, setOceanCommunitySwapFee] = useState<string>('')
|
||||
const { chainId } = useWeb3()
|
||||
const { appConfig } = useMarketMetadata()
|
||||
|
||||
useEffect(() => {
|
||||
getOpcFees(chainId || 1).then((response: OpcFeesData) => {
|
||||
setOpcFees(response)
|
||||
setOceanCommunitySwapFee(
|
||||
response?.swapOceanFee
|
||||
? new Decimal(response.swapOceanFee).mul(100).toString()
|
||||
: '0'
|
||||
)
|
||||
})
|
||||
}, [chainId])
|
||||
|
||||
@ -76,10 +81,10 @@ export default function Fees({
|
||||
)}
|
||||
|
||||
<Default
|
||||
title="Community Fee"
|
||||
title="Community Swap Fee"
|
||||
name="communityFee"
|
||||
tooltip={tooltips.communityFee}
|
||||
value={opcFees?.swapOceanFee || '0'}
|
||||
value={oceanCommunitySwapFee}
|
||||
/>
|
||||
|
||||
<Default
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
publisherMarketPoolSwapFee,
|
||||
publisherMarketFixedSwapFee
|
||||
} from '../../../app.config'
|
||||
import { sanitizeUrl } from '@utils/url'
|
||||
|
||||
export function getFieldContent(
|
||||
fieldName: string,
|
||||
@ -95,9 +96,9 @@ export async function transformPublishFormToDdo(
|
||||
|
||||
// Transform from files[0].url to string[] assuming only 1 file
|
||||
const filesTransformed = files?.length &&
|
||||
files[0].valid && [files[0].url.replace('javascript:', '')]
|
||||
files[0].valid && [sanitizeUrl(files[0].url)]
|
||||
const linksTransformed = links?.length &&
|
||||
links[0].valid && [links[0].url.replace('javascript:', '')]
|
||||
links[0].valid && [sanitizeUrl(links[0].url)]
|
||||
|
||||
const newMetadata: Metadata = {
|
||||
created: currentTime,
|
||||
|
@ -134,7 +134,7 @@ export function getSearchQuery(
|
||||
from: (Number(page) - 1 || 0) * (Number(offset) || 21),
|
||||
size: Number(offset) || 21
|
||||
},
|
||||
sortOptions: { sortBy: sort, sortDirection: sortDirection },
|
||||
sortOptions: { sortBy: sort, sortDirection },
|
||||
filters
|
||||
} as BaseQueryParams
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user