1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00
This commit is contained in:
Bogdan Fazakas 2022-05-12 14:42:28 +03:00
commit 936985a0f8
50 changed files with 41723 additions and 6003 deletions

View File

@ -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",

View File

@ -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
View File

@ -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
View 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
View File

@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect'

47
.storybook/main.js Normal file
View 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
View 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$/
}
}
}

View 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())
}
})

View File

@ -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
```

View File

@ -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"
}
}

46847
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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:

View File

@ -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
}
}
}

View File

@ -10,8 +10,10 @@ export interface PoolInfo {
weightDt: string
datatokenSymbol: string
datatokenAddress: string
datatokenDecimals: number
baseTokenSymbol: string
baseTokenAddress: string
baseTokenDecimals: number
totalPoolTokens: string
}

View File

@ -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={

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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
View 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
}

View File

@ -26,7 +26,7 @@ export default function AddToken({
const styleClasses = cx({
button: true,
minimal: minimal,
minimal,
[className]: className
})

View File

@ -29,7 +29,7 @@ export default function FileIcon({
}): ReactElement {
const styleClasses = cx({
file: true,
small: small,
small,
[className]: className
})

View File

@ -16,7 +16,7 @@ export default function PageHeader({
}): ReactElement {
const styleClasses = cx({
header: true,
center: center
center
})
return (

View File

@ -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;

View 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!')
}
}

View File

@ -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,

View 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!')
}
}

View 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')
})

View File

@ -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)

View File

@ -15,7 +15,7 @@ export default function Container({
}): ReactElement {
const styleClasses = cx({
container: true,
narrow: narrow,
narrow,
[className]: className
})

View File

@ -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)

View File

@ -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}

View File

@ -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,

View File

@ -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)
}}
/>
)
)}
</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."

View File

@ -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') {

View File

@ -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,7 +184,6 @@ export default function FormTrade({
>
{({ isSubmitting, setSubmitting, submitForm, values, isValid }) => (
<>
{isWarningAccepted ? (
<Swap
asset={asset}
balance={balance}
@ -178,23 +192,9 @@ export default function FormTrade({
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>
)}
<Actions
isDisabled={
!isValid ||
!isWarningAccepted ||
!isAssetNetwork ||
values.datatoken === undefined ||
values.baseToken === undefined

View File

@ -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)

View File

@ -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>
}

View File

@ -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,

View File

@ -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,

View File

@ -33,7 +33,7 @@ export default function FormEditComputeDataset({
const { publisherTrustedAlgorithms } = getServiceByName(
asset,
'compute'
)?.compute
).compute
async function getAlgorithmList(
publisherTrustedAlgorithms: PublisherTrustedAlgorithm[]

View File

@ -117,7 +117,7 @@ export default function HomePage(): ReactElement {
})
const baseParams = {
chainIds: chainIds,
chainIds,
esPaginationOptions: {
size: 9
},

View File

@ -21,8 +21,10 @@ export default function NumberUnit({
return (
<div className={styles.unit}>
<div className={`${styles.number} ${small && styles.small}`}>
<>
{icon && icon}
{value}
</>
</div>
<span className={styles.label}>
{label}{' '}

View File

@ -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

View File

@ -49,8 +49,10 @@ export default function Coin({
{datatokenOptions?.symbol === 'OCEAN' && (
<Conversion price={field.value} />
)}
<div>
<Error meta={meta} />
</div>
</div>
</div>
)
}

View File

@ -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

View File

@ -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,

View File

@ -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