diff --git a/.env.example b/.env.example index b48a7d5e3..137053bed 100644 --- a/.env.example +++ b/.env.example @@ -2,11 +2,14 @@ VITE_BASE_URL= VITE_ALCHEMY_API_KEY= +VITE_PRIVY_APP_ID= + VITE_PK_ENCRYPTION_KEY= VITE_ROUTER_TYPE= VITE_V3_TOKEN_ADDRESS= VITE_TOKEN_MIGRATION_URI= +VITE_NUMIA_BASE_URL= AMPLITUDE_API_KEY= AMPLITUDE_SERVER_URL= diff --git a/.eslintignore b/.eslintignore index 6b2c0fe2a..49959a3b9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,13 +2,13 @@ vite-env.d.ts polyfills.ts vite.config.ts +commitlint.config.ts # Temporarily ignore, we will slowly remove each directory as we fix files to follow ESLint rules -/src/components -/src/forms -/src/hooks -/src/lib -/src/main.tsx -/src/menus -/src/pages -/src/views +/src/views/forms + +# components dir is being worked on +/src/components/* + +# unignored components +!/src/components/Table.tsx \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 943c7b712..97098d8b1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,7 +25,7 @@ "import/no-extraneous-dependencies": [ "error", { - "devDependencies": ["./scripts/*.js"] + "devDependencies": ["./scripts/*.js", "./__tests__/**/*.ts", "./src/lib/__test__/**/*.ts"] } ], "import/no-named-as-default": "off", @@ -44,6 +44,7 @@ "no-use-before-define": "off", "prefer-destructuring": "off", "prettier/prettier": "error", + "react/display-name": "off", "react/forbid-prop-types": "off", "react/function-component-definition": [ "error", @@ -60,9 +61,11 @@ "react/react-in-jsx-scope": "off", "react/require-default-props": "off", "react/sort-comp": "off", + "react/jsx-pascal-case": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/comma-dangle": "off", "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/prefer-nullish-coalescing": "error", "@typescript-eslint/member-delimiter-style": [ "error", { diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..b2c6c7fff --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# prettier update +b5c3cd5bca0b775d3e471844f461e4a431e66df6 \ No newline at end of file diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 000000000..2a0a56ecd --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,30 @@ +name: Lint PR Title +on: + pull_request: + types: ['opened', 'edited', 'reopened', 'synchronize'] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up pnpm + uses: dydxprotocol/setup-pnpm@v1 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + + - name: Install dependencies + run: | + pnpm install @commitlint/config-conventional + + - name: Lint PR Title + run: | + echo "${PR_TITLE}" | pnpx commitlint --config commitlint.config.ts + env: + PR_TITLE: '${{ github.event.pull_request.title }}' diff --git a/.github/workflows/run-e2e.yml b/.github/workflows/run-e2e.yml new file mode 100644 index 000000000..29d8bd66a --- /dev/null +++ b/.github/workflows/run-e2e.yml @@ -0,0 +1,51 @@ +name: E2E Tests + +on: + deployment_status: +jobs: + run-e2es: + runs-on: ubuntu-latest + if: > + github.event_name == 'deployment_status' && + github.event.deployment_status.state == 'success' + steps: + - name: Check environment URL + id: check_env + run: | + if [[ "${{ github.event.deployment_status.environment_url }}" == *"v4-staging"* ]]; then + echo "::set-output name=should_run_tests::true" + else + echo "::set-output name=should_run_tests::false" + echo "This deployment does not require E2E tests. Exiting..." + fi + + - name: Checkout + if: steps.check_env.outputs.should_run_tests == 'true' + uses: actions/checkout@v3 + + - name: Set up pnpm + if: steps.check_env.outputs.should_run_tests == 'true' + uses: dydxprotocol/setup-pnpm@v1 + + - name: Set up Node + if: steps.check_env.outputs.should_run_tests == 'true' + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + + - name: Install dependencies + if: steps.check_env.outputs.should_run_tests == 'true' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + pnpm install --loglevel warn + + - name: Run e2e tests + if: steps.check_env.outputs.should_run_tests == 'true' + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + E2E_ENVIRONMENT_PASSWORD: ${{ secrets.E2E_ENVIRONMENT_PASSWORD }} + E2E_ENVIRONMENT_URL: ${{ github.event.deployment_status.environment_url }} + run: pnpm run wdio diff --git a/.github/workflows/update-release.yml b/.github/workflows/update-release.yml new file mode 100644 index 000000000..87cd84fea --- /dev/null +++ b/.github/workflows/update-release.yml @@ -0,0 +1,44 @@ +name: Workflow for Release + +on: + push: + tags: + - 'release/v*' + - 'hotfix/v*' + +jobs: + sync-release-branch: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.WORKFLOW_TOKEN }} + + - name: Fetch all history + run: git fetch --unshallow || git fetch --all + + - name: Determine Tag Type + id: tagtype + run: | + if [[ ${{ github.ref }} == refs/tags/release/v* ]]; then + echo "::set-output name=type::release" + elif [[ ${{ github.ref }} == refs/tags/hotfix/v* ]]; then + echo "::set-output name=type::hotfix" + fi + + - name: Check out the release branch + run: | + git checkout release || git checkout -b release + + - name: Sync release branch to tag + env: + GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} + run: | + git reset --hard ${{ github.ref }} + git push -f origin release diff --git a/.github/workflows/validate-other-market-data.yml b/.github/workflows/validate-other-market-data.yml new file mode 100644 index 000000000..d774fc435 --- /dev/null +++ b/.github/workflows/validate-other-market-data.yml @@ -0,0 +1,52 @@ +name: Validate Other Market Data + +on: + pull_request: + paths: + - 'public/configs/otherMarketData.json' + - 'scripts/validate-other-market-data.ts' + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up pnpm + uses: dydxprotocol/setup-pnpm@v1 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + + - name: Install dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + pnpm install --loglevel warn + + - name: Install Go + uses: actions/setup-go@v3 + with: + go-version: 1.21 + + - name: Checkout v4-chain repo + uses: actions/checkout@v3 + with: + repository: 'dydxprotocol/v4-chain' + ref: 'd4e0f0d1ac28f128c787e40c5a0cdc7c481e6c42' + path: 'v4-chain' + + - name: Start v4 localnet + run: | + cd v4-chain/protocol + echo "Building v4-chain/protocol..." + make build + echo "Starting localnet..." + DOCKER_BUILDKIT=1 make localnet-startd + + - name: Validate other market data + run: pnpx tsx scripts/validate-other-market-data.ts diff --git a/.gitignore b/.gitignore index 27e504ade..047ac2ff2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ dist-ssr *.key .env +index.html +entry_points/ + # Editor directories and files .vscode/* !.vscode/extensions.json @@ -30,3 +33,5 @@ dist-ssr # Charting Library public/tradingview public/datafeed + +local-abacus-hash diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..98475b507 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm test diff --git a/.ladle/components.tsx b/.ladle/components.tsx index b8e857461..f8b22dc29 100644 --- a/.ladle/components.tsx +++ b/.ladle/components.tsx @@ -1,17 +1,20 @@ import '@/polyfills'; + import { useEffect, useState } from 'react'; + +import '@/index.css'; +import { GrazProvider } from 'graz'; +import { QueryClient, QueryClientProvider } from 'react-query'; import { Provider } from 'react-redux'; import styled from 'styled-components'; import { WagmiConfig } from 'wagmi'; -import { GrazProvider } from 'graz'; -import { QueryClient, QueryClientProvider } from 'react-query'; import { SupportedLocales } from '@/constants/localization'; import { AccountsProvider } from '@/hooks/useAccounts'; import { AppThemeAndColorModeProvider } from '@/hooks/useAppThemeAndColorMode'; -import { DydxProvider } from '@/hooks/useDydxClient'; import { DialogAreaProvider } from '@/hooks/useDialogArea'; +import { DydxProvider } from '@/hooks/useDydxClient'; import { LocaleProvider } from '@/hooks/useLocaleSeparators'; import { PotentialMarketsProvider } from '@/hooks/usePotentialMarkets'; import { RestrictionProvider } from '@/hooks/useRestrictions'; @@ -19,22 +22,20 @@ import { SubaccountProvider } from '@/hooks/useSubaccount'; import { GlobalStyle } from '@/styles/globalStyle'; -import { SelectMenu, SelectItem } from '@/components/SelectMenu'; +import { SelectItem, SelectMenu } from '@/components/SelectMenu'; +import { store } from '@/state/_store'; import { + AppColorMode, AppTheme, AppThemeSystemSetting, - AppColorMode, - setAppThemeSetting, setAppColorMode, + setAppThemeSetting, } from '@/state/configs'; - import { setLocaleLoaded, setSelectedLocale } from '@/state/localization'; -import { store } from '@/state/_store'; import { config } from '@/lib/wagmi'; -import '@/index.css'; import './ladle.css'; const queryClient = new QueryClient(); diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..a4a60ea7d --- /dev/null +++ b/.nvmrc @@ -0,0 +1,2 @@ +18 +engine-strict=true diff --git a/.prettierrc.json b/.prettierrc.json index ab7b77859..c9605d1a5 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,5 +2,19 @@ "printWidth": 100, "singleQuote": true, "trailingComma": "es5", - "jsxBracketSameLine": false + "jsxBracketSameLine": false, + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "importOrder": [ + "^./polyfills$", + "^react$", + "", + "^@/(constants|abi)(.*)$", + "^@/(hooks|contexts)(.*)$", + "^@/(styles|icons)(.*)$", + "^@/(components|views|pages|layout)(.*)$", + "^@/state(.*)$", + "^@/lib(.*)$", + "^[./]" + ], + "importOrderSeparation": true } diff --git a/.vscode/settings.json b/.vscode/settings.json index d42566a8a..4a4b1b572 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,13 @@ { - "explorer.sortOrder": "mixed" -} \ No newline at end of file + "explorer.sortOrder": "mixed", + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "files.watcherExclude": { + "**/.git": true, + "**/node_modules/*": true, + "**/node_modules/@dydxprotocol/v4-abacus": false + }, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f779958e..2b8ef54ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,32 @@ Pull requests are the best way to propose changes to the codebase (we use [Githu 3. Make sure your code lints. 4. Issue that pull request! +## We use [Conventional Commits](https://github.com/conventional-changelog/commitlint) +We use a commit-msg hook to check if your commit messages meet the conventional commit format. + +In general the pattern mostly looks like this: + +`type(scope?): subject` #scope is optional; multiple scopes are supported (current delimiter options: "/", "\" and ",") + +### Real world examples can look like this: +`chore: run tests on travis ci` +`fix(server): send cors headers` +`feat(blog): add comment section` + +Common types according to commitlint-config-conventional can be: + +build +chore +ci +docs +feat +fix +perf +refactor +revert +style +test + ## Any contributions you make will be under the same License When you submit code changes, your submissions are understood to be under the same [License](https://github.com/dydxprotocol/v4-web/blob/master/LICENSE) that covers the project. diff --git a/README.md b/README.md index 39fb04c72..fb2a4936f 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,11 @@ This will automatically open your default browser at `http://localhost:61000`. ## Part 3: Configuring environment -Add or modify the relevant endpoints, links and options in `/public/configs/env.json`. +Add or modify the relevant endpoints, links and options in `/public/configs/v1/env.json`. + +NOTE: There exists a deprecated file `/public/configs/env.json`. If you have users running older mobile versions you may +need to keep feature flags between the two files in sync but may otherwise ignore it. + You'll need to provide a Wallet Connect project id to enable onboarding and wallet connection: - Create a project on https://cloud.walletconnect.com/app @@ -86,6 +90,90 @@ Set environment variables via `.env`. - `INTERCOM_APP_ID` (optional): Used for enabling Intercom; utilized with `pnpm run build:inject-intercom`. - `STATUS_PAGE_SCRIPT_URI` (optional): Used for enabling the status page; used with `pnpm run build:inject-statuspage`. - `SMARTBANNER_APP_NAME`, `SMARTBANNER_ORG_NAME`, `SMARTBANNER_ICON_URL`, `SMARTBANNER_APPSTORE_URL` (optional): Used for enabling the smart app banner; used with `pnpm run build:inject-smartbanner`. +- `VITE_PRIVY_APP_ID` (optional): App ID used for enabling Privy authentication. For deployment of DYDX token holders use `clua5njf801bncvpa0woolzq4`. + +## Part 5: Configure entry points + +### HTML files + +Edit `scripts/generate-entry-points.js` and set up entry points according to your SEO needs. At least one entry point must be configured, +i.e. at least one element must be present in the `ENTRY_POINTS` array. This array consists of objects of the form: + +``` +{ + title: 'Page title', + description: 'Page description.', + fileName: 'HTML entry point file name, e.g.: index.html', +}, +``` + +The build script will traverse these entries and create files in `entry-points` directory, modifying the `template.html` file accordingly +for each entry. The `rollupOptions` config option in `vite.config.ts` informs the framework about the location of all the entry points +created above. + +### Rewrite rules + +Edit `vercel.json` and configure the `rewrites` configuration option. It is an array of objects of the form: + +``` + { + "source": "Regexp for matching the URL path, e.g.: /portfolio(/?.*)", + "destination": "Entry point file to use, e.g.: /entry-points/portfolio.html" + }, +``` + +Note: The first matching rule takes precedence over anything defined afterwards in the array. + +# Testing + +## Unit testing + +Run unit tests with the following command: `pnpm run test` + +## Functional Testing + +Functional testing is supported via Browserstack. To run the tests you need to set the following environment variables: + +- `BROWSERSTACK_USERNAME`: username of your browserstack account +- `BROWSERSTACK_ACCESS_KEY`: access key of your browserstack account +- `E2E_ENVIRONMENT_URL`: the URL you want to run the functional tests against + +To run the tests run: `pnpm run wdio` + +# Local Abacus Development + +## Directory structure + +Our tooling assumes that the [v4-abacus repo](https://github.com/dydxprotocol/v4-abacus) is checked out alongside v4-web: + +``` +--- parent folder + |___ v4-web + |___ v4-abacus +``` + +## Using your local v4-abacus repo + +Whenever you have changes in v4-abacus that you'd like to test in your local v4-web branch, use the following command: + +``` +pnpm run install-local-abacus --clean +``` + +The `--clean` option will do some extra cleaning, **it is not needed on subsequent runs.** + +## Reverting to remote abacus + +Revert any changes to @dydxprotocol/v4-abacus in package.json and pnpm-lock.yaml. If you haven't made any other package changes, you can use: + +``` +git restore main package.json +git restore main pnpm-lock.yaml +``` + +Then run `pnpm install` + +**Remember to revert to remote abacus before making a PR.** # Deployments diff --git a/__tests__/e2e/test.e2e.ts b/__tests__/e2e/test.e2e.ts new file mode 100644 index 000000000..1ec07ac1f --- /dev/null +++ b/__tests__/e2e/test.e2e.ts @@ -0,0 +1,15 @@ +import { $, browser, expect } from '@wdio/globals'; + +describe('Smoke test', () => { + it('should authenticate with vercel and load website', async () => { + await browser.url(process.env.E2E_ENVIRONMENT_URL || ''); + browser.setWindowSize(1920, 1080); + + await $('input[type=password]').setValue(process.env.E2E_ENVIRONMENT_PASSWORD || ''); + await $('button.submit').click(); + + await expect($('main')).toBeExisting(); + await expect($('header')).toBeExisting(); + await expect($('footer')).toBeExisting(); + }); +}); diff --git a/__tests__/tsconfig.json b/__tests__/tsconfig.json new file mode 100644 index 000000000..3c43903cf --- /dev/null +++ b/__tests__/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/commitlint.config.ts b/commitlint.config.ts new file mode 100644 index 000000000..9d7fff982 --- /dev/null +++ b/commitlint.config.ts @@ -0,0 +1,8 @@ +export default { + extends: ['@commitlint/config-conventional'], + rules: { + 'subject-case': [0, 'never'], + 'header-max-length': [2, 'always', 120], + 'body-max-line-length': [0, 'always', 120], + }, +}; diff --git a/package.json b/package.json index 7838064ea..a52154537 100644 --- a/package.json +++ b/package.json @@ -8,28 +8,34 @@ "node": ">=18" }, "scripts": { - "dev": "vite", - "build": "vite build", + "dev": "cp ./template.html ./index.html && vite", + "build": "pnpm run build:set-last-commit-and-tag && pnpm run build:generate-entry-points && tsc && vite build", "build:inject-app-deeplinks": "sh scripts/inject-app-deeplinks.sh", "build:inject-amplitude": "node scripts/inject-amplitude.js", "build:inject-bugsnag": "node scripts/inject-bugsnag.js", "build:inject-intercom": "node scripts/inject-intercom.js", "build:inject-statuspage": "node scripts/inject-statuspage.js", "build:inject-smartbanner": "node scripts/inject-smartbanner.js", + "build:set-last-commit-and-tag": "sh scripts/set-last-commit-and-tag.sh", + "build:generate-entry-points": "node scripts/generate-entry-points.js", "deploy:ipfs": "node scripts/upload-ipfs.js --verbose", "deploy:update-ipns": "node scripts/update-ipns.js", "deploy:update-dnslink": "node scripts/update-dnslink.js", "coverage": "vitest run --coverage", "clean-install": "rm -rf node_modules/ && pnpm i", "preview": "vite preview", + "install-local-abacus": "node scripts/install-local-abacus", "ladle": "ladle serve", "ladle-b": "ladle build", "ladle-p": "ladle preview", "lint": "eslint --ext .ts,.tsx src/", "fix-lint": "eslint --fix --ext .ts,.tsx src/", - "test": "vitest", + "test": "vitest run", "tsc": "tsc", - "postinstall": "tar -xzC public -f tradingview/tradingview.tgz" + "postinstall": "tar -xzC public -f tradingview/tradingview.tgz", + "prepare": "husky", + "commitlint": "commitlint --edit", + "wdio": "wdio run ./wdio.conf.ts" }, "packageManager": "pnpm@8.6.6", "dependencies": { @@ -40,16 +46,19 @@ "@cosmjs/proto-signing": "^0.32.1", "@cosmjs/stargate": "^0.32.1", "@cosmjs/tendermint-rpc": "^0.32.1", - "@dydxprotocol/v4-abacus": "^1.4.13", - "@dydxprotocol/v4-client-js": "^1.0.20", - "@dydxprotocol/v4-localization": "^1.1.35", + "@dydxprotocol/v4-abacus": "1.7.18", + "@dydxprotocol/v4-client-js": "^1.1.10", + "@dydxprotocol/v4-localization": "^1.1.86", "@ethersproject/providers": "^5.7.2", "@js-joda/core": "^5.5.3", + "@privy-io/react-auth": "^1.59.7", + "@privy-io/wagmi-connector": "^0.1.12", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", + "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.1.3", "@radix-ui/react-popover": "^1.0.6", @@ -96,10 +105,12 @@ "cosmjs-types": "^0.9.0", "crypto-js": "^4.1.1", "ethers": "^6.6.1", + "export-to-csv": "^1.2.3", "graz": "^0.0.43", "lodash": "^4.17.21", "long": "^5.2.3", "luxon": "^3.3.0", + "prometheus-query": "^3.4.0", "qr-code-styling": "1.6.0-rc.1", "query-string": "^8.1.0", "react": "^18.2.0", @@ -118,17 +129,32 @@ }, "devDependencies": { "@babel/core": "^7.22.5", + "@commitlint/cli": "^19.0.3", + "@commitlint/config-conventional": "^19.0.3", + "@dydxprotocol/v4-proto": "5.0.0-dev.0", "@ladle/react": "^4.0.2", + "@testing-library/webdriverio": "^3.2.1", + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/color": "^3.0.3", "@types/crypto-js": "^4.1.1", "@types/luxon": "^3.3.0", + "@types/mocha": "^10.0.6", "@types/node": "^20.3.1", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "@vitejs/plugin-react": "^4.0.1", + "@wdio/browserstack-service": "^8.32.4", + "@wdio/cli": "^8.32.4", + "@wdio/globals": "^8.32.4", + "@wdio/local-runner": "^8.32.4", + "@wdio/mocha-framework": "^8.32.4", + "@wdio/spec-reporter": "^8.32.4", + "@wdio/types": "^8.32.4", + "ajv": "^8.12.0", "assert": "^2.0.0", + "axios": "^1.6.7", "babel-loader": "^9.1.2", "babel-plugin-styled-components": "^2.1.4", "browserify-zlib": "^0.2.0", @@ -145,17 +171,23 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "husky": "^9.0.11", "minimist": "^1.2.8", "node-fetch": "^3.3.1", "pnpm": "^8.6.6", "prettier": "^2.8.8", + "rollup-plugin-sourcemaps": "^0.6.3", + "ts-node": "^10.9.2", + "tsx": "^4.7.1", "typescript": "^5.1.3", "url-polyfill": "^1.1.12", "util": "^0.12.5", "vite": "^4.3.9", + "vite-plugin-restart": "^0.4.0", "vite-plugin-svgr": "^3.2.0", "vitest": "^0.32.2", "w3name": "^1.0.8", + "wdio-wait-for": "^3.0.11", "web3.storage": "^4.5.4" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28af4c6cb..10e1dc76d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,20 +30,26 @@ dependencies: specifier: ^0.32.1 version: 0.32.2 '@dydxprotocol/v4-abacus': - specifier: ^1.4.13 - version: 1.4.13 + specifier: 1.7.18 + version: 1.7.18 '@dydxprotocol/v4-client-js': - specifier: ^1.0.20 - version: 1.0.20 + specifier: ^1.1.10 + version: 1.1.10 '@dydxprotocol/v4-localization': - specifier: ^1.1.35 - version: 1.1.35 + specifier: ^1.1.86 + version: 1.1.86 '@ethersproject/providers': specifier: ^5.7.2 version: 5.7.2 '@js-joda/core': specifier: ^5.5.3 version: 5.5.3 + '@privy-io/react-auth': + specifier: ^1.59.7 + version: 1.59.9(@babel/core@7.22.5)(@types/react@18.2.14)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(typescript@5.1.3) + '@privy-io/wagmi-connector': + specifier: ^0.1.12 + version: 0.1.13(@privy-io/react-auth@1.59.9)(react-dom@18.2.0)(react@18.2.0)(viem@1.20.0)(wagmi@1.4.13) '@radix-ui/react-accordion': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) @@ -59,6 +65,9 @@ dependencies: '@radix-ui/react-dropdown-menu': specifier: ^2.0.5 version: 2.0.5(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-hover-card': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) @@ -209,6 +218,9 @@ dependencies: luxon: specifier: ^3.3.0 version: 3.3.0 + prometheus-query: + specifier: ^3.4.0 + version: 3.4.0 qr-code-styling: specifier: 1.6.0-rc.1 version: 1.6.0-rc.1 @@ -259,9 +271,24 @@ devDependencies: '@babel/core': specifier: ^7.22.5 version: 7.22.5 + '@commitlint/cli': + specifier: ^19.0.3 + version: 19.0.3(@types/node@20.3.1)(typescript@5.1.3) + '@commitlint/config-conventional': + specifier: ^19.0.3 + version: 19.0.3 + '@dydxprotocol/v4-proto': + specifier: 5.0.0-dev.0 + version: 5.0.0-dev.0 '@ladle/react': specifier: ^4.0.2 version: 4.0.2(@types/node@20.3.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3) + '@testing-library/webdriverio': + specifier: ^3.2.1 + version: 3.2.1(webdriverio@8.33.1) + '@trivago/prettier-plugin-sort-imports': + specifier: ^4.3.0 + version: 4.3.0(prettier@2.8.8) '@types/color': specifier: ^3.0.3 version: 3.0.3 @@ -271,6 +298,9 @@ devDependencies: '@types/luxon': specifier: ^3.3.0 version: 3.3.0 + '@types/mocha': + specifier: ^10.0.6 + version: 10.0.6 '@types/node': specifier: ^20.3.1 version: 20.3.1 @@ -289,9 +319,36 @@ devDependencies: '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.0.1(vite@4.3.9) + '@wdio/browserstack-service': + specifier: ^8.32.4 + version: 8.33.1(@wdio/cli@8.33.1)(typescript@5.1.3) + '@wdio/cli': + specifier: ^8.32.4 + version: 8.33.1(typescript@5.1.3) + '@wdio/globals': + specifier: ^8.32.4 + version: 8.33.1(typescript@5.1.3) + '@wdio/local-runner': + specifier: ^8.32.4 + version: 8.33.1(typescript@5.1.3) + '@wdio/mocha-framework': + specifier: ^8.32.4 + version: 8.33.1 + '@wdio/spec-reporter': + specifier: ^8.32.4 + version: 8.32.4 + '@wdio/types': + specifier: ^8.32.4 + version: 8.32.4 + ajv: + specifier: ^8.12.0 + version: 8.12.0 assert: specifier: ^2.0.0 version: 2.0.0 + axios: + specifier: ^1.6.7 + version: 1.6.7 babel-loader: specifier: ^9.1.2 version: 9.1.2(@babel/core@7.22.5)(webpack@5.89.0) @@ -340,6 +397,9 @@ devDependencies: eslint-plugin-react-hooks: specifier: ^4.6.0 version: 4.6.0(eslint@8.43.0) + husky: + specifier: ^9.0.11 + version: 9.0.11 minimist: specifier: ^1.2.8 version: 1.2.8 @@ -352,6 +412,15 @@ devDependencies: prettier: specifier: ^2.8.8 version: 2.8.8 + rollup-plugin-sourcemaps: + specifier: ^0.6.3 + version: 0.6.3(@types/node@20.3.1)(rollup@2.79.1) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.3.1)(typescript@5.1.3) + tsx: + specifier: ^4.7.1 + version: 4.7.1 typescript: specifier: ^5.1.3 version: 5.1.3 @@ -364,15 +433,21 @@ devDependencies: vite: specifier: ^4.3.9 version: 4.3.9(@types/node@20.3.1) + vite-plugin-restart: + specifier: ^0.4.0 + version: 0.4.0(vite@4.3.9) vite-plugin-svgr: specifier: ^3.2.0 - version: 3.2.0(vite@4.3.9) + version: 3.2.0(rollup@2.79.1)(vite@4.3.9) vitest: specifier: ^0.32.2 - version: 0.32.2 + version: 0.32.2(webdriverio@8.33.1) w3name: specifier: ^1.0.8 version: 1.0.8 + wdio-wait-for: + specifier: ^3.0.11 + version: 3.0.11 web3.storage: specifier: ^4.5.4 version: 4.5.4(node-fetch@3.3.1) @@ -486,6 +561,15 @@ packages: - supports-color dev: true + /@babel/generator@7.17.7: + resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + jsesc: 2.5.2 + source-map: 0.5.7 + dev: true + /@babel/generator@7.22.10: resolution: {integrity: sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==} engines: {node: '>=6.9.0'} @@ -787,6 +871,24 @@ packages: transitivePeerDependencies: - supports-color + /@babel/traverse@7.23.2: + resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + debug: 4.3.4(supports-color@5.5.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/traverse@7.23.9: resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} engines: {node: '>=6.9.0'} @@ -805,6 +907,14 @@ packages: - supports-color dev: true + /@babel/types@7.17.0: + resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + /@babel/types@7.22.10: resolution: {integrity: sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==} engines: {node: '>=6.9.0'} @@ -862,6 +972,189 @@ packages: - utf-8-validate dev: false + /@coinbase/wallet-sdk@3.9.3: + resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} + dependencies: + bn.js: 5.2.1 + buffer: 6.0.3 + clsx: 1.2.1 + eth-block-tracker: 7.1.0 + eth-json-rpc-filters: 6.0.1 + eventemitter3: 5.0.1 + keccak: 3.0.3 + preact: 10.17.0 + sha.js: 2.4.11 + transitivePeerDependencies: + - supports-color + dev: false + + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: true + + /@commitlint/cli@19.0.3(@types/node@20.3.1)(typescript@5.1.3): + resolution: {integrity: sha512-mGhh/aYPib4Vy4h+AGRloMY+CqkmtdeKPV9poMcZeImF5e3knQ5VYaSeAM0mEzps1dbKsHvABwaDpafLUuM96g==} + engines: {node: '>=v18'} + hasBin: true + dependencies: + '@commitlint/format': 19.0.3 + '@commitlint/lint': 19.0.3 + '@commitlint/load': 19.0.3(@types/node@20.3.1)(typescript@5.1.3) + '@commitlint/read': 19.0.3 + '@commitlint/types': 19.0.3 + execa: 8.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + dev: true + + /@commitlint/config-conventional@19.0.3: + resolution: {integrity: sha512-vh0L8XeLaEzTe8VCxSd0gAFvfTK0RFolrzw4o431bIuWJfi/yRCHJlsDwus7wW2eJaFFDR0VFXJyjGyDQhi4vA==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/types': 19.0.3 + conventional-changelog-conventionalcommits: 7.0.2 + dev: true + + /@commitlint/config-validator@19.0.3: + resolution: {integrity: sha512-2D3r4PKjoo59zBc2auodrSCaUnCSALCx54yveOFwwP/i2kfEAQrygwOleFWswLqK0UL/F9r07MFi5ev2ohyM4Q==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/types': 19.0.3 + ajv: 8.12.0 + dev: true + + /@commitlint/ensure@19.0.3: + resolution: {integrity: sha512-SZEpa/VvBLoT+EFZVb91YWbmaZ/9rPH3ESrINOl0HD2kMYsjvl0tF7nMHh0EpTcv4+gTtZBAe1y/SS6/OhfZzQ==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/types': 19.0.3 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + dev: true + + /@commitlint/execute-rule@19.0.0: + resolution: {integrity: sha512-mtsdpY1qyWgAO/iOK0L6gSGeR7GFcdW7tIjcNFxcWkfLDF5qVbPHKuGATFqRMsxcO8OUKNj0+3WOHB7EHm4Jdw==} + engines: {node: '>=v18'} + dev: true + + /@commitlint/format@19.0.3: + resolution: {integrity: sha512-QjjyGyoiVWzx1f5xOteKHNLFyhyweVifMgopozSgx1fGNrGV8+wp7k6n1t6StHdJ6maQJ+UUtO2TcEiBFRyR6Q==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/types': 19.0.3 + chalk: 5.3.0 + dev: true + + /@commitlint/is-ignored@19.0.3: + resolution: {integrity: sha512-MqDrxJaRSVSzCbPsV6iOKG/Lt52Y+PVwFVexqImmYYFhe51iVJjK2hRhOG2jUAGiUHk4jpdFr0cZPzcBkSzXDQ==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/types': 19.0.3 + semver: 7.6.0 + dev: true + + /@commitlint/lint@19.0.3: + resolution: {integrity: sha512-uHPyRqIn57iIplYa5xBr6oNu5aPXKGC4WLeuHfqQHclwIqbJ33g3yA5fIA+/NYnp5ZM2EFiujqHFaVUYj6HlKA==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/is-ignored': 19.0.3 + '@commitlint/parse': 19.0.3 + '@commitlint/rules': 19.0.3 + '@commitlint/types': 19.0.3 + dev: true + + /@commitlint/load@19.0.3(@types/node@20.3.1)(typescript@5.1.3): + resolution: {integrity: sha512-18Tk/ZcDFRKIoKfEcl7kC+bYkEQ055iyKmGsYDoYWpKf6FUvBrP9bIWapuy/MB+kYiltmP9ITiUx6UXtqC9IRw==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/config-validator': 19.0.3 + '@commitlint/execute-rule': 19.0.0 + '@commitlint/resolve-extends': 19.0.3 + '@commitlint/types': 19.0.3 + chalk: 5.3.0 + cosmiconfig: 8.3.6(typescript@5.1.3) + cosmiconfig-typescript-loader: 5.0.0(@types/node@20.3.1)(cosmiconfig@8.3.6)(typescript@5.1.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + dev: true + + /@commitlint/message@19.0.0: + resolution: {integrity: sha512-c9czf6lU+9oF9gVVa2lmKaOARJvt4soRsVmbR7Njwp9FpbBgste5i7l/2l5o8MmbwGh4yE1snfnsy2qyA2r/Fw==} + engines: {node: '>=v18'} + dev: true + + /@commitlint/parse@19.0.3: + resolution: {integrity: sha512-Il+tNyOb8VDxN3P6XoBBwWJtKKGzHlitEuXA5BP6ir/3loWlsSqDr5aecl6hZcC/spjq4pHqNh0qPlfeWu38QA==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/types': 19.0.3 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + dev: true + + /@commitlint/read@19.0.3: + resolution: {integrity: sha512-b5AflTyAXkUx5qKw4TkjjcOccXZHql3JqMi522knTQktq2AubKXFz60Sws+K4FsefwPws6fGz9mqiI/NvsvxFA==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/top-level': 19.0.0 + '@commitlint/types': 19.0.3 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + dev: true + + /@commitlint/resolve-extends@19.0.3: + resolution: {integrity: sha512-18BKmta8OC8+Ub+Q3QGM9l27VjQaXobloVXOrMvu8CpEwJYv62vC/t7Ka5kJnsW0tU9q1eMqJFZ/nN9T/cOaIA==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/config-validator': 19.0.3 + '@commitlint/types': 19.0.3 + global-directory: 4.0.1 + import-meta-resolve: 4.0.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + dev: true + + /@commitlint/rules@19.0.3: + resolution: {integrity: sha512-TspKb9VB6svklxNCKKwxhELn7qhtY1rFF8ls58DcFd0F97XoG07xugPjjbVnLqmMkRjZDbDIwBKt9bddOfLaPw==} + engines: {node: '>=v18'} + dependencies: + '@commitlint/ensure': 19.0.3 + '@commitlint/message': 19.0.0 + '@commitlint/to-lines': 19.0.0 + '@commitlint/types': 19.0.3 + execa: 8.0.1 + dev: true + + /@commitlint/to-lines@19.0.0: + resolution: {integrity: sha512-vkxWo+VQU5wFhiP9Ub9Sre0FYe019JxFikrALVoD5UGa8/t3yOJEpEhxC5xKiENKKhUkTpEItMTRAjHw2SCpZw==} + engines: {node: '>=v18'} + dev: true + + /@commitlint/top-level@19.0.0: + resolution: {integrity: sha512-KKjShd6u1aMGNkCkaX4aG1jOGdn7f8ZI8TR1VEuNqUOjWTOdcDSsmglinglJ18JTjuBX5I1PtjrhQCRcixRVFQ==} + engines: {node: '>=v18'} + dependencies: + find-up: 7.0.0 + dev: true + + /@commitlint/types@19.0.3: + resolution: {integrity: sha512-tpyc+7i6bPG9mvaBbtKUeghfyZSDgWquIDfMgqYtTbmZ9Y9VzEm2je9EYcQ0aoz5o7NvGS+rcDec93yO08MHYA==} + engines: {node: '>=v18'} + dependencies: + '@types/conventional-commits-parser': 5.0.0 + chalk: 5.3.0 + dev: true + /@confio/ics23@0.6.8: resolution: {integrity: sha512-wB6uo+3A50m0sW/EWcU64xpV/8wShZ6bMTa7pF8eYsTrSkQA7oLUIJcs/wb8g4y2Oyq701BaGiO6n/ak5WXO1w==} dependencies: @@ -1265,7 +1558,7 @@ packages: '@cosmjs/socket': 0.32.2 '@cosmjs/stream': 0.32.2 '@cosmjs/utils': 0.32.2 - axios: 1.6.5 + axios: 1.6.7 readonly-date: 1.0.0 xstream: 11.14.0 transitivePeerDependencies: @@ -1290,12 +1583,22 @@ packages: resolution: {integrity: sha512-Gg5t+eR7vPJMAmhkFt6CZrzPd0EKpAslWwk5rFVYZpJsM8JG5KT9XQ99hgNM3Ov6ScNoIWbXkpX27F6A9cXR4Q==} dev: false - /@dydxprotocol/v4-abacus@1.4.13: - resolution: {integrity: sha512-+Avd0TvbdQ4OvwwMOpQgLMW0+yQ87FTYTWHZIkmsMsBH5yoYHG751i2bsGp0z+VUKMScCpcibUIGFkgTqLHIlA==} + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@dydxprotocol/v4-abacus@1.7.18: + resolution: {integrity: sha512-nskKgvkxzd6Snxt2oWYvRGKCmOVU6WCV8oc4SdY7ajqR0KZvUxW+Dv3YH+8rKr+uOTyPz09qJbSd0rhuYjg06A==} + dependencies: + '@js-joda/core': 3.2.0 + format-util: 1.0.5 dev: false - /@dydxprotocol/v4-client-js@1.0.20: - resolution: {integrity: sha512-dXKW2NC1XlVVIRKvHWVDofLZSCPTJAaRY5eXzxH5CcXpnl2kdXorr7ykqWZxW0jHFPWWvRSJtUDqZN1qFrEe/w==} + /@dydxprotocol/v4-client-js@1.1.10: + resolution: {integrity: sha512-XesAXsDJXVJ3qg7Ffexh9u//4DjglNA1y9ziIvgTg7bGj9m6BsK1rS9tWBejUNt8vdAVaaMo2zjRYI14iaUoeg==} dependencies: '@cosmjs/amino': 0.32.2 '@cosmjs/encoding': 0.32.2 @@ -1304,7 +1607,7 @@ packages: '@cosmjs/stargate': 0.32.2 '@cosmjs/tendermint-rpc': 0.32.2 '@cosmjs/utils': 0.32.2 - '@dydxprotocol/v4-proto': 4.0.0-dev.0 + '@dydxprotocol/v4-proto': 5.0.0-dev.0 '@osmonauts/lcd': 0.6.0 '@scure/bip32': 1.3.2 '@scure/bip39': 1.2.1 @@ -1323,15 +1626,14 @@ packages: - utf-8-validate dev: false - /@dydxprotocol/v4-localization@1.1.35: - resolution: {integrity: sha512-q5JFYoL/QanHXOtFqRa2owBZJibi1sMpSm3dAcxs9x0/xe8mo6fWcnbQfhl8k7g0/tv7PsBc+e3rbWD0EfvGiA==} + /@dydxprotocol/v4-localization@1.1.86: + resolution: {integrity: sha512-xaLw6reqdmK2KAC5Ct3TdVw40nnnIQHVFFPZ/s4+vd2Ejd8XCHDiHVmYbcft8hftZEwBPd3V6Zd/X4c3Ioyw1g==} dev: false - /@dydxprotocol/v4-proto@4.0.0-dev.0: - resolution: {integrity: sha512-PC/xq5YJIisAd3jjIULJGrnujbrYkr5h4ehepnLc6U34nJT720iumKVMiPaezwRC+kHKTI1culpKNnlMnbeYBA==} + /@dydxprotocol/v4-proto@5.0.0-dev.0: + resolution: {integrity: sha512-AdwfSF/nWyunEyhJudY1b3tniFLZJDs5XtRZAH9uIw76TYeBaDtuF3NQZhgyFJgWAnrPh2a4YphPTUeaqe1feQ==} dependencies: protobufjs: 6.11.4 - dev: false /@emotion/is-prop-valid@1.2.1: resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} @@ -1789,6 +2091,38 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@ethereumjs/common@3.2.0: + resolution: {integrity: sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==} + dependencies: + '@ethereumjs/util': 8.1.0 + crc-32: 1.2.2 + dev: false + + /@ethereumjs/rlp@4.0.1: + resolution: {integrity: sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==} + engines: {node: '>=14'} + hasBin: true + dev: false + + /@ethereumjs/tx@4.2.0: + resolution: {integrity: sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==} + engines: {node: '>=14'} + dependencies: + '@ethereumjs/common': 3.2.0 + '@ethereumjs/rlp': 4.0.1 + '@ethereumjs/util': 8.1.0 + ethereum-cryptography: 2.1.2 + dev: false + + /@ethereumjs/util@8.1.0: + resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} + engines: {node: '>=14'} + dependencies: + '@ethereumjs/rlp': 4.0.1 + ethereum-cryptography: 2.1.2 + micro-ftch: 0.3.1 + dev: false + /@ethersproject/abi@5.7.0: resolution: {integrity: sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==} dependencies: @@ -2166,6 +2500,27 @@ packages: tslib: 2.6.2 dev: false + /@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + dependencies: + '@tanstack/react-virtual': 3.2.0(react-dom@18.2.0)(react@18.2.0) + client-only: 0.0.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@heroicons/react@2.1.3(react@18.2.0): + resolution: {integrity: sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==} + peerDependencies: + react: '>= 16' + dependencies: + react: 18.2.0 + dev: false + /@humanwhocodes/config-array@0.11.10: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} @@ -2243,6 +2598,46 @@ packages: multiformats: 9.9.0 dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dependencies: + jest-get-type: 29.6.3 + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.10.5 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -2282,6 +2677,17 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@js-joda/core@3.2.0: + resolution: {integrity: sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==} + dev: false + /@js-joda/core@5.5.3: resolution: {integrity: sha512-7dqNYwG8gCt4hfg5PKgM7xLEcgSBcx/UgC92OMnhMmvAnq11QzDFPrxUkNR/u5kn17WWLZ8beZ4A3Qrz4pZcmQ==} dev: false @@ -2434,6 +2840,23 @@ packages: '@lit-labs/ssr-dom-shim': 1.1.2 dev: false + /@ljharb/through@2.3.13: + resolution: {integrity: sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /@marsidev/react-turnstile@0.4.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-uZusUW9mPr0csWpls8bApe5iuRK0YK7H1PCKqfM4djW3OA9GB9rU68irjk7xRO8qlHyj0aDTeVu9tTLPExBO4Q==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@mdx-js/mdx@3.0.0: resolution: {integrity: sha512-Icm0TBKBLYqroYbNW3BPnzMGn+7mwpQOK310aZ7+fkCtiU3aqv2cdcX+nd0Ydo3wI5Rx8bX2Z2QmGb/XcAClCw==} dependencies: @@ -2475,18 +2898,111 @@ packages: react: 18.2.0 dev: true + /@metamask/abi-utils@1.2.0: + resolution: {integrity: sha512-Hf7fnBDM9ptCPDtq/wQffWbw859CdVGMwlpWUEsTH6gLXhXONGrRXHA2piyYPRuia8YYTdJvRC/zSK1/nyLvYg==} + engines: {node: '>=14.0.0'} + dependencies: + '@metamask/utils': 3.6.0 + superstruct: 1.0.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@metamask/eth-json-rpc-provider@1.0.1: + resolution: {integrity: sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==} + engines: {node: '>=14.0.0'} + dependencies: + '@metamask/json-rpc-engine': 7.3.3 + '@metamask/safe-event-emitter': 3.1.1 + '@metamask/utils': 5.0.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@metamask/eth-sig-util@6.0.2: + resolution: {integrity: sha512-D6IIefM2vS+4GUGGtezdBbkwUYQC4bCosYx/JteUuF0zfe6lyxR4cruA8+2QHoUg7F7edNH1xymYpqYq1BeOkw==} + engines: {node: '>=14.0.0'} + dependencies: + '@ethereumjs/util': 8.1.0 + '@metamask/abi-utils': 1.2.0 + '@metamask/utils': 5.0.2 + ethereum-cryptography: 2.1.2 + ethjs-util: 0.1.6 + tweetnacl: 1.0.3 + tweetnacl-util: 0.15.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@metamask/json-rpc-engine@7.3.3: + resolution: {integrity: sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==} + engines: {node: '>=16.0.0'} + dependencies: + '@metamask/rpc-errors': 6.2.1 + '@metamask/safe-event-emitter': 3.1.1 + '@metamask/utils': 8.4.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@metamask/rpc-errors@6.2.1: + resolution: {integrity: sha512-VTgWkjWLzb0nupkFl1duQi9Mk8TGT9rsdnQg6DeRrYEFxtFOh0IF8nAwxM/4GWqDl6uIB06lqUBgUrAVWl62Bw==} + engines: {node: '>=16.0.0'} + dependencies: + '@metamask/utils': 8.4.0 + fast-safe-stringify: 2.1.1 + transitivePeerDependencies: + - supports-color + dev: false + /@metamask/safe-event-emitter@2.0.0: resolution: {integrity: sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==} dev: false + /@metamask/safe-event-emitter@3.1.1: + resolution: {integrity: sha512-ihb3B0T/wJm1eUuArYP4lCTSEoZsClHhuWyfo/kMX3m/odpqNcPfsz5O2A3NT7dXCAgWPGDQGPqygCpgeniKMw==} + engines: {node: '>=12.0.0'} + dev: false + /@metamask/utils@3.6.0: resolution: {integrity: sha512-9cIRrfkWvHblSiNDVXsjivqa9Ak0RYo/1H6tqTqTbAx+oBK2Sva0lWDHxGchOqA7bySGUJKAWSNJvH6gdHZ0gQ==} engines: {node: '>=14.0.0'} dependencies: + '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 debug: 4.3.4(supports-color@5.5.0) - semver: 7.5.4 + semver: 7.6.0 + superstruct: 1.0.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@metamask/utils@5.0.2: + resolution: {integrity: sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==} + engines: {node: '>=14.0.0'} + dependencies: + '@ethereumjs/tx': 4.2.0 + '@types/debug': 4.1.12 + debug: 4.3.4(supports-color@5.5.0) + semver: 7.6.0 + superstruct: 1.0.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@metamask/utils@8.4.0: + resolution: {integrity: sha512-dbIc3C7alOe0agCuBHM1h71UaEaEqOk2W8rAtEn8QGz4haH2Qq7MoK6i7v2guzvkJVVh79c+QCzIqphC3KvrJg==} + engines: {node: '>=16.0.0'} + dependencies: + '@ethereumjs/tx': 4.2.0 + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.5 + '@types/debug': 4.1.12 + debug: 4.3.4(supports-color@5.5.0) + pony-cause: 2.1.10 + semver: 7.6.0 superstruct: 1.0.3 + uuid: 9.0.1 transitivePeerDependencies: - supports-color dev: false @@ -2656,6 +3172,10 @@ packages: outvariant: 1.4.2 dev: true + /@open-draft/until@1.0.3: + resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} + dev: true + /@open-draft/until@2.1.0: resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} dev: true @@ -2811,22 +3331,138 @@ packages: '@parcel/watcher-win32-x64': 2.3.0 dev: false - /@pkgr/utils@2.4.2: - resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + /@percy/appium-app@2.0.4: + resolution: {integrity: sha512-H2TBFTOPJITmkhjudVPbhHZ1NXRl1P6ty+B67nWKzExdjWR6cfuTedDh/3N0qN49CI/gBPkIuskk5EekdPx8gg==} + engines: {node: '>=14'} dependencies: - cross-spawn: 7.0.3 - fast-glob: 3.3.1 - is-glob: 4.0.3 - open: 9.1.0 - picocolors: 1.0.0 - tslib: 2.6.1 + '@percy/sdk-utils': 1.28.1 + tmp: 0.2.3 dev: true - /@protobufjs/aspromise@1.1.2: - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + /@percy/sdk-utils@1.28.1: + resolution: {integrity: sha512-joS3i5wjFYXRSVL/NbUvip+bB7ErgwNjoDcID31l61y/QaSYUVCOxl/Fy4nvePJtHVyE1hpV0O7XO3tkoG908g==} + engines: {node: '>=14'} + dev: true - /@protobufjs/base64@1.1.2: + /@percy/selenium-webdriver@2.0.5: + resolution: {integrity: sha512-bNj52xQm02dY872loFa+8OwyuGcdYHYvCKflmSEsF9EDRiSDj0Wr+XP+DDIgDAl9xXschA7OOdXCLTWV4zEQWA==} + engines: {node: '>=14'} + dependencies: + '@percy/sdk-utils': 1.28.1 + node-request-interceptor: 0.6.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@pkgr/utils@2.4.2: + resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dependencies: + cross-spawn: 7.0.3 + fast-glob: 3.3.1 + is-glob: 4.0.3 + open: 9.1.0 + picocolors: 1.0.0 + tslib: 2.6.1 + dev: true + + /@privy-io/react-auth@1.59.9(@babel/core@7.22.5)(@types/react@18.2.14)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(typescript@5.1.3): + resolution: {integrity: sha512-JxxMSbMbsF7GspWXR0tjXL1BvZzrG0+EMfMOqBQ0+JAPy9XS+Wngu48IV4bYjdR6lxQrWY8kl3cQ8pIiWwZCkA==} + peerDependencies: + react: ^18 + react-dom: ^18 + dependencies: + '@coinbase/wallet-sdk': 3.9.3 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/contracts': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/providers': 5.7.2 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/units': 5.7.0 + '@headlessui/react': 1.7.18(react-dom@18.2.0)(react@18.2.0) + '@heroicons/react': 2.1.3(react@18.2.0) + '@marsidev/react-turnstile': 0.4.1(react-dom@18.2.0)(react@18.2.0) + '@metamask/eth-sig-util': 6.0.2 + '@walletconnect/ethereum-provider': 2.12.0(@types/react@18.2.14)(encoding@0.1.13)(react@18.2.0) + '@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0) + base64-js: 1.5.1 + dotenv: 16.4.5 + encoding: 0.1.13 + eventemitter3: 5.0.1 + fast-password-entropy: 1.1.1 + jose: 4.15.5 + js-cookie: 3.0.5 + libphonenumber-js: 1.10.59 + lokijs: 1.5.12 + md5: 2.3.0 + mipd: 0.0.5(typescript@5.1.3) + ofetch: 1.3.3 + pino-pretty: 10.3.1 + qrcode: 1.5.3 + react: 18.2.0 + react-device-detect: 2.2.3(react-dom@18.2.0)(react@18.2.0) + react-dom: 18.2.0(react@18.2.0) + react-use: 17.5.0(react-dom@18.2.0)(react@18.2.0) + secure-password-utilities: 0.2.1 + styled-components: 5.3.11(@babel/core@7.22.5)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + tinycolor2: 1.6.0 + uuid: 9.0.1 + web3-core: 1.10.4(encoding@0.1.13) + web3-core-helpers: 1.10.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@babel/core' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - react-is + - supports-color + - typescript + - utf-8-validate + - zod + dev: false + + /@privy-io/wagmi-connector@0.1.13(@privy-io/react-auth@1.59.9)(react-dom@18.2.0)(react@18.2.0)(viem@1.20.0)(wagmi@1.4.13): + resolution: {integrity: sha512-dbel4pYvbJM+28m12DE7LvEKzJ8ni/rDkuHpF3RGwkph+HsgDNDxJy4OTgUjaKi6yJsjZ5nvhsZdNNVXbVFKkg==} + peerDependencies: + '@privy-io/react-auth': ^1.33.0 + react: ^18 + react-dom: ^18 + viem: '>=0.3.35' + wagmi: '>=1.4.12 <2' + dependencies: + '@privy-io/react-auth': 1.59.9(@babel/core@7.22.5)(@types/react@18.2.14)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(typescript@5.1.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + viem: 1.20.0(typescript@5.1.3) + wagmi: 1.4.13(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.20.0) + dev: false + + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + /@protobufjs/base64@1.1.2: resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} /@protobufjs/codegen@2.0.4: @@ -2856,6 +3492,45 @@ packages: /@protobufjs/utf8@1.1.0: resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + /@puppeteer/browsers@1.4.6(typescript@5.1.3): + resolution: {integrity: sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==} + engines: {node: '>=16.3.0'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.7.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + debug: 4.3.4(supports-color@5.5.0) + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.0 + tar-fs: 3.0.4 + typescript: 5.1.3 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@puppeteer/browsers@1.9.1: + resolution: {integrity: sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==} + engines: {node: '>=16.3.0'} + hasBin: true + dependencies: + debug: 4.3.4(supports-color@5.5.0) + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.1 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: true + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -3166,6 +3841,31 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.14)(react@18.2.0) + '@types/react': 18.2.14 + '@types/react-dom': 18.2.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-dropdown-menu@2.0.5(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==} peerDependencies: @@ -3253,6 +3953,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-hover-card@1.0.7(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@types/react': 18.2.14 + '@types/react-dom': 18.2.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-icons@1.3.0(react@18.2.0): resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: @@ -3423,6 +4152,36 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@floating-ui/react-dom': 2.0.1(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.14)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + '@types/react': 18.2.14 + '@types/react-dom': 18.2.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-portal@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==} peerDependencies: @@ -3456,6 +4215,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.14 + '@types/react-dom': 18.2.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} peerDependencies: @@ -5410,7 +6190,19 @@ packages: engines: {node: '>=14'} dev: false - /@rollup/pluginutils@5.0.3: + /@rollup/pluginutils@3.1.0(rollup@2.79.1): + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@rollup/pluginutils@5.0.3(rollup@2.79.1): resolution: {integrity: sha512-hfllNN4a80rwNQ9QCxhxuHCGHMAvabXqxNdaChUSSadMre7t4iEUI6fFAhBOn/eIYTgYVhBv7vCLsAJ4u3lf3g==} engines: {node: '>=14.0.0'} peerDependencies: @@ -5422,6 +6214,7 @@ packages: '@types/estree': 1.0.1 estree-walker: 2.0.2 picomatch: 2.3.1 + rollup: 2.79.1 dev: true /@rollup/rollup-android-arm-eabi@4.9.6: @@ -5557,7 +6350,7 @@ packages: /@safe-global/safe-gateway-typescript-sdk@3.9.0: resolution: {integrity: sha512-DxRM/sBBQhv955dPtdo0z2Bf2fXxrzoRUnGyTa3+4Z0RAhcyiqnffRP1Bt3tyuvlyfZnFL0RsvkqDcAIKzq3RQ==} dependencies: - cross-fetch: 3.1.8 + cross-fetch: 3.1.8(encoding@0.1.13) transitivePeerDependencies: - encoding dev: false @@ -5608,6 +6401,15 @@ packages: '@scure/base': 1.1.5 dev: false + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sindresorhus/is@5.6.0: + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + dev: true + /@sindresorhus/merge-streams@1.0.0: resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==} engines: {node: '>=18'} @@ -5635,7 +6437,7 @@ packages: buffer: 6.0.3 fast-stable-stringify: 1.0.0 jayson: 4.1.0 - node-fetch: 2.6.12 + node-fetch: 2.6.12(encoding@0.1.13) rpc-websockets: 7.6.0 superstruct: 0.14.2 transitivePeerDependencies: @@ -6023,6 +6825,13 @@ packages: resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==} dev: true + /@szmarczak/http-timer@5.0.1: + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + dependencies: + defer-to-connect: 2.0.1 + dev: true + /@tanstack/match-sorter-utils@8.8.4: resolution: {integrity: sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==} engines: {node: '>=12'} @@ -6092,12 +6901,97 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false + /@tanstack/react-virtual@3.2.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/virtual-core': 3.2.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@tanstack/virtual-core@3.2.0: + resolution: {integrity: sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==} + dev: false + + /@testing-library/dom@8.20.1: + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} + engines: {node: '>=12'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/runtime': 7.22.10 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + + /@testing-library/webdriverio@3.2.1(webdriverio@8.33.1): + resolution: {integrity: sha512-mgMyCiwW+4zCidmlab9lwcO+UBz+PzlWnz9idDQ4ZS1SIHVSfJwvRLMWi+s3vNGFmc8duQxTiUHf1alW/Z48Og==} + peerDependencies: + webdriverio: '*' + dependencies: + '@babel/runtime': 7.22.10 + '@testing-library/dom': 8.20.1 + simmerjs: 0.5.6 + webdriverio: 8.33.1(typescript@5.1.3) + dev: true + + /@tootallnate/quickjs-emscripten@0.23.0: + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + requiresBuild: true + dev: true + + /@trivago/prettier-plugin-sort-imports@4.3.0(prettier@2.8.8): + resolution: {integrity: sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + dependencies: + '@babel/generator': 7.17.7 + '@babel/parser': 7.23.9 + '@babel/traverse': 7.23.2 + '@babel/types': 7.17.0 + javascript-natural-sort: 0.7.1 + lodash: 4.17.21 + prettier: 2.8.8 + transitivePeerDependencies: + - supports-color + dev: true + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + /@types/acorn@4.0.6: resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} dependencies: '@types/estree': 1.0.5 dev: true + /@types/aria-query@5.0.4: + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -6127,7 +7021,13 @@ packages: '@babel/types': 7.23.9 dev: true - /@types/chai-subset@1.3.3: + /@types/bn.js@5.1.5: + resolution: {integrity: sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==} + dependencies: + '@types/node': 20.10.5 + dev: false + + /@types/chai-subset@1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: '@types/chai': 4.3.5 @@ -6159,6 +7059,12 @@ packages: '@types/node': 20.10.5 dev: false + /@types/conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + dependencies: + '@types/node': 20.10.5 + dev: true + /@types/cookie@0.6.0: resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} dev: true @@ -6226,6 +7132,10 @@ packages: '@types/estree': 1.0.5 dev: true + /@types/estree@0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: true @@ -6234,6 +7144,10 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true + /@types/gitconfiglocal@2.0.3: + resolution: {integrity: sha512-W6hyZux6TrtKfF2I9XNLVcsFr4xRr0T+S6hrJ9nDkhA2vzsFPIEAbnY4vgb6v2yKXQ9MJVcbLsARNlMfg4EVtQ==} + dev: true + /@types/hast@3.0.4: resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} dependencies: @@ -6247,6 +7161,33 @@ packages: hoist-non-react-statics: 3.3.2 dev: false + /@types/http-cache-semantics@4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + requiresBuild: true + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + requiresBuild: true + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + dev: true + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + requiresBuild: true + dependencies: + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/js-cookie@2.2.7: + resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + dev: false + /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true @@ -6288,6 +7229,10 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true + /@types/mocha@10.0.6: + resolution: {integrity: sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==} + dev: true + /@types/ms@0.7.34: resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} @@ -6346,6 +7291,11 @@ packages: resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} dev: true + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + requiresBuild: true + dev: true + /@types/statuses@2.0.4: resolution: {integrity: sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==} dev: true @@ -6358,6 +7308,10 @@ packages: csstype: 3.1.2 dev: false + /@types/triple-beam@1.3.5: + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + dev: true + /@types/trusted-types@2.0.7: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: false @@ -6374,12 +7328,43 @@ packages: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} dev: false + /@types/which@2.0.2: + resolution: {integrity: sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==} + dev: true + /@types/ws@7.4.7: resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} dependencies: '@types/node': 20.10.5 dev: false + /@types/ws@8.5.10: + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + requiresBuild: true + dependencies: + '@types/node': 20.10.5 + dev: true + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + requiresBuild: true + dev: true + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + requiresBuild: true + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + + /@types/yauzl@2.10.3: + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + requiresBuild: true + dependencies: + '@types/node': 20.10.5 + dev: true + optional: true + /@typescript-eslint/eslint-plugin@5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.43.0)(typescript@5.1.3): resolution: {integrity: sha512-78B+anHLF1TI8Jn/cD0Q00TBYdMgjdOn980JfAVa9yw5sop8nyTfVOQAv6LWywkOGLclDBtv5z3oxN4w7jxyNg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6866,6 +7851,14 @@ packages: pretty-format: 27.5.1 dev: true + /@vitest/snapshot@1.3.1: + resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==} + dependencies: + magic-string: 0.30.8 + pathe: 1.1.1 + pretty-format: 29.7.0 + dev: true + /@vitest/spy@0.32.2: resolution: {integrity: sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==} dependencies: @@ -7014,7 +8007,46 @@ packages: '@walletconnect/types': 2.11.0 '@walletconnect/utils': 2.11.0 events: 3.3.0 - isomorphic-unfetch: 3.1.0 + isomorphic-unfetch: 3.1.0(encoding@0.1.13) + lodash.isequal: 4.5.0 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + + /@walletconnect/core@2.12.0(encoding@0.1.13): + resolution: {integrity: sha512-CORck4dRvCpIn6hl2ZtUnjrSJ0JHt9TRteGCViwPyXNSuvXz70RvaIkvPoybYZBGCRQR4WTJ4dMdqeQpuyrL/g==} + dependencies: + '@walletconnect/heartbeat': 1.2.1 + '@walletconnect/jsonrpc-provider': 1.0.13 + '@walletconnect/jsonrpc-types': 1.0.3 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.14 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.0 + '@walletconnect/relay-api': 1.0.9 + '@walletconnect/relay-auth': 1.0.4 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.12.0 + '@walletconnect/utils': 2.12.0 + events: 3.3.0 + isomorphic-unfetch: 3.1.0(encoding@0.1.13) lodash.isequal: 4.5.0 uint8arrays: 3.1.1 transitivePeerDependencies: @@ -7064,7 +8096,7 @@ packages: /@walletconnect/ethereum-provider@2.11.0(@types/react@18.2.14)(react@18.2.0): resolution: {integrity: sha512-YrTeHVjuSuhlUw7SQ6xBJXDuJ6iAC+RwINm9nVhoKYJSHAy3EVSJZOofMKrnecL0iRMtD29nj57mxAInIBRuZA==} dependencies: - '@walletconnect/jsonrpc-http-connection': 1.0.7 + '@walletconnect/jsonrpc-http-connection': 1.0.7(encoding@0.1.13) '@walletconnect/jsonrpc-provider': 1.0.13 '@walletconnect/jsonrpc-types': 1.0.3 '@walletconnect/jsonrpc-utils': 1.0.8 @@ -7095,6 +8127,40 @@ packages: - utf-8-validate dev: false + /@walletconnect/ethereum-provider@2.12.0(@types/react@18.2.14)(encoding@0.1.13)(react@18.2.0): + resolution: {integrity: sha512-sX7vQHTRxByU+3/gY6eDTvt4jxQHfiX6WwqRI08UTN/Ixz+IJSBo3UnNRxNmPaC4vG8zUpsFQ4xYSsDnhfaviw==} + dependencies: + '@walletconnect/jsonrpc-http-connection': 1.0.7(encoding@0.1.13) + '@walletconnect/jsonrpc-provider': 1.0.13 + '@walletconnect/jsonrpc-types': 1.0.3 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0) + '@walletconnect/sign-client': 2.12.0(encoding@0.1.13) + '@walletconnect/types': 2.12.0 + '@walletconnect/universal-provider': 2.12.0(encoding@0.1.13) + '@walletconnect/utils': 2.12.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - encoding + - react + - supports-color + - utf-8-validate + dev: false + /@walletconnect/events@1.0.1: resolution: {integrity: sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==} dependencies: @@ -7110,12 +8176,12 @@ packages: tslib: 1.14.1 dev: false - /@walletconnect/jsonrpc-http-connection@1.0.7: + /@walletconnect/jsonrpc-http-connection@1.0.7(encoding@0.1.13): resolution: {integrity: sha512-qlfh8fCfu8LOM9JRR9KE0s0wxP6ZG9/Jom8M0qsoIQeKF3Ni0FyV4V1qy/cc7nfI46SLQLSl4tgWSfLiE1swyQ==} dependencies: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/safe-json': 1.0.2 - cross-fetch: 3.1.8 + cross-fetch: 3.1.8(encoding@0.1.13) tslib: 1.14.1 transitivePeerDependencies: - encoding @@ -7210,7 +8276,7 @@ packages: /@walletconnect/legacy-provider@2.0.0: resolution: {integrity: sha512-A8xPebMI1A+50HbWwTpFCbwP7G+1NGKdTKyg8BUUg3h3Y9JucpC1W6w/x0v1Xw7qFEqQnz74LoIN/A3ytH9xrQ==} dependencies: - '@walletconnect/jsonrpc-http-connection': 1.0.7 + '@walletconnect/jsonrpc-http-connection': 1.0.7(encoding@0.1.13) '@walletconnect/jsonrpc-provider': 1.0.13 '@walletconnect/legacy-client': 2.0.0 '@walletconnect/legacy-modal': 2.0.0 @@ -7246,6 +8312,13 @@ packages: tslib: 1.14.1 dev: false + /@walletconnect/logger@2.1.0: + resolution: {integrity: sha512-lyCRHlxlBHxvj1fJXa2YOW4whVNucPKF7Oc0D1UvYhfArpIIjlJJiTe5cLm8g4ZH4z5lKp14N/c9oRHlyv5v4A==} + dependencies: + '@walletconnect/safe-json': 1.0.2 + pino: 7.11.0 + dev: false + /@walletconnect/modal-core@2.6.2(@types/react@18.2.14)(react@18.2.0): resolution: {integrity: sha512-cv8ibvdOJQv2B+nyxP9IIFdxvQznMz8OOr/oR/AaUZym4hjXNL/l1a2UlSQBXrVjo3xxbouMxLb3kBsHoYP2CA==} dependencies: @@ -7371,6 +8444,37 @@ packages: - utf-8-validate dev: false + /@walletconnect/sign-client@2.12.0(encoding@0.1.13): + resolution: {integrity: sha512-JUHJVZtW9iJmn3I2byLzhMRSFiQicTPU92PLuHIF2nG98CqsvlPn8Cu8Cx5CEPFrxPQWwLA+Dv/F+wuSgQiD/w==} + dependencies: + '@walletconnect/core': 2.12.0(encoding@0.1.13) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.0.1 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.12.0 + '@walletconnect/utils': 2.12.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + /@walletconnect/time@1.0.2: resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} dependencies: @@ -7427,10 +8531,35 @@ packages: - supports-color dev: false + /@walletconnect/types@2.12.0: + resolution: {integrity: sha512-uhB3waGmujQVJcPgJvGOpB8RalgYSBT+HpmVbfl4Qe0xJyqpRUo4bPjQa0UYkrHaW20xIw94OuP4+FMLYdeemg==} + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.1 + '@walletconnect/jsonrpc-types': 1.0.3 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.0.1 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/kv' + - supports-color + dev: false + /@walletconnect/universal-provider@2.11.0: resolution: {integrity: sha512-zgJv8jDvIMP4Qse/D9oIRXGdfoNqonsrjPZanQ/CHNe7oXGOBiQND2IIeX+tS0H7uNA0TPvctljCLiIN9nw4eA==} dependencies: - '@walletconnect/jsonrpc-http-connection': 1.0.7 + '@walletconnect/jsonrpc-http-connection': 1.0.7(encoding@0.1.13) '@walletconnect/jsonrpc-provider': 1.0.13 '@walletconnect/jsonrpc-types': 1.0.3 '@walletconnect/jsonrpc-utils': 1.0.8 @@ -7458,6 +8587,37 @@ packages: - utf-8-validate dev: false + /@walletconnect/universal-provider@2.12.0(encoding@0.1.13): + resolution: {integrity: sha512-CMo10Lh6/DyCznVRMg1nHptWCTeVqMzXBcPNNyCnr3SazE0Shsne/5v/7Kr6j+Yts2hVbLp6lkI2F9ZAFpL6ug==} + dependencies: + '@walletconnect/jsonrpc-http-connection': 1.0.7(encoding@0.1.13) + '@walletconnect/jsonrpc-provider': 1.0.13 + '@walletconnect/jsonrpc-types': 1.0.3 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.0.1 + '@walletconnect/sign-client': 2.12.0(encoding@0.1.13) + '@walletconnect/types': 2.12.0 + '@walletconnect/utils': 2.12.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + /@walletconnect/utils@2.10.6: resolution: {integrity: sha512-oRsWWhN2+hi3aiDXrQEOfysz6FHQJGXLsNQPVt+WIBJplO6Szmdau9dbleD88u1iiT4GKPqE0R9FOYvvPm1H/w==} dependencies: @@ -7524,6 +8684,39 @@ packages: - supports-color dev: false + /@walletconnect/utils@2.12.0: + resolution: {integrity: sha512-GIpfHUe1Bjp1Tjda0SkJEizKOT2biuv7VPFnKsOLT1T+8QxEP9NruC+K2UUEvijS1Qr/LKH9P5004RYNgrch+w==} + dependencies: + '@stablelib/chacha20poly1305': 1.0.1 + '@stablelib/hkdf': 1.0.1 + '@stablelib/random': 1.0.2 + '@stablelib/sha256': 1.0.1 + '@stablelib/x25519': 1.0.3 + '@walletconnect/relay-api': 1.0.9 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.12.0 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/kv' + - supports-color + dev: false + /@walletconnect/window-getters@1.0.1: resolution: {integrity: sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==} dependencies: @@ -7537,29 +8730,260 @@ packages: tslib: 1.14.1 dev: false - /@web-std/blob@3.0.4: - resolution: {integrity: sha512-+dibyiw+uHYK4dX5cJ7HA+gtDAaUUe6JsOryp2ZpAC7h4ICsh49E34JwHoEKPlPvP0llCrNzz45vvD+xX5QDBg==} - dependencies: - '@web-std/stream': 1.0.0 - web-encoding: 1.1.5 - dev: true - - /@web-std/fetch@3.0.3: - resolution: {integrity: sha512-PtaKr6qvw2AmKChugzhQWuTa12dpbogHRBxwcleAZ35UhWucnfD4N+g3f7qYK2OeioSWTK3yMf6n/kOOfqxHaQ==} - engines: {node: ^10.17 || >=12.3} + /@wdio/browserstack-service@8.33.1(@wdio/cli@8.33.1)(typescript@5.1.3): + resolution: {integrity: sha512-CcGZSp0xJXo0eHWM/LQBtTmV0Y9NsvJuVsj4tmAf5d/92GNHcF6fW22Udsp7or9g2tJyj6LmJI0lcaCOprliiw==} + engines: {node: ^16.13 || >=18} + peerDependencies: + '@wdio/cli': ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@web-std/blob': 3.0.4 - '@web-std/form-data': 3.0.2 - '@web3-storage/multipart-parser': 1.0.0 - data-uri-to-buffer: 3.0.1 + '@percy/appium-app': 2.0.4 + '@percy/selenium-webdriver': 2.0.5 + '@types/gitconfiglocal': 2.0.3 + '@wdio/cli': 8.33.1(typescript@5.1.3) + '@wdio/logger': 8.28.0 + '@wdio/reporter': 8.32.4 + '@wdio/types': 8.32.4 + browserstack-local: 1.5.5 + chalk: 5.3.0 + csv-writer: 1.6.0 + formdata-node: 5.0.1 + git-repo-info: 2.1.1 + gitconfiglocal: 2.1.0 + got: 12.6.1 + uuid: 9.0.1 + webdriverio: 8.33.1(typescript@5.1.3) + winston-transport: 4.7.0 + yauzl: 3.1.2 + transitivePeerDependencies: + - bufferutil + - devtools + - encoding + - supports-color + - typescript + - utf-8-validate dev: true - /@web-std/fetch@4.1.2: - resolution: {integrity: sha512-NUX+nnCTjC6URLtFC2O9dX9FtzCS5nlbF/vZwkPlheq5h6+rQxluH/aO+ORbLjGY4z4iQOulfEGoHcXwx5GFUQ==} - engines: {node: ^10.17 || >=12.3} + /@wdio/cli@8.33.1(typescript@5.1.3): + resolution: {integrity: sha512-Ngt5R6YAmErkSKnWLWt1JilLIKDPIB0P93bzQhb9bQhmg1arFBcl75uiwe6kf6T355vzcNslMaEJyeuqGChmCg==} + engines: {node: ^16.13 || >=18} + hasBin: true dependencies: - '@web-std/blob': 3.0.4 - '@web-std/form-data': 3.0.2 + '@types/node': 20.10.5 + '@vitest/snapshot': 1.3.1 + '@wdio/config': 8.33.1 + '@wdio/globals': 8.33.1(typescript@5.1.3) + '@wdio/logger': 8.28.0 + '@wdio/protocols': 8.32.0 + '@wdio/types': 8.32.4 + '@wdio/utils': 8.33.1 + async-exit-hook: 2.0.1 + chalk: 5.3.0 + chokidar: 3.5.3 + cli-spinners: 2.9.2 + dotenv: 16.4.5 + ejs: 3.1.9 + execa: 8.0.1 + import-meta-resolve: 4.0.0 + inquirer: 9.2.12 + lodash.flattendeep: 4.4.0 + lodash.pickby: 4.6.0 + lodash.union: 4.6.0 + read-pkg-up: 10.0.0 + recursive-readdir: 2.2.3 + webdriverio: 8.33.1(typescript@5.1.3) + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - devtools + - encoding + - supports-color + - typescript + - utf-8-validate + dev: true + + /@wdio/config@8.33.1: + resolution: {integrity: sha512-JB7+tRkEsDJ4QAgJIZ3AaZvlp8pfBH6A5cKcGsaOuLVYMnsRPVkEGQc6n2akN9EPlDA2UjyrPOX6KZHbsSty7w==} + engines: {node: ^16.13 || >=18} + dependencies: + '@wdio/logger': 8.28.0 + '@wdio/types': 8.32.4 + '@wdio/utils': 8.33.1 + decamelize: 6.0.0 + deepmerge-ts: 5.1.0 + glob: 10.3.10 + import-meta-resolve: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@wdio/globals@8.33.1(typescript@5.1.3): + resolution: {integrity: sha512-1ud9oq7n9MMNywS/FoMRRWqW6uhcoxgnpXoGeLE2Tr+4f937ABOl+sfZgjycXujyvR7yTL8AROOYajp1Yuv1Xg==} + engines: {node: ^16.13 || >=18} + optionalDependencies: + expect-webdriverio: 4.11.9(typescript@5.1.3) + webdriverio: 8.33.1(typescript@5.1.3) + transitivePeerDependencies: + - bufferutil + - devtools + - encoding + - supports-color + - typescript + - utf-8-validate + dev: true + + /@wdio/local-runner@8.33.1(typescript@5.1.3): + resolution: {integrity: sha512-eQp12wHIkyh5zl9fun1qjv5Qvf4mCHPgLs5sKbfo3OK4LadzmD4/QNvDG8DYq/9cyuhVvnHgbLQ3XAnkoPde3w==} + engines: {node: ^16.13 || >=18} + dependencies: + '@types/node': 20.10.5 + '@wdio/logger': 8.28.0 + '@wdio/repl': 8.24.12 + '@wdio/runner': 8.33.1(typescript@5.1.3) + '@wdio/types': 8.32.4 + async-exit-hook: 2.0.1 + split2: 4.2.0 + stream-buffers: 3.0.2 + transitivePeerDependencies: + - bufferutil + - devtools + - encoding + - supports-color + - typescript + - utf-8-validate + dev: true + + /@wdio/logger@8.28.0: + resolution: {integrity: sha512-/s6zNCqwy1hoc+K4SJypis0Ud0dlJ+urOelJFO1x0G0rwDRWyFiUP6ijTaCcFxAm29jYEcEPWijl2xkVIHwOyA==} + engines: {node: ^16.13 || >=18} + dependencies: + chalk: 5.3.0 + loglevel: 1.9.1 + loglevel-plugin-prefix: 0.8.4 + strip-ansi: 7.1.0 + dev: true + + /@wdio/mocha-framework@8.33.1: + resolution: {integrity: sha512-CxYLE22+tgnMnruElvDGJGR+dE0pxvMZ95agIUYYen69DJ705a74XtTR6zX9COWu6RooBezHgEs3fXev0XL79Q==} + engines: {node: ^16.13 || >=18} + dependencies: + '@types/mocha': 10.0.6 + '@types/node': 20.10.5 + '@wdio/logger': 8.28.0 + '@wdio/types': 8.32.4 + '@wdio/utils': 8.33.1 + mocha: 10.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@wdio/protocols@8.32.0: + resolution: {integrity: sha512-inLJRrtIGdTz/YPbcsvpSvPlYQFTVtF3OYBwAXhG2FiP1ZwE1CQNLP/xgRGye1ymdGCypGkexRqIx3KBGm801Q==} + dev: true + + /@wdio/repl@8.24.12: + resolution: {integrity: sha512-321F3sWafnlw93uRTSjEBVuvWCxTkWNDs7ektQS15drrroL3TMeFOynu4rDrIz0jXD9Vas0HCD2Tq/P0uxFLdw==} + engines: {node: ^16.13 || >=18} + dependencies: + '@types/node': 20.10.5 + dev: true + + /@wdio/reporter@8.32.4: + resolution: {integrity: sha512-kZXbyNuZSSpk4kBavDb+ac25ODu9NVZED6WwZafrlMSnBHcDkoMt26Q0Jp3RKUj+FTyuKH0HvfeLrwVkk6QKDw==} + engines: {node: ^16.13 || >=18} + dependencies: + '@types/node': 20.10.5 + '@wdio/logger': 8.28.0 + '@wdio/types': 8.32.4 + diff: 5.2.0 + object-inspect: 1.12.3 + dev: true + + /@wdio/runner@8.33.1(typescript@5.1.3): + resolution: {integrity: sha512-i0eRwMCePKkQocWsdkPQpBb1jELyNR5JCwnmOgM3g9fQI6KAf5D4oEUkNDFL/vD4UtgbSRmux7b7j5G01VvuqQ==} + engines: {node: ^16.13 || >=18} + dependencies: + '@types/node': 20.10.5 + '@wdio/config': 8.33.1 + '@wdio/globals': 8.33.1(typescript@5.1.3) + '@wdio/logger': 8.28.0 + '@wdio/types': 8.32.4 + '@wdio/utils': 8.33.1 + deepmerge-ts: 5.1.0 + expect-webdriverio: 4.11.9(typescript@5.1.3) + gaze: 1.1.3 + webdriver: 8.33.1 + webdriverio: 8.33.1(typescript@5.1.3) + transitivePeerDependencies: + - bufferutil + - devtools + - encoding + - supports-color + - typescript + - utf-8-validate + dev: true + + /@wdio/spec-reporter@8.32.4: + resolution: {integrity: sha512-3TbD/KrK+EhUex5d5/11qSEKqyNiMHqm27my86tdiK0Ltt9pc/9Ybg1YBiWKlzV9U9MI4seVBRZCXltG17ky/A==} + engines: {node: ^16.13 || >=18} + dependencies: + '@wdio/reporter': 8.32.4 + '@wdio/types': 8.32.4 + chalk: 5.3.0 + easy-table: 1.2.0 + pretty-ms: 7.0.1 + dev: true + + /@wdio/types@8.32.4: + resolution: {integrity: sha512-pDPGcCvq0MQF8u0sjw9m4aMI2gAKn6vphyBB2+1IxYriL777gbbxd7WQ+PygMBvYVprCYIkLPvhUFwF85WakmA==} + engines: {node: ^16.13 || >=18} + dependencies: + '@types/node': 20.10.5 + dev: true + + /@wdio/utils@8.33.1: + resolution: {integrity: sha512-W0ArrZbs4M23POv8+FPsgHDFxg+wwklfZgLSsjVq2kpCmBCfIPxKSAOgTo/XrcH4We/OnshgBzxLcI+BHDgi4w==} + engines: {node: ^16.13 || >=18} + dependencies: + '@puppeteer/browsers': 1.9.1 + '@wdio/logger': 8.28.0 + '@wdio/types': 8.32.4 + decamelize: 6.0.0 + deepmerge-ts: 5.1.0 + edgedriver: 5.3.10 + geckodriver: 4.3.3 + get-port: 7.0.0 + import-meta-resolve: 4.0.0 + locate-app: 2.2.24 + safaridriver: 0.1.2 + split2: 4.2.0 + wait-port: 1.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@web-std/blob@3.0.4: + resolution: {integrity: sha512-+dibyiw+uHYK4dX5cJ7HA+gtDAaUUe6JsOryp2ZpAC7h4ICsh49E34JwHoEKPlPvP0llCrNzz45vvD+xX5QDBg==} + dependencies: + '@web-std/stream': 1.0.0 + web-encoding: 1.1.5 + dev: true + + /@web-std/fetch@3.0.3: + resolution: {integrity: sha512-PtaKr6qvw2AmKChugzhQWuTa12dpbogHRBxwcleAZ35UhWucnfD4N+g3f7qYK2OeioSWTK3yMf6n/kOOfqxHaQ==} + engines: {node: ^10.17 || >=12.3} + dependencies: + '@web-std/blob': 3.0.4 + '@web-std/form-data': 3.0.2 + '@web3-storage/multipart-parser': 1.0.0 + data-uri-to-buffer: 3.0.1 + dev: true + + /@web-std/fetch@4.1.2: + resolution: {integrity: sha512-NUX+nnCTjC6URLtFC2O9dX9FtzCS5nlbF/vZwkPlheq5h6+rQxluH/aO+ORbLjGY4z4iQOulfEGoHcXwx5GFUQ==} + engines: {node: ^10.17 || >=12.3} + dependencies: + '@web-std/blob': 3.0.4 + '@web-std/form-data': 3.0.2 '@web-std/stream': 1.0.1 '@web3-storage/multipart-parser': 1.0.0 data-uri-to-buffer: 3.0.1 @@ -7734,6 +9158,10 @@ packages: '@xtuc/long': 4.2.2 dev: true + /@xobotyi/scrollbar-width@1.9.5: + resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + dev: false + /@xtuc/ieee754@1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} dev: true @@ -7754,7 +9182,6 @@ packages: dependencies: jsonparse: 1.3.1 through: 2.3.8 - dev: false /abitype@0.8.7(typescript@5.1.3): resolution: {integrity: sha512-wQ7hV8Yg/yKmGyFpqrNZufCxbszDe5es4AZGYPBitocfSqXtjrTG9JMWFcc4N30ukl2ve48aBTwt7NJxVQdU3w==} @@ -7787,7 +9214,10 @@ packages: engines: {node: '>=6.5'} dependencies: event-target-shim: 5.0.1 - dev: true + + /abortcontroller-polyfill@1.7.5: + resolution: {integrity: sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==} + dev: false /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} @@ -7848,6 +9278,24 @@ packages: resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} dev: false + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + + /agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + /agentkeepalive@4.5.0: resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} engines: {node: '>= 8.0.0'} @@ -7907,6 +9355,11 @@ packages: string-width: 4.2.3 dev: true + /ansi-colors@4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: true + /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -7967,6 +9420,38 @@ packages: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} dev: false + /archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + glob: 10.3.10 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.5.2 + dev: true + + /archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + archiver-utils: 5.0.2 + async: 3.2.5 + buffer-crc32: 1.0.0 + readable-stream: 4.5.2 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + dev: true + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + /arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} dev: false @@ -7982,6 +9467,12 @@ packages: tslib: 2.6.1 dev: false + /aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.2.3 + dev: true + /aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} dependencies: @@ -7995,6 +9486,10 @@ packages: is-array-buffer: 3.0.2 dev: true + /array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + dev: true + /array-includes@3.1.6: resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} engines: {node: '>= 0.4'} @@ -8075,20 +9570,42 @@ packages: resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} dev: true + /ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + requiresBuild: true + dependencies: + tslib: 2.6.2 + dev: true + /astring@1.8.6: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true dev: true + /async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + dev: true + /async-mutex@0.2.6: resolution: {integrity: sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==} dependencies: tslib: 2.6.2 dev: false + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: true + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false + + /atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + dev: true /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} @@ -8099,6 +9616,13 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + /axe-core@4.7.2: resolution: {integrity: sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==} engines: {node: '>=4'} @@ -8136,15 +9660,14 @@ packages: - debug dev: false - /axios@1.6.5: - resolution: {integrity: sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==} + /axios@1.6.7: + resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: follow-redirects: 1.15.3 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: false /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -8152,6 +9675,11 @@ packages: dequal: 2.0.3 dev: true + /b4a@1.6.6: + resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + requiresBuild: true + dev: true + /babel-loader@9.1.2(@babel/core@7.22.5)(webpack@5.89.0): resolution: {integrity: sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==} engines: {node: '>= 14.15.0'} @@ -8190,6 +9718,37 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /bare-events@2.2.1: + resolution: {integrity: sha512-9GYPpsPFvrWBkelIhOhTWtkeZxVxZOdb3VnFTCzlOo3OjvmTvzLoZFUT8kNFACx0vJej6QPney1Cf9BvzCNE/A==} + requiresBuild: true + dev: true + optional: true + + /bare-fs@2.2.2: + resolution: {integrity: sha512-X9IqgvyB0/VA5OZJyb5ZstoN62AzD7YxVGog13kkfYWYqJYcK0kcqLZ6TrmH5qr4/8//ejVcX4x/a0UvaogXmA==} + requiresBuild: true + dependencies: + bare-events: 2.2.1 + bare-os: 2.2.1 + bare-path: 2.1.0 + streamx: 2.16.1 + dev: true + optional: true + + /bare-os@2.2.1: + resolution: {integrity: sha512-OwPyHgBBMkhC29Hl3O4/YfxW9n7mdTr2+SsO29XBWKKJsbgj3mnorDB80r5TiCQgQstgE5ga1qNYrpes6NvX2w==} + requiresBuild: true + dev: true + optional: true + + /bare-path@2.1.0: + resolution: {integrity: sha512-DIIg7ts8bdRKwJRJrUMy/PICEaQZaPGZ26lsSx9MJSwIhSrcdHn7/C8W+XmnG/rKi6BaRcz+JO00CjZteybDtw==} + requiresBuild: true + dependencies: + bare-os: 2.2.1 + dev: true + optional: true + /base-x@3.0.9: resolution: {integrity: sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==} dependencies: @@ -8199,6 +9758,12 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + requiresBuild: true + dev: true + /bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} dev: true @@ -8232,6 +9797,13 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + /binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + dev: true + /bind-decorator@1.0.11: resolution: {integrity: sha512-yzkH0uog6Vv/vQ9+rhSKxecnqGUZHYncg7qS7voz3Q76+TAi1SGiOKk2mlOvusQnFz9Dc4BC/NMkeXu11YgjJg==} dev: false @@ -8296,10 +9868,18 @@ packages: multiformats: 9.9.0 dev: true + /bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + dev: true + /blueimp-md5@2.19.0: resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} dev: true + /bn.js@4.11.6: + resolution: {integrity: sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==} + dev: false + /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} dev: false @@ -8347,6 +9927,12 @@ packages: balanced-match: 1.0.2 concat-map: 0.0.1 + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} @@ -8374,6 +9960,10 @@ packages: resolution: {integrity: sha512-+12sHB+Br8HIh6VAMVEG5r3UXCyESIgDW7kzk3BjIXa43DVqVwL7GC5TW3jeh+72dtcH99pPVpw0X8i0jt+/kw==} dev: true + /browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: true + /browserify-zlib@0.2.0: resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} dependencies: @@ -8401,6 +9991,18 @@ packages: update-browserslist-db: 1.0.13(browserslist@4.22.2) dev: true + /browserstack-local@1.5.5: + resolution: {integrity: sha512-jKne7yosrMcptj3hqxp36TP9k0ZW2sCqhyurX24rUL4G3eT7OLgv+CSQN8iq5dtkv5IK+g+v8fWvsiC/S9KxMg==} + dependencies: + agent-base: 6.0.2 + https-proxy-agent: 5.0.1 + is-running: 2.1.0 + ps-tree: 1.2.0 + temp-fs: 0.9.9 + transitivePeerDependencies: + - supports-color + dev: true + /bs58@4.0.1: resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} dependencies: @@ -8415,10 +10017,25 @@ packages: safe-buffer: 5.2.1 dev: false + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + requiresBuild: true + dev: true + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true + /buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + dev: true + /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -8432,13 +10049,17 @@ packages: base64-js: 1.5.1 ieee754: 1.2.1 + /buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + dev: true + /bufferutil@4.0.8: resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} engines: {node: '>=6.14.2'} requiresBuild: true dependencies: node-gyp-build: 4.6.0 - dev: false /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} @@ -8466,12 +10087,41 @@ packages: ylru: 1.3.2 dev: true + /cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + dev: true + + /cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 6.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.1 + responselike: 3.0.0 + dev: true + /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: function-bind: 1.1.1 get-intrinsic: 1.2.1 + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: true + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -8541,6 +10191,12 @@ packages: type-detect: 4.0.8 dev: true + /chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + dependencies: + traverse: 0.3.9 + dev: true + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -8582,6 +10238,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + /check-error@1.0.2: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true @@ -8605,6 +10265,22 @@ packages: engines: {node: '>=6.0'} dev: true + /chromium-bidi@0.4.16(devtools-protocol@0.0.1147663): + resolution: {integrity: sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==} + requiresBuild: true + peerDependencies: + devtools-protocol: '*' + dependencies: + devtools-protocol: 0.0.1147663 + mitt: 3.0.0 + dev: true + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + requiresBuild: true + dev: true + /cipher-base@1.0.4: resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} dependencies: @@ -8647,6 +10323,15 @@ packages: engines: {node: '>= 10'} dev: true + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: true + + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: false + /clipboardy@3.0.0: resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8664,6 +10349,14 @@ packages: wrap-ansi: 6.2.0 dev: false + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -8676,6 +10369,7 @@ packages: /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + requiresBuild: true dev: true /clsx@1.2.1: @@ -8748,12 +10442,15 @@ packages: color-string: 1.9.1 dev: false + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: false /comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -8771,10 +10468,34 @@ packages: /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + dev: true + /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true + /compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + dev: true + + /compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.5.2 + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -8813,6 +10534,31 @@ packages: engines: {node: '>= 0.6'} dev: true + /conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + dependencies: + compare-func: 2.0.0 + dev: true + + /conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + dependencies: + compare-func: 2.0.0 + dev: true + + /conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + dev: true + /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -8850,6 +10596,25 @@ packages: toggle-selection: 1.0.6 dev: false + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + requiresBuild: true + dev: true + + /cosmiconfig-typescript-loader@5.0.0(@types/node@20.3.1)(cosmiconfig@8.3.6)(typescript@5.1.3): + resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} + engines: {node: '>=v16'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=8.2' + typescript: '>=4' + dependencies: + '@types/node': 20.3.1 + cosmiconfig: 8.3.6(typescript@5.1.3) + jiti: 1.21.0 + typescript: 5.1.3 + dev: true + /cosmiconfig@8.2.0: resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} engines: {node: '>=14'} @@ -8860,6 +10625,22 @@ packages: path-type: 4.0.0 dev: true + /cosmiconfig@8.3.6(typescript@5.1.3): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.1.3 + dev: true + /cosmjs-types@0.7.2: resolution: {integrity: sha512-vf2uLyktjr/XVAgEq0DjMxeAWh1yYREe7AMHDKd7EiHVqxBPCaBS+qEEQUkXbR9ndnckqr1sUG8BQhazh4X5lA==} dependencies: @@ -8882,13 +10663,28 @@ packages: resolution: {integrity: sha512-WIdaQ8uW1vIbYvNnAVunkC6yxTrneJC7VQ5UUQ0kuw8b0C0A39KTIpoQHCfc8tV7o9vF4niwRhdXEdfAgQEsQQ==} dependencies: cosmos-directory-types: 0.0.6 - node-fetch-native: 1.6.2 + node-fetch-native: 1.6.4 dev: false /cosmos-directory-types@0.0.6: resolution: {integrity: sha512-9qlQ3kTNTHvhYglTXSnllGqKhrtGB08Weatw56ZqV5OqcmjuZdlY9iMtD00odgQLTEpTSQQL3gFGuqTkGdIDPA==} dev: false + /crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + requiresBuild: true + + /crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + crc-32: 1.2.2 + readable-stream: 4.5.2 + dev: true + /create-hash@1.2.0: resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} dependencies: @@ -8910,14 +10706,26 @@ packages: sha.js: 2.4.11 dev: false - /cross-fetch@3.1.8: + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /cross-fetch@3.1.8(encoding@0.1.13): resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} dependencies: - node-fetch: 2.6.12 + node-fetch: 2.6.12(encoding@0.1.13) transitivePeerDependencies: - encoding dev: false + /cross-fetch@4.0.0(encoding@0.1.13): + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + requiresBuild: true + dependencies: + node-fetch: 2.6.12(encoding@0.1.13) + transitivePeerDependencies: + - encoding + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -8926,6 +10734,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + /crypto-js@4.1.1: resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==} dev: false @@ -8934,10 +10746,21 @@ packages: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} engines: {node: '>=4'} + /css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + dependencies: + hyphenate-style-name: 1.0.4 + dev: false + /css-selector-parser@3.0.4: resolution: {integrity: sha512-pnmS1dbKsz6KA4EW4BznyPL2xxkNDRg62hcD0v8g6DEw2W7hxOln5M953jsp9hmw5Dg57S6o/A8GOn37mbAgcQ==} dev: true + /css-shorthand-properties@1.1.1: + resolution: {integrity: sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==} + requiresBuild: true + dev: true + /css-to-react-native@3.2.0: resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} dependencies: @@ -8945,9 +10768,26 @@ packages: css-color-keywords: 1.0.0 postcss-value-parser: 4.2.0 + /css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + dev: false + + /css-value@0.0.1: + resolution: {integrity: sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==} + requiresBuild: true + dev: true + /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + /csv-writer@1.6.0: + resolution: {integrity: sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==} + dev: true + /d3-array@2.12.1: resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} dependencies: @@ -9030,10 +10870,23 @@ packages: resolution: {integrity: sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==} dev: false + /d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + dependencies: + es5-ext: 0.10.64 + type: 2.7.2 + dev: false + /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true + /dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + dev: true + /data-uri-to-buffer@3.0.1: resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} engines: {node: '>= 6'} @@ -9044,6 +10897,12 @@ packages: engines: {node: '>= 12'} dev: true + /data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + requiresBuild: true + dev: true + /date-time@3.1.0: resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} engines: {node: '>=6'} @@ -9051,10 +10910,25 @@ packages: time-zone: 1.0.0 dev: true + /dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dev: false + /debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} dev: false + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + /debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -9078,6 +10952,19 @@ packages: ms: 2.1.2 supports-color: 5.5.0 + /debug@4.3.4(supports-color@8.1.1): + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -9090,6 +10977,16 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + /decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: true + + /decamelize@6.0.0: + resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} dependencies: @@ -9099,12 +10996,18 @@ packages: /decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} - dev: false /decode-uri-component@0.4.1: resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} engines: {node: '>=14.16'} + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: true + /deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} @@ -9116,10 +11019,39 @@ packages: resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} dev: true + /deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.7 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.4 + is-arguments: 1.1.1 + is-array-buffer: 3.0.2 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.2 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + dev: true + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deepmerge-ts@5.1.0: + resolution: {integrity: sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==} + engines: {node: '>=16.0.0'} + dev: true + /default-browser-id@3.0.0: resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} engines: {node: '>=12'} @@ -9140,10 +11072,25 @@ packages: /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + requiresBuild: true dependencies: clone: 1.0.4 dev: true + /defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: true + /define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -9153,13 +11100,34 @@ packages: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} engines: {node: '>= 0.4'} dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 has-property-descriptors: 1.0.0 object-keys: 1.1.1 + dev: true /defu@6.1.3: resolution: {integrity: sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ==} dev: false + /degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + dev: true + /delay@4.4.1: resolution: {integrity: sha512-aL3AhqtfhOlT/3ai6sWXeqwnw63ATNpnUiN4HL7x9q+My5QtHlO3OIkasmug9LKzpheLdmUKGRKnYXYAS7FQkQ==} engines: {node: '>=6'} @@ -9173,7 +11141,6 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: false /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -9232,11 +11199,42 @@ packages: dequal: 2.0.3 dev: true + /devtools-protocol@0.0.1147663: + resolution: {integrity: sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==} + requiresBuild: true + dev: true + + /devtools-protocol@0.0.1263784: + resolution: {integrity: sha512-k0SCZMwj587w4F8QYbP5iIbSonL6sd3q8aVJch036r9Tv2t9b5/Oq7AiJ/FJvRuORm/pJNXZtrdNNWlpRnl56A==} + requiresBuild: true + dev: true + /diff-sequences@29.4.3: resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dev: true + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /diff@5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: true + + /diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + dev: true + /dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} dev: false @@ -9278,6 +11276,31 @@ packages: esutils: 2.0.3 dev: true + /dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + + /dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dependencies: + is-obj: 2.0.0 + dev: true + + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + /duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + dependencies: + readable-stream: 2.3.8 + dev: true + + /duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dev: true + /duplexify@4.1.2: resolution: {integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==} dependencies: @@ -9291,10 +11314,47 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true + /easy-table@1.2.0: + resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} + dependencies: + ansi-regex: 5.0.1 + optionalDependencies: + wcwidth: 1.0.1 + dev: true + + /edge-paths@3.0.5: + resolution: {integrity: sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==} + engines: {node: '>=14.0.0'} + dependencies: + '@types/which': 2.0.2 + which: 2.0.2 + dev: true + + /edgedriver@5.3.10: + resolution: {integrity: sha512-RFSHYMNtcF1PjaGZCA2rdQQ8hSTLPZgcYgeY1V6dC+tR4NhZXwFAku+8hCbRYh7ZlwKKrTbVu9FwknjFddIuuw==} + hasBin: true + requiresBuild: true + dependencies: + '@wdio/logger': 8.28.0 + decamelize: 6.0.0 + edge-paths: 3.0.5 + node-fetch: 3.3.2 + unzipper: 0.10.14 + which: 4.0.0 + dev: true + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true + /ejs@3.1.9: + resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.8.7 + dev: true + /electron-fetch@1.9.1: resolution: {integrity: sha512-M9qw6oUILGVrcENMSRRefE1MbHPIz0h79EKIeJWK9v563aT9Qkh8aEHPO1H5vi970wPirNY+jO9OpFoLiMsMGA==} engines: {node: '>=6'} @@ -9341,13 +11401,11 @@ packages: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} dependencies: iconv-lite: 0.6.3 - dev: true /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: once: 1.4.0 - dev: false /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} @@ -9372,6 +11430,12 @@ packages: is-arrayish: 0.2.1 dev: true + /error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + dependencies: + stackframe: 1.3.4 + dev: false + /es-abstract@1.22.1: resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} engines: {node: '>= 0.4'} @@ -9417,6 +11481,32 @@ packages: which-typed-array: 1.1.11 dev: true + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: true + /es-module-lexer@1.4.1: resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} dev: true @@ -9445,6 +11535,25 @@ packages: is-symbol: 1.0.4 dev: true + /es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + requiresBuild: true + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + dev: false + + /es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + dev: false + /es6-object-assign@1.1.0: resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==} dev: true @@ -9459,6 +11568,14 @@ packages: es6-promise: 4.2.8 dev: false + /es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + dependencies: + d: 1.0.2 + ext: 1.7.0 + dev: false + /esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -9532,6 +11649,12 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + requiresBuild: true + dev: true + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -9542,6 +11665,19 @@ packages: engines: {node: '>=12'} dev: true + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + requiresBuild: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.27.5)(eslint@8.43.0): resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} engines: {node: ^10.12.0 || >=12.0.0} @@ -9915,6 +12051,16 @@ packages: - supports-color dev: true + /esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.2 + dev: false + /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -9924,6 +12070,13 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + requiresBuild: true + dev: true + /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} @@ -9982,6 +12135,10 @@ packages: '@types/unist': 3.0.2 dev: true + /estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true @@ -10009,6 +12166,19 @@ packages: - supports-color dev: false + /eth-block-tracker@7.1.0: + resolution: {integrity: sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg==} + engines: {node: '>=14.0.0'} + dependencies: + '@metamask/eth-json-rpc-provider': 1.0.1 + '@metamask/safe-event-emitter': 3.1.1 + '@metamask/utils': 5.0.2 + json-rpc-random-id: 1.0.1 + pify: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /eth-json-rpc-filters@5.1.0: resolution: {integrity: sha512-fos+9xmoa1A2Ytsc9eYof17r81BjdJOUcGcgZn4K/tKdCCTb+a8ytEtwlu1op5qsXFDlgGmstTELFrDEc89qEQ==} engines: {node: '>=14.0.0'} @@ -10020,6 +12190,17 @@ packages: pify: 5.0.0 dev: false + /eth-json-rpc-filters@6.0.1: + resolution: {integrity: sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig==} + engines: {node: '>=14.0.0'} + dependencies: + '@metamask/safe-event-emitter': 3.1.1 + async-mutex: 0.2.6 + eth-query: 2.1.2 + json-rpc-engine: 6.1.0 + pify: 5.0.0 + dev: false + /eth-query@2.1.2: resolution: {integrity: sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA==} dependencies: @@ -10039,6 +12220,12 @@ packages: fast-safe-stringify: 2.1.1 dev: false + /ethereum-bloom-filters@1.0.10: + resolution: {integrity: sha512-rxJ5OFN3RwjQxDcFP2Z5+Q9ho4eIdEmSc2ht0fCu8Se9nbXjZ7/031uXoUYJ87KHCOdVeiUuwSnoS7hmYAGVHA==} + dependencies: + js-sha3: 0.8.0 + dev: false + /ethereum-cryptography@2.1.2: resolution: {integrity: sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==} dependencies: @@ -10102,15 +12289,58 @@ packages: - utf-8-validate dev: false + /ethjs-unit@0.1.6: + resolution: {integrity: sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + bn.js: 4.11.6 + number-to-bn: 1.7.0 + dev: false + + /ethjs-util@0.1.6: + resolution: {integrity: sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + is-hex-prefixed: 1.0.0 + strip-hex-prefix: 1.0.0 + dev: false + bundledDependencies: false + + /event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + dev: false + + /event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + dev: true + /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - dev: true + + /eventemitter3@4.0.4: + resolution: {integrity: sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==} + dev: false /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false + /eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -10144,6 +12374,68 @@ packages: strip-final-newline: 3.0.0 dev: true + /export-to-csv@1.2.3: + resolution: + { + integrity: sha512-N+hWdpEQNSn4BeltgusTuW65gC+2B5hmOWe4Eu0gw0NwKq92dzD+5ObSSBUeKTy7k+PGG7xDqaThPg8elZ7k+g==, + } + engines: { node: ^v12.20.0 || >=v14.13.0 } + dev: false + + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + + /expect-webdriverio@4.11.9(typescript@5.1.3): + resolution: {integrity: sha512-nHVLoC4W8wuVAyfpitJ07iDMLjeQ2OeYVjrKEb7dMeG4fqlegzN1SGYnnsKay+KWrte9KzuW1pZ7h5Nmbm/hAQ==} + engines: {node: '>=16 || >=18 || >=20'} + dependencies: + '@vitest/snapshot': 1.3.1 + expect: 29.7.0 + jest-matcher-utils: 29.7.0 + lodash.isequal: 4.5.0 + optionalDependencies: + '@wdio/globals': 8.33.1(typescript@5.1.3) + '@wdio/logger': 8.28.0 + webdriverio: 8.33.1(typescript@5.1.3) + transitivePeerDependencies: + - bufferutil + - devtools + - encoding + - supports-color + - typescript + - utf-8-validate + dev: true + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + + /ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + dependencies: + type: 2.7.2 + dev: false + /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: true @@ -10157,11 +12449,34 @@ packages: tmp: 0.0.33 dev: true + /extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.3.4(supports-color@5.5.0) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + dev: true + /eyes@0.1.8: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} dev: false + /fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + dev: false + + /fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + requiresBuild: true + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -10203,6 +12518,14 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast-loops@1.1.3: + resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==} + dev: false + + /fast-password-entropy@1.1.1: + resolution: {integrity: sha512-dxm29/BPFrNgyEDygg/lf9c2xQR0vnQhG7+hZjAI39M/3um9fD4xiqG6F0ZjW6bya5m9CI0u6YryHGRtxCGCiw==} + dev: false + /fast-redact@3.3.0: resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==} engines: {node: '>=6'} @@ -10212,16 +12535,35 @@ packages: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} dev: false + /fast-shallow-equal@1.0.0: + resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + dev: false + /fast-stable-stringify@1.0.0: resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} dev: false + /fastest-stable-stringify@2.0.2: + resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} + dev: false + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: reusify: 1.0.4 dev: true + /fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + requiresBuild: true + dependencies: + pend: 1.2.0 + dev: true + + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: true + /fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -10237,6 +12579,14 @@ packages: escape-string-regexp: 1.0.5 dev: true + /figures@5.0.0: + resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} + engines: {node: '>=14'} + dependencies: + escape-string-regexp: 5.0.0 + is-unicode-supported: 1.3.0 + dev: true + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -10248,6 +12598,12 @@ packages: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} dev: false + /filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.6 + dev: true + /files-from-path@0.2.6: resolution: {integrity: sha512-Mz4UNkv+WcRLxcCXAORbfpwYiXI60SN9C1ZfeyGFv0xQUmblgbOkSWwFwX+Ov/TaR3FEyzwDyPnCQjpPRGSxKA==} dependencies: @@ -10296,6 +12652,23 @@ packages: path-exists: 4.0.0 dev: true + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: true + + /find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + dev: true + /flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -10304,6 +12677,11 @@ packages: rimraf: 3.0.2 dev: true + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + /flatted@3.2.7: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true @@ -10316,13 +12694,25 @@ packages: peerDependenciesMeta: debug: optional: true - dev: false /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + dev: true + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -10330,8 +12720,19 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 + + /format-util@1.0.5: + resolution: {integrity: sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==} dev: false + /formdata-node@5.0.1: + resolution: {integrity: sha512-8xnIjMYGKPj+rY2BTbAmpqVpi8der/2FT4d9f7J32FlsCpO5EzZPq3C/N56zdv8KweHzVF6TGijsS1JT6r1H2g==} + engines: {node: '>= 14.17'} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + dev: true + /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -10344,6 +12745,20 @@ packages: engines: {node: '>= 0.6'} dev: true + /from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + dev: true + + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + requiresBuild: true + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -10354,9 +12769,23 @@ packages: requiresBuild: true optional: true + /fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + dev: true + /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + /function.prototype.name@1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} engines: {node: '>= 0.4'} @@ -10371,6 +12800,31 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /gaze@1.1.3: + resolution: {integrity: sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==} + engines: {node: '>= 4.0.0'} + dependencies: + globule: 1.3.4 + dev: true + + /geckodriver@4.3.3: + resolution: {integrity: sha512-we2c2COgxFkLVuoknJNx+ioP+7VDq0sr6SCqWHTzlA4kzIbzR0EQ1Pps34s8WrsOnQqPC8a4sZV9dRPROOrkSg==} + engines: {node: ^16.13 || >=18 || >=20} + hasBin: true + requiresBuild: true + dependencies: + '@wdio/logger': 8.28.0 + decamelize: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + node-fetch: 3.3.2 + tar-fs: 3.0.5 + unzipper: 0.10.14 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -10391,6 +12845,17 @@ packages: has-proto: 1.0.1 has-symbols: 1.0.3 + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: true + /get-iterator@1.0.2: resolution: {integrity: sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==} dev: true @@ -10409,10 +12874,23 @@ packages: engines: {node: '>=16'} dev: true + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + pump: 3.0.0 + dev: true + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + /get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} @@ -10427,6 +12905,46 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + + /get-uri@6.0.3: + resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.3.4(supports-color@5.5.0) + fs-extra: 11.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + dev: true + + /git-repo-info@2.1.1: + resolution: {integrity: sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==} + engines: {node: '>= 4.0'} + dev: true + + /gitconfiglocal@2.1.0: + resolution: {integrity: sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==} + dependencies: + ini: 1.3.8 + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -10444,6 +12962,29 @@ packages: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + + /glob@7.1.7: + resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -10454,6 +12995,24 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.0.1 + once: 1.4.0 + dev: true + + /global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + dependencies: + ini: 4.1.1 + dev: true + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -10510,11 +13069,37 @@ packages: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true + /globule@1.3.4: + resolution: {integrity: sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==} + engines: {node: '>= 0.10'} + dependencies: + glob: 7.1.7 + lodash: 4.17.21 + minimatch: 3.0.8 + dev: true + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.1 + /got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + dev: true + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true @@ -10622,6 +13207,12 @@ packages: dependencies: get-intrinsic: 1.2.1 + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: true + /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} @@ -10635,6 +13226,14 @@ packages: engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 + dev: true + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} @@ -10658,6 +13257,13 @@ packages: minimalistic-assert: 1.0.1 dev: false + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + /hast-util-classnames@3.0.0: resolution: {integrity: sha512-tI3JjoGDEBVorMAWK4jNRsfLMYmih1BUOG3VV36pH36njs1IEl7xkNrVTD2mD2yYHmQCa5R/fj61a8IAF4bRaQ==} dependencies: @@ -10728,7 +13334,7 @@ packages: unist-util-visit: 5.0.0 zwitch: 2.0.4 dev: true - + /hast-util-to-estree@3.1.0: resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} dependencies: @@ -10808,10 +13414,23 @@ packages: space-separated-tokens: 2.0.2 dev: true + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + /headers-polyfill@4.0.2: resolution: {integrity: sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==} dev: true + /headers-utils@1.2.5: + resolution: {integrity: sha512-DAzV5P/pk3wTU/8TLZN+zFTDv4Xa1QDTU8pRvovPetcOMbmqq8CwsAvZBLPZHH6usxyy31zMp7I4aCYb6XIf6w==} + dev: true + + /help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + dev: false + /hey-listen@1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} dev: false @@ -10846,6 +13465,13 @@ packages: lru-cache: 6.0.0 dev: true + /hosted-git-info@7.0.1: + resolution: {integrity: sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + lru-cache: 10.1.0 + dev: true + /html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} dev: true @@ -10858,6 +13484,10 @@ packages: http-errors: 1.8.1 dev: true + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true + /http-errors@1.8.1: resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} engines: {node: '>= 0.6'} @@ -10869,11 +13499,53 @@ packages: toidentifier: 1.0.1 dev: true + /http-https@1.0.0: + resolution: {integrity: sha512-o0PWwVCSp3O0wS6FvNr6xfBCHgt0m1tvPLFOCc2iFDKTRAXhB7m8klDf7ErowFH8POa6dVdGatKU5I1YYwzUyg==} + dev: false + + /http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + /http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: false + /http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + + /https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -10883,12 +13555,27 @@ packages: engines: {node: '>=14.18.0'} dev: true + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + /humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} dependencies: ms: 2.1.3 dev: false + /husky@9.0.11: + resolution: {integrity: sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==} + engines: {node: '>=18'} + hasBin: true + dev: true + + /hyphenate-style-name@1.0.4: + resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} + dev: false + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -10901,7 +13588,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 - dev: true /idb-keyval@6.2.1: resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} @@ -10926,6 +13612,10 @@ packages: resolve-from: 4.0.0 dev: true + /import-meta-resolve@4.0.0: + resolution: {integrity: sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==} + dev: true + /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -10945,6 +13635,15 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + + /ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} dev: true @@ -10953,6 +13652,13 @@ packages: resolution: {integrity: sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ==} dev: true + /inline-style-prefixer@7.0.0: + resolution: {integrity: sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==} + dependencies: + css-in-js-utils: 3.1.0 + fast-loops: 1.1.3 + dev: false + /inquirer@8.2.6: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} @@ -10974,6 +13680,27 @@ packages: wrap-ansi: 6.2.0 dev: true + /inquirer@9.2.12: + resolution: {integrity: sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==} + engines: {node: '>=14.18.0'} + dependencies: + '@ljharb/through': 2.3.13 + ansi-escapes: 4.3.2 + chalk: 5.3.0 + cli-cursor: 3.1.0 + cli-width: 4.1.0 + external-editor: 3.1.0 + figures: 5.0.0 + lodash: 4.17.21 + mute-stream: 1.0.0 + ora: 5.4.1 + run-async: 3.0.0 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + /interface-blockstore@2.0.3: resolution: {integrity: sha512-OwVUnlNcx7H5HloK0Myv6c/C1q9cNG11HX6afdeU6q6kbuNj8jKCwVnmJHhC94LZaJ+9hvVOk4IUstb3Esg81w==} dependencies: @@ -11043,6 +13770,15 @@ packages: - supports-color dev: false + /ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + requiresBuild: true + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + dev: true + /ip-regex@4.3.0: resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} engines: {node: '>=8'} @@ -11186,7 +13922,7 @@ packages: merge-options: 3.0.4 nanoid: 3.3.6 native-fetch: 3.0.0(node-fetch@2.6.12) - node-fetch: 2.6.12 + node-fetch: 2.6.12(encoding@0.1.13) react-native-fetch-api: 3.0.0 stream-to-it: 0.2.4 transitivePeerDependencies: @@ -11269,6 +14005,10 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + /is-buffer@2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} @@ -11330,6 +14070,11 @@ packages: dependencies: is-extglob: 2.1.1 + /is-hex-prefixed@1.0.0: + resolution: {integrity: sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==} + engines: {node: '>=6.5.0', npm: '>=3'} + dev: false + /is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} dev: true @@ -11354,6 +14099,11 @@ packages: ip-regex: 4.3.0 dev: true + /is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + dev: true + /is-nan@1.3.2: resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} engines: {node: '>= 0.4'} @@ -11382,6 +14132,11 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: true + /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -11416,6 +14171,15 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-running@2.1.0: + resolution: {integrity: sha512-mjJd3PujZMl7j+D395WTIO5tU5RIDBfVSRtRR4VOJou3H66E38UjbjvDGh3slJzPuolsb+yQFqwHNNdyp5jg3w==} + dev: true + + /is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + dev: true + /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: @@ -11445,6 +14209,13 @@ packages: has-symbols: 1.0.3 dev: true + /is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + dependencies: + text-extensions: 2.4.0 + dev: true + /is-typed-array@1.1.12: resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} @@ -11460,12 +14231,30 @@ packages: engines: {node: '>=10'} dev: true + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: true + + /is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + dev: true + /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: call-bind: 1.0.2 dev: true + /is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + /is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -11477,6 +14266,11 @@ packages: dependencies: is-docker: 2.2.1 + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + requiresBuild: true + dev: true + /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: true @@ -11484,6 +14278,11 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + dev: true + /iso-random-stream@2.0.2: resolution: {integrity: sha512-yJvs+Nnelic1L2vH2JzWvvPQFA4r7kSTnpST/+LkAQjSz0hos2oqLD+qIVi9Qk38Hoe7mNDt3j0S27R58MVjLQ==} engines: {node: '>=10'} @@ -11497,10 +14296,10 @@ packages: engines: {node: '>=12'} dev: true - /isomorphic-unfetch@3.1.0: + /isomorphic-unfetch@3.1.0(encoding@0.1.13): resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} dependencies: - node-fetch: 2.6.12 + node-fetch: 2.6.12(encoding@0.1.13) unfetch: 4.2.0 transitivePeerDependencies: - encoding @@ -11593,6 +14392,30 @@ packages: readable-stream: 3.6.2 dev: true + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /jake@10.8.7: + resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.5 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: true + + /javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + dev: true + /jayson@4.1.0: resolution: {integrity: sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==} engines: {node: '>=8'} @@ -11615,6 +14438,63 @@ packages: - utf-8-validate dev: false + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dependencies: + '@babel/code-frame': 7.23.5 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + requiresBuild: true + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.10.5 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + /jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -11627,6 +14507,23 @@ packages: /jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true + + /jose@4.15.5: + resolution: {integrity: sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==} + dev: false + + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: false + + /js-cookie@2.2.1: + resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + dev: false + + /js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} dev: false /js-sha3@0.8.0: @@ -11648,15 +14545,29 @@ packages: argparse: 2.0.1 dev: true + /jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + requiresBuild: true + dev: true + /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true + /json-parse-even-better-errors@3.0.1: + resolution: {integrity: sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /json-rpc-engine@6.1.0: resolution: {integrity: sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==} engines: {node: '>=10.0.0'} @@ -11700,10 +14611,18 @@ packages: /jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + requiresBuild: true + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + /jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - dev: false /jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} @@ -11732,6 +14651,12 @@ packages: tsscmp: 1.0.6 dev: true + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + /keyvaluestorage-interface@1.0.0: resolution: {integrity: sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==} dev: false @@ -11788,6 +14713,12 @@ packages: - supports-color dev: true + /ky@0.33.3: + resolution: {integrity: sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==} + engines: {node: '>=14.16'} + requiresBuild: true + dev: true + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: true @@ -11798,6 +14729,14 @@ packages: language-subtag-registry: 0.3.22 dev: true + /lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + requiresBuild: true + dependencies: + readable-stream: 2.3.8 + dev: true + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -11820,6 +14759,10 @@ packages: uint8arrays: 3.1.1 dev: true + /libphonenumber-js@1.10.59: + resolution: {integrity: sha512-HeTsOrDF/hWhEiKqZVwg9Cqlep5x2T+IYDENvT2VRj3iX8JQ7Y+omENv+AIn0vC8m6GYhivfCed5Cgfw27r5SA==} + dev: false + /libsodium-sumo@0.7.11: resolution: {integrity: sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA==} dev: false @@ -11844,6 +14787,15 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + dev: true + /listhen@1.5.5: resolution: {integrity: sha512-LXe8Xlyh3gnxdv4tSjTjscD1vpr/2PRpzq8YIaMJgyKzRG8wdISlWVWnGThJfHnlJ6hmLt2wq1yeeix0TEbuoA==} hasBin: true @@ -11907,6 +14859,14 @@ packages: engines: {node: '>=14'} dev: true + /locate-app@2.2.24: + resolution: {integrity: sha512-vdoBy+xJYzEw+AIZkSW1SYEuWvZOMyhIW1dMIdD+MJP6K7DHfxJcCxktDbIS0aBtPyL2MkqiEa+GoDUz47zTlg==} + dependencies: + n12: 1.8.27 + type-fest: 2.13.0 + userhome: 1.0.0 + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -11920,22 +14880,102 @@ packages: p-locate: 5.0.0 dev: true + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: true + + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + requiresBuild: true + dev: true + /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} dev: false + /lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + dev: true + + /lodash.flatmap@4.5.0: + resolution: {integrity: sha512-/OcpcAGWlrZyoHGeHh3cAoa6nGdX6QYtmzNP84Jqol6UEQQ2gIaU3H+0eICcjcKGl0/XF8LWOujNn9lffsnaOg==} + dev: true + + /lodash.flattendeep@4.4.0: + resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} + dev: true + /lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} dev: false /lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: false + + /lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + dev: true + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: true + + /lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + dev: true /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + dev: true + + /lodash.pickby@4.6.0: + resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} + dev: true + + /lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + dev: true + + /lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + dev: true + + /lodash.take@4.1.1: + resolution: {integrity: sha512-3T118EQjnhr9c0aBKCCMhQn0OBwRMz/O2WaRU6VH0TSKoMCmFtUpr0iUp+eWKODEiRXtYOK7R7SiBneKHdk7og==} + dev: true + + /lodash.takeright@4.1.1: + resolution: {integrity: sha512-/I41i2h8VkHtv3PYD8z1P4dkLIco5Z3z35hT/FJl18AxwSdifcATaaiBOxuQOT3T/F1qfRTct3VWMFSj1xCtAw==} + dev: true + + /lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + dev: true + + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: true + + /lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + dev: true + + /lodash.zip@4.2.0: + resolution: {integrity: sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==} + requiresBuild: true + dev: true + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -11947,6 +14987,31 @@ packages: is-unicode-supported: 0.1.0 dev: true + /logform@2.6.0: + resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.4.1 + dev: true + + /loglevel-plugin-prefix@0.8.4: + resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} + dev: true + + /loglevel@1.9.1: + resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==} + engines: {node: '>= 0.6.0'} + dev: true + + /lokijs@1.5.12: + resolution: {integrity: sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q==} + dev: false + /long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} @@ -11970,10 +15035,14 @@ packages: get-func-name: 2.0.0 dev: true + /lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /lru-cache@10.1.0: resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} engines: {node: 14 || >=16.14} - dev: false /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -11986,11 +15055,21 @@ packages: dependencies: yallist: 4.0.0 + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: true + /luxon@3.3.0: resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} engines: {node: '>=12'} dev: false + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /magic-string@0.30.2: resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} engines: {node: '>=12'} @@ -11998,6 +15077,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -12005,6 +15091,10 @@ packages: semver: 6.3.1 dev: true + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -12015,6 +15105,10 @@ packages: engines: {node: '>=8'} dev: true + /map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + dev: true + /markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -12050,6 +15144,14 @@ packages: safe-buffer: 5.2.1 dev: false + /md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + /mdast-util-find-and-replace@3.0.1: resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} dependencies: @@ -12245,11 +15347,20 @@ packages: '@types/mdast': 4.0.3 dev: true + /mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + dev: false + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} dev: true + /meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + dev: true + /meow@9.0.0: resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} engines: {node: '>=10'} @@ -12283,6 +15394,10 @@ packages: engines: {node: '>= 8'} dev: true + /micro-ftch@0.3.1: + resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} + dev: false + /micromark-core-commonmark@2.0.0: resolution: {integrity: sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==} dependencies: @@ -12653,6 +15768,16 @@ packages: engines: {node: '>=12'} dev: true + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: true + + /mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -12666,11 +15791,38 @@ packages: resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} dev: false + /minimatch@3.0.8: + resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + dependencies: + brace-expansion: 1.1.11 + dev: true + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 + /minimatch@5.0.1: + resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -12682,12 +15834,49 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} dev: true + /mipd@0.0.5(typescript@5.1.3): + resolution: {integrity: sha512-gbKA784D2WKb5H/GtqEv+Ofd1S9Zj+Z/PGDIl1u1QAbswkxD28BQ5bSXQxkeBzPBABg1iDSbiwGG1XqlOxRspA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 5.1.3 + viem: 1.20.0(typescript@5.1.3) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + dev: false + /mitt@2.1.0: resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==} dev: false + /mitt@3.0.0: + resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==} + requiresBuild: true + dev: true + + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + requiresBuild: true + dev: true + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + /mlly@1.4.0: resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} dependencies: @@ -12705,6 +15894,33 @@ packages: ufo: 1.3.2 dev: false + /mocha@10.3.0: + resolution: {integrity: sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==} + engines: {node: '>= 14.0.0'} + hasBin: true + dependencies: + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4(supports-color@8.1.1) + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + dev: true + /motion@10.16.2: resolution: {integrity: sha512-p+PurYqfUdcJZvtnmAqu5fJgV2kR0uLFQuBKtLeFVTrYEVllI99tiOTSefVNYuip9ELTEkepIIDftNdze76NAQ==} dependencies: @@ -12733,6 +15949,10 @@ packages: engines: {node: '>=10'} dev: true + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -12808,10 +16028,37 @@ packages: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /n12@1.8.27: + resolution: {integrity: sha512-mYeuH53HBGNBjWaFAaJ9+OTzJIVu4ViyC4aleux7RdkPChUj9jTfnO070FAj3PwsY4/Wlj2vAZ1WITXXG0SAmQ==} + dev: true + /nan@2.18.0: resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} dev: false + /nano-css@5.6.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==} + peerDependencies: + react: '*' + react-dom: '*' + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + css-tree: 1.1.3 + csstype: 3.1.2 + fastest-stable-stringify: 2.0.2 + inline-style-prefixer: 7.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + rtl-css-js: 1.16.1 + stacktrace-js: 2.0.2 + stylis: 4.3.1 + dev: false + /nano-time@1.0.0: resolution: {integrity: sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==} dependencies: @@ -12847,7 +16094,7 @@ packages: peerDependencies: node-fetch: '*' dependencies: - node-fetch: 2.6.12 + node-fetch: 2.6.12(encoding@0.1.13) dev: true /native-fetch@3.0.0(node-fetch@3.3.1): @@ -12875,6 +16122,16 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true + /netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + requiresBuild: true + dev: true + + /next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + dev: false + /node-addon-api@2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} dev: false @@ -12888,11 +16145,11 @@ packages: engines: {node: '>=10.5.0'} dev: true - /node-fetch-native@1.6.2: - resolution: {integrity: sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==} + /node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} dev: false - /node-fetch@2.6.12: + /node-fetch@2.6.12(encoding@0.1.13): resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} engines: {node: 4.x || >=6.0.0} peerDependencies: @@ -12901,6 +16158,7 @@ packages: encoding: optional: true dependencies: + encoding: 0.1.13 whatwg-url: 5.0.0 /node-fetch@3.3.1: @@ -12912,6 +16170,15 @@ packages: formdata-polyfill: 4.0.10 dev: true + /node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + dev: true + /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} @@ -12919,7 +16186,6 @@ packages: /node-gyp-build@4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true - dev: false /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} @@ -12928,6 +16194,17 @@ packages: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true + /node-request-interceptor@0.6.3: + resolution: {integrity: sha512-8I2V7H2Ch0NvW7qWcjmS0/9Lhr0T6x7RD6PDirhvWEkUQvy83x8BA4haYMr09r/rig7hcgYSjYh6cd4U7G1vLA==} + dependencies: + '@open-draft/until': 1.0.3 + debug: 4.3.4(supports-color@5.5.0) + headers-utils: 1.2.5 + strict-event-emitter: 0.1.0 + transitivePeerDependencies: + - supports-color + dev: true + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -12947,10 +16224,25 @@ packages: validate-npm-package-license: 3.0.4 dev: true + /normalize-package-data@6.0.0: + resolution: {integrity: sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + hosted-git-info: 7.0.1 + is-core-module: 2.13.0 + semver: 7.6.0 + validate-npm-package-license: 3.0.4 + dev: true + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + /normalize-url@8.0.1: + resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} + engines: {node: '>=14.16'} + dev: true + /not@0.1.0: resolution: {integrity: sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==} dev: true @@ -12974,6 +16266,14 @@ packages: boolbase: 1.0.0 dev: true + /number-to-bn@1.7.0: + resolution: {integrity: sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + bn.js: 4.11.6 + strip-hex-prefix: 1.0.0 + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -13041,11 +16341,17 @@ packages: resolution: {integrity: sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==} dev: false + /oboe@2.1.5: + resolution: {integrity: sha512-zRFWiF+FoicxEs3jNI/WYUrVEgA7DeET/InK0XQuudGHRg8iIob3cNPrJTKaz4004uaA9Pbe+Dwa8iluhjLZWA==} + dependencies: + http-https: 1.0.0 + dev: false + /ofetch@1.3.3: resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==} dependencies: destr: 2.0.2 - node-fetch-native: 1.6.2 + node-fetch-native: 1.6.4 ufo: 1.3.2 dev: false @@ -13053,6 +16359,11 @@ packages: resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} dev: false + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + dev: false + /on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -13128,6 +16439,11 @@ packages: resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} dev: true + /p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + dev: true + /p-defer@3.0.0: resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} engines: {node: '>=8'} @@ -13173,6 +16489,13 @@ packages: p-limit: 3.1.0 dev: true + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: true + /p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} @@ -13185,6 +16508,31 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + /pac-proxy-agent@7.0.1: + resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} + engines: {node: '>= 14'} + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + get-uri: 6.0.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + dev: true + /pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} dev: true @@ -13221,12 +16569,28 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.22.10 + '@babel/code-frame': 7.23.5 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 dev: true + /parse-json@7.1.1: + resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} + engines: {node: '>=16'} + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 3.0.1 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + dev: true + + /parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + dev: true + /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: @@ -13242,6 +16606,11 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -13259,6 +16628,14 @@ packages: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.1.0 + minipass: 7.0.4 + dev: true + /path-to-regexp@6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} dev: true @@ -13280,6 +16657,12 @@ packages: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} dev: true + /pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + dependencies: + through: 2.3.8 + dev: true + /peer-id@0.16.0: resolution: {integrity: sha512-EmL7FurFUduU9m1PS9cfJ5TAuCvxKQ7DKpfx3Yj6IKWyBRtosriFuOag/l3ni/dtPgPLwiA4R9IvpL7hsDLJuQ==} engines: {node: '>=15.0.0'} @@ -13291,6 +16674,10 @@ packages: uint8arrays: 3.1.1 dev: true + /pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + dev: true + /periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} dependencies: @@ -13323,6 +16710,33 @@ packages: split2: 4.2.0 dev: false + /pino-abstract-transport@1.1.0: + resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + dev: false + + /pino-pretty@10.3.1: + resolution: {integrity: sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==} + hasBin: true + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.1.0 + pump: 3.0.0 + readable-stream: 4.5.2 + secure-json-parse: 2.7.0 + sonic-boom: 3.8.1 + strip-json-comments: 3.1.1 + dev: false + /pino-std-serializers@4.0.0: resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} dev: false @@ -13369,6 +16783,16 @@ packages: hasBin: true dev: true + /pony-cause@2.1.10: + resolution: {integrity: sha512-3IKLNXclQgkU++2fSi93sQ6BznFuxSLB11HdvZQ6JW/spahf/P1pAHBQEahr20rs0htZW0UDkM1HmA+nZkXKsw==} + engines: {node: '>=12.0.0'} + dev: false + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + /postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -13421,6 +16845,22 @@ packages: react-is: 17.0.2 dev: true + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + dependencies: + parse-ms: 2.1.0 + dev: true + /prism-react-renderer@2.3.1(react@18.2.0): resolution: {integrity: sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==} peerDependencies: @@ -13431,10 +16871,33 @@ packages: react: 18.2.0 dev: true + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + requiresBuild: true + dev: true + /process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} dev: false + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + requiresBuild: true + + /progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: true + + /prometheus-query@3.4.0: + resolution: {integrity: sha512-PGNwYVjXxenfj2PR4FKEUv5O4XO8ciHT92GX83J5ZJm5ki3YzLLiv+TfbmQSUxvHcXofLg9PYH6CBCSplcvr9g==} + dependencies: + axios: 1.6.7 + transitivePeerDependencies: + - debug + dev: false + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -13465,19 +16928,89 @@ packages: '@types/node': 20.10.5 long: 4.0.0 + /proxy-agent@6.3.0: + resolution: {integrity: sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.1 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /proxy-agent@6.3.1: + resolution: {integrity: sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.1 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: true + /proxy-compare@2.5.1: resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==} dev: false /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: false + + /ps-tree@1.2.0: + resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} + engines: {node: '>= 0.10'} + hasBin: true + dependencies: + event-stream: 3.3.4 + dev: true + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} dev: true + /puppeteer-core@20.9.0(typescript@5.1.3): + resolution: {integrity: sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==} + engines: {node: '>=16.3.0'} + requiresBuild: true + peerDependencies: + typescript: '>= 4.7.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@puppeteer/browsers': 1.4.6(typescript@5.1.3) + chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) + cross-fetch: 4.0.0(encoding@0.1.13) + debug: 4.3.4(supports-color@5.5.0) + devtools-protocol: 0.0.1147663 + typescript: 5.1.3 + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: true + /qr-code-styling@1.6.0-rc.1: resolution: {integrity: sha512-ModRIiW6oUnsP18QzrRYZSc/CFKFKIdj7pUs57AEVH20ajlglRpN3HukjHk0UbNMTlKGuaYl7Gt6/O5Gg2NU2Q==} dependencies: @@ -13506,6 +17039,11 @@ packages: side-channel: 1.0.4 dev: false + /query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + requiresBuild: true + dev: true + /query-string@6.14.1: resolution: {integrity: sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==} engines: {node: '>=6'} @@ -13538,6 +17076,11 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + requiresBuild: true + dev: true + /quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: false @@ -13547,6 +17090,11 @@ packages: engines: {node: '>=8'} dev: true + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + /rabin-wasm@0.1.5: resolution: {integrity: sha512-uWgQTo7pim1Rnj5TuWcCewRDTf0PEFTSlaUjWP4eY9EbLV9em08v89oCz/WO+wRxpYuO36XEHp4wgYQnAgOHzA==} hasBin: true @@ -13555,7 +17103,7 @@ packages: bl: 5.1.0 debug: 4.3.4(supports-color@5.5.0) minimist: 1.2.8 - node-fetch: 2.6.12 + node-fetch: 2.6.12(encoding@0.1.13) readable-stream: 3.6.2 transitivePeerDependencies: - encoding @@ -13617,6 +17165,17 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-device-detect@2.2.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==} + peerDependencies: + react: '>= 0.14.0' + react-dom: '>= 0.14.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + ua-parser-js: 1.0.37 + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -13853,15 +17412,49 @@ packages: tslib: 2.6.2 dev: false + /react-universal-interface@0.6.2(react@18.2.0)(tslib@2.6.2): + resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} + peerDependencies: + react: '*' + tslib: '*' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + /react-use-measure@2.1.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==} peerDependencies: react: '>=16.13' react-dom: '>=16.13' dependencies: - debounce: 1.2.1 + debounce: 1.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-use@17.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} + peerDependencies: + react: '*' + react-dom: '*' + dependencies: + '@types/js-cookie': 2.2.7 + '@xobotyi/scrollbar-width': 1.9.5 + copy-to-clipboard: 3.3.3 + fast-deep-equal: 3.1.3 + fast-shallow-equal: 1.0.0 + js-cookie: 2.2.1 + nano-css: 5.6.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + react-universal-interface: 0.6.2(react@18.2.0)(tslib@2.6.2) + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + set-harmonic-interval: 1.0.1 + throttle-debounce: 3.0.1 + ts-easing: 0.2.0 + tslib: 2.6.2 dev: false /react@18.2.0: @@ -13870,6 +17463,15 @@ packages: dependencies: loose-envify: 1.4.0 + /read-pkg-up@10.0.0: + resolution: {integrity: sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==} + engines: {node: '>=16'} + dependencies: + find-up: 6.3.0 + read-pkg: 8.1.0 + type-fest: 3.13.1 + dev: true + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -13889,6 +17491,28 @@ packages: type-fest: 0.6.0 dev: true + /read-pkg@8.1.0: + resolution: {integrity: sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==} + engines: {node: '>=16'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 6.0.0 + parse-json: 7.1.1 + type-fest: 4.10.2 + dev: true + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + /readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -13897,6 +17521,24 @@ packages: string_decoder: 1.3.0 util-deprecate: 1.0.2 + /readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + requiresBuild: true + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + /readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + requiresBuild: true + dependencies: + minimatch: 5.1.6 + dev: true + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -13918,6 +17560,13 @@ packages: ms: 2.1.3 dev: true + /recursive-readdir@2.2.3: + resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} + engines: {node: '>=6.0.0'} + dependencies: + minimatch: 3.1.2 + dev: true + /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -13978,6 +17627,16 @@ packages: functions-have-names: 1.2.3 dev: true + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + dev: true + /rehype-class-names@1.0.14: resolution: {integrity: sha512-eFBt6Qxb7K77y6P82tUtN9rKpU7guWlaK4XA4RrrSFHkUTCvr2D3cgb9OR5d4t1AaGOvR59FH9nRwUnbpn9AEg==} dependencies: @@ -14067,11 +17726,24 @@ packages: resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} dev: false + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + + /resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: true + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} dev: true + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true @@ -14094,6 +17766,20 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + dependencies: + lowercase-keys: 3.0.0 + dev: true + + /resq@1.11.0: + resolution: {integrity: sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==} + requiresBuild: true + dependencies: + fast-deep-equal: 2.0.1 + dev: true + /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -14116,6 +17802,25 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true + /rgb2hex@0.2.5: + resolution: {integrity: sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==} + requiresBuild: true + dev: true + + /rimraf@2.5.4: + resolution: {integrity: sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -14129,6 +17834,30 @@ packages: inherits: 2.0.4 dev: false + /rollup-plugin-sourcemaps@0.6.3(@types/node@20.3.1)(rollup@2.79.1): + resolution: {integrity: sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==} + engines: {node: '>=10.0.0'} + peerDependencies: + '@types/node': '>=10.0.0' + rollup: '>=0.31.2' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + '@types/node': 20.3.1 + rollup: 2.79.1 + source-map-resolve: 0.6.0 + dev: true + + /rollup@2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + /rollup@3.28.0: resolution: {integrity: sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -14172,6 +17901,12 @@ packages: utf-8-validate: 5.0.10 dev: false + /rtl-css-js@1.16.1: + resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + dependencies: + '@babel/runtime': 7.22.10 + dev: false + /run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} engines: {node: '>=12'} @@ -14184,6 +17919,11 @@ packages: engines: {node: '>=0.12.0'} dev: true + /run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -14203,6 +17943,10 @@ packages: tslib: 2.6.2 dev: true + /safaridriver@0.1.2: + resolution: {integrity: sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==} + dev: true + /safe-array-concat@1.0.0: resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} engines: {node: '>=0.4'} @@ -14213,6 +17957,11 @@ packages: isarray: 2.0.5 dev: true + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + requiresBuild: true + dev: true + /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -14227,11 +17976,9 @@ packages: /safe-stable-stringify@2.4.3: resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} engines: {node: '>=10'} - dev: false /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} @@ -14257,10 +18004,23 @@ packages: ajv-keywords: 5.1.0(ajv@8.12.0) dev: true + /screenfull@5.2.0: + resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} + engines: {node: '>=0.10.0'} + dev: false + /scrypt-js@3.0.1: resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} dev: false + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: false + + /secure-password-utilities@0.2.1: + resolution: {integrity: sha512-znUg8ae3cpuAaogiFBhP82gD2daVkSz4Qv/L7OWjB7wWvfbCdeqqQuJkm2/IvhKQPOV0T739YPR6rb7vs0uWaw==} + dev: false + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -14276,6 +18036,28 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 + dev: true + + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /serialize-error@11.0.3: + resolution: {integrity: sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==} + engines: {node: '>=14.16'} + requiresBuild: true + dependencies: + type-fest: 2.19.0 + dev: true + + /serialize-javascript@6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + dependencies: + randombytes: 2.1.0 + dev: true /serialize-javascript@6.0.1: resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} @@ -14287,6 +18069,37 @@ packages: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: false + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: true + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + + /set-harmonic-interval@1.0.1: + resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} + engines: {node: '>=6.9'} + dev: false + + /setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: true + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: true @@ -14326,6 +18139,21 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /simmerjs@0.5.6: + resolution: {integrity: sha512-Z00zGHUp2IVSDUuni6gzBxVVQwAEZ7jVHnqL97+2RaHVWTYKfgCNyCvgm68Uc1M6X84hjatxvtOc24Y9ECLPWQ==} + dependencies: + lodash.difference: 4.5.0 + lodash.flatmap: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.take: 4.1.1 + lodash.takeright: 4.1.1 + dev: true + /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: @@ -14347,17 +18175,57 @@ packages: engines: {node: '>=14.16'} dev: true + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + requiresBuild: true + dev: true + + /socks-proxy-agent@8.0.2: + resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + socks: 2.8.1 + transitivePeerDependencies: + - supports-color + dev: true + + /socks@2.8.1: + resolution: {integrity: sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + requiresBuild: true + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + dev: true + /sonic-boom@2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} dependencies: atomic-sleep: 1.0.0 dev: false + /sonic-boom@3.8.1: + resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} + dependencies: + atomic-sleep: 1.0.0 + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} dev: true + /source-map-resolve@0.6.0: + resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + dev: true + /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: @@ -14365,10 +18233,19 @@ packages: source-map: 0.6.1 dev: true + /source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: true + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} @@ -14417,12 +18294,55 @@ packages: /split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + + /split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + dependencies: + through: 2.3.8 + dev: true + + /sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + requiresBuild: true + dev: true + + /stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + dependencies: + stackframe: 1.3.4 dev: false + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + escape-string-regexp: 2.0.0 + dev: true + /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + dev: false + + /stacktrace-gps@3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + dev: false + + /stacktrace-js@2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + dependencies: + error-stack-parser: 2.1.4 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 + dev: false + /standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} dev: false @@ -14445,6 +18365,13 @@ packages: resolution: {integrity: sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==} dev: false + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.5 + dev: true + /stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} dependencies: @@ -14452,6 +18379,17 @@ packages: readable-stream: 3.6.2 dev: false + /stream-buffers@3.0.2: + resolution: {integrity: sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==} + engines: {node: '>= 0.10.0'} + dev: true + + /stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + dependencies: + duplexer: 0.1.2 + dev: true + /stream-shift@1.0.1: resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} dev: false @@ -14467,6 +18405,20 @@ packages: engines: {node: '>=10'} dev: true + /streamx@2.16.1: + resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} + requiresBuild: true + dependencies: + fast-fifo: 1.3.0 + queue-tick: 1.0.1 + optionalDependencies: + bare-events: 2.2.1 + dev: true + + /strict-event-emitter@0.1.0: + resolution: {integrity: sha512-8hSYfU+WKLdNcHVXJ0VxRXiPESalzRe7w1l8dg9+/22Ry+iZQUoQuoJ27R30GMD1TiyYINWsIEGY05WrskhSKw==} + dev: true + /strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} dev: true @@ -14531,6 +18483,13 @@ packages: es-abstract: 1.22.1 dev: true + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + requiresBuild: true + dependencies: + safe-buffer: 5.1.2 + dev: true + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -14570,6 +18529,13 @@ packages: engines: {node: '>=12'} dev: true + /strip-hex-prefix@1.0.0: + resolution: {integrity: sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + is-hex-prefixed: 1.0.0 + dev: false + /strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -14580,7 +18546,6 @@ packages: /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - dev: true /strip-literal@1.3.0: resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} @@ -14624,6 +18589,10 @@ packages: transitivePeerDependencies: - '@babel/core' + /stylis@4.3.1: + resolution: {integrity: sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==} + dev: false + /superjson@1.13.3: resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==} engines: {node: '>=10'} @@ -14687,6 +18656,39 @@ packages: engines: {node: '>=6'} dev: true + /tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.7 + dev: true + + /tar-fs@3.0.5: + resolution: {integrity: sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==} + dependencies: + pump: 3.0.0 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 2.2.2 + bare-path: 2.1.0 + dev: true + + /tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + dependencies: + b4a: 1.6.6 + fast-fifo: 1.3.0 + streamx: 2.16.1 + dev: true + + /temp-fs@0.9.9: + resolution: {integrity: sha512-WfecDCR1xC9b0nsrzSaxPf3ZuWeWLUWblW4vlDQAa1biQaKHiImHnJfeQocQe/hXKMcolRzgkcVX/7kK4zoWbw==} + engines: {node: '>=0.8.0'} + dependencies: + rimraf: 2.5.4 + dev: true + /terser-webpack-plugin@5.3.9(webpack@5.89.0): resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} engines: {node: '>= 10.13.0'} @@ -14726,6 +18728,11 @@ packages: resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} dev: false + /text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -14736,6 +18743,11 @@ packages: real-require: 0.1.0 dev: false + /throttle-debounce@3.0.1: + resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} + engines: {node: '>=10'} + dev: false + /throttled-queue@2.1.4: resolution: {integrity: sha512-YGdk8sdmr4ge3g+doFj/7RLF5kLM+Mi7DEciu9PHxnMJZMeVuZeTj31g4VE7ekUffx/IdbvrtOCiz62afg0mkg==} dev: true @@ -14776,6 +18788,10 @@ packages: resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} dev: true + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + dev: false + /tinypool@0.5.0: resolution: {integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==} engines: {node: '>=14.0.0'} @@ -14798,6 +18814,11 @@ packages: os-tmpdir: 1.0.2 dev: true + /tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + dev: true + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -14819,6 +18840,11 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + requiresBuild: true + + /traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + dev: true /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -14829,10 +18855,50 @@ packages: engines: {node: '>=8'} dev: true + /triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + dev: true + /trough@2.1.0: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: true + /ts-easing@0.2.0: + resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} + dev: false + + /ts-node@10.9.2(@types/node@20.3.1)(typescript@5.1.3): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.3.1 + acorn: 8.11.2 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.1.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /tsconfck@3.0.1(typescript@5.1.3): resolution: {integrity: sha512-7ppiBlF3UEddCLeI1JRx5m2Ryq+xk4JrZuq4EuYXykipebaq1dV0Fhgr1hb7CkmHt32QSgOZlcqVLEtHBG4/mg==} engines: {node: ^18 || >=20} @@ -14883,6 +18949,25 @@ packages: typescript: 5.1.3 dev: true + /tsx@4.7.1: + resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.19.12 + get-tsconfig: 4.7.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /tweetnacl-util@0.15.1: + resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==} + dev: false + + /tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -14920,11 +19005,21 @@ packages: engines: {node: '>=8'} dev: true + /type-fest@2.13.0: + resolution: {integrity: sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==} + engines: {node: '>=12.20'} + dev: true + /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} dev: true + /type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + dev: true + /type-fest@4.10.2: resolution: {integrity: sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==} engines: {node: '>=16'} @@ -14938,6 +19033,10 @@ packages: mime-types: 2.1.35 dev: true + /type@2.7.2: + resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} + dev: false + /typed-array-buffer@1.0.0: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} @@ -14991,6 +19090,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + /ua-parser-js@1.0.37: + resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} + dev: false + /ufo@1.2.0: resolution: {integrity: sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==} @@ -15012,6 +19115,13 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + dependencies: + buffer: 5.7.1 + through: 2.3.8 + dev: true + /uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} dev: false @@ -15025,7 +19135,7 @@ packages: consola: 3.2.3 defu: 6.1.3 mime: 3.0.0 - node-fetch-native: 1.6.2 + node-fetch-native: 1.6.4 pathe: 1.1.1 dev: false @@ -15114,6 +19224,12 @@ packages: unist-util-visit-parents: 6.0.1 dev: true + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dev: true + /unload@2.2.0: resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==} dependencies: @@ -15171,7 +19287,7 @@ packages: listhen: 1.5.5 lru-cache: 10.1.0 mri: 1.2.0 - node-fetch-native: 1.6.2 + node-fetch-native: 1.6.4 ofetch: 1.3.3 ufo: 1.3.2 transitivePeerDependencies: @@ -15192,6 +19308,21 @@ packages: pathe: 1.1.1 dev: false + /unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + dev: true + /update-browserslist-db@1.0.11(browserslist@4.21.10): resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true @@ -15293,12 +19424,20 @@ packages: react: 18.2.0 dev: false + /userhome@1.0.0: + resolution: {integrity: sha512-ayFKY3H+Pwfy4W98yPdtH1VqH4psDeyW8lYYFzfecR9d6hqLpqhecktvYR3SEEXt7vG0S1JEpciI3g94pMErig==} + engines: {node: '>= 0.8.0'} + dev: true + /utf-8-validate@5.0.10: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} requiresBuild: true dependencies: node-gyp-build: 4.6.0 + + /utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} dev: false /util-deprecate@1.0.2: @@ -15323,6 +19462,14 @@ packages: hasBin: true dev: false + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -15453,12 +19600,21 @@ packages: - terser dev: true - /vite-plugin-svgr@3.2.0(vite@4.3.9): + /vite-plugin-restart@0.4.0(vite@4.3.9): + resolution: {integrity: sha512-SXeyKQAzRFmEmEyGP2DjaTbx22D1K5MapyNiAP7Xa14UyFgNSDjZ86bfjWksA0pqn+bZyxnVLJpCiqDuG+tOcg==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + dependencies: + micromatch: 4.0.5 + vite: 4.3.9(@types/node@20.3.1) + dev: true + + /vite-plugin-svgr@3.2.0(rollup@2.79.1)(vite@4.3.9): resolution: {integrity: sha512-Uvq6niTvhqJU6ga78qLKBFJSDvxWhOnyfQSoKpDPMAGxJPo5S3+9hyjExE5YDj6Lpa4uaLkGc1cBgxXov+LjSw==} peerDependencies: vite: ^2.6.0 || 3 || 4 dependencies: - '@rollup/pluginutils': 5.0.3 + '@rollup/pluginutils': 5.0.3(rollup@2.79.1) '@svgr/core': 7.0.0 '@svgr/plugin-jsx': 7.0.0 vite: 4.3.9(@types/node@20.3.1) @@ -15553,7 +19709,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@0.32.2: + /vitest@0.32.2(webdriverio@8.33.1): resolution: {integrity: sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==} engines: {node: '>=v14.18.0'} hasBin: true @@ -15608,6 +19764,7 @@ packages: tinypool: 0.5.0 vite: 4.3.9(@types/node@20.3.1) vite-node: 0.32.2(@types/node@20.3.1) + webdriverio: 8.33.1(typescript@5.1.3) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -15675,6 +19832,18 @@ packages: - zod dev: false + /wait-port@1.1.0: + resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chalk: 4.1.2 + commander: 9.5.0 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + /watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} @@ -15689,6 +19858,11 @@ packages: defaults: 1.0.4 dev: true + /wdio-wait-for@3.0.11: + resolution: {integrity: sha512-kck1TeQeIzI9fdP8efy7izzdkBiOZJR8lMOkKpxYp2/k7r2F2+8SHWBGPt1TfSiehKHLsIalB7G1RzJKF+PqDA==} + engines: {node: ^16.13 || >=18} + dev: true + /web-encoding@1.1.5: resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} dependencies: @@ -15706,6 +19880,158 @@ packages: engines: {node: '>= 8'} dev: true + /web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + dev: true + + /web3-core-helpers@1.10.3: + resolution: {integrity: sha512-Yv7dQC3B9ipOc5sWm3VAz1ys70Izfzb8n9rSiQYIPjpqtJM+3V4EeK6ghzNR6CO2es0+Yu9CtCkw0h8gQhrTxA==} + engines: {node: '>=8.0.0'} + dependencies: + web3-eth-iban: 1.10.3 + web3-utils: 1.10.3 + dev: false + + /web3-core-helpers@1.10.4: + resolution: {integrity: sha512-r+L5ylA17JlD1vwS8rjhWr0qg7zVoVMDvWhajWA5r5+USdh91jRUYosp19Kd1m2vE034v7Dfqe1xYRoH2zvG0g==} + engines: {node: '>=8.0.0'} + dependencies: + web3-eth-iban: 1.10.4 + web3-utils: 1.10.4 + dev: false + + /web3-core-method@1.10.4: + resolution: {integrity: sha512-uZTb7flr+Xl6LaDsyTeE2L1TylokCJwTDrIVfIfnrGmnwLc6bmTWCCrm71sSrQ0hqs6vp/MKbQYIYqUN0J8WyA==} + engines: {node: '>=8.0.0'} + dependencies: + '@ethersproject/transactions': 5.7.0 + web3-core-helpers: 1.10.4 + web3-core-promievent: 1.10.4 + web3-core-subscriptions: 1.10.4 + web3-utils: 1.10.4 + dev: false + + /web3-core-promievent@1.10.4: + resolution: {integrity: sha512-2de5WnJQ72YcIhYwV/jHLc4/cWJnznuoGTJGD29ncFQHAfwW/MItHFSVKPPA5v8AhJe+r6y4Y12EKvZKjQVBvQ==} + engines: {node: '>=8.0.0'} + dependencies: + eventemitter3: 4.0.4 + dev: false + + /web3-core-requestmanager@1.10.4(encoding@0.1.13): + resolution: {integrity: sha512-vqP6pKH8RrhT/2MoaU+DY/OsYK9h7HmEBNCdoMj+4ZwujQtw/Mq2JifjwsJ7gits7Q+HWJwx8q6WmQoVZAWugg==} + engines: {node: '>=8.0.0'} + dependencies: + util: 0.12.5 + web3-core-helpers: 1.10.4 + web3-providers-http: 1.10.4(encoding@0.1.13) + web3-providers-ipc: 1.10.4 + web3-providers-ws: 1.10.4 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /web3-core-subscriptions@1.10.4: + resolution: {integrity: sha512-o0lSQo/N/f7/L76C0HV63+S54loXiE9fUPfHFcTtpJRQNDBVsSDdWRdePbWwR206XlsBqD5VHApck1//jEafTw==} + engines: {node: '>=8.0.0'} + dependencies: + eventemitter3: 4.0.4 + web3-core-helpers: 1.10.4 + dev: false + + /web3-core@1.10.4(encoding@0.1.13): + resolution: {integrity: sha512-B6elffYm81MYZDTrat7aEhnhdtVE3lDBUZft16Z8awYMZYJDbnykEbJVS+l3mnA7AQTnSDr/1MjWofGDLBJPww==} + engines: {node: '>=8.0.0'} + dependencies: + '@types/bn.js': 5.1.5 + '@types/node': 12.20.55 + bignumber.js: 9.1.1 + web3-core-helpers: 1.10.4 + web3-core-method: 1.10.4 + web3-core-requestmanager: 1.10.4(encoding@0.1.13) + web3-utils: 1.10.4 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /web3-eth-iban@1.10.3: + resolution: {integrity: sha512-ZCfOjYKAjaX2TGI8uif5ah+J3BYFuo+47JOIV1RIz2l7kD9VfnxvRH5UiQDRyMALQC7KFd2hUqIEtHklapNyKA==} + engines: {node: '>=8.0.0'} + dependencies: + bn.js: 5.2.1 + web3-utils: 1.10.3 + dev: false + + /web3-eth-iban@1.10.4: + resolution: {integrity: sha512-0gE5iNmOkmtBmbKH2aTodeompnNE8jEyvwFJ6s/AF6jkw9ky9Op9cqfzS56AYAbrqEFuClsqB/AoRves7LDELw==} + engines: {node: '>=8.0.0'} + dependencies: + bn.js: 5.2.1 + web3-utils: 1.10.4 + dev: false + + /web3-providers-http@1.10.4(encoding@0.1.13): + resolution: {integrity: sha512-m2P5Idc8hdiO0l60O6DSCPw0kw64Zgi0pMjbEFRmxKIck2Py57RQMu4bxvkxJwkF06SlGaEQF8rFZBmuX7aagQ==} + engines: {node: '>=8.0.0'} + dependencies: + abortcontroller-polyfill: 1.7.5 + cross-fetch: 4.0.0(encoding@0.1.13) + es6-promise: 4.2.8 + web3-core-helpers: 1.10.4 + transitivePeerDependencies: + - encoding + dev: false + + /web3-providers-ipc@1.10.4: + resolution: {integrity: sha512-YRF/bpQk9z3WwjT+A6FI/GmWRCASgd+gC0si7f9zbBWLXjwzYAKG73bQBaFRAHex1hl4CVcM5WUMaQXf3Opeuw==} + engines: {node: '>=8.0.0'} + dependencies: + oboe: 2.1.5 + web3-core-helpers: 1.10.4 + dev: false + + /web3-providers-ws@1.10.4: + resolution: {integrity: sha512-j3FBMifyuFFmUIPVQR4pj+t5ILhAexAui0opgcpu9R5LxQrLRUZxHSnU+YO25UycSOa/NAX8A+qkqZNpcFAlxA==} + engines: {node: '>=8.0.0'} + dependencies: + eventemitter3: 4.0.4 + web3-core-helpers: 1.10.4 + websocket: 1.0.34 + transitivePeerDependencies: + - supports-color + dev: false + + /web3-utils@1.10.3: + resolution: {integrity: sha512-OqcUrEE16fDBbGoQtZXWdavsPzbGIDc5v3VrRTZ0XrIpefC/viZ1ZU9bGEemazyS0catk/3rkOOxpzTfY+XsyQ==} + engines: {node: '>=8.0.0'} + dependencies: + '@ethereumjs/util': 8.1.0 + bn.js: 5.2.1 + ethereum-bloom-filters: 1.0.10 + ethereum-cryptography: 2.1.2 + ethjs-unit: 0.1.6 + number-to-bn: 1.7.0 + randombytes: 2.1.0 + utf8: 3.0.0 + dev: false + + /web3-utils@1.10.4: + resolution: {integrity: sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==} + engines: {node: '>=8.0.0'} + dependencies: + '@ethereumjs/util': 8.1.0 + bn.js: 5.2.1 + ethereum-bloom-filters: 1.0.10 + ethereum-cryptography: 2.1.2 + ethjs-unit: 0.1.6 + number-to-bn: 1.7.0 + randombytes: 2.1.0 + utf8: 3.0.0 + dev: false + /web3.storage@4.5.4(node-fetch@3.3.1): resolution: {integrity: sha512-QSdiPEMgXCkk9Y0y3U1pyTu8n1TOOctwq7h9Loz7NYPla9QZesbg4lSxe0XWPltzyJEkI43yC1hy8gNxNEiizA==} dependencies: @@ -15731,8 +20057,71 @@ packages: - supports-color dev: true + /webdriver@8.33.1: + resolution: {integrity: sha512-QREF4c08omN9BPh3QDmz5h+OEvjdzDliuEcrDuXoDnHSMxIj1rsonzsgRaM2PXhFZuPeMIiKZYqc7Qg9BGbh6A==} + engines: {node: ^16.13 || >=18} + dependencies: + '@types/node': 20.10.5 + '@types/ws': 8.5.10 + '@wdio/config': 8.33.1 + '@wdio/logger': 8.28.0 + '@wdio/protocols': 8.32.0 + '@wdio/types': 8.32.4 + '@wdio/utils': 8.33.1 + deepmerge-ts: 5.1.0 + got: 12.6.1 + ky: 0.33.3 + ws: 8.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /webdriverio@8.33.1(typescript@5.1.3): + resolution: {integrity: sha512-1DsF8sx1a46AoVYCUpEwJYU74iBAW/U2H5r6p+60ct7dIiFmxmc4uCbOqtf7NLOTgrIzAOaRnT0EsrRICpg5Qw==} + engines: {node: ^16.13 || >=18} + peerDependencies: + devtools: ^8.14.0 + peerDependenciesMeta: + devtools: + optional: true + dependencies: + '@types/node': 20.10.5 + '@wdio/config': 8.33.1 + '@wdio/logger': 8.28.0 + '@wdio/protocols': 8.32.0 + '@wdio/repl': 8.24.12 + '@wdio/types': 8.32.4 + '@wdio/utils': 8.33.1 + archiver: 7.0.1 + aria-query: 5.3.0 + css-shorthand-properties: 1.1.1 + css-value: 0.0.1 + devtools-protocol: 0.0.1263784 + grapheme-splitter: 1.0.4 + import-meta-resolve: 4.0.0 + is-plain-obj: 4.1.0 + lodash.clonedeep: 4.5.0 + lodash.zip: 4.2.0 + minimatch: 9.0.3 + puppeteer-core: 20.9.0(typescript@5.1.3) + query-selector-shadow-dom: 1.0.1 + resq: 1.11.0 + rgb2hex: 0.2.5 + serialize-error: 11.0.3 + webdriver: 8.33.1 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - typescript + - utf-8-validate + dev: true + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + requiresBuild: true /webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} @@ -15779,6 +20168,20 @@ packages: - uglify-js dev: true + /websocket@1.0.34: + resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==} + engines: {node: '>=4.0.0'} + dependencies: + bufferutil: 4.0.8 + debug: 2.6.9 + es5-ext: 0.10.64 + typedarray-to-buffer: 3.1.5 + utf-8-validate: 5.0.10 + yaeti: 0.0.6 + transitivePeerDependencies: + - supports-color + dev: false + /well-known-symbols@2.0.0: resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} engines: {node: '>=6'} @@ -15786,6 +20189,7 @@ packages: /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + requiresBuild: true dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 @@ -15800,6 +20204,16 @@ packages: is-symbol: 1.0.4 dev: true + /which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + dev: true + /which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} dev: false @@ -15814,6 +20228,17 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -15821,6 +20246,14 @@ packages: dependencies: isexe: 2.0.0 + /which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + isexe: 3.1.1 + dev: true + /why-is-node-running@2.2.2: resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} engines: {node: '>=8'} @@ -15843,6 +20276,19 @@ packages: bs58check: 2.1.2 dev: false + /winston-transport@4.7.0: + resolution: {integrity: sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==} + engines: {node: '>= 12.0.0'} + dependencies: + logform: 2.6.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + dev: true + + /workerpool@6.2.1: + resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + dev: true + /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -15909,7 +20355,6 @@ packages: optional: true utf-8-validate: optional: true - dev: false /ws@8.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): resolution: {integrity: sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==} @@ -15925,7 +20370,6 @@ packages: dependencies: bufferutil: 4.0.8 utf-8-validate: 5.0.10 - dev: false /ws@8.5.0: resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==} @@ -15961,6 +20405,11 @@ packages: engines: {node: '>=10'} dev: true + /yaeti@0.0.6: + resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} + engines: {node: '>=0.10.32'} + dev: false + /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -15975,6 +20424,11 @@ packages: decamelize: 1.2.0 dev: false + /yargs-parser@20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: true + /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -15985,6 +20439,16 @@ packages: engines: {node: '>=12'} dev: true + /yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: true + /yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} @@ -16002,6 +20466,33 @@ packages: yargs-parser: 18.1.3 dev: false + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: true + + /yargs@17.7.1: + resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} + engines: {node: '>=12'} + requiresBuild: true + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -16015,11 +20506,32 @@ packages: yargs-parser: 21.1.1 dev: true + /yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + requiresBuild: true + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + dev: true + + /yauzl@3.1.2: + resolution: {integrity: sha512-621iCPgEG1wXViDZS/L3h9F8TgrdQV1eayJlJ8j5A2SZg8OdY/1DLf+VxNeD+q5QbMFEAbjjR8nITj7g4nKa0Q==} + engines: {node: '>=12'} + dependencies: + buffer-crc32: 0.2.13 + pend: 1.2.0 + dev: true + /ylru@1.3.2: resolution: {integrity: sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==} engines: {node: '>= 4.0.0'} dev: true + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -16030,6 +20542,16 @@ packages: engines: {node: '>=12.20'} dev: true + /zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.5.2 + dev: true + /zustand@4.4.7(@types/react@18.2.14)(react@18.2.0): resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} engines: {node: '>=12.7.0'} diff --git a/public/configs/cctp.json b/public/configs/cctp.json index a14cb3c9e..7373c4b2f 100644 --- a/public/configs/cctp.json +++ b/public/configs/cctp.json @@ -7,7 +7,8 @@ { "chainId": "5", "tokenAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F", - "name": "Ethereum Goerli" + "name": "Ethereum Goerli", + "isTestnet": true }, { "chainId": "43114", @@ -18,5 +19,10 @@ "chainId": "10", "tokenAddress": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", "name": "optimism" + }, + { + "chainId": "42161", + "tokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "name": "arbitrum" } ] diff --git a/public/configs/env.json b/public/configs/env.json index 20845e1d0..237a92a36 100644 --- a/public/configs/env.json +++ b/public/configs/env.json @@ -1,4 +1,5 @@ { + "notes": "THIS FILE IS MOSTLY DEPRECATED BUT OLDER MOBILE VERSIONS STILL EXPECT IT. This means that any FF updates still apply.", "apps": { "ios": { "scheme": "dydx-t-v4" @@ -79,6 +80,7 @@ ], "0xsquid": "https://testnet.api.0xsquid.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4dev.dydx.exchange" }, "links": { @@ -98,7 +100,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -126,7 +129,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-dev-2": { @@ -163,7 +168,8 @@ "http://54.92.118.111" ], "0xsquid": "https://testnet.api.0xsquid.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "links": { "tos": "https://dydx.exchange/v4-terms", @@ -182,7 +188,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -210,7 +217,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-dev-4": { @@ -248,6 +257,7 @@ ], "0xsquid": "https://testnet.api.0xsquid.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4dev4.dydx.exchange" }, "links": { @@ -267,7 +277,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -295,7 +306,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-dev-5": { @@ -332,7 +345,8 @@ "http://18.223.78.50" ], "0xsquid": "https://testnet.api.0xsquid.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "links": { "tos": "https://dydx.exchange/v4-terms", @@ -351,7 +365,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -379,7 +394,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-staging": { @@ -417,7 +434,8 @@ "https://validator.v4staging.dydx.exchange" ], "0xsquid": "https://testnet.api.squidrouter.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "links": { "tos": "https://dydx.exchange/v4-terms", @@ -437,7 +455,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -465,7 +484,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-staging-forced-update": { @@ -503,7 +524,8 @@ "https://validator.v4staging.dydx.exchange" ], "0xsquid": "https://testnet.api.squidrouter.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "links": { "tos": "https://dydx.exchange/v4-terms", @@ -514,7 +536,8 @@ "community": "https://discord.com/invite/dydx", "feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform", "blogs": "https://www.dydx.foundation/blog", - "newMarketProposalLearnMore": "https://dydx.exchange/blog/new-market-proposals" + "newMarketProposalLearnMore": "https://dydx.exchange/blog/new-market-proposals", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -549,7 +572,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-staging-west": { @@ -587,7 +612,8 @@ "https://validator-uswest1.v4staging.dydx.exchange" ], "0xsquid": "https://testnet.api.squidrouter.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "links": { "tos": "https://dydx.exchange/v4-terms", @@ -607,7 +633,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -635,7 +662,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-testnet": { @@ -677,6 +706,7 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "links": { @@ -697,7 +727,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -725,7 +756,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-testnet-dydx": { @@ -763,6 +796,7 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "links": { @@ -784,7 +818,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -812,7 +847,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-testnet-nodefleet": { @@ -850,6 +887,7 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "links": { @@ -871,7 +909,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -899,7 +938,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-testnet-kingnodes": { @@ -937,6 +978,7 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "links": { @@ -958,7 +1000,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -986,7 +1029,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-testnet-liquify": { @@ -1024,6 +1069,7 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "links": { @@ -1045,7 +1091,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "walletLearnMore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -1073,7 +1120,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-testnet-polkachu": { @@ -1111,6 +1160,7 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "links": { @@ -1123,7 +1173,8 @@ "community": "https://discord.com/invite/dydx", "feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform", "blogs": "https://www.dydx.foundation/blog", - "newMarketProposalLearnMore": "https://dydx.exchange/blog/new-market-proposals" + "newMarketProposalLearnMore": "https://dydx.exchange/blog/new-market-proposals", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -1151,7 +1202,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-testnet-bware": { @@ -1189,6 +1242,7 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "links": { @@ -1210,7 +1264,8 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnmore": "https://help.dydx.exchange", - "walletLearnmore": "https://www.dydx.academy/video/defi-wallet" + "walletLearnmore": "https://www.dydx.academy/video/defi-wallet", + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665" }, "wallets": { "walletconnect": { @@ -1238,7 +1293,9 @@ } }, "featureFlags": { - "reduceOnlySupported": true + "reduceOnlySupported": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true } }, "dydxprotocol-mainnet": { @@ -1277,7 +1334,8 @@ "[Validator endpoint n]" ], "0xsquid": "[0xSquid endpoint for mainnet]", - "nobleValidator": "[noble validator endpoint for mainnet]" + "nobleValidator": "[noble validator endpoint for mainnet]", + "geo": "[geo endpoint for mainnet]" }, "links": { "tos": "[HTTP link to TOS]", @@ -1297,7 +1355,8 @@ "keplrDashboard": "[HTTP link to keplr dashboard, can be null]", "strideZoneApp": "[HTTP link to stride zone app, can be null]", "accountExportLearnMore": "[HTTP link to account export learn more, can be null]", - "walletLearnMore": "[HTTP link to wallet learn more, can be null]" + "walletLearnMore": "[HTTP link to wallet learn more, can be null]", + "withdrawalGateLearnMore": "[HTTP link to withdrawal gate learn more, can be null]" }, "wallets": { "walletconnect": { @@ -1325,8 +1384,9 @@ } }, "featureFlags": { - "reduceOnlySupported": false + "reduceOnlySupported": false, + "withdrawalSafetyEnabled": false } } } -} \ No newline at end of file +} diff --git a/public/configs/markets.json b/public/configs/markets.json index 1a3429926..08d7bfcee 100644 --- a/public/configs/markets.json +++ b/public/configs/markets.json @@ -20,6 +20,13 @@ "whitepaperLink": "https://why.cardano.org/en/introduction/motivation/", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/cardano/" }, + "AEVO-USD": { + "name": "Aevo", + "tags": ["Defi"], + "websiteLink": "https://www.aevo.xyz/", + "whitepaperLink": "https://docs.aevo.xyz/aevo-exchange/introduction", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/aevo/" + }, "AGIX-USD": { "name": "SingularityNET", "tags": ["AI"], @@ -41,6 +48,13 @@ "whitepaperLink": "https://apecoin.com/about", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/apecoin-ape/" }, + "API3-USD": { + "name": "API3", + "tags": [], + "websiteLink": "https://api3.org/", + "whitepaperLink": "https://drive.google.com/file/d/1JMVwk9pkGF7hvjkuu6ABA0-FrhRTzAwF/view", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/api3/" + }, "APT-USD": { "name": "Aptos", "tags": ["Layer 1"], @@ -50,11 +64,25 @@ }, "ARB-USD": { "name": "Arbitrum", - "tags": [], + "tags": ["Layer 2"], "websiteLink": "https://arbitrum.io/", "whitepaperLink": "https://github.com/OffchainLabs/nitro", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/arbitrum/" }, + "ARKM-USD": { + "name": "Arkham", + "tags": [], + "websiteLink": "https://www.arkhamintelligence.com/", + "whitepaperLink": "https://www.arkhamintelligence.com/whitepaper", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/arkham/" + }, + "ASTR-USD": { + "name": "Astar", + "tags": ["Layer 2"], + "websiteLink": "https://astar.network/", + "whitepaperLink": "https://docs.astar.network/docs/getting-started", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/astar/" + }, "ATOM-USD": { "name": "Cosmos", "tags": ["Layer 1"], @@ -69,6 +97,13 @@ "whitepaperLink": "https://assets.website-files.com/5d80307810123f5ffbb34d6e/6008d7bbf8b10d1eb01e7e16_Avalanche%20Platform%20Whitepaper.pdf", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/avalanche/" }, + "AXL-USD": { + "name": "Axelar", + "tags": [], + "websiteLink": "https://axelar.network/", + "whitepaperLink": "https://axelar.network/axelar_whitepaper.pdf", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/axelar/" + }, "BCH-USD": { "name": "Bitcoin Cash", "tags": ["Layer 1"], @@ -76,9 +111,15 @@ "whitepaperLink": "https://bitcoincash.org/", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/bitcoin-cash/" }, + "BOME-USD": { + "name": "BOOK OF MEME", + "tags": ["Meme"], + "websiteLink": "https://llwapirxnupqu7xw2fspfidormcfar7ek2yp65nu7k5opjwhdywq.arweave.net/WuwHojdtHwp-9tFk8qBuiwRQR-RWsP91tPq656bHHi0", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/book-of-meme/" + }, "BONK-USD": { "name": "BONK COIN", - "tags": [], + "tags": ["Meme"], "websiteLink": "https://bonkcoin.com/", "whitepaperLink": "https://bonkcoin.com/", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/bonk1/" @@ -135,7 +176,7 @@ }, "DOGE-USD": { "name": "Dogecoin", - "tags": ["Layer 1"], + "tags": ["Layer 1", "Meme"], "websiteLink": "https://dogecoin.com/", "whitepaperLink": "https://github.com/dogecoin/dogecoin", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/dogecoin/" @@ -147,6 +188,12 @@ "whitepaperLink": "https://polkadot.network/PolkaDotPaper.pdf", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/polkadot-new/" }, + "DYDX-USD": { + "name": "dYdX", + "tags": ["Layer 1", "Defi"], + "websiteLink": "https://dydx.exchange/", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/dydx-chain/" + }, "DYM-USD": { "name": "Dymension", "tags": [], @@ -191,6 +238,13 @@ "displayStepSize": "0.001", "displayTickSize": "0.1" }, + "ETHFI-USD": { + "name": "ether.fi", + "tags": [], + "websiteLink": "https://www.ether.fi/", + "whitepaperLink": "https://etherfi.gitbook.io/etherfi/ether.fi-whitepaper", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/ether-fi-ethfi/" + }, "FET-USD": { "name": "Fetch.ai", "tags": ["AI"], @@ -205,6 +259,13 @@ "whitepaperLink": "https://filecoin.io/filecoin.pdf", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/filecoin/" }, + "FLR-USD": { + "name": "Flare", + "tags": ["Layer 1"], + "websiteLink": "https://flare.network/", + "whitepaperLink": "https://docs.flare.network/", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/flare/" + }, "FTM-USD": { "name": "Fantom", "tags": [], @@ -310,6 +371,13 @@ "whitepaperLink": "https://litecoin.info/index.php/Main_Page", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/litecoin/" }, + "MAGIC-USD": { + "name": "MAGIC", + "tags": ["NFT"], + "websiteLink": "https://treasure.lol/", + "whitepaperLink": "https://files.treasure.lol/Treasure+Infinity+Chains+-+Litepaper+v1.0.pdf", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/magic-token/" + }, "MANA-USD": { "name": "Decentraland", "tags": ["AR/VR"], @@ -331,6 +399,13 @@ "whitepaperLink": "https://polygon.technology/lightpaper-polygon.pdf", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/polygon/" }, + "MEME-USD": { + "name": "Memecoin", + "tags": ["Meme"], + "websiteLink": "https://www.memecoin.org/", + "whitepaperLink": "https://www.memecoin.org/whitepaper", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/meme/" + }, "MINA-USD": { "name": "Mina", "tags": ["Layer 1"], @@ -352,6 +427,13 @@ "whitepaperLink": "https://near.org/papers/the-official-near-white-paper/", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/near-protocol/" }, + "OCEAN-USD": { + "name": "Ocean Protocol", + "tags": ["AI"], + "websiteLink": "https://oceanprotocol.com/", + "whitepaperLink": "https://oceanprotocol.com/tech-whitepaper.pdf", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/ocean-protocol/" + }, "ORDI-USD": { "name": "Ordinals", "tags": ["NFT"], @@ -361,17 +443,24 @@ }, "OP-USD": { "name": "Optimism", - "tags": [], + "tags": ["Layer 2"], "websiteLink": "https://www.optimism.io/", "whitepaperLink": "https://github.com/ethereum-optimism", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/optimism-ethereum/" }, "PEPE-USD": { "name": "Pepe", - "tags": [], + "tags": ["Meme"], "websiteLink": "https://www.pepe.vip/", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/pepe/" }, + "PORTAL-USD": { + "name": "PORTAL", + "tags": ["Gaming"], + "websiteLink": "https://www.portalgaming.com/", + "whitepaperLink": "https://portalxyz.nyc3.cdn.digitaloceanspaces.com/Portal_Whitepaper.pdf", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/portal-gaming/" + }, "PYTH-USD": { "name": "Pyth Network", "tags": [], @@ -409,7 +498,7 @@ }, "SHIB-USD": { "name": "Shiba Inu", - "tags": [], + "tags": ["Meme"], "websiteLink": "https://shibatoken.com/", "whitepaperLink": "https://docs.shibatoken.com/", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/shiba-inu/" @@ -463,6 +552,13 @@ "whitepaperLink": "https://arxiv.org/pdf/1905.09274.pdf", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/celestia/" }, + "TON-USD": { + "name": "Toncoin", + "tags": ["Layer 1"], + "websiteLink": "https://ton.org/", + "whitepaperLink": "https://ton.org/whitepaper.pdf", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/toncoin/" + }, "TRX-USD": { "name": "TRON", "tags": ["Defi"], @@ -484,13 +580,19 @@ "whitepaperLink": "https://uniswap.org/whitepaper-v3.pdf", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/uniswap/" }, + "WIF-USD": { + "name": "dogwifhat", + "tags": ["Meme"], + "websiteLink": "https://dogwifcoin.org/", + "coinMarketCapsLink": "https://coinmarketcap.com/currencies/dogwifhat/" + }, "WLD-USD": { "name": "Worldcoin", "tags": [], "websiteLink": "https://worldcoin.org/", "whitepaperLink": "https://whitepaper.worldcoin.org/", "coinMarketCapsLink": "https://coinmarketcap.com/currencies/worldcoin-org/" - }, + }, "WOO-USD": { "name": "WOO Network", "tags": ["Defi"], diff --git a/public/configs/otherMarketData.json b/public/configs/otherMarketData.json new file mode 100644 index 000000000..4e311150b --- /dev/null +++ b/public/configs/otherMarketData.json @@ -0,0 +1,4692 @@ +{ + "BNB": { + "title": "Add BNB-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a BNB-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 2, + "ticker": "BNB-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -7.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "BNBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "BNBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "BNB_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "bnbusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BNB-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "BNB_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "BNB-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -8.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "BNB", + "referencePrice": 569.4292213639478 + } + }, + "SOL": { + "title": "Add SOL-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a SOL-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 3, + "ticker": "SOL-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -7.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "SOLUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "SOL/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SOLUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SOL-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "solusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "SOLUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SOL-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SOL_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SOL-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -8.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Solana", + "referencePrice": 140.0695815613181 + } + }, + "TON": { + "title": "Add TON-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a TON-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 6, + "ticker": "TON-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Bybit", + "ticker": "TONUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "TON_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "tonusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "TON-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "TON_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "TON-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Toncoin", + "referencePrice": 5.398017199773806 + } + }, + "ADA": { + "title": "Add ADA-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ADA-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 7, + "ticker": "ADA-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ADAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "ADA/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ADAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ADA-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "adausdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "ADAUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ADA-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ADA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ADA-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Cardano", + "referencePrice": 0.45843945043506734 + } + }, + "SHIB": { + "title": "Add SHIB-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a SHIB-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 8, + "ticker": "SHIB-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -14.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "SHIBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "SHIB/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SHIBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SHIB-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "shibusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "SHIBUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SHIB-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SHIB_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SHIB-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -1.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Shiba Inu", + "referencePrice": 2.3375234454638113e-05 + } + }, + "AVAX": { + "title": "Add AVAX-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a AVAX-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 9, + "ticker": "AVAX-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -8.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "AVAXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "AVAX/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "AVAXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "AVAX-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "avaxusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "AVAXUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "AVAX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "AVAX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "AVAX-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -7.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Avalanche", + "referencePrice": 34.30079483175494 + } + }, + "TRX": { + "title": "Add TRX-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a TRX-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 10, + "ticker": "TRX-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "TRXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "TRXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "TRX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "trxusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "TRXUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "TRX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "TRX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "TRX-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "TRON", + "referencePrice": 0.12269761322340017 + } + }, + "DOT": { + "title": "Add DOT-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a DOT-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 11, + "ticker": "DOT-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "DOTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "DOT/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "DOTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "DOT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "dotusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "DOTUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "DOT-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "DOT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "DOT-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Polkadot", + "referencePrice": 7.192690961603802 + } + }, + "BCH": { + "title": "Add BCH-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a BCH-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 12, + "ticker": "BCH-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -7.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "BCHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "BCH/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "BCHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "BCH-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "bchusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "BCHUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BCH-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "BCH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "BCH-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -8.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Bitcoin Cash", + "referencePrice": 440.3540174612563 + } + }, + "LINK": { + "title": "Add LINK-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a LINK-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 13, + "ticker": "LINK-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -8.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "LINKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "LINK/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "LINKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "LINK-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "linkusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "LINKUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "LINK-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "LINK_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "LINK-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -7.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Chainlink", + "referencePrice": 13.740392173852204 + } + }, + "NEAR": { + "title": "Add NEAR-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a NEAR-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 14, + "ticker": "NEAR-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "NEARUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "NEARUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "NEAR-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "NEARUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "NEAR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "NEAR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "NEAR-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "NEAR Protocol", + "referencePrice": 6.401351018238966 + } + }, + "MATIC": { + "title": "Add MATIC-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a MATIC-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 15, + "ticker": "MATIC-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "MATICUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "MATIC/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "MATICUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "MATIC-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "maticusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "MATICUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "MATIC-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "MATIC_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "MATIC-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Polygon", + "referencePrice": 0.7250861858190705 + } + }, + "ICP": { + "title": "Add ICP-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ICP-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 16, + "ticker": "ICP-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -8.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ICPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ICPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ICP-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "ICPUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ICP-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ICP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ICP-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -7.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Internet Computer", + "referencePrice": 13.461883080028814 + } + }, + "DAI": { + "title": "Add DAI-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a DAI-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 18, + "ticker": "DAI-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Bybit", + "ticker": "DAIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "DAI-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "daiusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "DAIUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "DAI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "DAI-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Dai", + "referencePrice": 0.999973292519322 + } + }, + "UNI": { + "title": "Add UNI-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a UNI-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 19, + "ticker": "UNI-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "UNIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "UNI/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "UNIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "UNI-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "uniusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "UNIUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "UNI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "UNI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "UNI-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Uniswap", + "referencePrice": 7.192308868501533 + } + }, + "HBAR": { + "title": "Add HBAR-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a HBAR-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 20, + "ticker": "HBAR-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "HBARUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "HBARUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "HBAR-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "HBAR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "HBAR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "HBAR-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Hedera", + "referencePrice": 0.10622961548367621 + } + }, + "APT": { + "title": "Add APT-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a APT-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 22, + "ticker": "APT-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "APTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "APTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "APT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "APT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "aptusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "APTUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "APT-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "APT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "APT-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Aptos", + "referencePrice": 8.99553250384946 + } + }, + "ATOM": { + "title": "Add ATOM-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ATOM-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 23, + "ticker": "ATOM-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ATOMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ATOMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ATOM-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ATOM_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "atomusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "ATOMUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ATOM-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ATOM_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ATOM-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Cosmos", + "referencePrice": 8.715811388666577 + } + }, + "PEPE": { + "title": "Add PEPE-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a PEPE-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 24, + "ticker": "PEPE-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -15.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "PEPEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "PEPEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "pepeusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "PEPEUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "PEPE-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "PEPE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "PEPE-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": 0.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Pepe", + "referencePrice": 7.850448996650624e-06 + } + }, + "FIL": { + "title": "Add FIL-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a FIL-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 25, + "ticker": "FIL-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "FILUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "FILUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "FIL-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "filusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "FILUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "FIL-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "FIL_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "FIL-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Filecoin", + "referencePrice": 5.965447897886074 + } + }, + "IMX": { + "title": "Add IMX-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a IMX-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 26, + "ticker": "IMX-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "IMXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "IMX-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "IMXUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "IMX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "IMX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "IMX-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Immutable", + "referencePrice": 2.239870415647921 + } + }, + "STX": { + "title": "Add STX-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a STX-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 27, + "ticker": "STX-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "STXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "STXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "STX-USD" + }, + { + "exchangeName": "Gate", + "ticker": "STX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "STXUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "STX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "STX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "STX-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Stacks", + "referencePrice": 2.1246991809215925 + } + }, + "RNDR": { + "title": "Add RNDR-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a RNDR-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 29, + "ticker": "RNDR-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "RNDRUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "RNDRUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "RNDR-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "RNDRUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "RNDR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "RNDR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "RNDR-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Render", + "referencePrice": 7.915418042813452 + } + }, + "WIF": { + "title": "Add WIF-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a WIF-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 30, + "ticker": "WIF-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "WIFUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "WIFUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "WIF_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "wifusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "WIFUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "WIF-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "WIF_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "WIF-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "dogwifhat", + "referencePrice": 2.8554676486670507 + } + }, + "OP": { + "title": "Add OP-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a OP-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 31, + "ticker": "OP-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "OPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "OPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "OP-USD" + }, + { + "exchangeName": "Gate", + "ticker": "OP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "OPUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "OP-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "OP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "OP-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Optimism", + "referencePrice": 2.8811216328565834 + } + }, + "ARB": { + "title": "Add ARB-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ARB-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 32, + "ticker": "ARB-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ARBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ARBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ARB-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "ARBUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ARB-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ARB_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ARB-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Arbitrum", + "referencePrice": 1.0427973889055515 + } + }, + "MKR": { + "title": "Add MKR-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a MKR-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 33, + "ticker": "MKR-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -6.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "MKRUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "MKRUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "MKR-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "MKRUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "MKR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "MKR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "MKR-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -9.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Maker", + "referencePrice": 2785.422848265722 + } + }, + "GRT": { + "title": "Add GRT-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a GRT-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 34, + "ticker": "GRT-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "GRTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "GRTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "GRT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "GRT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "GRTUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "GRT-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "GRT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "GRT-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "The Graph", + "referencePrice": 0.2585049312196988 + } + }, + "SUI": { + "title": "Add SUI-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a SUI-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 35, + "ticker": "SUI-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "SUIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SUIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SUI-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "suiusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "SUIUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SUI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SUI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SUI-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Sui", + "referencePrice": 1.1113392464358447 + } + }, + "INJ": { + "title": "Add INJ-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a INJ-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 36, + "ticker": "INJ-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -8.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "INJUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "INJUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "INJ-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "INJUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "INJ-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "INJ_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "INJ-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -7.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Injective", + "referencePrice": 23.697718423026043 + } + }, + "FTM": { + "title": "Add FTM-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a FTM-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 37, + "ticker": "FTM-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "FTMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "FTMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "FTMUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "FTM-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "FTM_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "FTM-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Fantom", + "referencePrice": 0.681419534254711 + } + }, + "LDO": { + "title": "Add LDO-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a LDO-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 38, + "ticker": "LDO-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "LDOUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "LDOUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "LDO-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "LDOUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "LDO-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "LDO_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "LDO-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Lido DAO", + "referencePrice": 2.0678068291656686 + } + }, + "FET": { + "title": "Add FET-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a FET-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 39, + "ticker": "FET-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "FETUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "FETUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "FET-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "FETUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "FET-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "FET_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "FET-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Fetch.ai", + "referencePrice": 2.0480362696772465 + } + }, + "TIA": { + "title": "Add TIA-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a TIA-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 40, + "ticker": "TIA-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -8.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "TIAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "TIAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "TIA-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "TIAUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "TIA-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "TIA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "TIA-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -7.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Celestia", + "referencePrice": 10.035079235290235 + } + }, + "RUNE": { + "title": "Add RUNE-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a RUNE-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 41, + "ticker": "RUNE-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "RUNEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "RUNEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "RUNE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "RUNEUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "RUNE-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "RUNE_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "THORChain", + "referencePrice": 5.076032669690655 + } + }, + "BONK": { + "title": "Add BONK-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a BONK-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 42, + "ticker": "BONK-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -14.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "BONKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "BONKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "BONK-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "BONKUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BONK-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "BONK_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "BONK-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -1.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Bonk", + "referencePrice": 2.6385543995978146e-05 + } + }, + "FLOKI": { + "title": "Add FLOKI-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a FLOKI-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 43, + "ticker": "FLOKI-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -13.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "FLOKIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "FLOKIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "FLOKI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "FLOKI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "FLOKI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "FLOKI-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -2.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "FLOKI", + "referencePrice": 0.00016765811114609883 + } + }, + "SEI": { + "title": "Add SEI-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a SEI-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 44, + "ticker": "SEI-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "SEIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SEIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SEI-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "seiusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "SEIUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SEI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SEI_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Sei", + "referencePrice": 0.5471094469491297 + } + }, + "ALGO": { + "title": "Add ALGO-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ALGO-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 45, + "ticker": "ALGO-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ALGOUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ALGOUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ALGO-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "ALGOUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ALGO-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ALGO_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ALGO-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Algorand", + "referencePrice": 0.18543646474053593 + } + }, + "JUP": { + "title": "Add JUP-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a JUP-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 46, + "ticker": "JUP-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "JUPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "JUPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "JUP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "JUPUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "JUP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "JUP-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Jupiter", + "referencePrice": 1.0331907115121979 + } + }, + "FLOW": { + "title": "Add FLOW-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a FLOW-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 47, + "ticker": "FLOW-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "FLOWUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "FLOWUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "FLOW-USD" + }, + { + "exchangeName": "Gate", + "ticker": "FLOW_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "FLOWUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "FLOW_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "FLOW-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Flow", + "referencePrice": 0.8822620105345396 + } + }, + "GALA": { + "title": "Add GALA-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a GALA-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 48, + "ticker": "GALA-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -11.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "GALAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "GALAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "GALA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "GALAUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "GALA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "GALA-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -4.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Gala", + "referencePrice": 0.043510681174046696 + } + }, + "W": { + "title": "Add W-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a W-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 49, + "ticker": "W-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "WUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "WUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "W_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "W-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "W_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "W-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Wormhole", + "referencePrice": 0.726915001899745 + } + }, + "AAVE": { + "title": "Add AAVE-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a AAVE-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 50, + "ticker": "AAVE-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -8.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "AAVEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "AAVEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "AAVE-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "AAVEUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "AAVE-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "AAVE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "AAVE-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -7.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Aave", + "referencePrice": 86.1185187449062 + } + }, + "QNT": { + "title": "Add QNT-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a QNT-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 51, + "ticker": "QNT-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -7.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "QNTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "QNT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "QNT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "QNT-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "QNT_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -8.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Quant", + "referencePrice": 103.50167464219156 + } + }, + "PENDLE": { + "title": "Add PENDLE-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a PENDLE-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 52, + "ticker": "PENDLE-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "PENDLEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "PENDLEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "PENDLE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "PENDLE-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "PENDLE_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Pendle", + "referencePrice": 4.7604237828432865 + } + }, + "FLR": { + "title": "Add FLR-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a FLR-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 53, + "ticker": "FLR-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -11.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Bybit", + "ticker": "FLRUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "FLR-USD" + }, + { + "exchangeName": "Gate", + "ticker": "FLR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "FLRUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "FLR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "FLR-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -4.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Flare", + "referencePrice": 0.030788729815797576 + } + }, + "ENA": { + "title": "Add ENA-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ENA-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 54, + "ticker": "ENA-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ENAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ENAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "enausdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ENA-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ENA_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Ethena", + "referencePrice": 0.8165421882024005 + } + }, + "AGIX": { + "title": "Add AGIX-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a AGIX-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 55, + "ticker": "AGIX-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "AGIXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "AGIXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "AGIX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "agixusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "AGIX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "AGIX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "AGIX-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "SingularityNET", + "referencePrice": 0.8415390669605753 + } + }, + "CHZ": { + "title": "Add CHZ-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a CHZ-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 56, + "ticker": "CHZ-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "CHZUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "CHZ-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "CHZUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "CHZ-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "CHZ_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "CHZ-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Chiliz", + "referencePrice": 0.11782789017330461 + } + }, + "WLD": { + "title": "Add WLD-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a WLD-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 57, + "ticker": "WLD-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "WLDUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "WLDUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "WLD_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "WLD-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "WLD_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "WLD-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Worldcoin", + "referencePrice": 4.835923939590577 + } + }, + "SAND": { + "title": "Add SAND-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a SAND-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 58, + "ticker": "SAND-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "SANDUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SANDUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "SAND_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SAND-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SAND_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SAND-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "The Sandbox", + "referencePrice": 0.43981885830784884 + } + }, + "STRK": { + "title": "Add STRK-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a STRK-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 59, + "ticker": "STRK-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "STRKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "STRKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "STRK-USD" + }, + { + "exchangeName": "Gate", + "ticker": "STRK_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "STRKUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "STRK-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "STRK_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "STRK-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Starknet", + "referencePrice": 1.30890865863542 + } + }, + "EOS": { + "title": "Add EOS-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a EOS-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 60, + "ticker": "EOS-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "EOSUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "EOSUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "EOS-USD" + }, + { + "exchangeName": "Gate", + "ticker": "EOS_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "EOSUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "EOS-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "EOS_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "EOS-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "EOS", + "referencePrice": 0.8189170456499958 + } + }, + "SNX": { + "title": "Add SNX-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a SNX-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 61, + "ticker": "SNX-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "SNXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SNXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SNX-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "SNXUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SNX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SNX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SNX-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Synthetix", + "referencePrice": 2.7262155963302734 + } + }, + "JASMY": { + "title": "Add JASMY-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a JASMY-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 62, + "ticker": "JASMY-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -11.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "JASMYUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "JASMY-USD" + }, + { + "exchangeName": "Gate", + "ticker": "JASMY_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "JASMY-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "JASMY_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -4.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "JasmyCoin", + "referencePrice": 0.017340326288544587 + } + }, + "ORDI": { + "title": "Add ORDI-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ORDI-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 63, + "ticker": "ORDI-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -8.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ORDIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ORDIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ORDI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ORDI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ORDI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ORDI-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -7.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "ORDI", + "referencePrice": 37.726633571856134 + } + }, + "PYTH": { + "title": "Add PYTH-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a PYTH-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 65, + "ticker": "PYTH-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "PYTHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "PYTHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "PYTH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "PYTH-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "PYTH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "PYTH-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Pyth Network", + "referencePrice": 0.5258526269698547 + } + }, + "APE": { + "title": "Add APE-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a APE-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 66, + "ticker": "APE-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 2500, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "APEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "APEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "APE-USD" + }, + { + "exchangeName": "Gate", + "ticker": "APE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "APEUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "APE-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "APE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "APE-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 1, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "ApeCoin", + "referencePrice": 1.2198969896909202 + } + }, + "KAVA": { + "title": "Add KAVA-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a KAVA-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 67, + "ticker": "KAVA-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "KAVAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "KAVA-USD" + }, + { + "exchangeName": "Gate", + "ticker": "KAVA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "KAVAUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "KAVA-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "KAVA_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Kava", + "referencePrice": 0.669887831407206 + } + }, + "BLUR": { + "title": "Add BLUR-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a BLUR-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 68, + "ticker": "BLUR-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "BLURUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "BLURUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "BLUR-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "BLURUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BLUR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "BLUR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "BLUR-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Blur", + "referencePrice": 0.4049318042813457 + } + }, + "DYDX": { + "title": "Add DYDX-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a DYDX-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 69, + "ticker": "DYDX-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "DYDXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "DYDXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "DYDX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "DYDXUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "DYDX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "DYDX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "DYDX-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "dYdX (ethDYDX)", + "referencePrice": 2.1353224345766795 + } + }, + "BOME": { + "title": "Add BOME-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a BOME-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 70, + "ticker": "BOME-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -12.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Bybit", + "ticker": "BOMEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "BOME_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BOME-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "BOME_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -3.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "BOOK OF MEME", + "referencePrice": 0.009734598422711484 + } + }, + "OCEAN": { + "title": "Add OCEAN-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a OCEAN-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 72, + "ticker": "OCEAN-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "OCEANUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "OCEAN-USD" + }, + { + "exchangeName": "Gate", + "ticker": "OCEAN_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "OCEANUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "OCEAN-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "OCEAN_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Ocean Protocol", + "referencePrice": 0.8639639176875745 + } + }, + "ENS": { + "title": "Add ENS-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ENS-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 73, + "ticker": "ENS-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -8.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ENSUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ENS-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ENS_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ENS-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ENS_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ENS-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -7.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Ethereum Name Service", + "referencePrice": 15.111526941995683 + } + }, + "DYM": { + "title": "Add DYM-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a DYM-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 74, + "ticker": "DYM-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "DYMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "DYMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "DYM_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "DYM-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "DYM_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Dymension", + "referencePrice": 3.243814894569103 + } + }, + "ETHFI": { + "title": "Add ETHFI-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ETHFI-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 75, + "ticker": "ETHFI-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ETHFIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ETHFIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ETHFI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ETHFI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ETHFI-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "ether.fi", + "referencePrice": 3.811710106518133 + } + }, + "1INCH": { + "title": "Add 1INCH-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a 1INCH-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 76, + "ticker": "1INCH-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "1INCHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "1INCH-USD" + }, + { + "exchangeName": "Gate", + "ticker": "1INCH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "1INCH-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "1INCH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "1INCH-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "1inch Network", + "referencePrice": 0.3738838045665926 + } + }, + "ARKM": { + "title": "Add ARKM-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ARKM-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 77, + "ticker": "ARKM-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "ARKMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ARKMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ARKM-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ARKM_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ARKM_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Arkham", + "referencePrice": 1.974821650546914 + } + }, + "GMT": { + "title": "Add GMT-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a GMT-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 78, + "ticker": "GMT-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "GMTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "GMTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "GMT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "GMT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "GMT-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "GMT-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "GMT", + "referencePrice": 0.22263372152083444 + } + }, + "ZETA": { + "title": "Add ZETA-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a ZETA-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 80, + "ticker": "ZETA-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Bybit", + "ticker": "ZETAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ZETA-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ZETA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ZETA-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ZETA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ZETA-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "ZetaChain", + "referencePrice": 1.5606952628372133 + } + }, + "MEME": { + "title": "Add MEME-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a MEME-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 81, + "ticker": "MEME-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -11.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "MEMEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "MEMEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "MEME_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "MEME-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "MEME_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -4.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Memecoin", + "referencePrice": 0.024552139525164007 + } + }, + "MASK": { + "title": "Add MASK-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a MASK-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 82, + "ticker": "MASK-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "MASKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "MASKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "MASK-USD" + }, + { + "exchangeName": "Gate", + "ticker": "MASK_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "MASK-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "MASK_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "MASK-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Mask Network", + "referencePrice": 3.258105069878578 + } + }, + "XAI": { + "title": "Add XAI-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a XAI-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 83, + "ticker": "XAI-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "XAIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "XAIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "XAI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "XAI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "XAI_USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Xai", + "referencePrice": 0.7213160441229187 + } + }, + "AEVO": { + "title": "Add AEVO-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a AEVO-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 84, + "ticker": "AEVO-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -9.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "AEVOUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "AEVOUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "AEVO_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "AEVO-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "AEVO_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "AEVO-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -6.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Aevo", + "referencePrice": 1.4352432176346883 + } + }, + "TNSR": { + "title": "Add TNSR-USD perpetual market", + "summary": "Add the x/prices, x/perpetuals and x/clob parameters needed for a TNSR-USD perpetual market. Create the market in INITIALIZING status and transition it to ACTIVE status after 3600 blocks. Added via the new market widget.", + "params": { + "id": 85, + "ticker": "TNSR-USD", + "marketType": "PERPETUAL_MARKET_TYPE_CROSS", + "priceExponent": -10.0, + "minPriceChange": 4000, + "minExchanges": 3, + "exchangeConfigJson": [ + { + "exchangeName": "Binance", + "ticker": "TNSRUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "TNSRUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "TNSR-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "TNSR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "TNSR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "TNSR-USDT", + "adjustByMarket": "USDT-USD" + } + ], + "liquidityTier": 2, + "atomicResolution": -5.0, + "quantumConversionExponent": -9, + "defaultFundingPpm": 0, + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000, + "delayBlocks": 3600 + }, + "initialDeposit": { + "denom": "adydx", + "amount": "10000000000000000000000" + }, + "meta": { + "assetName": "Tensor", + "referencePrice": 0.8688762328845198 + } + } +} \ No newline at end of file diff --git a/public/configs/otherMarketDisclaimer.md b/public/configs/otherMarketDisclaimer.md index 68f8284df..c4b644091 100644 --- a/public/configs/otherMarketDisclaimer.md +++ b/public/configs/otherMarketDisclaimer.md @@ -1 +1 @@ -This file identifies parameters for the optimal performance of various assets with the dYdX v4 open source software ("dYdX Chain"). For information on which assets are likely to be best compatible with dYdX Chain and how likely software compatibility and optimal parameters are assessed, please review the documentation [here](https://docs.dydx.trade/governance/proposing_a_new_market#example-proposal-json). Users considering using the permissionless markets function of the dYdX Chain are encouraged to consult qualified legal counsel to ensure compliance with the laws of their jurisdiction. The information herein does not constitute and should not be relied on as an endorsement or recommendation for any specific market, or investment, legal, or any other form of professional advice. Use of the v4 software is prohibited in the United States, Canada, and sanctioned jurisdictions as described in the [v4 Terms of Use](https://dydx.exchange/v4-terms). +The otherMarketData.json file identifies parameters for the optimal performance of various assets with the dYdX v4 open source software ("dYdX Chain"). For information on which assets are likely to be best compatible with dYdX Chain and how likely software compatibility and optimal parameters are assessed, please review the documentation [here](https://docs.dydx.trade/governance/proposing_a_new_market#example-proposal-json). Users considering using the permissionless markets function of the dYdX Chain are encouraged to consult qualified legal counsel to ensure compliance with the laws of their jurisdiction. The information herein does not constitute and should not be relied on as an endorsement or recommendation for any specific market, or investment, legal, or any other form of professional advice. Use of the v4 software is prohibited in the United States, Canada, and sanctioned jurisdictions as described in the [v4 Terms of Use](https://dydx.exchange/v4-terms). diff --git a/public/configs/otherMarketExchangeConfig.json b/public/configs/otherMarketExchangeConfig.json deleted file mode 100644 index 0d800ef58..000000000 --- a/public/configs/otherMarketExchangeConfig.json +++ /dev/null @@ -1,616 +0,0 @@ -{ - "1INCH": [ - { "exchangeName": "Binance", "ticker": "1INCHUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "1INCH-USD" }, - { "exchangeName": "Gate", "ticker": "1INCH_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "1INCH-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "1INCH-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "1INCH_USDT", "adjustByMarket": "USDT-USD" } - ], - "AAVE": [ - { "exchangeName": "Binance", "ticker": "AAVEUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "AAVE-USD" }, - { "exchangeName": "Huobi", "ticker": "aaveusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "AAVEUSD" }, - { "exchangeName": "Kucoin", "ticker": "AAVE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "AAVE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "AAVE_USDT", "adjustByMarket": "USDT-USD" } - ], - "ADA": [ - { "exchangeName": "Binance", "ticker": "ADAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "ADA/USD" }, - { "exchangeName": "Bybit", "ticker": "ADAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ADA-USD" }, - { "exchangeName": "Huobi", "ticker": "adausdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "ADAUSD" }, - { "exchangeName": "Kucoin", "ticker": "ADA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ADA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ADA_USDT", "adjustByMarket": "USDT-USD" } - ], - "AGIX": [ - { "exchangeName": "Binance", "ticker": "AGIXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "AGIXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "AGIX_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "AGIX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "AGIX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "AGIX_USDT", "adjustByMarket": "USDT-USD" } - ], - "ALGO": [ - { "exchangeName": "Binance", "ticker": "ALGOUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ALGO-USD" }, - { "exchangeName": "Kraken", "ticker": "ALGOUSD" }, - { "exchangeName": "Kucoin", "ticker": "ALGO-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ALGO-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ALGO_USDT", "adjustByMarket": "USDT-USD" } - ], - "APE": [ - { "exchangeName": "Binance", "ticker": "APEUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "APE-USD" }, - { "exchangeName": "Gate", "ticker": "APE_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "APEUSD" }, - { "exchangeName": "Kucoin", "ticker": "APE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "APE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "APE_USDT", "adjustByMarket": "USDT-USD" } - ], - "APT": [ - { "exchangeName": "Binance", "ticker": "APTUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "APTUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "APT-USD" }, - { "exchangeName": "Gate", "ticker": "APT_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Huobi", "ticker": "aptusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "APTUSD" }, - { "exchangeName": "Kucoin", "ticker": "APT-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "APT-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "APT_USDT", "adjustByMarket": "USDT-USD" } - ], - "ARB": [ - { "exchangeName": "Binance", "ticker": "ARBUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "ARBUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ARB-USD" }, - { "exchangeName": "Huobi", "ticker": "arbusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "ARBUSD" }, - { "exchangeName": "Kucoin", "ticker": "ARB-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ARB-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ARB_USDT", "adjustByMarket": "USDT-USD" } - ], - "ATOM": [ - { "exchangeName": "Binance", "ticker": "ATOMUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "ATOMUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ATOM-USD" }, - { "exchangeName": "Gate", "ticker": "ATOM_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "ATOMUSD" }, - { "exchangeName": "Kucoin", "ticker": "ATOM-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ATOM-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ATOM_USDT", "adjustByMarket": "USDT-USD" } - ], - "AVAX": [ - { "exchangeName": "Binance", "ticker": "AVAXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "AVAX/USD" }, - { "exchangeName": "Bybit", "ticker": "AVAXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "AVAX-USD" }, - { "exchangeName": "Huobi", "ticker": "avaxusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "AVAXUSD" }, - { "exchangeName": "Kucoin", "ticker": "AVAX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "AVAX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "AVAX_USDT", "adjustByMarket": "USDT-USD" } - ], - "BCH": [ - { "exchangeName": "Binance", "ticker": "BCHUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "BCH/USD" }, - { "exchangeName": "Bybit", "ticker": "BCHUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "BCH-USD" }, - { "exchangeName": "Huobi", "ticker": "bchusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "BCHUSD" }, - { "exchangeName": "Kucoin", "ticker": "BCH-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "BCH-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "BCH_USDT", "adjustByMarket": "USDT-USD" } - ], - "BLUR": [ - { "exchangeName": "Binance", "ticker": "BLURUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "BLURUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "BLUR-USD" }, - { "exchangeName": "Kraken", "ticker": "BLURUSD" }, - { "exchangeName": "Kucoin", "ticker": "BLUR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "BLUR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "BLUR_USDT", "adjustByMarket": "USDT-USD" } - ], - "BNB": [ - { "exchangeName": "Binance", "ticker": "BNBUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "BNBUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "BNB_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "BNB-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "BNB-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "BNB_USDT", "adjustByMarket": "USDT-USD" } - ], - "BONK": [ - { "exchangeName": "Binance", "ticker": "BONKUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "BONKUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "BONK-USD" }, - { "exchangeName": "Kucoin", "ticker": "BONK-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "BONK-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "BONK_USDT", "adjustByMarket": "USDT-USD" } - ], - "BTC": [ - { "exchangeName": "Binance", "ticker": "BTCUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "BTC/USD" }, - { "exchangeName": "Bybit", "ticker": "BTCUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "BTC-USD" }, - { "exchangeName": "Huobi", "ticker": "btcusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "BTCUSD" }, - { "exchangeName": "Kucoin", "ticker": "BTC-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "BTC-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "BTC_USDT", "adjustByMarket": "USDT-USD" } - ], - "CHZ": [ - { "exchangeName": "Binance", "ticker": "CHZUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "CHZ-USD" }, - { "exchangeName": "Kraken", "ticker": "CHZUSD" }, - { "exchangeName": "Kucoin", "ticker": "CHZ-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "CHZ-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "CHZ_USDT", "adjustByMarket": "USDT-USD" } - ], - "CRV": [ - { "exchangeName": "Binance", "ticker": "CRVUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "CRV-USD" }, - { "exchangeName": "Kraken", "ticker": "CRVUSD" }, - { "exchangeName": "Kucoin", "ticker": "CRV-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "CRV-USDT", "adjustByMarket": "USDT-USD" } - ], - "DOGE": [ - { "exchangeName": "Binance", "ticker": "DOGEUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "DOGEUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "DOGE-USD" }, - { "exchangeName": "Huobi", "ticker": "dogeusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "DOGEUSD" }, - { "exchangeName": "Kucoin", "ticker": "DOGE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "DOGE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "DOGE_USDT", "adjustByMarket": "USDT-USD" } - ], - "DOT": [ - { "exchangeName": "Binance", "ticker": "DOTUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "DOTUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "DOT-USD" }, - { "exchangeName": "Huobi", "ticker": "dotusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "DOTUSD" }, - { "exchangeName": "Kucoin", "ticker": "DOT-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "DOT-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "DOT_USDT", "adjustByMarket": "USDT-USD" } - ], - "DYM": [ - { "exchangeName": "Binance", "ticker": "DYMUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "DYMUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "DYM_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "DYM-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "DYM_USDT", "adjustByMarket": "USDT-USD" } - ], - "ENS": [ - { "exchangeName": "Binance", "ticker": "ENSUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ENS-USD" }, - { "exchangeName": "Gate", "ticker": "ENS_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "ENS-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ENS-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ENS_USDT", "adjustByMarket": "USDT-USD" } - ], - "EOS": [ - { "exchangeName": "Binance", "ticker": "EOSUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "EOSUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "EOS-USD" }, - { "exchangeName": "Gate", "ticker": "EOS_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "EOSUSD" }, - { "exchangeName": "Kucoin", "ticker": "EOS-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "EOS-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "EOS_USDT", "adjustByMarket": "USDT-USD" } - ], - "ETC": [ - { "exchangeName": "Binance", "ticker": "ETCUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "ETCUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ETC-USD" }, - { "exchangeName": "Huobi", "ticker": "etcusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "ETCUSD" }, - { "exchangeName": "Kucoin", "ticker": "ETC-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ETC-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ETC_USDT", "adjustByMarket": "USDT-USD" } - ], - "ETH": [ - { "exchangeName": "Binance", "ticker": "ETHUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "ETH/USD" }, - { "exchangeName": "Bybit", "ticker": "ETHUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ETH-USD" }, - { "exchangeName": "Huobi", "ticker": "ethusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "ETHUSD" }, - { "exchangeName": "Kucoin", "ticker": "ETH-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ETH-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ETH_USDT", "adjustByMarket": "USDT-USD" } - ], - "FET": [ - { "exchangeName": "Binance", "ticker": "FETUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "FET-USD" }, - { "exchangeName": "Kraken", "ticker": "FETUSD" }, - { "exchangeName": "Kucoin", "ticker": "FET-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "FET-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "FET_USDT", "adjustByMarket": "USDT-USD" } - ], - "FIL": [ - { "exchangeName": "Binance", "ticker": "FILUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "FILUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "FIL-USD" }, - { "exchangeName": "Huobi", "ticker": "filusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "FILUSD" }, - { "exchangeName": "Kucoin", "ticker": "FIL-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "FIL-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "FIL_USDT", "adjustByMarket": "USDT-USD" } - ], - "FTM": [ - { "exchangeName": "Binance", "ticker": "FTMUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "FTMUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "FTMUSD" }, - { "exchangeName": "Kucoin", "ticker": "FTM-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "FTM-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "FTM_USDT", "adjustByMarket": "USDT-USD" } - ], - "GALA": [ - { "exchangeName": "Binance", "ticker": "GALAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "GALAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "GALA_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "GALAUSD" }, - { "exchangeName": "Okx", "ticker": "GALA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "GALA_USDT", "adjustByMarket": "USDT-USD" } - ], - "GMT": [ - { "exchangeName": "Binance", "ticker": "GMTUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "GMTUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "GMT-USD" }, - { "exchangeName": "Gate", "ticker": "GMT_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "GMT-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "GMT-USDT", "adjustByMarket": "USDT-USD" } - ], - "GRT": [ - { "exchangeName": "Binance", "ticker": "GRTUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "GRT-USD" }, - { "exchangeName": "Gate", "ticker": "GRT_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "GRTUSD" }, - { "exchangeName": "Kucoin", "ticker": "GRT-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "GRT-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "GRT_USDT", "adjustByMarket": "USDT-USD" } - ], - "HBAR": [ - { "exchangeName": "Binance", "ticker": "HBARUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "HBARUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "HBAR-USD" }, - { "exchangeName": "Kucoin", "ticker": "HBAR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "HBAR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "HBAR_USDT", "adjustByMarket": "USDT-USD" } - ], - "ICP": [ - { "exchangeName": "Binance", "ticker": "ICPUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "ICPUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ICP-USD" }, - { "exchangeName": "Kraken", "ticker": "ICPUSD" }, - { "exchangeName": "Kucoin", "ticker": "ICP-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ICP-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ICP_USDT", "adjustByMarket": "USDT-USD" } - ], - "IMX": [ - { "exchangeName": "Binance", "ticker": "IMXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "IMX-USD" }, - { "exchangeName": "Kraken", "ticker": "IMXUSD" }, - { "exchangeName": "Kucoin", "ticker": "IMX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "IMX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "IMX_USDT", "adjustByMarket": "USDT-USD" } - ], - "INJ": [ - { "exchangeName": "Binance", "ticker": "INJUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "INJUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "INJ-USD" }, - { "exchangeName": "Kraken", "ticker": "INJUSD" }, - { "exchangeName": "Kucoin", "ticker": "INJ-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "INJ-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "INJ_USDT", "adjustByMarket": "USDT-USD" } - ], - "JTO": [ - { "exchangeName": "Binance", "ticker": "JTOUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "JTOUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "JTO-USD" }, - { "exchangeName": "Kucoin", "ticker": "JTO-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "JTO-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "JTO_USDT", "adjustByMarket": "USDT-USD" } - ], - "JUP": [ - { "exchangeName": "Binance", "ticker": "JUPUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "JUPUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "JUP_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "JUP-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "JUP_USDT", "adjustByMarket": "USDT-USD" } - ], - "KAVA": [ - { "exchangeName": "Binance", "ticker": "KAVAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "KAVAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "KAVA-USD" }, - { "exchangeName": "Gate", "ticker": "KAVA_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "KAVAUSD" }, - { "exchangeName": "Kucoin", "ticker": "KAVA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "KAVA_USDT", "adjustByMarket": "USDT-USD" } - ], - "LDO": [ - { "exchangeName": "Binance", "ticker": "LDOUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "LDOUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "LDO-USD" }, - { "exchangeName": "Kraken", "ticker": "LDOUSD" }, - { "exchangeName": "Kucoin", "ticker": "LDO-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "LDO-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "LDO_USDT", "adjustByMarket": "USDT-USD" } - ], - "LINK": [ - { "exchangeName": "Binance", "ticker": "LINKUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "LINK/USD" }, - { "exchangeName": "Bybit", "ticker": "LINKUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "LINK-USD" }, - { "exchangeName": "Kraken", "ticker": "LINKUSD" }, - { "exchangeName": "Kucoin", "ticker": "LINK-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "LINK-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "LINK_USDT", "adjustByMarket": "USDT-USD" } - ], - "LTC": [ - { "exchangeName": "Binance", "ticker": "LTCUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "LTC/USD" }, - { "exchangeName": "Bybit", "ticker": "LTCUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "LTC-USD" }, - { "exchangeName": "Huobi", "ticker": "ltcusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "LTCUSD" }, - { "exchangeName": "Kucoin", "ticker": "LTC-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "LTC-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "LTC_USDT", "adjustByMarket": "USDT-USD" } - ], - "MANA": [ - { "exchangeName": "Binance", "ticker": "MANAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "MANA-USD" }, - { "exchangeName": "Gate", "ticker": "MANA_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "MANAUSD" }, - { "exchangeName": "Kucoin", "ticker": "MANA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "MANA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "MANA_USDT", "adjustByMarket": "USDT-USD" } - ], - "MASK": [ - { "exchangeName": "Binance", "ticker": "MASKUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "MASKUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "MASK-USD" }, - { "exchangeName": "Gate", "ticker": "MASK_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Huobi", "ticker": "maskusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "MASK-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "MASK-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "MASK_USDT", "adjustByMarket": "USDT-USD" } - ], - "MATIC": [ - { "exchangeName": "Binance", "ticker": "MATICUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "MATIC/USD" }, - { "exchangeName": "Bybit", "ticker": "MATICUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "MATIC-USD" }, - { "exchangeName": "Huobi", "ticker": "maticusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "MATICUSD" }, - { "exchangeName": "Kucoin", "ticker": "MATIC-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "MATIC-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "MATIC_USDT", "adjustByMarket": "USDT-USD" } - ], - "MINA": [ - { "exchangeName": "Binance", "ticker": "MINAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "MINA-USD" }, - { "exchangeName": "Gate", "ticker": "MINA_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "MINAUSD" }, - { "exchangeName": "Okx", "ticker": "MINA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "MINA_USDT", "adjustByMarket": "USDT-USD" } - ], - "MKR": [ - { "exchangeName": "Binance", "ticker": "MKRUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "MKR-USD" }, - { "exchangeName": "Kraken", "ticker": "MKRUSD" }, - { "exchangeName": "Kucoin", "ticker": "MKR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "MKR-USDT", "adjustByMarket": "USDT-USD" } - ], - "NEAR": [ - { "exchangeName": "Binance", "ticker": "NEARUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "NEARUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "NEAR-USD" }, - { "exchangeName": "Huobi", "ticker": "nearusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "NEARUSD" }, - { "exchangeName": "Kucoin", "ticker": "NEAR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "NEAR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "NEAR_USDT", "adjustByMarket": "USDT-USD" } - ], - "OP": [ - { "exchangeName": "Binance", "ticker": "OPUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "OPUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "OP-USD" }, - { "exchangeName": "Gate", "ticker": "OP_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "OPUSD" }, - { "exchangeName": "Kucoin", "ticker": "OP-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "OP-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "OP_USDT", "adjustByMarket": "USDT-USD" } - ], - "ORDI": [ - { "exchangeName": "Binance", "ticker": "ORDIUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "ORDIUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "ORDI_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Huobi", "ticker": "ordiusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "ORDI-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ORDI-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ORDI_USDT", "adjustByMarket": "USDT-USD" } - ], - "PEPE": [ - { "exchangeName": "Binance", "ticker": "PEPEUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "PEPEUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "PEPEUSD" }, - { "exchangeName": "Kucoin", "ticker": "PEPE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "PEPE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "PEPE_USDT", "adjustByMarket": "USDT-USD" } - ], - "PYTH": [ - { "exchangeName": "Binance", "ticker": "PYTHUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "PYTHUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "PYTH_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "PYTH-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "PYTH-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "PYTH_USDT", "adjustByMarket": "USDT-USD" } - ], - "RNDR": [ - { "exchangeName": "Binance", "ticker": "RNDRUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "RNDR-USD" }, - { "exchangeName": "Kraken", "ticker": "RNDRUSD" }, - { "exchangeName": "Kucoin", "ticker": "RNDR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "RNDR-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "RNDR_USDT", "adjustByMarket": "USDT-USD" } - ], - "RUNE": [ - { "exchangeName": "Binance", "ticker": "RUNEUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "RUNE_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "RUNEUSD" }, - { "exchangeName": "Kucoin", "ticker": "RUNE-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "RUNE_USDT", "adjustByMarket": "USDT-USD" } - ], - "SAND": [ - { "exchangeName": "Binance", "ticker": "SANDUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "SAND-USD" }, - { "exchangeName": "Gate", "ticker": "SAND_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "SAND-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "SAND-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "SAND_USDT", "adjustByMarket": "USDT-USD" } - ], - "SEI": [ - { "exchangeName": "Binance", "ticker": "SEIUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "SEIUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "SEI-USD" }, - { "exchangeName": "Huobi", "ticker": "seiusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "SEIUSD" }, - { "exchangeName": "Kucoin", "ticker": "SEI-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "SEI_USDT", "adjustByMarket": "USDT-USD" } - ], - "SHIB": [ - { "exchangeName": "Binance", "ticker": "SHIBUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "SHIBUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "SHIB-USD" }, - { "exchangeName": "Huobi", "ticker": "shibusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "SHIBUSD" }, - { "exchangeName": "Kucoin", "ticker": "SHIB-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "SHIB-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "SHIB_USDT", "adjustByMarket": "USDT-USD" } - ], - "SNX": [ - { "exchangeName": "Binance", "ticker": "SNXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "SNXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "SNX-USD" }, - { "exchangeName": "Kraken", "ticker": "SNXUSD" }, - { "exchangeName": "Kucoin", "ticker": "SNX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "SNX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "SNX_USDT", "adjustByMarket": "USDT-USD" } - ], - "SOL": [ - { "exchangeName": "Binance", "ticker": "SOLUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "SOL/USD" }, - { "exchangeName": "Bybit", "ticker": "SOLUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "SOL-USD" }, - { "exchangeName": "Huobi", "ticker": "solusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "SOLUSD" }, - { "exchangeName": "Kucoin", "ticker": "SOL-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "SOL-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "SOL_USDT", "adjustByMarket": "USDT-USD" } - ], - "STRK": [ - { "exchangeName": "Binance", "ticker": "STRKUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "STRKUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "STRKUSD" }, - { "exchangeName": "Kucoin", "ticker": "STRK-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "STRK-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "STRK_USDT", "adjustByMarket": "USDT-USD" } - ], - "STX": [ - { "exchangeName": "Binance", "ticker": "STXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "STXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "STX-USD" }, - { "exchangeName": "Gate", "ticker": "STX_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "STXUSD" }, - { "exchangeName": "Kucoin", "ticker": "STX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "STX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "STX_USDT", "adjustByMarket": "USDT-USD" } - ], - "SUI": [ - { "exchangeName": "Binance", "ticker": "SUIUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "SUIUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "SUI-USD" }, - { "exchangeName": "Huobi", "ticker": "suiusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "SUIUSD" }, - { "exchangeName": "Kucoin", "ticker": "SUI-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "SUI-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "SUI_USDT", "adjustByMarket": "USDT-USD" } - ], - "TIA": [ - { "exchangeName": "Binance", "ticker": "TIAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "TIAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "TIA-USD" }, - { "exchangeName": "Kraken", "ticker": "TIAUSD" }, - { "exchangeName": "Kucoin", "ticker": "TIA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "TIA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "TIA_USDT", "adjustByMarket": "USDT-USD" } - ], - "TRX": [ - { "exchangeName": "Binance", "ticker": "TRXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "TRXUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "TRX_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Huobi", "ticker": "trxusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "TRXUSD" }, - { "exchangeName": "Kucoin", "ticker": "TRX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "TRX-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "TRX_USDT", "adjustByMarket": "USDT-USD" } - ], - "UNI": [ - { "exchangeName": "Binance", "ticker": "UNIUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "UNIUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "UNI-USD" }, - { "exchangeName": "Kraken", "ticker": "UNIUSD" }, - { "exchangeName": "Kucoin", "ticker": "UNI-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "UNI-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "UNI_USDT", "adjustByMarket": "USDT-USD" } - ], - "WLD": [ - { "exchangeName": "Binance", "ticker": "WLDUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bybit", "ticker": "WLDUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "WLD_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "WLD-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "WLD-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "WLD_USDT", "adjustByMarket": "USDT-USD" } - ], - "WOO": [ - { "exchangeName": "Binance", "ticker": "WOOUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Gate", "ticker": "WOO_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "WOO-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "WOO-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "WOO_USDT", "adjustByMarket": "USDT-USD" } - ], - "XLM": [ - { "exchangeName": "Binance", "ticker": "XLMUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "XLM/USD" }, - { "exchangeName": "Bybit", "ticker": "XLMUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "XLM-USD" }, - { "exchangeName": "Kraken", "ticker": "XLMUSD" }, - { "exchangeName": "Kucoin", "ticker": "XLM-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "XLM-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "XLM_USDT", "adjustByMarket": "USDT-USD" } - ], - "XRP": [ - { "exchangeName": "Binance", "ticker": "XRPUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Bitstamp", "ticker": "XRP/USD" }, - { "exchangeName": "Bybit", "ticker": "XRPUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "XRP-USD" }, - { "exchangeName": "Huobi", "ticker": "xrpusdt", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kraken", "ticker": "XRPUSD" }, - { "exchangeName": "Kucoin", "ticker": "XRP-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "XRP-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "XRP_USDT", "adjustByMarket": "USDT-USD" } - ], - "ZETA": [ - { "exchangeName": "Bybit", "ticker": "ZETAUSDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "CoinbasePro", "ticker": "ZETA-USD" }, - { "exchangeName": "Gate", "ticker": "ZETA_USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Kucoin", "ticker": "ZETA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Okx", "ticker": "ZETA-USDT", "adjustByMarket": "USDT-USD" }, - { "exchangeName": "Mexc", "ticker": "ZETA_USDT", "adjustByMarket": "USDT-USD" } - ] -} diff --git a/public/configs/otherMarketParameters.json b/public/configs/otherMarketParameters.json deleted file mode 100644 index ffbfbced4..000000000 --- a/public/configs/otherMarketParameters.json +++ /dev/null @@ -1,1158 +0,0 @@ -[ - { - "baseAsset": "1INCH", - "referencePrice": 0.3398645158695157, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "1INCH", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "AAVE", - "referencePrice": 85.34916282287368, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "AAVE", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ADA", - "referencePrice": 0.5223295461752288, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Cardano", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "AGIX", - "referencePrice": 0.2445107154373193, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "SingularityNET", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ALGO", - "referencePrice": 0.1681287078892224, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Algorand", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "APE", - "referencePrice": 1.1944183891765008, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "ApeCoin", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "APT", - "referencePrice": 7.866427281197853, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Aptos", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ARB", - "referencePrice": 1.8941460935787378, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Arbitrum", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ATOM", - "referencePrice": 8.376447043890133, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Cosmos", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "AVAX", - "referencePrice": 35.51220693068919, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Avalanche", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "BCH", - "referencePrice": 242.55658383981574, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Bitcoin Cash", - "p": 2.0, - "atomicResolution": -8.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -7.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "BLUR", - "referencePrice": 0.6174156210414935, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Blur", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "BNB", - "referencePrice": 261.9562938974594, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Binance Coin", - "p": 2.0, - "atomicResolution": -8.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -7.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "BONK", - "referencePrice": 1.0481380626265717e-5, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Bonk Token", - "p": -5.0, - "atomicResolution": -1.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -14.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "BTC", - "referencePrice": 44848.16769478778, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Bitcoin", - "p": 4.0, - "atomicResolution": -10.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -5.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "CHZ", - "referencePrice": 0.10152209594142685, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Chiliz", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "CRV", - "referencePrice": 0.48346754113361784, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Curve DAO Token", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "DOGE", - "referencePrice": 0.08014457060243645, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Dogecoin", - "p": -2.0, - "atomicResolution": -4.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -11.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "DOT", - "referencePrice": 6.965370230174219, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Polkadot", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "DYM", - "referencePrice": 5.544949130771594, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Dymension", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ENS", - "referencePrice": 17.72913244361432, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Ethereum Name Service", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "EOS", - "referencePrice": 0.6306070212782233, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Eos", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ETC", - "referencePrice": 25.31458356285971, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Ethereum Classic", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ETH", - "referencePrice": 2431.2439928708804, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Ethereum", - "p": 3.0, - "atomicResolution": -9.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -6.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "FET", - "referencePrice": 0.5560893147613742, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Fetch.ai", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "FIL", - "referencePrice": 5.199120507932321, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Filecoin", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "FTM", - "referencePrice": 0.3711457058956354, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Fantom", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "GALA", - "referencePrice": 0.01898331222229838, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Gala", - "p": -2.0, - "atomicResolution": -4.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -11.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "GMT", - "referencePrice": 0.20404992804891367, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "GMT", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "GRT", - "referencePrice": 0.13921183278937196, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "The Graph", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "HBAR", - "referencePrice": 0.07686278766140874, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Hedera", - "p": -2.0, - "atomicResolution": -4.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -11.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ICP", - "referencePrice": 12.381985084851047, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Internet Computer", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "IMX", - "referencePrice": 2.3007875054054425, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Immutable X", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "INJ", - "referencePrice": 33.427379051845875, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Injective", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "JTO", - "referencePrice": 1.8587604617237268, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Jito", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "JUP", - "referencePrice": 0.4339790660244292, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Jupiter", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "KAVA", - "referencePrice": 0.6150293989811233, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Kava", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "LDO", - "referencePrice": 2.872990563900012, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Lido DAO", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "LINK", - "referencePrice": 18.695585099973265, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "ChainLink", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "LTC", - "referencePrice": 68.82098983684968, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Litecoin", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "MANA", - "referencePrice": 0.3835526503137334, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Decentraland", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "MASK", - "referencePrice": 3.0190606294111477, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Mask Network", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "MATIC", - "referencePrice": 0.8348216286259986, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Matic Network", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "MINA", - "referencePrice": 0.9996552741300538, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Mina", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "MKR", - "referencePrice": 1943.1865605831852, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Maker", - "p": 3.0, - "atomicResolution": -9.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -6.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "NEAR", - "referencePrice": 2.9412830334457607, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Near", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "OP", - "referencePrice": 2.959268526582415, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Optimism", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ORDI", - "referencePrice": 51.40287704643513, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Ordinals", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "PEPE", - "referencePrice": 9.65894964791238e-7, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Pepe", - "p": -7.0, - "atomicResolution": 1.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -16.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "PYTH", - "referencePrice": 0.4024907744770694, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Pyth Network", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "RNDR", - "referencePrice": 4.490794387737395, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Render Token", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "RUNE", - "referencePrice": 3.7264809096406806, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Thorchain", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "SAND", - "referencePrice": 0.37297439115035663, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "The Sandbox", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "SEI", - "referencePrice": 0.6414470586972892, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Sei", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "SHIB", - "referencePrice": 9.204638050039261e-6, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Shiba Inu", - "p": -6.0, - "atomicResolution": 0.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -15.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "SNX", - "referencePrice": 3.3742698571364094, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Synthetix Network Token", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "SOL", - "referencePrice": 102.57213563905393, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Solana", - "p": 2.0, - "atomicResolution": -8.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -7.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "STRK", - "referencePrice": 2.05, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Starknet", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "STX", - "referencePrice": 1.5144048703611412, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Stacks", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "SUI", - "referencePrice": 1.5452561101842979, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "SuiNetwork", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "TIA", - "referencePrice": 19.71178951640871, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Celestia", - "p": 1.0, - "atomicResolution": -7.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -8.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "TRX", - "referencePrice": 0.10782035164658409, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "TRON", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "UNI", - "referencePrice": 6.374086551681073, - "numOracles": 7, - "liquidityTier": 2, - "assetName": "Uniswap", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "WLD", - "referencePrice": 1.9810696413118303, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "Worldcoin WLD", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "WOO", - "referencePrice": 0.2944897275608197, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "WOO Network", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "XLM", - "referencePrice": 0.1093759305057456, - "numOracles": 8, - "liquidityTier": 1, - "assetName": "Stellar", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "XRP", - "referencePrice": 0.5140463170757591, - "numOracles": 9, - "liquidityTier": 1, - "assetName": "Ripple", - "p": -1.0, - "atomicResolution": -5.0, - "minExchanges": 3, - "minPriceChangePpm": 2500, - "priceExponent": -10.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - }, - { - "baseAsset": "ZETA", - "referencePrice": 1.0427131536820096, - "numOracles": 6, - "liquidityTier": 2, - "assetName": "ZetaChain", - "p": 0.0, - "atomicResolution": -6.0, - "minExchanges": 3, - "minPriceChangePpm": 4000, - "priceExponent": -9.0, - "stepBaseQuantum": 1000000, - "ticksizeExponent": -3, - "subticksPerTick": 1000000, - "minOrderSize": 1000000, - "quantumConversionExponent": -9 - } -] diff --git a/public/configs/v1/env.json b/public/configs/v1/env.json index 06535100b..6b8f4cd03 100644 --- a/public/configs/v1/env.json +++ b/public/configs/v1/env.json @@ -72,7 +72,12 @@ "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", - "launchIncentive": "https://cloud.chaoslabs.co" + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665", + "launchIncentive": "https://cloud.chaoslabs.co", + "tradingRewardsLearnMore": "https://docs.dydx.exchange/rewards/trading_rewards", + "exchangeStats": "https://app.mode.com/dydx_eng/reports/58822121650d?secret_key=391d9214fe6aefec35b7d35c", + "initialMarginFractionLearnMore": "https://docs.dydx.exchange/governance/functionalities#liquidity-tiers", + "complianceSupportEmail": "support@dydx.exchange" }, "dydx-testnet-4": { "tos": "https://dydx.exchange/v4-terms", @@ -94,7 +99,12 @@ "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", - "launchIncentive": "https://cloud.chaoslabs.co" + "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665", + "launchIncentive": "https://cloud.chaoslabs.co", + "tradingRewardsLearnMore": "https://docs.dydx.exchange/rewards/trading_rewards", + "exchangeStats": "https://app.mode.com/dydx_eng/reports/58822121650d?secret_key=391d9214fe6aefec35b7d35c", + "initialMarginFractionLearnMore": "https://docs.dydx.exchange/governance/functionalities#liquidity-tiers", + "complianceSupportEmail": "support@dydx.exchange" }, "[mainnet chain id]": { "tos": "[HTTP link to TOS]", @@ -116,7 +126,12 @@ "strideZoneApp": "[HTTP link to stride zone app, can be null]", "accountExportLearnMore": "[HTTP link to account export learn more, can be null]", "walletLearnMore": "[HTTP link to wallet learn more, can be null]", - "launchIncentive": "[HTTP link to launch incentive host, can be null]" + "withdrawalGateLearnMore": "[HTTP link to withdrawal gate learn more, can be null]", + "launchIncentive": "[HTTP link to launch incentive host, can be null]", + "tradingRewardsLearnMore": "[HTTP link to trading rewards learn more, can be null]", + "exchangeStats": "[HTTP link to exchange stats, can be null]", + "initialMarginFractionLearnMore": "[HTTP link to governance functionalities liquidity tiers, can be null]", + "complianceSupportEmail": "[Email address for compliance support, can be null]" } }, "wallets": { @@ -222,6 +237,7 @@ "environments": [ "dydxprotocol-dev", "dydxprotocol-dev-2", + "dydxprotocol-dev-3", "dydxprotocol-dev-4", "dydxprotocol-dev-5", "dydxprotocol-staging", @@ -258,11 +274,18 @@ ], "0xsquid": "https://testnet.api.0xsquid.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4dev.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": false + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-dev-2": { @@ -284,11 +307,52 @@ "http://54.92.118.111" ], "0xsquid": "https://testnet.api.0xsquid.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" + }, + "featureFlags": { + "reduceOnlySupported": true, + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false + } + }, + "dydxprotocol-dev-3": { + "name": "v4 Dev 3", + "ethereumChainId": "11155111", + "dydxChainId": "dydxprotocol-testnet", + "chainName": "dYdX Chain", + "chainLogo": "/dydx-chain.png", + "squidIntegratorId": "dYdX-api", + "isMainNet": false, + "endpoints": { + "indexers": [ + { + "api": "http://dev3-indexer-apne1-lb-public-1613790025.ap-northeast-1.elb.amazonaws.com", + "socket": "ws://dev3-indexer-apne1-lb-public-1613790025.ap-northeast-1.elb.amazonaws.com" + } + ], + "validators": [ + "http://validator-dev3-lb-1393802013.us-east-2.elb.amazonaws.com" + ], + "0xsquid": "https://testnet.api.0xsquid.com", + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", + "faucet": "http://dev3-faucet-lb-public-1644791410.us-east-2.elb.amazonaws.com" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": false + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-dev-4": { @@ -311,11 +375,18 @@ ], "0xsquid": "https://testnet.api.0xsquid.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4dev4.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": false + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-dev-5": { @@ -337,11 +408,18 @@ "http://18.223.78.50" ], "0xsquid": "https://testnet.api.0xsquid.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": false + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-staging": { @@ -364,11 +442,18 @@ "https://validator.v4staging.dydx.exchange" ], "0xsquid": "https://testnet.api.squidrouter.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": false + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-staging-forced-update": { @@ -391,7 +476,8 @@ "https://validator.v4staging.dydx.exchange" ], "0xsquid": "https://testnet.api.squidrouter.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "apps": { "ios": { @@ -402,7 +488,13 @@ }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": false + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-staging-west": { @@ -425,11 +517,18 @@ "https://validator-uswest1.v4staging.dydx.exchange" ], "0xsquid": "https://testnet.api.squidrouter.com", - "nobleValidator": "https://noble-testnet-rpc.polkachu.com/" + "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": false + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-testnet": { @@ -456,11 +555,18 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": true + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-testnet-dydx": { @@ -483,11 +589,17 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": true + "usePessimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-testnet-nodefleet": { @@ -510,11 +622,18 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": true + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-testnet-kingnodes": { @@ -537,11 +656,17 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": true + "usePessimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-testnet-liquify": { @@ -564,11 +689,18 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": true + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-testnet-polkachu": { @@ -591,11 +723,18 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": true + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-testnet-bware": { @@ -618,11 +757,18 @@ ], "0xsquid": "https://testnet.api.squidrouter.com", "nobleValidator": "https://noble-testnet-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", "faucet": "https://faucet.v4testnet.dydx.exchange" }, "featureFlags": { "reduceOnlySupported": true, - "usePessimisticCollateralCheck": true + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } }, "dydxprotocol-mainnet": { @@ -645,12 +791,19 @@ "[Validator endpoint n]" ], "0xsquid": "[0xSquid endpoint for mainnet]", - "nobleValidator": "[noble validator endpoint for mainnet]" + "nobleValidator": "[noble validator endpoint for mainnet]", + "geo": "[geo endpoint for mainnet]" }, "featureFlags": { - "reduceOnlySupported": false, - "usePessimisticCollateralCheck": true + "reduceOnlySupported": true, + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, + "withdrawalSafetyEnabled": true, + "CCTPWithdrawalOnly": true, + "CCTPDepositOnly": true, + "isSlTpEnabled": true, + "isSlTpLimitOrdersEnabled": false } } } -} \ No newline at end of file +} diff --git a/public/currencies/aevo.png b/public/currencies/aevo.png new file mode 100644 index 000000000..dfde5a91e Binary files /dev/null and b/public/currencies/aevo.png differ diff --git a/public/currencies/api3.png b/public/currencies/api3.png new file mode 100644 index 000000000..39af95ed0 Binary files /dev/null and b/public/currencies/api3.png differ diff --git a/public/currencies/arkm.png b/public/currencies/arkm.png new file mode 100644 index 000000000..caf472fd6 Binary files /dev/null and b/public/currencies/arkm.png differ diff --git a/public/currencies/astr.png b/public/currencies/astr.png new file mode 100644 index 000000000..f2aca1cfb Binary files /dev/null and b/public/currencies/astr.png differ diff --git a/public/currencies/axl.png b/public/currencies/axl.png new file mode 100644 index 000000000..15a0dd708 Binary files /dev/null and b/public/currencies/axl.png differ diff --git a/public/currencies/bome.png b/public/currencies/bome.png new file mode 100644 index 000000000..cd9aeebce Binary files /dev/null and b/public/currencies/bome.png differ diff --git a/public/currencies/ethfi.png b/public/currencies/ethfi.png new file mode 100644 index 000000000..f777e5859 Binary files /dev/null and b/public/currencies/ethfi.png differ diff --git a/public/currencies/flr.png b/public/currencies/flr.png new file mode 100644 index 000000000..1e25c92f7 Binary files /dev/null and b/public/currencies/flr.png differ diff --git a/public/currencies/magic.png b/public/currencies/magic.png new file mode 100644 index 000000000..5cc1c9de4 Binary files /dev/null and b/public/currencies/magic.png differ diff --git a/public/currencies/meme.png b/public/currencies/meme.png new file mode 100644 index 000000000..394f48089 Binary files /dev/null and b/public/currencies/meme.png differ diff --git a/public/currencies/ocean.png b/public/currencies/ocean.png new file mode 100644 index 000000000..599f2f9e5 Binary files /dev/null and b/public/currencies/ocean.png differ diff --git a/public/currencies/portal.png b/public/currencies/portal.png new file mode 100644 index 000000000..ed47bdb1d Binary files /dev/null and b/public/currencies/portal.png differ diff --git a/public/currencies/ton.png b/public/currencies/ton.png new file mode 100644 index 000000000..d0a96ad77 Binary files /dev/null and b/public/currencies/ton.png differ diff --git a/public/currencies/wif.png b/public/currencies/wif.png new file mode 100644 index 000000000..71f31e03d Binary files /dev/null and b/public/currencies/wif.png differ diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 000000000..e6dd65594 Binary files /dev/null and b/public/og-image.png differ diff --git a/scripts/generate-entry-points.js b/scripts/generate-entry-points.js new file mode 100755 index 000000000..609ed3c92 --- /dev/null +++ b/scripts/generate-entry-points.js @@ -0,0 +1,32 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const currentPath = fileURLToPath(import.meta.url); +const projectRoot = path.dirname(currentPath); +const templateFilePath = path.resolve(projectRoot, '../template.html'); +const entryPointsDir = path.resolve(projectRoot, '../entry-points'); + +const ENTRY_POINTS = [ + { + title: 'dYdX', + description: 'dYdX', + fileName: 'index.html', + }, +]; + +try { + fs.mkdir(entryPointsDir, { recursive: true }); + + for (const entryPoint of ENTRY_POINTS) { + const html = await fs.readFile(templateFilePath, 'utf-8'); + const destinationFilePath = path.resolve(entryPointsDir, entryPoint.fileName); + const injectedHtml = html.replace( + 'dYdX', + `${entryPoint.title}\n ` + ); + await fs.writeFile(destinationFilePath, injectedHtml, 'utf-8'); + } +} catch (err) { + console.error('Error generating entry points:', err); +} diff --git a/scripts/inject-amplitude.js b/scripts/inject-amplitude.js index b1bdc79c8..3caa733f6 100644 --- a/scripts/inject-amplitude.js +++ b/scripts/inject-amplitude.js @@ -7,56 +7,62 @@ const AMPLITUDE_SERVER_URL = process.env.AMPLITUDE_SERVER_URL; const currentPath = fileURLToPath(import.meta.url); const projectRoot = path.dirname(currentPath); -const htmlFilePath = path.resolve(projectRoot, '../dist/index.html'); if (AMPLITUDE_API_KEY) { try { - const html = await fs.readFile(htmlFilePath, 'utf-8'); - - const amplitudeCdnScript = ` - `; - - const amplitudeListenerScript = ``; - - const injectedHtml = html.replace( - '
', - `
\n${amplitudeCdnScript}\n${amplitudeListenerScript}` - ); - - await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); - - console.log('Amplitude scripts successfully injected.'); + const files = await fs.readdir('entry-points'); + for (const file of files) { + inject(file); + }; } catch (err) { console.error('Error injecting Amplitude scripts:', err); } } + +async function inject(fileName) { + const htmlFilePath = path.resolve(projectRoot, `../dist/entry-points/${fileName}`); + const html = await fs.readFile(htmlFilePath, 'utf-8'); + + const amplitudeCdnScript = ` + `; + + const amplitudeListenerScript = ``; + + const injectedHtml = html.replace( + '
', + `
\n${amplitudeCdnScript}\n${amplitudeListenerScript}` + ); + + await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); + + console.log(`Amplitude scripts successfully injected (${fileName}).`); +} \ No newline at end of file diff --git a/scripts/inject-bugsnag.js b/scripts/inject-bugsnag.js index 114ec87a6..7dff7cef5 100644 --- a/scripts/inject-bugsnag.js +++ b/scripts/inject-bugsnag.js @@ -6,76 +6,80 @@ const BUGSNAG_API_KEY = process.env.BUGSNAG_API_KEY; const currentPath = fileURLToPath(import.meta.url); const projectRoot = path.dirname(currentPath); -const htmlFilePath = path.resolve(projectRoot, '../dist/index.html'); try { + const files = await fs.readdir('entry-points'); + for (const file of files) { + inject(file); + }; +} catch (err) { + console.error('Error injecting Bugsnag scripts:', err); +} + +async function inject(fileName) { + const htmlFilePath = path.resolve(projectRoot, `../dist/entry-points/${fileName}`); const html = await fs.readFile(htmlFilePath, 'utf-8'); const scripts = ` - + + + + + `; - globalThis.addEventListener('dydx:identify', function (event) { - var property = event.detail.property; - var value = event.detail.propertyValue; - - switch (property) { - case 'walletType': - walletType = value; - break; - default: - break; - } - }); - - globalThis.addEventListener('dydx:log', function (event) { - var error = event.detail.error; - var metadata = event.detail.metadata; - var location = event.detail.location; - - if (BUGSNAG_API_KEY && Bugsnag.isStarted()) { - Bugsnag.notify(error, function (event) { - event.context = location; - if (metadata) { - event.addMetadata('metadata', metadata); - } - if (walletType) { - event.addMetadata('walletType', walletType); - } - }); - } else { - console.warn(location, error, metadata); - } - }); - })(); - - - `; - - const injectedHtml = html.replace( - '
', - `
\n${scripts}\n` - ); + const injectedHtml = html.replace('
', `
\n${scripts}\n`); await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); - console.log('Bugsnag scripts successfully injected.'); -} catch (err) { - console.error('Error injecting Bugsnag scripts:', err); -} + console.log(`Bugsnag scripts successfully injected (${fileName}).`); +} \ No newline at end of file diff --git a/scripts/inject-intercom.js b/scripts/inject-intercom.js index f344e92f5..f6442d9de 100644 --- a/scripts/inject-intercom.js +++ b/scripts/inject-intercom.js @@ -7,70 +7,77 @@ const INTERCOM_APP_ID = process.env.INTERCOM_APP_ID; const currentPath = fileURLToPath(import.meta.url); const projectRoot = path.dirname(currentPath); -const htmlFilePath = path.resolve(projectRoot, '../dist/index.html'); if (INTERCOM_APP_ID) { try { - const html = await fs.readFile(htmlFilePath, 'utf-8'); + const files = await fs.readdir('entry-points'); + for (const file of files) { + inject(file); + }; + } catch (err) { + console.error('Error injecting Intercom scripts:', err); + } +} - const intercomScripts = ` - - +async function inject(fileName) { + const htmlFilePath = path.resolve(projectRoot, `../dist/entry-points/${fileName}`); + const html = await fs.readFile(htmlFilePath, 'utf-8'); + + const intercomScripts = ` + + - - `; + } + })(); + + `; - const injectedHtml = html.replace( - '
', - `
\n${intercomScripts}\n` - ); + const injectedHtml = html.replace( + '
', + `
\n${intercomScripts}\n` + ); - await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); + await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); - console.log('Intercom scripts successfully injected.'); - } catch (err) { - console.error('Error injecting Intercom scripts:', err); - } -} + console.log(`Intercom scripts successfully injected (${fileName}).`); +} \ No newline at end of file diff --git a/scripts/inject-smartbanner.js b/scripts/inject-smartbanner.js index b5f4767e7..639fbe7fe 100644 --- a/scripts/inject-smartbanner.js +++ b/scripts/inject-smartbanner.js @@ -11,7 +11,6 @@ const SMARTBANNER_GOOGLEPLAY_URL = process.env.SMARTBANNER_GOOGLEPLAY_URL; const currentPath = fileURLToPath(import.meta.url); const projectRoot = path.dirname(currentPath); -const htmlFilePath = path.resolve(projectRoot, '../dist/index.html'); const smartbannerFilePath = path.resolve(projectRoot, '../dist/smartbanner.html'); if ( @@ -21,40 +20,48 @@ if ( (SMARTBANNER_APPSTORE_URL || SMARTBANNER_GOOGLEPLAY_URL) ) { try { - const html = await fs.readFile(htmlFilePath, 'utf-8'); - let smartbanner = await fs.readFile(smartbannerFilePath, 'utf-8'); - smartbanner = smartbanner - .replace('SMARTBANNER_APP_NAME', SMARTBANNER_APP_NAME) - .replace('SMARTBANNER_ORG_NAME', SMARTBANNER_ORG_NAME) - .replace('SMARTBANNER_ICON_URL', SMARTBANNER_ICON_URL) - .replace('SMARTBANNER_ICON_URL', SMARTBANNER_ICON_URL); - - /* hardcoded injection depending on whether the app is available on App Store and/or Google Play */ - - if (SMARTBANNER_APPSTORE_URL) { - smartbanner = `\t\n` + smartbanner; - } + const files = await fs.readdir('entry-points'); + for (const file of files) { + inject(file); + }; + } catch (err) { + console.error('Error injecting Smartbanner scripts:', err); + } +} + +async function inject(fileName) { + const htmlFilePath = path.resolve(projectRoot, `../dist/entry-points/${fileName}`); + const html = await fs.readFile(htmlFilePath, 'utf-8'); + let smartbanner = await fs.readFile(smartbannerFilePath, 'utf-8'); + smartbanner = smartbanner + .replace('SMARTBANNER_APP_NAME', SMARTBANNER_APP_NAME) + .replace('SMARTBANNER_ORG_NAME', SMARTBANNER_ORG_NAME) + .replace('SMARTBANNER_ICON_URL', SMARTBANNER_ICON_URL) + .replace('SMARTBANNER_ICON_URL', SMARTBANNER_ICON_URL); + + /* hardcoded injection depending on whether the app is available on App Store and/or Google Play */ + + if (SMARTBANNER_APPSTORE_URL) { + smartbanner = `\t\n` + smartbanner; + } + if (SMARTBANNER_GOOGLEPLAY_URL) { + smartbanner = `\t\n` + smartbanner; + } + if (SMARTBANNER_APPSTORE_URL) { if (SMARTBANNER_GOOGLEPLAY_URL) { - smartbanner = `\t\n` + smartbanner; - } - if (SMARTBANNER_APPSTORE_URL) { - if (SMARTBANNER_GOOGLEPLAY_URL) { - smartbanner = `\t\n` + smartbanner; - } else { - smartbanner = `\t\n` + smartbanner; - } + smartbanner = `\t\n` + smartbanner; } else { - if (SMARTBANNER_GOOGLEPLAY_URL) { - smartbanner = `\t\n` + smartbanner; - } + smartbanner = `\t\n` + smartbanner; } + } else { + if (SMARTBANNER_GOOGLEPLAY_URL) { + smartbanner = `\t\n` + smartbanner; + } + } - const injectedHtml = html.replace('', `${smartbanner}\n`); + const injectedHtml = html.replace('', `${smartbanner}\n`); - await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); + await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); - console.log('Smartbanner scripts successfully injected.'); - } catch (err) { - console.error('Error injecting Smartbanner scripts:', err); - } -} + console.log(`Smartbanner scripts successfully injected (${fileName}).`); +} \ No newline at end of file diff --git a/scripts/inject-statuspage.js b/scripts/inject-statuspage.js index 0315cbfba..34d9d82b8 100644 --- a/scripts/inject-statuspage.js +++ b/scripts/inject-statuspage.js @@ -6,23 +6,29 @@ const STATUS_PAGE_SCRIPT_URI = process.env.STATUS_PAGE_SCRIPT_URI; const currentPath = fileURLToPath(import.meta.url); const projectRoot = path.dirname(currentPath); -const htmlFilePath = path.resolve(projectRoot, '../dist/index.html'); if (STATUS_PAGE_SCRIPT_URI) { try { - const html = await fs.readFile(htmlFilePath, 'utf-8'); + const files = await fs.readdir('entry-points'); + for (const file of files) { + inject(file); + }; + } catch (err) { + console.error('Error injecting StatusPage scripts:', err); + } +} - const statusPageScript = ``; +async function inject(fileName) { + const htmlFilePath = path.resolve(projectRoot, `../dist/entry-points/${fileName}`); + const html = await fs.readFile(htmlFilePath, 'utf-8'); + const statusPageScript = ``; - const injectedHtml = html.replace( - '
', - `
\n${statusPageScript}\n` - ); + const injectedHtml = html.replace( + '
', + `
\n${statusPageScript}\n` + ); - await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); + await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); - console.log('StatusPage script successfully injected.'); - } catch (err) { - console.error('Error injecting StatusPage scripts:', err); - } + console.log(`StatusPage script successfully injected (${fileName}).`); } diff --git a/scripts/install-local-abacus.js b/scripts/install-local-abacus.js new file mode 100644 index 000000000..a5b9b56a6 --- /dev/null +++ b/scripts/install-local-abacus.js @@ -0,0 +1,48 @@ +import { execSync } from 'child_process'; + +const clean = process.argv.includes('--clean'); + +if (clean) { + infoMessage('Running deep clean.'); + nonFatalExec('pnpm remove @dydxprotocol/v4-abacus'); // remove abacus from node_modules + nonFatalExec('cd ../v4-abacus && ./gradlew clean'); // cleanup gradle build outputs + nonFatalExec('cd ../v4-abacus && ./gradlew --stop'); // stop any running gradle daemons + nonFatalExec('rm -rf ~/.gradle/caches'); // nuke the gradle cache +} + +infoMessage('Cleaning up any previously built abacus packages...'); +nonFatalExec('rm ../v4-abacus/build/packages/*.tgz'); + +infoMessage('Building abacus...'); +fatalExec('cd ../v4-abacus && ./gradlew packJsPackage'); + +infoMessage('Installing local abacus package...'); +fatalExec("find ../v4-abacus/build/packages -name '*.tgz' | head -n 1 | xargs pnpm install"); +infoMessage('Successfully installed local abacus package.'); + +infoMessage('Generating local-abacus-hash...'); +fatalExec( + "find ../v4-abacus/build/packages -name '*.tgz' | head -n 1 | shasum > local-abacus-hash" +); + +infoMessage('Vite dev server should have restarted automatically.'); + +function nonFatalExec(cmd) { + try { + execSync(cmd, { stdio: 'inherit' }); + } catch (error) { + // Do nothing. + } +} + +function fatalExec(cmd) { + try { + execSync(cmd, { stdio: 'inherit' }); + } catch (error) { + process.exit(1); + } +} + +function infoMessage(message) { + console.log('\n**** install-local-abacus.js: ' + message + '\n'); +} diff --git a/scripts/set-last-commit-and-tag.sh b/scripts/set-last-commit-and-tag.sh new file mode 100755 index 000000000..34a298a62 --- /dev/null +++ b/scripts/set-last-commit-and-tag.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Add the original repository as a remote if not already present +if ! git remote | grep -q upstream; then + git remote add upstream https://github.com/dydxprotocol/v4-web.git +fi + +# Fetch latest changes from the original repository +git fetch --unshallow upstream + +# Find the last common commit +VITE_LAST_ORIGINAL_COMMIT=$(git merge-base HEAD upstream/main) + +# Check if the command succeeded and VITE_LAST_ORIGINAL_COMMIT is not empty +if [ -z "$VITE_LAST_ORIGINAL_COMMIT" ]; then + echo "Unable to determine the last original commit." + exit 0 +fi + +# Find the tag the commit lives in +VITE_LAST_TAG=$(git describe --exact-match $VITE_LAST_ORIGINAL_COMMIT) + +# Update or add VITE_LAST_ORIGINAL_COMMIT in .env +if grep -q "VITE_LAST_ORIGINAL_COMMIT=" .env; then + # Variable exists, replace it + awk -v lc="$VITE_LAST_ORIGINAL_COMMIT" '/^VITE_LAST_ORIGINAL_COMMIT=/ {$0="VITE_LAST_ORIGINAL_COMMIT="lc} 1' .env > .env.tmp && mv .env.tmp .env +else + # Variable does not exist, append it + echo "VITE_LAST_ORIGINAL_COMMIT=$VITE_LAST_ORIGINAL_COMMIT" >> .env +fi + +echo "VITE_LAST_ORIGINAL_COMMIT set as $VITE_LAST_ORIGINAL_COMMIT" + +# Update or add VITE_LAST_TAG in .env +if grep -q "VITE_LAST_TAG=" .env; then + # Variable exists, replace it + awk -v lc="$VITE_LAST_TAG" '/^VITE_LAST_TAG=/ {$0="VITE_LAST_TAG="lc} 1' .env > .env.tmp && mv .env.tmp .env +else + # Variable does not exist, append it + echo "VITE_LAST_TAG=$VITE_LAST_TAG" >> .env +fi + +echo "VITE_LAST_TAG set as $VITE_LAST_TAG" \ No newline at end of file diff --git a/scripts/validate-other-market-data.ts b/scripts/validate-other-market-data.ts new file mode 100644 index 000000000..9c0ae1df2 --- /dev/null +++ b/scripts/validate-other-market-data.ts @@ -0,0 +1,718 @@ +/* eslint-disable import/no-extraneous-dependencies */ + +/* eslint-disable no-plusplus */ + +/* eslint-disable no-console */ + +/* eslint-disable no-restricted-syntax */ + +/* eslint-disable no-await-in-loop */ +import { EncodeObject } from '@cosmjs/proto-signing'; +import { Account, StdFee } from '@cosmjs/stargate'; +import { Method } from '@cosmjs/tendermint-rpc'; +import { BroadcastTxSyncResponse } from '@cosmjs/tendermint-rpc/build/tendermint37'; +import { + CompositeClient, + LocalWallet as LocalWalletType, + Network, + TransactionOptions, + VoteOption, + ProposalStatus, +} from '@dydxprotocol/v4-client-js'; +import { + Perpetual, + PerpetualMarketType, +} from '@dydxprotocol/v4-client-js/build/node_modules/@dydxprotocol/v4-proto/src/codegen/dydxprotocol/perpetuals/perpetual'; +import { MsgVote } from '@dydxprotocol/v4-proto/src/codegen/cosmos/gov/v1/tx'; +import { ClobPair } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/clob_pair'; +import { MarketPrice } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/prices/market_price'; +import Ajv from 'ajv'; +import axios from 'axios'; +import { readFileSync } from 'fs'; +import Long from 'long'; +import { PrometheusDriver } from 'prometheus-query'; + +const LocalWalletModule = await import( + '@dydxprotocol/v4-client-js/src/clients/modules/local-wallet' +); +const LocalWallet = LocalWalletModule.default; + +const PATH_TO_PROPOSALS = 'public/configs/otherMarketData.json'; +// TODO: Query MIN_DEPOSIT and VOTING_PERIOD_SECONDS from chain. +const MIN_DEPOSIT = '10000000'; +const VOTING_PERIOD_SECONDS = 300; +const VOTE_FEE: StdFee = { + amount: [ + { + amount: '25000000000000000', + denom: 'adv4tnt', + }, + ], + gas: '1000000', +}; + +const PROMETHEUS_SERVER_URL = 'http://localhost:9091'; + +const MNEMONICS = [ + // alice + // Consensus Address: dydxvalcons1zf9csp5ygq95cqyxh48w3qkuckmpealrw2ug4d + 'merge panther lobster crazy road hollow amused security before critic about cliff exhibit cause coyote talent happy where lion river tobacco option coconut small', + + // bob + // Consensus Address: dydxvalcons1s7wykslt83kayxuaktep9fw8qxe5n73ucftkh4 + 'color habit donor nurse dinosaur stable wonder process post perfect raven gold census inside worth inquiry mammal panic olive toss shadow strong name drum', + + // carl + // Consensus Address: dydxvalcons1vy0nrh7l4rtezrsakaadz4mngwlpdmhy64h0ls + 'school artefact ghost shop exchange slender letter debris dose window alarm hurt whale tiger find found island what engine ketchup globe obtain glory manage', + + // dave + // Consensus Address: dydxvalcons1stjspktkshgcsv8sneqk2vs2ws0nw2wr272vtt + 'switch boring kiss cash lizard coconut romance hurry sniff bus accident zone chest height merit elevator furnace eagle fetch quit toward steak mystery nest', +]; + +interface Exchange { + exchangeName: ExchangeName; + ticker: string; + adjustByMarket?: string; +} + +interface Params { + id: number; + ticker: string; + marketType: 'PERPETUAL_MARKET_TYPE_ISOLATED' | 'PERPETUAL_MARKET_TYPE_CROSS'; + priceExponent: number; + minPriceChange: number; + minExchanges: number; + exchangeConfigJson: Exchange[]; + liquidityTier: number; + atomicResolution: number; + quantumConversionExponent: number; + defaultFundingPpm: number; + stepBaseQuantums: number; + subticksPerTick: number; + delayBlocks: number; +} + +interface Proposal { + title: string; + summary: string; + params: Params; +} + +enum ExchangeName { + Binance = 'Binance', + BinanceUS = 'BinanceUS', + Bitfinex = 'Bitfinex', + Bitstamp = 'Bitstamp', + Bybit = 'Bybit', + CoinbasePro = 'CoinbasePro', + CryptoCom = 'CryptoCom', + Gate = 'Gate', + Huobi = 'Huobi', + Kraken = 'Kraken', + Kucoin = 'Kucoin', + Mexc = 'Mexc', + Okx = 'Okx', +} + +interface PrometheusTimeSeries { + // value of the time serie + value: number; +} + +interface ExchangeInfo { + url: string; + tickers: Map | null; + parseResp: (response: any) => Map; + slinkyProviderName: string; +} + +const EXCHANGE_INFO: { [key in ExchangeName]: ExchangeInfo } = { + [ExchangeName.Binance]: { + url: 'https://data-api.binance.vision/api/v3/ticker/24hr', + tickers: null, + parseResp: (response: any) => { + return Array.from(response).reduce((acc: Map, item: any) => { + acc.set(item.symbol, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'binance_api', + }, + [ExchangeName.BinanceUS]: { + url: 'https://api.binance.us/api/v3/ticker/24hr', + tickers: null, + parseResp: (response: any) => { + return Array.from(response).reduce((acc: Map, item: any) => { + acc.set(item.symbol, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'binance_api', + }, + [ExchangeName.Bitfinex]: { + url: 'https://api-pub.bitfinex.com/v2/tickers?symbols=ALL', + tickers: null, + parseResp: (response: any) => { + return Array.from(response).reduce((acc: Map, item: any) => { + acc.set(item[0], {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'bitfinex_ws', + }, + [ExchangeName.Bitstamp]: { + url: 'https://www.bitstamp.net/api/v2/ticker/', + tickers: null, + parseResp: (response: any) => { + return Array.from(response).reduce((acc: Map, item: any) => { + acc.set(item.pair, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'bitstamp_ws', + }, + [ExchangeName.Bybit]: { + url: 'https://api.bybit.com/v5/market/tickers?category=spot', + tickers: null, + parseResp: (response: any) => { + return Array.from(response.result.list).reduce((acc: Map, item: any) => { + acc.set(item.symbol, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'bybit_ws', + }, + [ExchangeName.CoinbasePro]: { + url: 'https://api.exchange.coinbase.com/products', + tickers: null, + parseResp: (response: any) => { + return Array.from(response).reduce((acc: Map, item: any) => { + acc.set(item.id, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'coinbase_api', + }, + [ExchangeName.CryptoCom]: { + url: 'https://api.crypto.com/v2/public/get-ticker', + tickers: null, + parseResp: (response: any) => { + return Array.from(response.result.data).reduce((acc: Map, item: any) => { + acc.set(item.i, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'crypto_dot_com_ws', + }, + [ExchangeName.Gate]: { + url: 'https://api.gateio.ws/api/v4/spot/tickers', + tickers: null, + parseResp: (response: any) => { + return Array.from(response).reduce((acc: Map, item: any) => { + acc.set(item.currency_pair, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'gate_ws', + }, + [ExchangeName.Huobi]: { + url: 'https://api.huobi.pro/market/tickers', + tickers: null, + parseResp: (response: any) => { + return Array.from(response.data).reduce((acc: Map, item: any) => { + acc.set(item.symbol, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'huobi_ws', + }, + [ExchangeName.Kraken]: { + url: 'https://api.kraken.com/0/public/Ticker', + tickers: null, + parseResp: (response: any) => { + return new Map(Object.entries(response.result)); + }, + slinkyProviderName: 'kraken_api', + }, + [ExchangeName.Kucoin]: { + url: 'https://api.kucoin.com/api/v1/market/allTickers', + tickers: null, + parseResp: (response: any) => { + return Array.from(response.data.ticker).reduce((acc: Map, item: any) => { + acc.set(item.symbol, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'kucoin_ws', + }, + [ExchangeName.Mexc]: { + url: 'https://www.mexc.com/open/api/v2/market/ticker', + tickers: null, + parseResp: (response: any) => { + return Array.from(response.data).reduce((acc: Map, item: any) => { + acc.set(item.symbol, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'mexc_ws', + }, + [ExchangeName.Okx]: { + url: 'https://www.okx.com/api/v5/market/tickers?instType=SPOT', + tickers: null, + parseResp: (response: any) => { + return Array.from(response.data).reduce((acc: Map, item: any) => { + acc.set(item.instId, {}); + return acc; + }, new Map()); + }, + slinkyProviderName: 'okx_ws', + }, +}; + +async function validateExchangeConfigJson(exchangeConfigJson: Exchange[]): Promise { + const exchanges: Set = new Set(); + for (const exchange of exchangeConfigJson) { + if (!(exchange.exchangeName in EXCHANGE_INFO)) { + throw new Error(`Exchange ${exchange.exchangeName} not supported`); + } + // Each exchange should be unique. + if (exchanges.has(exchange.exchangeName)) { + throw new Error(`Found duplicate exchange: ${exchange.exchangeName}`); + } + exchanges.add(exchange.exchangeName); + + // `adjustByMarket` should be set if ticker doesn't end in usd or USD. + if ( + (!/usd$/i.test(exchange.ticker) && exchange.adjustByMarket === undefined) || + exchange.adjustByMarket === '' + ) { + throw new Error( + `adjustByMarket is not set for ticker ${exchange.ticker} on exchange ${exchange.exchangeName}` + ); + } + const { url, tickers, parseResp } = EXCHANGE_INFO[exchange.exchangeName]; + + // TODO: Skip Bybit exchange until we can query from non-US IP. + if (exchange.exchangeName === ExchangeName.Bybit) { + return; // exit the current iteration of the loop. + } + + // Query exchange tickers if not yet. + if (tickers === null) { + try { + const response = await axios.get(url); + EXCHANGE_INFO[exchange.exchangeName].tickers = parseResp(response.data); + console.log(`Fetched tickers from exchange ${exchange.exchangeName}`); + } catch (error) { + throw new Error(`Error fetching tickers for exchange ${exchange.exchangeName}: ${error}`); + } + } + + // Validate ticker. + if (!EXCHANGE_INFO[exchange.exchangeName].tickers?.has(exchange.ticker)) { + throw new Error(`Ticker ${exchange.ticker} not found for exchange ${exchange.exchangeName}`); + } + console.log(`Validated ticker ${exchange.ticker} for exchange ${exchange.exchangeName}`); + } +} + +// Vote YES on all `proposalIds` from `wallet`. +async function voteOnProposals( + proposalIds: number[], + client: CompositeClient, + wallet: LocalWalletType +): Promise { + // Construct Tx. + const encodedVotes: EncodeObject[] = proposalIds.map((proposalId) => { + return { + typeUrl: '/cosmos.gov.v1.MsgVote', + value: { + proposalId: Long.fromNumber(proposalId), + voter: wallet.address!, + option: VoteOption.VOTE_OPTION_YES, + metadata: '', + } as MsgVote, + } as EncodeObject; + }); + const account: Account = await client.validatorClient.get.getAccount(wallet.address!); + const signedTx = await wallet.signTransaction( + encodedVotes, + { + sequence: account.sequence, + accountNumber: account.accountNumber, + chainId: client.network.validatorConfig.chainId, + } as TransactionOptions, + VOTE_FEE + ); + + // Broadcast Tx. + const resp = await client.validatorClient.get.tendermintClient.broadcastTransaction( + signedTx, + Method.BroadcastTxSync + ); + if ((resp as BroadcastTxSyncResponse).code) { + throw new Error(`Failed to vote on proposals ${proposalIds}`); + } else { + console.log(`Voted on proposals ${proposalIds} with wallet ${wallet.address}`); + } +} + +async function validateAgainstLocalnet(proposals: Proposal[]): Promise { + // Initialize wallets. + const network = Network.local(); + const client = await CompositeClient.connect(network); + const wallets: LocalWalletType[] = await Promise.all( + MNEMONICS.map((mnemonic) => { + return LocalWallet.fromMnemonic(mnemonic, 'dydx'); + }) + ); + + // Send proposals to add all markets (unless a market with that ticker already exists). + const allPerps = await client.validatorClient.get.getAllPerpetuals(); + const allTickers = allPerps.perpetual.map((perp) => perp.params!.ticker); + const filteredProposals = proposals.filter( + (proposal) => !allTickers.includes(proposal.params.ticker) + ); + + const numExistingMarkets = allPerps.perpetual.reduce( + (max, perp) => (perp.params!.id > max ? perp.params!.id : max), + 0 + ); + const marketsProposed = new Map(); // marketId -> Proposal + + for (let i = 0; i < filteredProposals.length; i += 4) { + // Send out proposals in groups of 4 or fewer. + const proposalsToSend = filteredProposals.slice(i, i + 4); + const proposalIds: number[] = []; + for (let j = 0; j < proposalsToSend.length; j++) { + // Use wallets[j] to send out proposalsToSend[j] + const proposal = proposalsToSend[j]; + const proposalId: number = i + j + 1; + const marketId: number = numExistingMarkets + proposalId; + + // Send proposal. + const exchangeConfigString = `{"exchanges":${JSON.stringify( + proposal.params.exchangeConfigJson + )}}`; + const tx = await retry(() => + client.submitGovAddNewMarketProposal( + wallets[j], + // @ts-ignore: marketType is not a valid parameter for addNewMarketProposal + { + id: marketId, + ticker: proposal.params.ticker, + priceExponent: proposal.params.priceExponent, + minPriceChange: proposal.params.minPriceChange, + minExchanges: proposal.params.minExchanges, + exchangeConfigJson: exchangeConfigString, + liquidityTier: proposal.params.liquidityTier, + atomicResolution: proposal.params.atomicResolution, + quantumConversionExponent: proposal.params.quantumConversionExponent, + stepBaseQuantums: Long.fromNumber(proposal.params.stepBaseQuantums), + subticksPerTick: proposal.params.subticksPerTick, + delayBlocks: proposal.params.delayBlocks, + marketType: + proposal.params.marketType === 'PERPETUAL_MARKET_TYPE_ISOLATED' + ? PerpetualMarketType.PERPETUAL_MARKET_TYPE_ISOLATED + : PerpetualMarketType.PERPETUAL_MARKET_TYPE_CROSS, + }, + proposal.title, + proposal.summary, + MIN_DEPOSIT + ) + ); + console.log( + `Tx by wallet ${j} to add market ${marketId} with ticker ${proposal.params.ticker}`, + tx + ); + + // Record proposed market. + marketsProposed.set(marketId, proposal); + proposalIds.push(proposalId); + } + + // Wait 10 seconds for proposals to be processed. + await sleep(10000); + + // Vote YES on proposals from every wallet. + for (const wallet of wallets) { + retry(() => voteOnProposals(proposalIds, client, wallet)); + } + + // Wait 10 seconds for votes to be processed. + await sleep(10000); + } + + // Wait for voting period to end. + console.log(`\nWaiting for ${VOTING_PERIOD_SECONDS} seconds for voting period to end...`); + await sleep(VOTING_PERIOD_SECONDS * 1000); + + // Check that no proposal failed. + console.log('\nChecking that no proposal failed...'); + const proposalsFailed = await client.validatorClient.get.getAllGovProposals( + ProposalStatus.PROPOSAL_STATUS_FAILED + ); + if (proposalsFailed.proposals.length > 0) { + const failedIds = proposalsFailed.proposals.map((proposal) => proposal.id); + throw new Error(`Proposals ${failedIds} failed: ${proposalsFailed.proposals}`); + } + + // Wait for prices to update. + console.log('\nWaiting for 300 seconds for prices to update...'); + await sleep(300 * 1000); + + // Check markets on chain. + console.log('\nChecking price, clob pair, and perpetual on chain for each market proposed...'); + for (const [marketId, proposal] of marketsProposed.entries()) { + console.log(`\nChecking ${proposal?.params?.ticker}`); + const isDydxUsd = proposal.params.ticker.toLowerCase() === 'dydx-usd'; + // Validate price. + const price = await client.validatorClient.get.getPrice(isDydxUsd ? 1000001 : marketId); + validatePrice(price.marketPrice!, proposal); + + // Validate clob pair. + const clobPair = await client.validatorClient.get.getClobPair(marketId); + validateClobPair(clobPair.clobPair!, proposal); + + // Validate perpetual. + const perpetual = await client.validatorClient.get.getPerpetual(marketId); + validatePerpetual(perpetual.perpetual!, proposal); + } + + console.log(`\nValidated ${marketsProposed.size} proposals against localnet`); + + // for all markets proposed, determine if the slinky metrics are ok + for (const proposal of marketsProposed.values()) { + for (const exchange of proposal.params.exchangeConfigJson) { + validateSlinkyMetricsPerTicker( + dydxTickerToSlinkyTicker(proposal.params.ticker), + exchange.ticker.toLowerCase(), + EXCHANGE_INFO[exchange.exchangeName].slinkyProviderName + ); + } + } +} + +// convert a ticker like BTC-USD -> btc/usd +function dydxTickerToSlinkyTicker(ticker: string): string { + return ticker.toLowerCase().replace('-', '/'); +} + +function validateSlinkyMetricsPerTicker( + ticker: string, + exchangeSpecificTicker: string, + exchange: string +): void { + const prometheus = new PrometheusDriver({ + endpoint: PROMETHEUS_SERVER_URL, + baseURL: '/api/v1', + }); + + const exchangeAPIQuerySuccessRate = `( + sum(rate(side_car_provider_status_responses_per_id{status = "success", provider="${exchange}", id="${exchangeSpecificTicker}"}[1m])) by (provider, id) + ) / + ( + sum(rate(side_car_provider_status_responses_per_id{provider="${exchange}", id="${exchangeSpecificTicker}"}[1m])) by (provider, id) + )`; + + const slinkyPriceAggregationQuery = `( + sum(rate(side_car_health_check_ticker_updates_total{id="${ticker}"}[1m])) by (instance, job) + / + sum(rate(side_car_health_check_system_updates_total[1m])) by (instance, job) +)`; + + const slinkyProviderPricesQuery = `sum(rate(side_car_health_check_provider_updates_total{provider="${exchange}", id="${ticker}", success='true'}[1m])) by (provider, id) + / + sum(rate(side_car_health_check_provider_updates_total{provider="${exchange}", id="${ticker}"}[1m])) by (provider, id)`; + + const start = new Date().getTime() - 3 * 60 * 1000; + const end = new Date().getTime(); + const step = 60; + + // determine success-rate for slinky queries to each exchange + makePrometheusRateQuery(prometheus, exchangeAPIQuerySuccessRate, start, end, step, 0.7); + + // determine success rate for slinky price aggregation per market + makePrometheusRateQuery(prometheus, slinkyPriceAggregationQuery, start, end, step, 0.7); + + // determine success rate for slinky price provider per market + makePrometheusRateQuery(prometheus, slinkyProviderPricesQuery, start, end, step, 0.7); +} + +function makePrometheusRateQuery( + prometheus: PrometheusDriver, + query: string, + start: number, + end: number, + step: number, + threshold: number +): void { + prometheus + .rangeQuery(query, start, end, step) + .then((response) => { + const series = response.result; + series.forEach((s) => { + const values = s.values; + let totalSuccessRate = 0; + values.forEach((v: PrometheusTimeSeries) => { + // take the average of all success-rates over the interval + if (!Number.isNaN(v.value)) { + // we see NaN when there have been no successes from the provider + totalSuccessRate += v.value; + } + }); + if (values.length === 0 || totalSuccessRate / values.length < threshold) { + throw new Error( + `slinky metrics for ${query} is below success rate threshold ${threshold}: ${ + totalSuccessRate / values.length + }` + ); + } + }); + }) + .catch((error) => { + throw error; + }); +} + +function validatePrice(price: MarketPrice, proposal: Proposal): void { + if (price.exponent !== proposal.params.priceExponent) { + throw new Error(`Price exponent mismatch for price ${price.id}`); + } + if (price.price.isZero()) { + throw new Error(`Price is 0 for price ${price.id}`); + } +} + +function validateClobPair(clobPair: ClobPair, proposal: Proposal): void { + if (clobPair.quantumConversionExponent !== proposal.params.quantumConversionExponent) { + throw new Error(`Quantum conversion exponent mismatch for clob pair ${clobPair.id}`); + } + if (!clobPair.stepBaseQuantums.equals(proposal.params.stepBaseQuantums)) { + throw new Error(`Step base quantums mismatch for clob pair ${clobPair.id}`); + } + if (clobPair.subticksPerTick !== proposal.params.subticksPerTick) { + throw new Error(`Subticks per tick mismatch for clob pair ${clobPair.id}`); + } +} + +function validatePerpetual(perpetual: Perpetual, proposal: Proposal): void { + if (perpetual.params!.atomicResolution !== proposal.params.atomicResolution) { + throw new Error(`Atomic resolution mismatch for perpetual ${perpetual.params!.id}`); + } + if (perpetual.params!.liquidityTier !== proposal.params.liquidityTier) { + throw new Error(`Liquidity tier mismatch for perpetual ${perpetual.params!.id}`); + } +} + +function validateParamsSchema(proposal: Proposal): void { + const ajv = new Ajv(); + + const schema = { + type: 'object', + properties: { + id: { type: 'number' }, + ticker: { type: 'string' }, + priceExponent: { type: 'number' }, + minPriceChange: { type: 'number' }, + minExchanges: { type: 'number' }, + exchangeConfigJson: { + type: 'array', + items: { + type: 'object', + properties: { + exchangeName: { type: 'string' }, + ticker: { type: 'string' }, + adjustByMarket: { type: 'string', nullable: true }, + }, + required: ['exchangeName', 'ticker'], + additionalProperties: false, + }, + }, + liquidityTier: { type: 'number' }, + atomicResolution: { type: 'number' }, + quantumConversionExponent: { type: 'number' }, + stepBaseQuantums: { type: 'number' }, + subticksPerTick: { type: 'number' }, + delayBlocks: { type: 'number' }, + }, + required: [ + 'id', + 'ticker', + 'priceExponent', + 'minPriceChange', + 'minExchanges', + 'exchangeConfigJson', + 'liquidityTier', + 'atomicResolution', + 'quantumConversionExponent', + 'stepBaseQuantums', + 'subticksPerTick', + 'delayBlocks', + ], + }; + + const validateParams = ajv.compile(schema); + validateParams(proposal.params); + if (validateParams.errors) { + console.error(validateParams.errors); + throw new Error(`Json schema validation failed for proposal ${proposal.params.ticker}`); + } +} + +async function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +async function retry( + fn: () => Promise, + retries: number = 5, + delay: number = 2000 +): Promise { + try { + return await fn(); + } catch (error) { + console.error(`Function ${fn.name} failed: ${error}. Retrying in ${delay}ms...`); + if (retries <= 0) { + throw error; + } + await sleep(delay); + return retry(fn, retries - 1, delay); + } +} + +async function main(): Promise { + // Read proposals from json file. + const fileContent = readFileSync(PATH_TO_PROPOSALS, 'utf8'); + const proposals: Proposal[] = Object.values(JSON.parse(fileContent)); + + // Validate JSON schema. + console.log('Validating JSON schema of params...\n'); + for (const proposal of proposals) { + validateParamsSchema(proposal); + } + + // Validate proposal parameters. + console.log('\nValidating proposal parameters...\n'); + for (const proposal of proposals) { + // Validate exchange configuration of the market. + await validateExchangeConfigJson(proposal.params.exchangeConfigJson); + } + + // Validate proposals against localnet. + console.log('\nTesting proposals against localnet...\n'); + await validateAgainstLocalnet(proposals); +} + +main() + .then(() => { + console.log('\nAll proposals validated successfully.'); + }) + .catch((error) => { + console.error('\nError validating proposals:', error); + process.exit(1); + }); diff --git a/src/App.tsx b/src/App.tsx index fb52216e6..dd177a58c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,50 +1,52 @@ import { lazy, Suspense, useMemo } from 'react'; + +import { PrivyProvider } from '@privy-io/react-auth'; +import { PrivyWagmiConnector } from '@privy-io/wagmi-connector'; +import { GrazProvider } from 'graz'; +import { QueryClient, QueryClientProvider } from 'react-query'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; -import styled, { AnyStyledComponent, css } from 'styled-components'; +import styled, { css } from 'styled-components'; import { WagmiConfig } from 'wagmi'; -import { QueryClient, QueryClientProvider } from 'react-query'; -import { GrazProvider } from 'graz'; import { AppRoute, DEFAULT_TRADE_ROUTE, MarketsRoute } from '@/constants/routes'; -import { - useBreakpoints, - useTokenConfigs, - useInitializePage, - useShouldShowFooter, - useAnalytics, -} from '@/hooks'; -import { DydxProvider } from '@/hooks/useDydxClient'; import { AccountsProvider } from '@/hooks/useAccounts'; import { AppThemeAndColorModeProvider } from '@/hooks/useAppThemeAndColorMode'; import { DialogAreaProvider, useDialogArea } from '@/hooks/useDialogArea'; +import { DydxProvider } from '@/hooks/useDydxClient'; +import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications'; import { LocaleProvider } from '@/hooks/useLocaleSeparators'; import { NotificationsProvider } from '@/hooks/useNotifications'; -import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications'; import { PotentialMarketsProvider } from '@/hooks/usePotentialMarkets'; import { RestrictionProvider } from '@/hooks/useRestrictions'; import { SubaccountProvider } from '@/hooks/useSubaccount'; +import { breakpoints } from '@/styles'; +import '@/styles/constants.css'; +import '@/styles/fonts.css'; +import { GlobalStyle } from '@/styles/globalStyle'; +import { layoutMixins } from '@/styles/layoutMixins'; +import '@/styles/web3modal.css'; + import { GuardedMobileRoute } from '@/components/GuardedMobileRoute'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; - -import { HeaderDesktop } from '@/layout/Header/HeaderDesktop'; +import { DialogManager } from '@/layout/DialogManager'; import { FooterDesktop } from '@/layout/Footer/FooterDesktop'; import { FooterMobile } from '@/layout/Footer/FooterMobile'; +import { HeaderDesktop } from '@/layout/Header/HeaderDesktop'; import { NotificationsToastArea } from '@/layout/NotificationsToastArea'; -import { DialogManager } from '@/layout/DialogManager'; import { GlobalCommandDialog } from '@/views/dialogs/GlobalCommandDialog'; import { parseLocationHash } from '@/lib/urlUtils'; -import { config } from '@/lib/wagmi'; +import { config, configureChainsConfig, privyConfig } from '@/lib/wagmi'; -import { breakpoints } from '@/styles'; -import { GlobalStyle } from '@/styles/globalStyle'; -import { layoutMixins } from '@/styles/layoutMixins'; - -import '@/styles/constants.css'; -import '@/styles/fonts.css'; -import '@/styles/web3modal.css'; +import { ComplianceStates } from './constants/compliance'; +import { useAnalytics } from './hooks/useAnalytics'; +import { useBreakpoints } from './hooks/useBreakpoints'; +import { useComplianceState } from './hooks/useComplianceState'; +import { useInitializePage } from './hooks/useInitializePage'; +import { useShouldShowFooter } from './hooks/useShouldShowFooter'; +import { useTokenConfigs } from './hooks/useTokenConfigs'; const NewMarket = lazy(() => import('@/pages/markets/NewMarket')); const MarketsPage = lazy(() => import('@/pages/markets/Markets')); @@ -60,8 +62,6 @@ const TokenPage = lazy(() => import('@/pages/token/Token')); const queryClient = new QueryClient(); const Content = () => { - const { setDialogArea } = useDialogArea(); - useInitializePage(); useAnalytics(); @@ -71,6 +71,8 @@ const Content = () => { const { chainTokenLabel } = useTokenConfigs(); const location = useLocation(); + const { complianceState } = useComplianceState(); + const pathFromHash = useMemo(() => { if (location.hash === '') { return ''; @@ -78,13 +80,14 @@ const Content = () => { return parseLocationHash(location.hash); }, [location.hash]); + const { dialogAreaRef } = useDialogArea() ?? {}; return ( <> - + <$Content isShowingHeader={isShowingHeader} isShowingFooter={isShowingFooter}> {isNotTablet && } - + <$Main> }> @@ -96,7 +99,18 @@ const Content = () => { } /> } /> - } /> + + + ) : ( + + ) + } + /> + {isTablet && ( <> } /> @@ -117,18 +131,18 @@ const Content = () => { /> - + {isTablet ? : } - + <$NotificationsToastArea /> - + <$DialogArea ref={dialogAreaRef}> - + - + ); }; @@ -141,8 +155,13 @@ const wrapProvider = (Component: React.ComponentType, props?: any) => { }; const providers = [ + wrapProvider(PrivyProvider, { + appId: import.meta.env.VITE_PRIVY_APP_ID ?? 'dummyappiddummyappiddummy', + config: privyConfig, + }), wrapProvider(QueryClientProvider, { client: queryClient }), wrapProvider(GrazProvider), + wrapProvider(PrivyWagmiConnector, { wagmiChainsConfig: configureChainsConfig }), wrapProvider(WagmiConfig, { config }), wrapProvider(LocaleProvider), wrapProvider(RestrictionProvider), @@ -162,9 +181,7 @@ const App = () => { }, ); }; -const Styled: Record = {}; - -Styled.Content = styled.div<{ isShowingHeader: boolean; isShowingFooter: boolean }>` +const $Content = styled.div<{ isShowingHeader: boolean; isShowingFooter: boolean }>` /* Computed */ --page-currentHeaderHeight: 0px; --page-currentFooterHeight: 0px; @@ -188,12 +205,12 @@ Styled.Content = styled.div<{ isShowingHeader: boolean; isShowingFooter: boolean --page-currentFooterHeight: var(--page-footer-height-mobile); } `} - - /* Rules */ - ${layoutMixins.contentContainer} - - ${layoutMixins.scrollArea} - --scrollArea-height: 100vh; + + /* Rules */ + ${layoutMixins.contentContainer} + + ${layoutMixins.scrollArea} + --scrollArea-height: 100vh; @supports (-webkit-touch-callout: none) { height: -webkit-fill-available; @@ -216,7 +233,7 @@ Styled.Content = styled.div<{ isShowingHeader: boolean; isShowingFooter: boolean transition: 0.3s var(--ease-out-expo); `; -Styled.Main = styled.main` +const $Main = styled.main` ${layoutMixins.contentSectionAttached} box-shadow: none; @@ -227,12 +244,12 @@ Styled.Main = styled.main` position: relative; `; -Styled.NotificationsToastArea = styled(NotificationsToastArea)` +const $NotificationsToastArea = styled(NotificationsToastArea)` grid-area: Main; z-index: 2; `; -Styled.DialogArea = styled.aside` +const $DialogArea = styled.aside` position: fixed; height: 100%; z-index: 1; diff --git a/src/components/Accordion.stories.tsx b/src/components/Accordion.stories.tsx index be7b479be..40a532a46 100644 --- a/src/components/Accordion.stories.tsx +++ b/src/components/Accordion.stories.tsx @@ -23,4 +23,4 @@ Accordion.args = { content: 'Answer 2.', }, ], -}; \ No newline at end of file +}; diff --git a/src/components/Accordion.tsx b/src/components/Accordion.tsx index 1bdad5c2c..99056f093 100644 --- a/src/components/Accordion.tsx +++ b/src/components/Accordion.tsx @@ -1,11 +1,9 @@ -import styled, { keyframes, type AnyStyledComponent } from 'styled-components'; - -import { Root, Item, Header, Trigger, Content } from '@radix-ui/react-accordion'; - -import { layoutMixins } from '@/styles/layoutMixins'; -import { breakpoints } from '@/styles'; +import { Content, Header, Item, Root, Trigger } from '@radix-ui/react-accordion'; +import styled, { keyframes } from 'styled-components'; import { PlusIcon } from '@/icons'; +import { breakpoints } from '@/styles'; +import { layoutMixins } from '@/styles/layoutMixins'; export type AccordionItem = { header: React.ReactNode; @@ -18,26 +16,23 @@ export type AccordionProps = { }; export const Accordion = ({ items, className }: AccordionProps) => ( - + <$Root className={className} type="single" collapsible> {items.map(({ header, content }, idx) => ( - + <$Item key={idx} value={idx.toString()}>
- + <$Trigger> {header} - + <$Icon> - - + +
- {content} -
+ <$Content>{content} + ))} -
+ ); - -const Styled: Record = {}; - -Styled.Root = styled(Root)` +const $Root = styled(Root)` --accordion-paddingY: 1rem; --accordion-paddingX: 1rem; @@ -50,9 +45,9 @@ Styled.Root = styled(Root)` } `; -Styled.Item = styled(Item)``; +const $Item = styled(Item)``; -Styled.Icon = styled.div` +const $Icon = styled.div` display: inline-flex; justify-content: center; align-items: center; @@ -73,7 +68,7 @@ Styled.Icon = styled.div` } `; -Styled.Trigger = styled(Trigger)` +const $Trigger = styled(Trigger)` ${layoutMixins.spacedRow} width: 100%; padding: var(--accordion-paddingY) var(--accordion-paddingX); @@ -83,7 +78,7 @@ Styled.Trigger = styled(Trigger)` text-align: start; &:hover { - ${Styled.Icon} { + ${$Icon} { color: var(--color-text-2); filter: brightness(var(--hover-filter-base)); } @@ -99,7 +94,7 @@ Styled.Trigger = styled(Trigger)` } `; -Styled.Content = styled(Content)` +const $Content = styled(Content)` overflow: hidden; margin: 0 var(--accordion-paddingX) var(--accordion-paddingY); diff --git a/src/components/AlertMessage.tsx b/src/components/AlertMessage.tsx index 7fdc0019a..d66f1a52f 100644 --- a/src/components/AlertMessage.tsx +++ b/src/components/AlertMessage.tsx @@ -1,6 +1,7 @@ import styled, { css } from 'styled-components'; import { AlertType } from '@/constants/alerts'; + import { layoutMixins } from '@/styles/layoutMixins'; type StyleProps = { @@ -35,21 +36,25 @@ const AlertContainer = styled.div` case AlertType.Error: { return css` --alert-accent-color: var(--color-error); + --alert-background: var(--color-gradient-error); `; } case AlertType.Info: { return css` --alert-accent-color: var(--color-text-1); + --alert-background: var(--color-layer-7); `; } case AlertType.Success: { return css` --alert-accent-color: var(--color-success); + --alert-background: var(--color-gradient-success); `; } case AlertType.Warning: { return css` --alert-accent-color: var(--color-warning); + --alert-background: var(--color-gradient-warning); `; } default: diff --git a/src/components/AssetIcon.tsx b/src/components/AssetIcon.tsx index af1d4031c..1470b67a4 100644 --- a/src/components/AssetIcon.tsx +++ b/src/components/AssetIcon.tsx @@ -1,4 +1,4 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { Nullable } from '@/constants/abacus'; @@ -8,16 +8,22 @@ const assetIcons = { '1INCH': '/currencies/1inch.png', AAVE: '/currencies/aave.png', ADA: '/currencies/ada.png', + AEVO: '/currencies/aevo.png', AGIX: '/currencies/agix.png', ALGO: '/currencies/algo.png', APE: '/currencies/ape.png', + API3: '/currencies/api3.png', APT: '/currencies/apt.png', ARB: '/currencies/arb.png', + ARKM: '/currencies/arkm.png', + ASTR: '/currencies/astr.png', ATOM: '/currencies/atom.png', AVAX: '/currencies/avax.png', + AXL: '/currencies/axl.png', BCH: '/currencies/bch.png', BLUR: '/currencies/blur.png', BNB: '/currencies/bnb.png', + BOME: '/currencies/bome.png', BONK: '/currencies/bonk.png', BTC: '/currencies/btc.png', CELO: '/currencies/celo.png', @@ -34,8 +40,10 @@ const assetIcons = { EOS: '/currencies/eos.png', ETC: '/currencies/etc.png', ETH: '/currencies/eth.png', + ETHFI: '/currencies/ethfi.png', FET: '/currencies/fet.png', FIL: '/currencies/fil.png', + FLR: '/currencies/flr.png', FTM: '/currencies/ftm.png', GALA: '/currencies/gala.png', GMT: '/currencies/gmt.png', @@ -50,15 +58,19 @@ const assetIcons = { LDO: '/currencies/ldo.png', LINK: '/currencies/link.png', LTC: '/currencies/ltc.png', + MAGIC: '/currencies/magic.png', MANA: '/currencies/mana.png', MATIC: '/currencies/matic.png', MASK: '/currencies/mask.png', + MEME: '/currencies/meme.png', MINA: '/currencies/mina.png', MKR: '/currencies/mkr.png', NEAR: '/currencies/near.png', + OCEAN: '/currencies/ocean.png', ORDI: '/currencies/ordi.png', OP: '/currencies/op.png', PEPE: '/currencies/pepe.png', + PORTAL: '/currencies/portal.png', PYTH: '/currencies/pyth.png', RNDR: '/currencies/rndr.png', RUNE: '/currencies/rune.png', @@ -72,6 +84,7 @@ const assetIcons = { SUI: '/currencies/sui.png', SUSHI: '/currencies/sushi.png', TIA: '/currencies/tia.png', + TON: '/currencies/ton.png', TRX: '/currencies/trx.png', UMA: '/currencies/uma.png', UNI: '/currencies/uni.png', @@ -79,6 +92,7 @@ const assetIcons = { USDT: '/currencies/usdt.png', WBTC: '/currencies/wbtc.png', WETH: '/currencies/weth.png', + WIF: '/currencies/wif.png', WOO: '/currencies/woo.png', WLD: '/currencies/wld.png', XLM: '/currencies/xlm.png', @@ -101,16 +115,13 @@ export const AssetIcon = ({ symbol?: Nullable; className?: string; }) => ( - ); - -const Styled: Record = {}; - -Styled.Img = styled.img` +const $Img = styled.img` width: auto; height: 1em; `; diff --git a/src/components/BackButton.tsx b/src/components/BackButton.tsx index 9c40b64fd..524fc69bb 100644 --- a/src/components/BackButton.tsx +++ b/src/components/BackButton.tsx @@ -7,7 +7,12 @@ type ElementProps = { onClick?: () => void; }; +type StyleProps = { + className?: string; +}; + export const BackButton = ({ + className, onClick = () => { // @ts-ignore const navigation = globalThis.navigation; @@ -21,8 +26,9 @@ export const BackButton = ({ navigation.navigate('/', { replace: true }); } }, -}: ElementProps) => ( +}: ElementProps & StyleProps) => ( - { - <> - {state[ButtonState.Loading] ? ( - - ) : ( - <> - {slotLeft} - {children} - {slotRight} - - )} - - } + <> + {state[ButtonState.Loading] ? ( + + ) : ( + <> + {slotLeft} + {children} + {slotRight} + + )} + ); } @@ -129,34 +127,46 @@ const buttonActionVariants = { `, }; -const buttonStateVariants: Record< - ButtonState, - FlattenSimpleInterpolation | FlattenInterpolation> -> = { +const getDisabledStateForButtonAction = (action?: ButtonAction) => { + switch (action) { + case ButtonAction.Navigation: + return css` + --button-textColor: var(--color-text-0); + --button-hover-filter: none; + --button-cursor: not-allowed; + `; + default: + return css` + --button-textColor: var(--color-text-0); + --button-backgroundColor: var(--color-layer-2); + --button-border: solid var(--border-width) var(--color-layer-6); + --button-hover-filter: none; + --button-cursor: not-allowed; + `; + } +}; + +const buttonStateVariants = ( + action?: ButtonAction +): Record>> => ({ [ButtonState.Default]: css``, - [ButtonState.Disabled]: css` - --button-textColor: var(--color-text-0); - --button-backgroundColor: var(--color-layer-2); - --button-border: solid var(--border-width) var(--color-layer-6); - --button-hover-filter: none; - --button-cursor: not-allowed; - `, + [ButtonState.Disabled]: getDisabledStateForButtonAction(action), [ButtonState.Loading]: css` - ${() => buttonStateVariants[ButtonState.Disabled]} + ${() => buttonStateVariants(action)[ButtonState.Disabled]} min-width: 4em; `, -}; +}); const StyledBaseButton = styled(BaseButton)` ${({ action }) => action && buttonActionVariants[action]} - ${({ state }) => + ${({ action, state }) => state && css` // Ordered from lowest to highest priority (ie. Disabled should overwrite Active and Loading states) - ${state[ButtonState.Loading] && buttonStateVariants[ButtonState.Loading]} - ${state[ButtonState.Disabled] && buttonStateVariants[ButtonState.Disabled]} + ${state[ButtonState.Loading] && buttonStateVariants(action)[ButtonState.Loading]} + ${state[ButtonState.Disabled] && buttonStateVariants(action)[ButtonState.Disabled]} `} `; diff --git a/src/components/Checkbox.stories.tsx b/src/components/Checkbox.stories.tsx index 3a4f24a97..b5ad9c547 100644 --- a/src/components/Checkbox.stories.tsx +++ b/src/components/Checkbox.stories.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; + import type { Story } from '@ladle/react'; import { Checkbox, CheckboxProps } from '@/components/Checkbox'; @@ -28,5 +29,5 @@ CheckboxStory.argTypes = { options: [true, false], control: { type: 'select' }, defaultValue: false, - } -} + }, +}; diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 091ebf295..acd081945 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,6 +1,6 @@ -import styled, { css, type AnyStyledComponent } from 'styled-components'; -import { Root, Indicator } from '@radix-ui/react-checkbox'; +import { Indicator, Root } from '@radix-ui/react-checkbox'; import { CheckIcon } from '@radix-ui/react-icons'; +import styled, { css } from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -26,35 +26,32 @@ export const Checkbox: React.FC = ({ label, disabled, }) => ( - - + <$Root className={className} checked={checked} disabled={disabled} onCheckedChange={onCheckedChange} id={id} > - + <$Indicator> - - + + {label && ( - + <$Label disabled={disabled} htmlFor={id}> {label} - + )} - + ); - -const Styled: Record = {}; - -Styled.Container = styled.div` +const $Container = styled.div` ${layoutMixins.row} gap: 1ch; font: var(--font-small-book); `; -Styled.Root = styled(Root)` +const $Root = styled(Root)` --checkbox-backgroundColor: var(--color-layer-0); --checkbox-borderColor: var(--color-border); @@ -75,7 +72,7 @@ Styled.Root = styled(Root)` } `; -Styled.Indicator = styled(Indicator)` +const $Indicator = styled(Indicator)` display: flex; align-items: center; justify-content: center; @@ -83,7 +80,7 @@ Styled.Indicator = styled(Indicator)` color: var(--color-text-button); `; -Styled.Label = styled.label<{ disabled?: boolean }>` +const $Label = styled.label<{ disabled?: boolean }>` cursor: pointer; color: var(--color-text-2); diff --git a/src/components/Collapsible.stories.tsx b/src/components/Collapsible.stories.tsx index f47a2f500..4d4c75b08 100644 --- a/src/components/Collapsible.stories.tsx +++ b/src/components/Collapsible.stories.tsx @@ -1,11 +1,14 @@ +import { useState } from 'react'; + import type { Story } from '@ladle/react'; import { Collapsible, type CollapsibleProps } from '@/components/Collapsible'; -import { StoryWrapper } from '.ladle/components'; +import { Checkbox } from './Checkbox'; import { IconName } from './Icon'; +import { StoryWrapper } from '.ladle/components'; -export const CollapsibleStory: Story = (args) => ( +export const CollapsibleStoryWithIconTrigger: Story = (args) => (
    @@ -18,13 +21,13 @@ export const CollapsibleStory: Story = (args) => ( ); -CollapsibleStory.args = { +CollapsibleStoryWithIconTrigger.args = { disabled: false, withTrigger: true, label: 'Collapsible List of Items', }; -CollapsibleStory.argTypes = { +CollapsibleStoryWithIconTrigger.argTypes = { triggerIcon: { options: Object.values(IconName), control: { type: 'select' }, @@ -36,3 +39,38 @@ CollapsibleStory.argTypes = { defaultValue: 'left', }, }; + +export const CollapsibleStoryWithSlotTrigger: Story = (args) => { + const [checked, setChecked] = useState(false); + return ( + + } + open={checked} + onOpenChange={setChecked} + > +
      +
    • Collapsible Item 1
    • +
    • Collapsible Item 2
    • +
    • Collapsible Item 3
    • +
    • Collapsible Item 4
    • +
    +
    +
    + ); +}; + +CollapsibleStoryWithSlotTrigger.args = { + disabled: false, + withTrigger: true, + label: 'Collapsible List of Items', +}; + +CollapsibleStoryWithSlotTrigger.argTypes = { + triggerIconSide: { + options: ['left', 'right'], + control: { type: 'select' }, + defaultValue: 'left', + }, +}; diff --git a/src/components/Collapsible.tsx b/src/components/Collapsible.tsx index 51483141c..6d4f7c429 100644 --- a/src/components/Collapsible.tsx +++ b/src/components/Collapsible.tsx @@ -1,10 +1,12 @@ -import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components'; -import { Root, Trigger, Content } from '@radix-ui/react-collapsible'; +import React from 'react'; + +import { Content, Root, Trigger } from '@radix-ui/react-collapsible'; +import styled, { css, keyframes } from 'styled-components'; import { popoverMixins } from '@/styles/popoverMixins'; -import { HorizontalSeparatorFiller } from '@/components/Separator'; import { Icon, IconName } from '@/components/Icon'; +import { HorizontalSeparatorFiller } from '@/components/Separator'; type ElementProps = { defaultOpen?: boolean; @@ -13,6 +15,7 @@ type ElementProps = { onOpenChange?: (open: boolean) => void; label: React.ReactNode; triggerIcon?: IconName; + slotTrigger?: React.ReactNode; children: React.ReactNode; withTrigger?: boolean; }; @@ -36,38 +39,47 @@ export const Collapsible = ({ transitionDuration, triggerIcon = IconName.Caret, triggerIconSide = 'left', + slotTrigger, fullWidth, className, withTrigger = true, -}: CollapsibleProps) => ( - - {withTrigger && ( - - {triggerIconSide === 'right' && ( - <> - {label} - {fullWidth && } - - )} - - - - - {triggerIconSide === 'left' && ( - <> - {fullWidth && } - {label} - - )} - - )} - {children} - -); - -const Styled: Record = {}; - -Styled.Root = styled(Root)` +}: CollapsibleProps) => { + const trigger = slotTrigger ? ( + <$TriggerSlot> + {triggerIconSide === 'right' && label} + + {slotTrigger} + + {triggerIconSide === 'left' && label} + + ) : ( + <$Trigger className={className} disabled={disabled}> + {triggerIconSide === 'right' && ( + <> + {label} + {fullWidth && } + + )} + <$TriggerIcon> + + + {triggerIconSide === 'left' && ( + <> + {fullWidth && } + {label} + + )} + + ); + + return ( + <$Root defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}> + {withTrigger && trigger} + <$Content $transitionDuration={transitionDuration}>{children} + + ); +}; +const $Root = styled(Root)` display: grid; &[data-state='open'] { @@ -75,26 +87,32 @@ Styled.Root = styled(Root)` } `; -Styled.Trigger = styled(Trigger)` +const $Trigger = styled(Trigger)` ${popoverMixins.trigger} --trigger-textColor: inherit; --trigger-icon-width: 0.75em; --trigger-icon-color: inherit; `; -Styled.TriggerIcon = styled.span` +const $TriggerSlot = styled.div` + display: flex; + align-items: center; + gap: 0.5em; +`; + +const $TriggerIcon = styled.span` width: var(--trigger-icon-width); display: inline-flex; transition: rotate 0.3s var(--ease-out-expo); color: var(--trigger-icon-color); - ${Styled.Trigger}[data-state='open'] & { + ${$Trigger}[data-state='open'] & { rotate: -0.5turn; } `; -Styled.Content = styled(Content)<{ $transitionDuration: number }>` +const $Content = styled(Content)<{ $transitionDuration?: number }>` display: grid; --transition-duration: 0.25s; diff --git a/src/components/CollapsibleNavigationMenu.tsx b/src/components/CollapsibleNavigationMenu.tsx index 0994e949a..d4639bd41 100644 --- a/src/components/CollapsibleNavigationMenu.tsx +++ b/src/components/CollapsibleNavigationMenu.tsx @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react'; -import styled, { css, type AnyStyledComponent } from 'styled-components'; -import _ from 'lodash'; -import { matchPath, NavLink, useLocation } from 'react-router-dom'; -import { type MenuItem } from '@/constants/menus'; +import { Item, Link, List, Root, Sub } from '@radix-ui/react-navigation-menu'; +import { NavLink, matchPath, useLocation } from 'react-router-dom'; +import styled, { css } from 'styled-components'; -import { Root, List, Item, Sub, Link } from '@radix-ui/react-navigation-menu'; +import { type MenuItem } from '@/constants/menus'; import { popoverMixins } from '@/styles/popoverMixins'; + import { Collapsible } from './Collapsible'; type ElementProps = { @@ -27,20 +27,13 @@ const NavItem = ({ const location = useLocation(); return !href ? null : ( - + onSelect?.(value)} > - + {label} @@ -66,14 +59,10 @@ export const CollapsibleNavigationMenu = ({ return ( - + <$List> {items.map((item) => !item.subitems ? ( - + <$NavItem key={item.value} onSelect={onSelectItem} {...item} /> ) : ( ({ onOpenChange={(open) => { setExpandedKey(!open ? '' : item.value); }} - label={ - {item.label} - } + label={<$CollapsibleItem value={item.value}>{item.label}} transitionDuration={0.2} > - + <$Sub defaultValue={item.subitems?.[0]?.value}> {item.subitems.map((subitem: MenuItem) => ( - + <$NavItem key={subitem.value} onSelect={onSelectItem} {...subitem} /> ))} - + ) )} - + ); }; - -const Styled: Record = {}; - const navItemStyle = css` ${popoverMixins.item} --item-padding: 0.5em 0.75em; @@ -113,16 +93,16 @@ const navItemStyle = css` --item-checked-textColor: var(--color-text-0); `; -Styled.List = styled(List)` +const $List = styled(List)` gap: 0.5rem; `; -Styled.CollapsibleItem = styled(Item)` +const $CollapsibleItem = styled(Item)` ${navItemStyle} --item-padding: 0; `; -Styled.Sub = styled(Sub)` +const $Sub = styled(Sub)` margin: -0.25rem 0.5rem 0; padding-left: 0.5em; border-left: solid var(--border-width) var(--color-border); @@ -130,12 +110,12 @@ Styled.Sub = styled(Sub)` font-size: 0.92em; `; -Styled.NavItem = styled(NavItem)` +const $NavItem = styled(NavItem)` ${navItemStyle} margin: 0.25em 0; justify-content: flex-start; - ${Styled.Sub} & { + ${$Sub} & { --item-padding: 0.5em 0.7em; } -`; +` as typeof NavItem; diff --git a/src/components/CollapsibleTabs.tsx b/src/components/CollapsibleTabs.tsx index 991f0ac4f..e68f2ff1f 100644 --- a/src/components/CollapsibleTabs.tsx +++ b/src/components/CollapsibleTabs.tsx @@ -1,30 +1,31 @@ -import { type ReactNode, useState } from 'react'; -import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; +import { Dispatch, SetStateAction, type ReactNode } from 'react'; +import { + Content as CollapsibleContent, + Root as CollapsibleRoot, + Trigger as CollapsibleTrigger, +} from '@radix-ui/react-collapsible'; import { Content as TabsContent, List as TabsList, Root as TabsRoot, Trigger as TabsTrigger, } from '@radix-ui/react-tabs'; - -import { - Content as CollapsibleContent, - Root as CollapsibleRoot, - Trigger as CollapsibleTrigger, -} from '@radix-ui/react-collapsible'; +import styled, { css, keyframes } from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; -import { IconButton } from '@/components/IconButton'; import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { type TabItem } from '@/components/Tabs'; import { Tag } from '@/components/Tag'; import { Toolbar } from '@/components/Toolbar'; -import { type TabItem } from '@/components/Tabs'; type ElementProps = { - defaultValue?: TabItemsValue; - items: TabItem[]; + defaultTab?: TabItemsValue; + tab: TabItemsValue; + setTab?: Dispatch>; + tabItems: TabItem[]; slotToolbar?: ReactNode; defaultOpen?: boolean; onOpenChange?: (isOpen: boolean) => void; @@ -38,8 +39,10 @@ type StyleProps = { export type CollapsibleTabsProps = ElementProps & StyleProps; export const CollapsibleTabs = ({ - defaultValue, - items, + defaultTab, + tab, + setTab, + tabItems, slotToolbar, defaultOpen, onOpenChange, @@ -48,61 +51,52 @@ export const CollapsibleTabs = ({ className, }: CollapsibleTabsProps) => { - const [value, setValue] = useState(defaultValue); - - const currentItem = items.find((item) => item.value === value); + const currentTab = tabItems.find((tabItem) => tabItem.value === tab); return ( - setTab?.(v as TabItemsValue)} asChild > - onOpenChange?.(isOpen)} > - - - {items.map((item) => ( - onOpenChange?.(true)} - > - {item.label} - {item.tag && {item.tag}} - {item.slotRight} - + <$Header> + <$TabsList $fullWidthTabs={fullWidthTabs}> + {tabItems.map(({ value, label, tag, slotRight }) => ( + <$TabsTrigger key={value} value={value} onClick={() => onOpenChange?.(true)}> + {label} + {tag && {tag}} + {slotRight} + ))} - + - - {currentItem?.slotToolbar || slotToolbar} + <$Toolbar> + {currentTab?.slotToolbar || slotToolbar} - + <$IconButton iconName={IconName.Caret} isToggle /> - - + + - - {items.map(({ asChild, value, content }) => ( - + <$CollapsibleContent> + {tabItems.map(({ asChild, value, content }) => ( + <$TabsContent key={value} asChild={asChild} value={value}> {content} - + ))} - - - + + + ); }; - -const Styled: Record = {}; - -Styled.TabsRoot = styled(TabsRoot)` +const $TabsRoot = styled(TabsRoot)` /* Overrides */ --trigger-backgroundColor: var(--color-layer-2); --trigger-textColor: var(--color-text-0); @@ -125,7 +119,7 @@ Styled.TabsRoot = styled(TabsRoot)` ${layoutMixins.withInnerHorizontalBorders} `; -Styled.TabsList = styled(TabsList)<{ $fullWidthTabs?: boolean }>` +const $TabsList = styled(TabsList)<{ $fullWidthTabs?: boolean }>` ${layoutMixins.withOuterAndInnerBorders} align-self: stretch; @@ -145,7 +139,7 @@ Styled.TabsList = styled(TabsList)<{ $fullWidthTabs?: boolean }>` padding: 0 var(--border-width); `; -Styled.TabsTrigger = styled(TabsTrigger)` +const $TabsTrigger = styled(TabsTrigger)` ${layoutMixins.withOuterBorder} ${layoutMixins.row} @@ -164,11 +158,11 @@ Styled.TabsTrigger = styled(TabsTrigger)` } `; -Styled.Toolbar = styled(Toolbar)` +const $Toolbar = styled(Toolbar)` ${layoutMixins.inlineRow} `; -Styled.TabsContent = styled(TabsContent)` +const $TabsContent = styled(TabsContent)` ${layoutMixins.flexColumn} outline: none; @@ -203,9 +197,9 @@ Styled.TabsContent = styled(TabsContent)` } `; -Styled.CollapsibleRoot = styled(CollapsibleRoot)``; +const $CollapsibleRoot = styled(CollapsibleRoot)``; -Styled.Header = styled.header` +const $Header = styled.header` ${layoutMixins.sticky} height: var(--stickyArea-topHeight); @@ -215,20 +209,20 @@ Styled.Header = styled.header` ${layoutMixins.row} justify-content: space-between; - ${Styled.CollapsibleRoot}[data-state='closed'] & { + ${$CollapsibleRoot}[data-state='closed'] & { box-shadow: none; } `; -Styled.CollapsibleContent = styled(CollapsibleContent)` +const $CollapsibleContent = styled(CollapsibleContent)` ${layoutMixins.stack} box-shadow: none; `; -Styled.IconButton = styled(IconButton)` +const $IconButton = styled(IconButton)` --button-icon-size: 1em; - ${Styled.CollapsibleRoot}[data-state='closed'] & { + ${$CollapsibleRoot}[data-state='closed'] & { rotate: -0.5turn; } `; diff --git a/src/components/ComboboxDialogMenu.tsx b/src/components/ComboboxDialogMenu.tsx index cba1a4e3f..3f97efe7a 100644 --- a/src/components/ComboboxDialogMenu.tsx +++ b/src/components/ComboboxDialogMenu.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + +import styled from 'styled-components'; import { type MenuConfig } from '@/constants/menus'; -import { Dialog, DialogPlacement, type DialogProps } from '@/components/Dialog'; import { ComboboxMenu, type ComboboxMenuProps } from '@/components/ComboboxMenu'; +import { Dialog, DialogPlacement, type DialogProps } from '@/components/Dialog'; type ElementProps = { title?: React.ReactNode; @@ -71,7 +72,7 @@ export const ComboboxDialogMenu = < PickDialogProps & StyleProps) => ( // TODO: sub-menu state management - - {children} - + ); - -const Styled: Record = {}; - -Styled.Dialog = styled(Dialog)` +const $Dialog = styled(Dialog)` /* Params */ --comboboxDialogMenu-backgroundColor: var(--color-layer-2); --comboboxDialogMenu-item-gap: 0.5rem; @@ -126,8 +124,8 @@ Styled.Dialog = styled(Dialog)` } `; -Styled.ComboboxMenu = styled(ComboboxMenu)` +const $ComboboxMenu = styled(ComboboxMenu)` --comboboxMenu-backgroundColor: var(--comboboxDialogMenu-backgroundColor); --comboboxMenu-item-gap: var(--comboboxDialogMenu-item-gap); --comboboxMenu-item-padding: var(--comboboxDialogMenu-item-padding); -`; +` as typeof ComboboxMenu; diff --git a/src/components/ComboboxMenu.tsx b/src/components/ComboboxMenu.tsx index 92be2db31..16ea87548 100644 --- a/src/components/ComboboxMenu.tsx +++ b/src/components/ComboboxMenu.tsx @@ -1,10 +1,12 @@ -import { Fragment, type ReactNode, useState } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; +import { Fragment, useState, type ReactNode } from 'react'; + import { Command } from 'cmdk'; +import styled, { css } from 'styled-components'; + +import { type MenuConfig } from '@/constants/menus'; -import { MenuItem, type MenuConfig } from '@/constants/menus'; -import { popoverMixins } from '@/styles/popoverMixins'; import { layoutMixins } from '@/styles/layoutMixins'; +import { popoverMixins } from '@/styles/popoverMixins'; import { Tag } from '@/components/Tag'; @@ -12,7 +14,7 @@ type ElementProps; onItemSelected?: () => void; - title?: string; + title?: ReactNode; inputPlaceholder?: string; slotEmpty?: ReactNode; withSearch?: boolean; @@ -29,7 +31,10 @@ export type ComboboxMenuProps< MenuGroupValue extends string | number > = ElementProps & StyleProps; -export const ComboboxMenu = ({ +export const ComboboxMenu = < + MenuItemValue extends string | number, + MenuGroupValue extends string | number +>({ items, onItemSelected, @@ -46,8 +51,8 @@ export const ComboboxMenu = @@ -59,24 +64,23 @@ export const ComboboxMenu = {withSearch && ( - - + <$Input /** * Mobile Issue: Search Input will always trigger mobile keyboard drawer. There is no fix. * https://github.com/pacocoursey/cmdk/issues/127 */ autoFocus - type="search" value={searchValue} onValueChange={setSearchValue} placeholder={inputPlaceholder} /> - + )} - + <$List $withStickyLayout={withStickyLayout}> {items.map((group) => ( - {group.items.map((item) => ( - - { - <> - {item.slotBefore} - {item.slotCustomContent ?? ( - - - {typeof item.label === 'string' - ? `${item.label}${item.subitems?.length ? '…' : ''}` - : item.label} - {item.tag && ( - <> - {' '} - {item.tag} - - )} - - {item.description && {item.description}} - - )} - {item.slotAfter} - {item.subitems && '→'} - - } - + <> + {item.slotBefore} + {item.slotCustomContent ?? ( + <$ItemLabel> + + {typeof item.label === 'string' + ? `${item.label}${item.subitems?.length ? '…' : ''}` + : item.label} + {item.tag && ( + <> + {' '} + {item.tag} + + )} + + {item.description && {item.description}} + + )} + {item.slotAfter} + {item.subitems && '→'} + + {searchValue && item.subitems?.map((subitem) => ( - {subitem.slotBefore} - + <$ItemLabel> {subitem.label} {subitem.tag && ( @@ -160,24 +162,21 @@ export const ComboboxMenu = {item.description && {item.description}} - + {subitem.slotAfter} - + ))} ))} - + ))} - {slotEmpty && searchValue.trim() !== '' && {slotEmpty}} - - + {slotEmpty && searchValue.trim() !== '' && <$Empty>{slotEmpty}} + + ); }; - -const Styled: Record = {}; - -Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>` +const $Command = styled(Command)<{ $withStickyLayout?: boolean }>` --comboboxMenu-backgroundColor: var(--color-layer-2); --comboboxMenu-input-backgroundColor: var(--color-layer-3); @@ -209,13 +208,13 @@ Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>` --stickyArea1-topHeight: 4rem; ` : css` - ${() => Styled.List} { + ${() => $List} { overflow-y: auto; } `} `; -Styled.Header = styled.header<{ $withStickyLayout?: boolean }>` +const $Header = styled.header<{ $withStickyLayout?: boolean }>` display: grid; align-items: center; padding-left: 0.75rem; @@ -230,7 +229,7 @@ Styled.Header = styled.header<{ $withStickyLayout?: boolean }>` `} `; -Styled.Input = styled(Command.Input)` +const $Input = styled(Command.Input)` height: var(--comboboxMenu-input-height); padding: 0.5rem; background-color: var(--comboboxMenu-input-backgroundColor); @@ -238,7 +237,7 @@ Styled.Input = styled(Command.Input)` gap: 0.5rem; `; -Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLayout?: boolean }>` +const $Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLayout?: boolean }>` color: var(--color-text-0); > [cmdk-group-heading] { @@ -272,7 +271,7 @@ Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLa `} `; -Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>` +const $List = styled(Command.List)<{ $withStickyLayout?: boolean }>` isolation: isolate; background-color: var(--comboboxMenu-backgroundColor, inherit); @@ -296,7 +295,7 @@ Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>` `} `; -Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>` +const $Item = styled(Command.Item)<{ $withItemBorders?: boolean }>` ${layoutMixins.scrollSnapItem} ${popoverMixins.item} --item-checked-backgroundColor: var(--comboboxMenu-item-checked-backgroundColor); @@ -322,7 +321,7 @@ Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>` `} `; -Styled.ItemLabel = styled.div` +const $ItemLabel = styled.div` flex: 1; ${layoutMixins.rowColumn} @@ -342,7 +341,7 @@ Styled.ItemLabel = styled.div` min-width: 0; `; -Styled.Empty = styled(Command.Empty)` +const $Empty = styled(Command.Empty)` color: var(--color-text-0); padding: 1rem; height: 100%; diff --git a/src/components/ComingSoon.tsx b/src/components/ComingSoon.tsx index 0d620dd49..af3b5524f 100644 --- a/src/components/ComingSoon.tsx +++ b/src/components/ComingSoon.tsx @@ -1,7 +1,9 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; export const ComingSoon = () => { @@ -10,14 +12,11 @@ export const ComingSoon = () => { }; export const ComingSoonSpace = () => ( - + <$FullPageContainer> - + ); - -const Styled: Record = {}; - -Styled.FullPageContainer = styled.div` +const $FullPageContainer = styled.div` ${layoutMixins.centered} h1 { diff --git a/src/components/ContentSection.tsx b/src/components/ContentSection.tsx index 7aa265faa..b22f39059 100644 --- a/src/components/ContentSection.tsx +++ b/src/components/ContentSection.tsx @@ -1,4 +1,5 @@ import styled from 'styled-components'; + import { layoutMixins } from '@/styles/layoutMixins'; export const DetachedSection = styled.section` diff --git a/src/components/ContentSectionHeader.tsx b/src/components/ContentSectionHeader.tsx index 5f1e2dbe9..ff771d0d8 100644 --- a/src/components/ContentSectionHeader.tsx +++ b/src/components/ContentSectionHeader.tsx @@ -1,4 +1,4 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -21,19 +21,16 @@ export const ContentSectionHeader = ({ slotRight, className, }: ElementProps & StyleProps) => ( - + <$ContentSectionHeader className={className}> {slotLeft} - + <$Header> {title &&

    {title}

    } {subtitle &&

    {subtitle}

    } -
    + {slotRight} -
    + ); - -const Styled: Record = {}; - -Styled.ContentSectionHeader = styled.header` +const $ContentSectionHeader = styled.header` ${layoutMixins.contentSectionDetached} ${layoutMixins.row} @@ -49,7 +46,7 @@ Styled.ContentSectionHeader = styled.header` } `; -Styled.Header = styled.div` +const $Header = styled.div` ${layoutMixins.column} flex: 1; diff --git a/src/components/CopyButton.stories.tsx b/src/components/CopyButton.stories.tsx index f3d09344e..0ef633a8c 100644 --- a/src/components/CopyButton.stories.tsx +++ b/src/components/CopyButton.stories.tsx @@ -16,13 +16,13 @@ CopyButtonStory.args = { CopyButtonStory.argTypes = { buttonType: { - options: ["text", "icon", "default"], + options: ['text', 'icon', 'default'], control: { type: 'select' }, - defaultValue: "default", + defaultValue: 'default', }, children: { options: ['some text to copy'], control: { type: 'select' }, defaultValue: undefined, - } + }, }; diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index f5babf19b..8f76ec7c2 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -1,10 +1,12 @@ import { useState } from 'react'; -import styled, { css, type AnyStyledComponent } from 'styled-components'; + +import styled, { css } from 'styled-components'; import { ButtonAction } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { Button, ButtonProps } from './Button'; @@ -36,15 +38,15 @@ export const CopyButton = ({ }; return buttonType === 'text' ? ( - + <$InlineRow onClick={onCopy} copied={copied}> {children} - - + <$Icon copied={copied} iconName={copied ? IconName.Check : IconName.Copy} /> + ) : buttonType === 'icon' ? ( - ); }; - -const Styled: Record = {}; - -Styled.InlineRow = styled.div<{ copied: boolean }>` +const $InlineRow = styled.div<{ copied: boolean }>` ${layoutMixins.inlineRow} cursor: pointer; @@ -83,7 +82,7 @@ Styled.InlineRow = styled.div<{ copied: boolean }>` `} `; -Styled.Icon = styled(Icon)<{ copied: boolean }>` +const $Icon = styled(Icon)<{ copied: boolean }>` ${({ copied }) => copied && css` @@ -91,7 +90,7 @@ Styled.Icon = styled(Icon)<{ copied: boolean }>` `} `; -Styled.IconButton = styled(IconButton)<{ copied: boolean }>` +const $IconButton = styled(IconButton)<{ copied: boolean }>` ${({ copied }) => copied && css` diff --git a/src/components/Details.stories.tsx b/src/components/Details.stories.tsx index ed086fb98..45a326921 100644 --- a/src/components/Details.stories.tsx +++ b/src/components/Details.stories.tsx @@ -1,17 +1,17 @@ import type { Story } from '@ladle/react'; +import styled from 'styled-components'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { Details } from '@/components/Details'; import { StoryWrapper } from '.ladle/components'; -import styled, { type AnyStyledComponent } from 'styled-components'; -import { layoutMixins } from '@/styles/layoutMixins'; - export const DetailsStory: Story[0]> = (args) => ( - + <$Resizable>
    - + ); @@ -50,10 +50,7 @@ DetailsStory.argTypes = { defaultValue: 'column', }, }; - -const Styled: Record = {}; - -Styled.Resizable = styled.section` +const $Resizable = styled.section` ${layoutMixins.container} resize: horizontal; overflow: hidden; diff --git a/src/components/Details.tsx b/src/components/Details.tsx index 3a5729394..ddc3e2b03 100644 --- a/src/components/Details.tsx +++ b/src/components/Details.tsx @@ -1,27 +1,25 @@ import { Fragment } from 'react'; -import styled, { - type AnyStyledComponent, - css, - type FlattenInterpolation, - ThemeProps, -} from 'styled-components'; +import styled, { ThemeProps, css, type FlattenInterpolation } from 'styled-components'; + +import { Nullable } from '@/constants/abacus'; + +import { LoadingContext } from '@/contexts/LoadingContext'; import { layoutMixins } from '@/styles/layoutMixins'; import { WithSeparators } from '@/components/Separator'; import { WithTooltip } from '@/components/WithTooltip'; -import { LoadingContext } from '@/contexts/LoadingContext'; - export type DetailsItem = { key: string; tooltip?: string; tooltipParams?: Record; label: string | JSX.Element; - value?: string | JSX.Element | undefined; + value?: Nullable | JSX.Element | undefined; subitems?: DetailsItem[]; withTooltipIcon?: boolean; + allowUserSelection?: boolean; }; const DETAIL_LAYOUTS = { @@ -63,8 +61,9 @@ const DetailItem = ({ justifyItems, layout = 'column', withOverflow, + allowUserSelection, }: DetailsItem & StyleProps) => ( - + <$Item justifyItems={justifyItems} layout={layout} withOverflow={withOverflow}>
    -
    {value ?? ''}
    -
    + <$DetailsItemValue allowUserSelection={allowUserSelection}>{value ?? ''} + ); export const Details = ({ @@ -90,34 +89,46 @@ export const Details = ({ withSeparators = false, }: ElementProps & StyleProps) => ( - + <$Details layout={layout} withSeparators={withSeparators} className={className}> - {items.map(({ key, tooltip, tooltipParams, label, subitems, value, withTooltipIcon }) => ( - - - {subitems && showSubitems && layout === 'column' && ( - ( + + - )} - - ))} + {subitems && showSubitems && layout === 'column' && ( + <$SubDetails + items={subitems} + layout={DETAIL_LAYOUTS[layout]} + withSeparators={withSeparators} + /> + )} + + ) + )} - + ); @@ -209,10 +220,7 @@ const itemLayoutVariants: Record>> gap: 0.375rem; `, }; - -const Styled: Record = {}; - -Styled.Details = styled.dl<{ +const $Details = styled.dl<{ layout: 'column' | 'row' | 'rowColumns' | 'grid' | 'stackColumn'; withSeparators: boolean; }>` @@ -224,10 +232,10 @@ Styled.Details = styled.dl<{ ${({ layout }) => layout && detailsLayoutVariants[layout]} `; -Styled.Item = styled.div<{ +const $Item = styled.div<{ layout: 'column' | 'row' | 'rowColumns' | 'grid' | 'stackColumn'; justifyItems?: 'start' | 'end'; - withOverflow: boolean; + withOverflow?: boolean; }>` ${({ layout }) => layout && itemLayoutVariants[layout]} @@ -290,7 +298,7 @@ Styled.Item = styled.div<{ } `; -Styled.SubDetails = styled(Details)` +const $SubDetails = styled(Details)` padding-left: 1rem; position: relative; @@ -305,3 +313,13 @@ Styled.SubDetails = styled(Details)` border-radius: 0.25rem; } `; + +const $DetailsItemValue = styled.dd<{ + allowUserSelection?: boolean; +}>` + ${({ allowUserSelection }) => + allowUserSelection && + css` + user-select: all; + `} +`; diff --git a/src/components/DetailsDialog.tsx b/src/components/DetailsDialog.tsx index 87c186d32..115e0e3e6 100644 --- a/src/components/DetailsDialog.tsx +++ b/src/components/DetailsDialog.tsx @@ -1,8 +1,9 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { layoutMixins } from '@/styles/layoutMixins'; -import { useBreakpoints } from '@/hooks'; import { Details, type DetailsItem } from '@/components/Details'; import { Dialog, DialogPlacement } from '@/components/Dialog'; @@ -25,29 +26,26 @@ export const DetailsDialog = ({ slotIcon, title, items, slotFooter, setIsOpen }: title={title} placement={isTablet ? DialogPlacement.Default : DialogPlacement.Sidebar} > - - + <$Content> + <$Details withSeparators justifyItems="end" items={items} /> - {slotFooter} - + <$Footer>{slotFooter} + ); }; - -const Styled: Record = {}; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.expandingColumnWithStickyFooter} --stickyFooterBackdrop-outsetX: var(--dialog-paddingX); --stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom); gap: 1rem; `; -Styled.Details = styled(Details)` +const $Details = styled(Details)` font: var(--font-small-book); `; -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${layoutMixins.gridEqualColumns} gap: 0.66rem; `; diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index aead81003..c2aa756b1 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -1,24 +1,25 @@ import { useRef } from 'react'; -import styled, { type AnyStyledComponent, keyframes, css } from 'styled-components'; import { - Root, - Trigger, - Overlay, + Close, Content, - Title, Description, - Close, + DialogCloseProps, + Overlay, Portal, + Root, + Title, + Trigger, } from '@radix-ui/react-dialog'; +import styled, { css, keyframes } from 'styled-components'; + +import { useDialogArea } from '@/hooks/useDialogArea'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; -import { Icon, IconName } from '@/components/Icon'; import { BackButton } from '@/components/BackButton'; - -import { useDialogArea } from '@/hooks/useDialogArea'; +import { Icon, IconName } from '@/components/Icon'; export enum DialogPlacement { Default = 'Default', @@ -47,6 +48,7 @@ type StyleProps = { hasHeaderBorder?: boolean; children?: React.ReactNode; className?: string; + stacked?: boolean; withAnimation?: boolean; }; @@ -61,10 +63,11 @@ const DialogPortal = ({ container?: HTMLElement; children: React.ReactNode; }) => { - const { dialogArea } = useDialogArea(); - + const { + dialogAreaRef: { current }, + } = useDialogArea() ?? { dialogAreaRef: {} }; return withPortal ? ( - {children} + {children} ) : ( <>{children} ); @@ -81,6 +84,7 @@ export const Dialog = ({ slotTrigger, slotHeaderInner, slotFooter, + stacked, withClose = true, placement = DialogPlacement.Default, portalContainer, @@ -89,7 +93,7 @@ export const Dialog = ({ children, className, }: DialogProps) => { - const closeButtonRef = useRef(); + const closeButtonRef = useRef(null); const showOverlay = ![DialogPlacement.Inline, DialogPlacement.FullScreen].includes(placement); @@ -97,8 +101,8 @@ export const Dialog = ({ {slotTrigger && {slotTrigger}} - {showOverlay && } - } + <$Container placement={placement} className={className} onEscapeKeyDown={() => { @@ -109,40 +113,58 @@ export const Dialog = ({ e.preventDefault(); } }} + $stacked={stacked} $withAnimation={withAnimation} > - - - {onBack && } - - {slotIcon && {slotIcon}} + {stacked ? ( + <$StackedHeaderTopRow $withBorder={hasHeaderBorder}> + {onBack && <$BackButton onClick={onBack} />} - {title && {title}} + {slotIcon} {!preventClose && withClose && ( - + <$Close ref={closeButtonRef} $absolute={stacked}> - + )} - - {description && {description}} + {title && <$Title>{title}} + + {description && <$Description>{description}} + + {slotHeaderInner} + + ) : ( + <$Header $withBorder={hasHeaderBorder}> + <$HeaderTopRow> + {onBack && } + + {slotIcon && <$Icon>{slotIcon}} + + {title && <$Title>{title}} + + {!preventClose && withClose && ( + <$Close ref={closeButtonRef}> + + + )} + + + {description && <$Description>{description}} - {slotHeaderInner} - + {slotHeaderInner} + + )} - {children} + <$Content>{children} - {slotFooter && {slotFooter}} - + {slotFooter && <$Footer>{slotFooter}} + ); }; - -const Styled: Record = {}; - -Styled.Overlay = styled(Overlay)` +const $Overlay = styled(Overlay)` z-index: 1; position: fixed; @@ -173,7 +195,11 @@ Styled.Overlay = styled(Overlay)` } `; -Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?: boolean }>` +const $Container = styled(Content)<{ + placement: DialogPlacement; + $stacked?: boolean; + $withAnimation?: boolean; +}>` /* Params */ --dialog-inset: 1rem; --dialog-width: 30rem; @@ -353,9 +379,16 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation? bottom: 0; `, }[placement])} + + ${({ $stacked }) => + $stacked && + css` + justify-content: center; + text-align: center; + `} `; -Styled.Header = styled.header<{ $withBorder: boolean }>` +const $Header = styled.header<{ $withBorder: boolean }>` ${layoutMixins.stickyHeader} z-index: var(--dialog-header-z); @@ -374,17 +407,29 @@ Styled.Header = styled.header<{ $withBorder: boolean }>` `}; `; -Styled.HeaderTopRow = styled.div` +const $HeaderTopRow = styled.div` ${layoutMixins.row} gap: var(--dialog-title-gap); `; -Styled.HeaderTopRow = styled.div` - ${layoutMixins.row} - gap: var(--dialog-title-gap); +const $StackedHeaderTopRow = styled.div<{ $withBorder: boolean }>` + ${layoutMixins.flexColumn} + align-items: center; + justify-content: center; + padding: var(--dialog-header-paddingTop) var(--dialog-header-paddingLeft) + var(--dialog-header-paddingBottom) var(--dialog-header-paddingRight); + border-top-left-radius: inherit; + border-top-right-radius: inherit; + + ${({ $withBorder }) => + $withBorder && + css` + ${layoutMixins.withOuterBorder}; + background: var(--dialog-backgroundColor); + `}; `; -Styled.Content = styled.div` +const $Content = styled.div` flex: 1; ${layoutMixins.column} @@ -402,7 +447,7 @@ Styled.Content = styled.div` isolation: isolate; `; -Styled.Icon = styled.div` +const $Icon = styled.div` ${layoutMixins.row} width: 1em; @@ -412,7 +457,7 @@ Styled.Icon = styled.div` line-height: 1; `; -Styled.Close = styled(Close)` +const $Close = styled(Close)<{ $absolute?: boolean }>` width: 0.7813rem; height: 0.7813rem; @@ -438,14 +483,30 @@ Styled.Close = styled(Close)` color: var(--color-text-2); } + ${({ $absolute }) => + $absolute && + css` + position: absolute; + right: var(--dialog-header-paddingRight); + top: var(--dialog-header-paddingTop); + `} + @media ${breakpoints.tablet} { width: 1rem; height: 1rem; outline: none; } +` as React.ForwardRefExoticComponent< + { $absolute?: boolean } & DialogCloseProps & React.RefAttributes +>; + +const $BackButton = styled(BackButton)` + position: absolute; + left: var(--dialog-header-paddingLeft); + top: var(--dialog-header-paddingTop); `; -Styled.Title = styled(Title)` +const $Title = styled(Title)` flex: 1; font: var(--font-large-medium); @@ -455,13 +516,13 @@ Styled.Title = styled(Title)` text-overflow: ellipsis; `; -Styled.Description = styled(Description)` +const $Description = styled(Description)` margin-top: 0.5rem; color: var(--color-text-0); font: var(--font-base-book); `; -Styled.Footer = styled.footer` +const $Footer = styled.footer` display: grid; ${layoutMixins.stickyFooter} ${layoutMixins.withStickyFooterBackdrop} diff --git a/src/components/DiffArrow.tsx b/src/components/DiffArrow.tsx index 6007683fb..330ad12f8 100644 --- a/src/components/DiffArrow.tsx +++ b/src/components/DiffArrow.tsx @@ -16,14 +16,11 @@ type StyleProps = { export type DiffArrowProps = ElementProps & StyleProps; export const DiffArrow = ({ className, direction = 'right', sign }: DiffArrowProps) => ( - + <$DiffArrowContainer className={className} direction={direction} sign={sign}> - + ); - -const Styled: Record = {}; - -Styled.DiffArrowContainer = styled.span` +const $DiffArrowContainer = styled.span` --diffArrow-color: inherit; --diffArrow-color-positive: var(--color-positive); --diffArrow-color-negative: var(--color-negative); @@ -33,8 +30,8 @@ Styled.DiffArrowContainer = styled.span` color: var(--diffArrow-color); svg { - width: 0.5em; - height: 0.5em; + width: 0.75em; + height: 0.75em; } ${({ sign }) => diff --git a/src/components/DiffOutput.tsx b/src/components/DiffOutput.tsx index 665aacff0..0e511e934 100644 --- a/src/components/DiffOutput.tsx +++ b/src/components/DiffOutput.tsx @@ -1,9 +1,9 @@ -import styled, { type AnyStyledComponent, css } from 'styled-components'; +import styled, { css } from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; import { DiffArrow, type DiffArrowProps } from '@/components/DiffArrow'; -import { Output, type OutputProps, OutputType } from '@/components/Output'; +import { Output, OutputType, type OutputProps } from '@/components/Output'; import { BigNumberish } from '@/lib/numbers'; @@ -38,7 +38,7 @@ export const DiffOutput = ({ value, newValue, }: DiffOutputProps) => ( - + <$DiffOutput className={className} layout={layout} withDiff={withDiff}> {withDiff && ( - + <$DiffValue hasInvalidNewValue={hasInvalidNewValue}> - + )} - + ); - -const Styled: Record = {}; - -Styled.DiffValue = styled.div<{ hasInvalidNewValue?: boolean }>` +const $DiffValue = styled.div<{ hasInvalidNewValue?: boolean }>` ${layoutMixins.row} gap: 0.25rem; color: var(--color-text-2); @@ -79,7 +76,7 @@ Styled.DiffValue = styled.div<{ hasInvalidNewValue?: boolean }>` `} `; -Styled.DiffOutput = styled.div<{ layout: 'row' | 'column'; withDiff?: boolean }>` +const $DiffOutput = styled.div<{ layout: 'row' | 'column'; withDiff?: boolean }>` --diffOutput-gap: 0.25rem; --diffOutput-value-color: var(--color-text-1); --diffOutput-newValue-color: var(--color-text-2); @@ -94,10 +91,10 @@ Styled.DiffOutput = styled.div<{ layout: 'row' | 'column'; withDiff?: boolean }> ${({ layout }) => ({ - ['row']: ` + row: ` ${layoutMixins.row} `, - ['column']: ` + column: ` ${layoutMixins.column} `, }[layout])} diff --git a/src/components/DropdownHeaderMenu.stories.tsx b/src/components/DropdownHeaderMenu.stories.tsx index 25340103c..cd3664289 100644 --- a/src/components/DropdownHeaderMenu.stories.tsx +++ b/src/components/DropdownHeaderMenu.stories.tsx @@ -1,11 +1,13 @@ import { useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + import type { Story } from '@ladle/react'; +import styled from 'styled-components'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { DropdownHeaderMenu } from '@/components/DropdownHeaderMenu'; import { StoryWrapper } from '.ladle/components'; -import { layoutMixins } from '@/styles/layoutMixins'; export const DropdownHeaderMenuStory: Story> = (args) => { const [view, setView] = useState(); @@ -45,15 +47,12 @@ export const DropdownHeaderMenuStory: Story - + <$Container> {view ?? 'Overview'} - + ); }; - -const Styled: Record = {}; - -Styled.Container = styled.section` +const $Container = styled.section` ${layoutMixins.container} `; diff --git a/src/components/DropdownHeaderMenu.tsx b/src/components/DropdownHeaderMenu.tsx index cbc8b1d73..209639c20 100644 --- a/src/components/DropdownHeaderMenu.tsx +++ b/src/components/DropdownHeaderMenu.tsx @@ -1,15 +1,16 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; -import { Root, Trigger, Content, Portal, Item } from '@radix-ui/react-dropdown-menu'; +import { Content, Item, Portal, Root, Trigger } from '@radix-ui/react-dropdown-menu'; +import styled from 'styled-components'; import { type MenuItem } from '@/constants/menus'; -import { popoverMixins } from '@/styles/popoverMixins'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; + import { breakpoints } from '@/styles'; -import { useBreakpoints } from '@/hooks'; +import { layoutMixins } from '@/styles/layoutMixins'; +import { popoverMixins } from '@/styles/popoverMixins'; -import { IconButton } from '@/components/IconButton'; import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; type ElementProps = { items: MenuItem[]; @@ -33,39 +34,36 @@ export const DropdownHeaderMenu = ({ return ( - + <$Trigger className={className} asChild>
    {children} - + <$DropdownIconButton iconName={IconName.Caret} isToggle />
    -
    + - {items.map(({ value, label, description, onSelect, disabled }) => ( - (onSelect ?? onValueChange)?.(value)} disabled={disabled} > - {label} - {description} - + <$ItemLabel>{label} + <$Description>{description} + ))} - +
    ); }; - -const Styled: Record = {}; - -Styled.Trigger = styled(Trigger)` +const $Trigger = styled(Trigger)` ${popoverMixins.trigger} ${popoverMixins.backdropOverlay} @@ -91,15 +89,15 @@ Styled.Trigger = styled(Trigger)` } `; -Styled.DropdownIconButton = styled(IconButton)` +const $DropdownIconButton = styled(IconButton)` --button-textColor: var(--color-text-2); - ${Styled.Trigger}[data-state='open'] & { + ${$Trigger}[data-state='open'] & { rotate: -0.5turn; } `; -Styled.Content = styled(Content)` +const $Content = styled(Content)` ${layoutMixins.withOuterAndInnerBorders} ${popoverMixins.popover} ${popoverMixins.popoverAnimation} @@ -116,7 +114,7 @@ Styled.Content = styled(Content)` } `; -Styled.Item = styled(Item)` +const $Item = styled(Item)` ${popoverMixins.item} --item-padding: 0.75rem 1rem; @@ -125,12 +123,12 @@ Styled.Item = styled(Item)` gap: 0.5rem; `; -Styled.ItemLabel = styled.span` +const $ItemLabel = styled.span` color: var(--color-text-2); font: var(--font-medium-book); `; -Styled.Description = styled.span` +const $Description = styled.span` color: var(--color-text-0); font: var(--font-small-book); `; diff --git a/src/components/DropdownMenu.tsx b/src/components/DropdownMenu.tsx index 5d19be814..972c7d663 100644 --- a/src/components/DropdownMenu.tsx +++ b/src/components/DropdownMenu.tsx @@ -1,17 +1,27 @@ -import { type Ref, forwardRef } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; -import { Root, Trigger, Content, Portal, Item, Separator } from '@radix-ui/react-dropdown-menu'; +import { Fragment, type Ref } from 'react'; + +import { + Content, + Item, + Portal, + Root, + Separator, + Trigger, + type DropdownMenuProps as RadixDropdownMenuProps, +} from '@radix-ui/react-dropdown-menu'; +import styled from 'styled-components'; import { popoverMixins } from '@/styles/popoverMixins'; import { Icon, IconName } from '@/components/Icon'; -import { Fragment } from 'react'; + +import { forwardRefFn } from '@/lib/genericFunctionalComponentUtils'; export type DropdownMenuItem = { value: T; icon?: React.ReactNode; label: React.ReactNode; - onSelect?: () => void; + onSelect?: (e: Event) => void; separator?: boolean; highlightColor?: 'accent' | 'create' | 'destroy'; }; @@ -29,9 +39,9 @@ type ElementProps = { slotTopContent?: React.ReactNode; }; -type DropdownMenuProps = StyleProps & ElementProps; +export type DropdownMenuProps = StyleProps & ElementProps & RadixDropdownMenuProps; -export const DropdownMenu = forwardRef( +export const DropdownMenu = forwardRefFn( ( { align = 'center', @@ -41,62 +51,62 @@ export const DropdownMenu = forwardRef( slotTopContent, side = 'bottom', sideOffset = 8, + ...rest }: DropdownMenuProps, - ref: Ref + ref: Ref ) => { return ( - - + + <$Trigger ref={ref} className={className}> {children} - - + + - + <$Content className={className} align={align} side={side} sideOffset={sideOffset}> {slotTopContent} {items.map((item: DropdownMenuItem) => ( - {item.icon} {item.label} - - {item.separator && } + + {item.separator && <$Separator />} ))} - + ); } ); - -const Styled: Record = {}; - -Styled.Separator = styled(Separator)` +const $Separator = styled(Separator)` border-bottom: solid var(--border-width) var(--color-border); margin: 0.25rem 1rem; `; -Styled.Item = styled(Item)<{ $highlightColor: 'accent' | 'create' | 'destroy' }>` +const $Item = styled(Item)<{ $highlightColor?: 'accent' | 'create' | 'destroy' }>` ${popoverMixins.item} --item-font-size: var(--dropdownMenu-item-font-size); ${({ $highlightColor }) => - ({ - ['accent']: ` + $highlightColor != null + ? { + accent: ` --item-highlighted-textColor: var(--color-accent); `, - ['create']: ` + create: ` --item-highlighted-textColor: var(--color-green); `, - ['destroy']: ` + destroy: ` --item-highlighted-textColor: var(--color-red); `, - }[$highlightColor])} + }[$highlightColor] + : undefined} justify-content: start; color: var(--color-text-0); @@ -106,23 +116,23 @@ Styled.Item = styled(Item)<{ $highlightColor: 'accent' | 'create' | 'destroy' }> } `; -Styled.Trigger = styled(Trigger)` +const $Trigger = styled(Trigger)` ${popoverMixins.trigger} ${popoverMixins.backdropOverlay} `; -Styled.DropdownIcon = styled.span` +const $DropdownIcon = styled.span` display: inline-flex; font-size: 0.375em; transition: transform 0.3s var(--ease-out-expo); align-items: center; - ${Styled.Trigger}[data-state='open'] & { + ${$Trigger}[data-state='open'] & { transform: scaleY(-1); } `; -Styled.Content = styled(Content)` +const $Content = styled(Content)` --dropdownMenu-item-font-size: inherit; ${popoverMixins.popover} diff --git a/src/components/DropdownSelectMenu.stories.tsx b/src/components/DropdownSelectMenu.stories.tsx index 2262d10d2..b9dba78ec 100644 --- a/src/components/DropdownSelectMenu.stories.tsx +++ b/src/components/DropdownSelectMenu.stories.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; + import type { Story } from '@ladle/react'; import { DropdownSelectMenu } from '@/components/DropdownSelectMenu'; diff --git a/src/components/DropdownSelectMenu.tsx b/src/components/DropdownSelectMenu.tsx index 6fe513ebb..d98813952 100644 --- a/src/components/DropdownSelectMenu.tsx +++ b/src/components/DropdownSelectMenu.tsx @@ -1,29 +1,30 @@ import { cloneElement } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; + import { - Root, - Trigger, Content, ItemIndicator, + Portal, RadioGroup, RadioItem, - Portal, + Root, + Trigger, } from '@radix-ui/react-dropdown-menu'; import { CheckIcon } from '@radix-ui/react-icons'; +import styled from 'styled-components'; import { type MenuItem } from '@/constants/menus'; +import { layoutMixins } from '@/styles/layoutMixins'; +import { popoverMixins } from '@/styles/popoverMixins'; + import { Icon, IconName } from '@/components/Icon'; import { Tag } from '@/components/Tag'; -import { popoverMixins } from '@/styles/popoverMixins'; -import { layoutMixins } from '@/styles/layoutMixins'; - type ElementProps = { disabled?: boolean; items: MenuItem[]; - value: MenuItemValue; - onValueChange: (value: MenuItemValue) => void; + value?: MenuItemValue; + onValueChange?: (value: MenuItemValue) => void; children?: React.ReactNode; slotTrigger?: JSX.Element; }; @@ -45,7 +46,7 @@ export const DropdownSelectMenu = ({ return ( <> {currentItem?.slotBefore} - {currentItem?.label ?? value} + <$ItemLabel>{currentItem?.label ?? value} ); })(), @@ -58,28 +59,30 @@ export const DropdownSelectMenu = ({ const triggerContent = ( <> {children} - + ); return ( - + <$Trigger disabled={disabled} className={className} asChild={!!slotTrigger}> {slotTrigger ? cloneElement(slotTrigger, { children: triggerContent }) : triggerContent} - + - + <$Content align={align} sideOffset={sideOffset} className={className}> onValueChange(value as MenuItemValue)} + onValueChange={ + onValueChange != null ? (value) => onValueChange(value as MenuItemValue) : undefined + } > {items.map(({ value, label, slotBefore, slotAfter, tag, disabled }) => ( - + <$RadioItem key={value} value={value} disabled={disabled}> {slotBefore} - + <$ItemLabel> {label} {tag && ( <> @@ -87,25 +90,22 @@ export const DropdownSelectMenu = ({ {tag} )} - + {slotAfter} - + <$ItemIndicator> - - + + ))} - + ); }; - -const Styled: Record = {}; - -Styled.Trigger = styled(Trigger)` +const $Trigger = styled(Trigger)` ${layoutMixins.row} gap: 1rem; @@ -113,33 +113,36 @@ Styled.Trigger = styled(Trigger)` ${popoverMixins.backdropOverlay} `; -Styled.DropdownIcon = styled.span` +const $DropdownIcon = styled.span` display: inline-flex; transition: transform 0.3s var(--ease-out-expo); font-size: 0.375em; - ${Styled.Trigger}[data-state='open'] & { + ${$Trigger}[data-state='open'] & { transform: scaleY(-1); } `; -Styled.Content = styled(Content)` +const $Content = styled(Content)` + --dropdownSelectMenu-item-font-size: inherit; + ${popoverMixins.popover} ${popoverMixins.popoverAnimation} `; -Styled.RadioItem = styled(RadioItem)` +const $RadioItem = styled(RadioItem)` ${popoverMixins.item} + --item-font-size: var(--dropdownSelectMenu-item-font-size); `; -Styled.ItemLabel = styled.span` +const $ItemLabel = styled.span` flex: 1; ${layoutMixins.inlineRow} `; -Styled.ItemIndicator = styled(ItemIndicator)` +const $ItemIndicator = styled(ItemIndicator)` margin-left: auto; display: inline-flex; diff --git a/src/components/FormInput.stories.tsx b/src/components/FormInput.stories.tsx index 0f7baf796..4d2ea2b7d 100644 --- a/src/components/FormInput.stories.tsx +++ b/src/components/FormInput.stories.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import type { Story } from '@ladle/react'; import { AlertType } from '@/constants/alerts'; @@ -10,24 +9,18 @@ import { InputType } from '@/components/Input'; import { StoryWrapper } from '.ladle/components'; export const FormInputWithValidationStory: Story = (args) => { - const [value, setValue] = useState(''); - return ( - ) => setValue(e.target.value)} - value={value} - /> + ); }; FormInputWithValidationStory.args = { decimals: 2, - max: '', - min: '', + max: 100, placeholder: '', + label: 'label', validationConfig: { attached: false, type: AlertType.Error, @@ -43,25 +36,19 @@ FormInputWithValidationStory.argTypes = { }, }; -export const FormInputStoryWithSlotOuterRight: Story = (args) => { - const [value, setValue] = useState(''); +export const FormInputStoryWithSlotRight: Story = (args) => { return ( - ) => setValue(e.target.value)} - slotOuterRight={} - value={value} - /> + Submit} {...args} /> ); }; -FormInputStoryWithSlotOuterRight.args = { +FormInputStoryWithSlotRight.args = { decimals: 2, - max: '', - min: '', + max: 100, placeholder: '', + label: 'label', validationConfig: { attached: false, type: AlertType.Error, @@ -69,7 +56,7 @@ FormInputStoryWithSlotOuterRight.args = { }, }; -FormInputStoryWithSlotOuterRight.argTypes = { +FormInputStoryWithSlotRight.argTypes = { type: { options: Object.values(InputType), control: { type: 'select' }, diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx index 588c5df80..b7671ddf1 100644 --- a/src/components/FormInput.tsx +++ b/src/components/FormInput.tsx @@ -1,9 +1,11 @@ -import { forwardRef } from 'react'; +import React, { forwardRef } from 'react'; + import styled, { AnyStyledComponent, css } from 'styled-components'; import { AlertType } from '@/constants/alerts'; -import { layoutMixins } from '@/styles/layoutMixins'; + import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; import { Input, InputProps } from '@/components/Input'; @@ -14,7 +16,7 @@ type StyleProps = { }; type ElementProps = { - label: React.ReactNode; + label?: React.ReactNode; slotRight?: React.ReactNode; validationConfig?: { attached?: boolean; @@ -27,30 +29,26 @@ export type FormInputProps = ElementProps & StyleProps & InputProps; export const FormInput = forwardRef( ({ id, label, slotRight, className, validationConfig, ...otherProps }, ref) => ( - - - + <$FormInputContainer className={className} isValidationAttached={validationConfig?.attached}> + <$InputContainer hasLabel={!!label} hasSlotRight={!!slotRight}> + {label ? ( + <$WithLabel label={label} inputID={id} disabled={otherProps?.disabled}> + + + ) : ( - + )} {slotRight} - + {validationConfig && ( - - {validationConfig.message} - + <$AlertMessage type={validationConfig.type}>{validationConfig.message} )} - + ) ); +const $AlertMessage = styled(AlertMessage)``; -const Styled: Record = {}; - -Styled.AlertMessage = styled(AlertMessage)``; - -Styled.FormInputContainer = styled.div<{ isValidationAttached?: boolean }>` +const $FormInputContainer = styled.div<{ isValidationAttached?: boolean }>` ${layoutMixins.flexColumn} gap: 0.5rem; @@ -59,7 +57,7 @@ Styled.FormInputContainer = styled.div<{ isValidationAttached?: boolean }>` css` --input-radius: 0.5em 0.5em 0 0; - ${Styled.AlertMessage} { + ${$AlertMessage} { border-left: none; margin: 0; border-radius: 0 0 0.5em 0.5em; @@ -67,10 +65,16 @@ Styled.FormInputContainer = styled.div<{ isValidationAttached?: boolean }>` `} `; -Styled.InputContainer = styled.div<{ hasSlotRight?: boolean }>` +const $InputContainer = styled.div<{ hasLabel?: boolean; hasSlotRight?: boolean }>` ${formMixins.inputContainer} input { + ${({ hasLabel }) => + !hasLabel && + css` + --form-input-paddingY: 0; + `} + padding: var(--form-input-paddingY) var(--form-input-paddingX); padding-top: 0; } @@ -85,7 +89,7 @@ Styled.InputContainer = styled.div<{ hasSlotRight?: boolean }>` `} `; -Styled.WithLabel = styled(WithLabel)<{ disabled?: boolean }>` +const $WithLabel = styled(WithLabel)<{ disabled?: boolean }>` ${formMixins.inputLabel} label { diff --git a/src/components/GradientCard.tsx b/src/components/GradientCard.tsx new file mode 100644 index 000000000..e00204c6f --- /dev/null +++ b/src/components/GradientCard.tsx @@ -0,0 +1,49 @@ +import styled, { css } from 'styled-components'; + +type GradientCardProps = React.PropsWithChildren<{ + className?: string; + fromColor?: 'positive' | 'negative' | 'neutral'; + toColor?: 'positive' | 'negative' | 'neutral'; +}>; + +export const GradientCard = ({ children, className, fromColor, toColor }: GradientCardProps) => { + return ( + <$GradientCard className={className} fromColor={fromColor} toColor={toColor}> + {children} + + ); +}; +const $GradientCard = styled.div<{ + fromColor?: GradientCardProps['fromColor']; + toColor?: GradientCardProps['toColor']; +}>` + // Props/defaults + --from-color: transparent; + --to-color: transparent; + + // Constants + --default-gradient: linear-gradient( + 342.62deg, + var(--color-gradient-base-0) -9.23%, + var(--color-gradient-base-1) 110.36% + ); + + ${({ fromColor, toColor }) => + css` + --from-color: ${{ + positive: css`var(--color-gradient-positive)`, + neutral: css`transparent`, + negative: css`var(--color-gradient-negative)`, + }[fromColor ?? 'neutral']}; + + --to-color: ${{ + positive: css`var(--color-gradient-positive)`, + neutral: css`transparent`, + negative: css`var(--color-gradient-negative)`, + }[toColor ?? 'neutral']}; + `} + + background: linear-gradient(130.25deg, var(--from-color) 0.9%, transparent 64.47%), + linear-gradient(227.14deg, var(--to-color) 1.6%, transparent 63.87%), + var(--default-gradient); +`; diff --git a/src/components/GreenCheckCircle.tsx b/src/components/GreenCheckCircle.tsx index f5b451f54..9830ff226 100644 --- a/src/components/GreenCheckCircle.tsx +++ b/src/components/GreenCheckCircle.tsx @@ -1,14 +1,11 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { Icon, IconName } from '@/components/Icon'; export const GreenCheckCircle = ({ className }: { className?: string }) => ( - + <$Icon className={className} iconName={IconName.CheckCircle} /> ); - -const Styled: Record = {}; - -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` --icon-size: 1.25rem; width: var(--icon-size); diff --git a/src/components/GuardedMobileRoute.tsx b/src/components/GuardedMobileRoute.tsx index dec5d758c..cd4c83eac 100644 --- a/src/components/GuardedMobileRoute.tsx +++ b/src/components/GuardedMobileRoute.tsx @@ -1,17 +1,16 @@ import { useEffect, useRef } from 'react'; -import { Outlet, useNavigate } from 'react-router-dom'; + import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { Outlet, useNavigate } from 'react-router-dom'; import { DialogTypes } from '@/constants/dialogs'; -import { useBreakpoints } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { calculateCanAccountTrade } from '@/state/accountCalculators'; import { openDialog } from '@/state/dialogs'; - import { getActiveDialog } from '@/state/dialogsSelectors'; -import { calculateCanAccountTrade } from '@/state/accountCalculators'; - export const GuardedMobileRoute = () => { const { isTablet } = useBreakpoints(); const navigate = useNavigate(); diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 81427adde..6588eb2be 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -11,12 +11,12 @@ import { BoxCloseIcon, CalculatorIcon, CaretIcon, - CautionCircleStrokeIcon, CautionCircleIcon, + CautionCircleStrokeIcon, ChaosLabsIcon, ChatIcon, - CheckIcon, CheckCircleIcon, + CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, @@ -48,17 +48,18 @@ import { MigrateIcon, MintscanIcon, MoonIcon, - OrderbookIcon, OrderCanceledIcon, OrderFilledIcon, OrderOpenIcon, OrderPartiallyFilledIcon, OrderPendingIcon, OrderUntriggeredIcon, + OrderbookIcon, OverviewIcon, PencilIcon, PlayIcon, PlusIcon, + PositionPartialIcon, PositionsIcon, PriceChartIcon, PrivacyIcon, @@ -68,10 +69,10 @@ import { SendIcon, ShareIcon, ShowIcon, - TogglesMenuIcon, StarIcon, SunIcon, TerminalIcon, + TogglesMenuIcon, TokenIcon, TradeIcon, TransferIcon, @@ -81,6 +82,7 @@ import { WebsiteIcon, WhitepaperIcon, WithdrawIcon, + DownloadIcon, } from '@/icons'; export enum IconName { @@ -141,6 +143,7 @@ export enum IconName { Pencil = 'Pencil', Play = 'Play', Plus = 'Plus', + PositionPartial = 'PositionPartial', Positions = 'Positions', PriceChart = 'PriceChart', Privacy = 'Privacy', @@ -163,6 +166,7 @@ export enum IconName { Website = 'Website', Whitepaper = 'Whitepaper', Withdraw = 'Withdraw', + Download = 'Download', } const icons = { @@ -222,6 +226,7 @@ const icons = { [IconName.Pencil]: PencilIcon, [IconName.Play]: PlayIcon, [IconName.Plus]: PlusIcon, + [IconName.PositionPartial]: PositionPartialIcon, [IconName.Positions]: PositionsIcon, [IconName.PriceChart]: PriceChartIcon, [IconName.Privacy]: PrivacyIcon, @@ -244,6 +249,7 @@ const icons = { [IconName.Website]: WebsiteIcon, [IconName.Whitepaper]: WhitepaperIcon, [IconName.Withdraw]: WithdrawIcon, + [IconName.Download]: DownloadIcon, } as Record; type ElementProps = { diff --git a/src/components/IconButton.stories.tsx b/src/components/IconButton.stories.tsx index 9f2280799..dcc68f979 100644 --- a/src/components/IconButton.stories.tsx +++ b/src/components/IconButton.stories.tsx @@ -1,6 +1,12 @@ import type { Story } from '@ladle/react'; -import { ButtonAction, ButtonShape, ButtonSize, ButtonState, ButtonType } from '@/constants/buttons'; +import { + ButtonAction, + ButtonShape, + ButtonSize, + ButtonState, + ButtonType, +} from '@/constants/buttons'; import { IconName } from '@/components/Icon'; import { IconButton, type IconButtonProps } from '@/components/IconButton'; @@ -54,5 +60,5 @@ IconButtonStory.argTypes = { options: [true, false], control: { type: 'select' }, defaultValue: false, - } + }, }; diff --git a/src/components/IconButton.tsx b/src/components/IconButton.tsx index 9802ee645..95b5e6f1b 100644 --- a/src/components/IconButton.tsx +++ b/src/components/IconButton.tsx @@ -1,9 +1,10 @@ import { forwardRef, type ElementType } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { ButtonShape, ButtonSize } from '@/constants/buttons'; +import styled, { css } from 'styled-components'; -import { Button, type ButtonProps } from '@/components/Button'; +import { ButtonAction, ButtonShape, ButtonSize, ButtonState } from '@/constants/buttons'; + +import { Button, ButtonStateConfig } from '@/components/Button'; import { Icon, IconName } from '@/components/Icon'; import { ToggleButton, type ToggleButtonProps } from '@/components/ToggleButton'; @@ -11,9 +12,11 @@ type ElementProps = { isToggle?: boolean; iconName?: IconName; iconComponent?: ElementType; + action?: ButtonAction; + state?: ButtonState | ButtonStateConfig; }; -export type IconButtonProps = ElementProps & ButtonProps & ToggleButtonProps; +export type IconButtonProps = ElementProps & ToggleButtonProps; export const IconButton = forwardRef( ( @@ -35,19 +38,19 @@ export const IconButton = forwardRef { return isToggle ? ( - - + ) : ( - - + ); } ); - -const Styled: Record = {}; - const buttonMixin = css` // Params --button-icon-size: 1.125em; @@ -75,10 +75,10 @@ const buttonMixin = css` } `; -Styled.IconButton = styled(Button)` +const $IconButton = styled(Button)` ${buttonMixin} `; -Styled.IconToggleButton = styled(ToggleButton)` +const $IconToggleButton = styled(ToggleButton)` ${buttonMixin} `; diff --git a/src/components/Input.stories.tsx b/src/components/Input.stories.tsx index f6c61f635..6794d9b8c 100644 --- a/src/components/Input.stories.tsx +++ b/src/components/Input.stories.tsx @@ -1,27 +1,20 @@ -import { useState } from 'react'; import type { Story } from '@ladle/react'; -import { Input, InputType, InputProps } from '@/components/Input'; +import { Input, InputProps, InputType } from '@/components/Input'; import { StoryWrapper } from '.ladle/components'; export const InputStory: Story = (args) => { - const [value, setValue] = useState(''); return ( - ) => setValue(e.target.value)} - value={value} - /> + ); }; InputStory.args = { decimals: 2, - max: '', - min: '', + max: 100, placeholder: '', }; diff --git a/src/components/Input.tsx b/src/components/Input.tsx index e8d3d7578..0be56bddc 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,7 +1,8 @@ import { Dispatch, forwardRef, SetStateAction } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; + import { NumericFormat, type NumberFormatValues, type SourceInfo } from 'react-number-format'; import type { SyntheticInputEvent } from 'react-number-format/types/types'; +import styled, { css } from 'styled-components'; import { LEVERAGE_DECIMALS, @@ -10,8 +11,9 @@ import { USD_DECIMALS, } from '@/constants/numbers'; +import { useLocaleSeparators } from '@/hooks/useLocaleSeparators'; + import { BIG_NUMBERS } from '@/lib/numbers'; -import { useLocaleSeparators } from '@/hooks'; export enum InputType { Currency = 'Currency', @@ -30,6 +32,7 @@ type ElementProps = { type?: InputType; value?: string | number | null; disabled?: boolean; + autoFocus?: boolean; id?: string; onBlur?: () => void; onFocus?: () => void; @@ -120,29 +123,29 @@ export const Input = forwardRef( : ''; return ( - + <$InputContainer className={className}> {type === InputType.Text || type === InputType.Search ? ( - } + ref={ref} id={id} // Events onBlur={onBlur} - onChange={onChange} + onChange={onChange as any} // TODO fix types onFocus={onFocus} - onInput={onInput} + onInput={onInput as any} // TODO fix type // Native disabled={disabled} placeholder={placeholder} - value={value} + value={value ?? undefined} // Other data-1p-ignore // prevent 1Password fill {...otherProps} /> ) : ( - >} + getInputRef={ref} id={id} // NumericFormat valueIsNumericString @@ -156,7 +159,7 @@ export const Input = forwardRef( suffix={numberFormatConfig?.suffix} // Events onBlur={onBlur} - onValueChange={onChange} + onValueChange={onChange as any} // TODO fix types onFocus={onFocus} onInput={(e: SyntheticInputEvent) => { if (!onInput) return; @@ -181,14 +184,11 @@ export const Input = forwardRef( {...otherProps} /> )} - + ); } ); - -const Styled: Record = {}; - -Styled.InputContainer = styled.div` +const $InputContainer = styled.div` width: 100%; min-height: 100%; height: 100%; @@ -228,11 +228,11 @@ const InputStyle = css` } `; -Styled.NumericFormat = styled(NumericFormat)` +const $NumericFormat = styled(NumericFormat)` ${InputStyle} font-feature-settings: var(--fontFeature-monoNumbers); `; -Styled.Input = styled.input` +const $Input = styled.input` ${InputStyle} `; diff --git a/src/components/Link.stories.tsx b/src/components/Link.stories.tsx index 2042b515a..9a0d06ac1 100644 --- a/src/components/Link.stories.tsx +++ b/src/components/Link.stories.tsx @@ -1,17 +1,18 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import type { Story } from '@ladle/react'; +import styled from 'styled-components'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { Link } from '@/components/Link'; import { StoryWrapper } from '.ladle/components'; -import { layoutMixins } from '@/styles/layoutMixins'; export const LinkStory: Story[0]> = (args) => { return ( - + <$Container> Trade Now - + ); }; @@ -19,10 +20,7 @@ export const LinkStory: Story[0]> = (args) => { LinkStory.args = { href: 'https://trade.dydx.exchange', }; - -const Styled: Record = {}; - -Styled.Container = styled.section` +const $Container = styled.section` background: var(--color-layer-3); ${layoutMixins.container} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index bbf89a0a8..e66253039 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,10 +1,11 @@ import { forwardRef } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; -import { Icon, IconName } from '@/components/Icon'; +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; +import { Icon, IconName } from '@/components/Icon'; + type ElementProps = { analyticsConfig?: { event: string; @@ -12,7 +13,7 @@ type ElementProps = { }; children: React.ReactNode; href?: string; - onClick?: (e: MouseEvent) => void; + onClick?: (e: React.MouseEvent) => void; withIcon?: boolean; }; @@ -33,11 +34,11 @@ export const Link = forwardRef( }: ElementProps & StyleProps, ref ) => ( - { + onClick={(e: React.MouseEvent) => { if (analyticsConfig) { console.log(analyticsConfig); } @@ -50,13 +51,10 @@ export const Link = forwardRef( > {children} {withIcon && } - + ) ); - -const Styled: Record = {}; - -Styled.A = styled.a` +const $A = styled.a` --link-color: inherit; color: var(--link-color); diff --git a/src/components/Loading/Loading.stories.tsx b/src/components/Loading/Loading.stories.tsx index c0e5942af..6d4f01b5b 100644 --- a/src/components/Loading/Loading.stories.tsx +++ b/src/components/Loading/Loading.stories.tsx @@ -1,8 +1,9 @@ import type { Story } from '@ladle/react'; import { LoadingDots, LoadingDotsProps } from '@/components/Loading/LoadingDots'; -import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; import { LoadingOutput } from '@/components/Loading/LoadingOutput'; +import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; + import { StoryWrapper } from '.ladle/components'; export const Dots: Story = (args) => { diff --git a/src/components/Loading/LoadingSpinner.tsx b/src/components/Loading/LoadingSpinner.tsx index 400438e7e..d462ad3ed 100644 --- a/src/components/Loading/LoadingSpinner.tsx +++ b/src/components/Loading/LoadingSpinner.tsx @@ -1,5 +1,6 @@ +import styled, { keyframes } from 'styled-components'; + import { layoutMixins } from '@/styles/layoutMixins'; -import styled, { type AnyStyledComponent, keyframes } from 'styled-components'; // In some strange cases, hiding a spinner on one part of the page causes the linearGradient to // be hidden on all other instances of the page. An id can be passed in to prevent this. @@ -9,8 +10,8 @@ export const LoadingSpinner: React.FC<{ disabled?: boolean; }> = ({ id, className, disabled = false }) => { return ( - - + <$LoadingSpinnerSvg id={id} width="38" height="38" @@ -34,32 +35,30 @@ export const LoadingSpinner: React.FC<{ strokeLinecap="round" /> )} - - + + ); }; export const LoadingSpace: React.FC<{ className?: string; id: string }> = ({ className, id }) => ( - + <$LoadingSpaceContainer className={className}> - + ); - -const Styled: Record = {}; - -Styled.LoadingSpaceContainer = styled.div` +const $LoadingSpaceContainer = styled.div` ${layoutMixins.centered} `; -Styled.Spinner = styled.div` +const $Spinner = styled.div` --spinner-width: auto; line-height: 0; color: var(--color-text-0); `; -Styled.LoadingSpinnerSvg = styled.svg` +const $LoadingSpinnerSvg = styled.svg` width: var(--spinner-width); + height: auto; animation: ${keyframes` to { diff --git a/src/components/MarginUsageRing.stories.tsx b/src/components/MarginUsageRing.stories.tsx index ae13f99d5..3b2aaa94d 100644 --- a/src/components/MarginUsageRing.stories.tsx +++ b/src/components/MarginUsageRing.stories.tsx @@ -1,5 +1,5 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import type { Story } from '@ladle/react'; +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -10,9 +10,9 @@ import { StoryWrapper } from '.ladle/components'; export const MarginUsageRingStory: Story<{ value: number }> = (args) => { return ( - + <$Container> - + ); }; @@ -24,9 +24,9 @@ MarginUsageRingStory.args = { export const MarginUsageRingStyled: Story<{ value: number }> = (args) => { return ( - - - + <$Container> + <$MarginUsageRing value={args.value / 100} /> + ); }; @@ -34,14 +34,11 @@ export const MarginUsageRingStyled: Story<{ value: number }> = (args) => { MarginUsageRingStyled.args = { value: 0, }; - -const Styled: Record = {}; - -Styled.MarginUsageRing = styled(MarginUsageRing)` +const $MarginUsageRing = styled(MarginUsageRing)` color: var(--color-accent); `; -Styled.Container = styled.section` +const $Container = styled.section` background: var(--color-layer-3); ${layoutMixins.container} diff --git a/src/components/MarginUsageRing.tsx b/src/components/MarginUsageRing.tsx index 60c1e6b12..419172be3 100644 --- a/src/components/MarginUsageRing.tsx +++ b/src/components/MarginUsageRing.tsx @@ -1,4 +1,4 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { RiskLevels } from '@/constants/abacus'; @@ -16,16 +16,13 @@ type StyleProps = { }; export const MarginUsageRing = ({ className, value }: ElementProps & StyleProps) => ( - ); - -const Styled: Record = {}; - -Styled.MarginUsageRing = styled(Ring)<{ riskLevel: RiskLevels }>` +const $MarginUsageRing = styled(Ring)<{ riskLevel: RiskLevels }>` ${({ riskLevel }) => UsageColorFromRiskLevel(riskLevel)} width: 1rem; height: 1rem; diff --git a/src/components/NavigationMenu.stories.tsx b/src/components/NavigationMenu.stories.tsx index 10929f85d..b3738b0d4 100644 --- a/src/components/NavigationMenu.stories.tsx +++ b/src/components/NavigationMenu.stories.tsx @@ -1,7 +1,7 @@ import type { Story } from '@ladle/react'; +import { HashRouter } from 'react-router-dom'; import { NavigationMenu } from '@/components/NavigationMenu'; -import { HashRouter } from 'react-router-dom'; import { StoryWrapper } from '.ladle/components'; diff --git a/src/components/NavigationMenu.tsx b/src/components/NavigationMenu.tsx index df278e31c..753d4c0ea 100644 --- a/src/components/NavigationMenu.tsx +++ b/src/components/NavigationMenu.tsx @@ -1,33 +1,41 @@ -import { forwardRef, Ref } from 'react'; -import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; -import { NavLink, matchPath, useLocation } from 'react-router-dom'; - -import { MenuConfig, MenuItem } from '@/constants/menus'; - -import { isExternalLink } from '@/lib/isExternalLink'; - -import { popoverMixins } from '@/styles/popoverMixins'; +import { Ref } from 'react'; import { - Root, - List, - Trigger, Content, Item, Link, + List, + Root, Sub, + Trigger, Viewport, } from '@radix-ui/react-navigation-menu'; +import { NavLink, matchPath, useLocation } from 'react-router-dom'; +import styled, { css, keyframes } from 'styled-components'; + +import { MenuConfig, MenuItem } from '@/constants/menus'; import { layoutMixins } from '@/styles/layoutMixins'; +import { popoverMixins } from '@/styles/popoverMixins'; + +import { forwardRefFn, getSimpleStyledOutputType } from '@/lib/genericFunctionalComponentUtils'; +import { isExternalLink } from '@/lib/isExternalLink'; -import { Tag } from './Tag'; import { Icon, IconName } from './Icon'; +import { Tag } from './Tag'; type ElementProps = { items: MenuConfig; onSelectItem?: (value: MenuItemValue) => void; onSelectGroup?: (value: MenuGroupValue) => void; + /** + * Optional slot to add content before the menu item rendering area + */ + slotBefore?: React.ReactNode; + /** + * Optional slot to add content after the menu item rendering area + */ + slotAfter?: React.ReactNode; }; type StyleProps = { @@ -38,73 +46,75 @@ type StyleProps = { className?: string; }; -const NavItem = forwardRef( - ( - { - value, - slotBefore, - label, - tag, - href, - slotAfter = isExternalLink(href) ? : undefined, - onSelect, - subitems, - ...props - }: MenuItem, - ref: Ref | Ref | Ref - ) => { - const location = useLocation(); - - const children = ( - <> - {slotBefore} - - {label} - {tag && ( - <> - {' '} - {tag} - - )} - - {slotAfter} - {subitems?.length && } - - ); - - return href ? ( - onSelect?.(value)} - asChild - target={isExternalLink(href) ? '_blank' : undefined} - > - } {...props}> - {children} - - - ) : props.onClick ? ( - onSelect?.(value)}> - - - ) : ( -
    } {...props}> +const NavItemWithRef = ( + { + value, + slotBefore, + label, + tag, + href, + slotAfter = isExternalLink(href) ? : undefined, + onSelect, + subitems, + type, + ...props + }: MenuItem, + ref: Ref +) => { + const location = useLocation(); + + const children = ( + <> + {slotBefore} + + {label} + {tag && ( + <> + {' '} + {tag} + + )} + + {slotAfter} + {subitems?.length && <$Icon iconName={IconName.Triangle} />} + + ); + + return href ? ( + onSelect?.(value)} + asChild + target={isExternalLink(href) ? '_blank' : undefined} + > + } type={`${type}`} {...props}> {children} -
    - ); - } -); + + + ) : props.onClick ? ( + onSelect?.(value)}> + + + ) : ( +
    } {...props}> + {children} +
    + ); +}; + +const NavItem = forwardRefFn(NavItemWithRef); export const NavigationMenu = ({ onSelectItem, - onSelectGroup, items, orientation = 'vertical', itemOrientation = 'horizontal', submenuPlacement = 'inline', // orientation === 'horizontal' ? 'viewport' : 'inline', dir = 'ltr', + slotAfter, + slotBefore, className, }: ElementProps & StyleProps) => { const renderSubitems = ({ @@ -115,74 +125,71 @@ export const NavigationMenu = ( <> - e.preventDefault()} - onPointerLeave={(e: MouseEvent) => e.preventDefault()} + onPointerMove={(e: React.MouseEvent) => e.preventDefault()} + onPointerLeave={(e: React.MouseEvent) => e.preventDefault()} > - - + <$NavItem onSelect={onSelectItem} orientation={itemOrientation} {...item} /> + - e.preventDefault()} - onPointerLeave={(e: MouseEvent) => e.preventDefault()} + <$Content + onPointerEnter={(e: React.MouseEvent) => e.preventDefault()} + onPointerLeave={(e: React.MouseEvent) => e.preventDefault()} data-placement={submenuPlacement} > - - + <$List data-orientation={depth > 0 ? 'menu' : orientation === 'vertical' ? 'vertical' : 'menu'} > {item?.subitems?.map((subitem) => ( - + <$ListItem key={subitem.value} value={subitem.value} data-item={subitem.value}> {subitem?.subitems ? ( renderSubitems({ item: subitem, depth: depth + 1 }) ) : ( - + <$NavItem onSelect={onSelectItem} orientation={itemOrientation} {...subitem} /> )} - + ))} - - - + + + ); return ( - + <$Root orientation={orientation} dir={dir} className={className}> + {slotBefore} + {items.map((group) => ( - + <$Group key={group.group}> {group.groupLabel && ( - + <$GroupHeader>

    {group.groupLabel}

    -
    + )} - + <$List data-orientation={orientation}> {group.items.map((item) => ( - + <$ListItem key={item.value} value={item.value} data-item={item.value}> {item.subitems ? ( renderSubitems({ item, depth: 0 }) ) : ( - + <$NavItem onSelect={onSelectItem} orientation={itemOrientation} {...item} /> )} - + ))} - -
    + + ))} - {submenuPlacement === 'viewport' && } -
    + {submenuPlacement === 'viewport' && <$Viewport data-orientation={orientation} />} + + {slotAfter} + ); }; - -const Styled: Record = {}; - -Styled.Root = styled(Root)` +const $Root = styled(Root)` /* Params */ --navigationMenu-height: auto; @@ -219,7 +226,7 @@ Styled.Root = styled(Root)` } `; -Styled.Viewport = styled(Viewport)` +const $Viewport = styled(Viewport)` ${popoverMixins.popover} ${popoverMixins.popoverAnimation} --popover-origin: center top; @@ -253,14 +260,29 @@ Styled.Viewport = styled(Viewport)` } `; -Styled.Content = styled(Content)` +const $List = styled(List)` + align-self: center; + + &[data-orientation='horizontal'] { + ${layoutMixins.row} + gap: 0.5rem; + align-items: start; + } + + &[data-orientation='vertical'] { + ${layoutMixins.flexColumn} + gap: 0.25rem; + } +`; + +const $Content = styled(Content)` ${popoverMixins.popoverAnimation} transform-origin: center top; &[data-placement='inline'] { max-height: 100vh; - ${Styled.List}[data-orientation="horizontal"] & { + ${$List}[data-orientation="horizontal"] & { /* position: absolute; top: calc(100% + var(--submenu-side-offset)); left: 50%; @@ -277,7 +299,7 @@ Styled.Content = styled(Content)` z-index: 2; } - ${Styled.List}[data-orientation="menu"] & { + ${$List}[data-orientation="menu"] & { position: absolute; left: 100%; top: 0; @@ -325,26 +347,26 @@ Styled.Content = styled(Content)` } `; -Styled.Sub = styled(Sub)` +const $Sub = styled(Sub)` &[data-placement='inline'] { ${popoverMixins.popover} --popover-width: max-content; overflow: visible; - ${Styled.List}[data-orientation="vertical"] > & { + ${$List}[data-orientation="vertical"] > & { margin-top: var(--gap, 0.25rem); padding: 0.5rem; } - ${Styled.List}[data-orientation="menu"] & { + ${$List}[data-orientation="menu"] & { border-top-left-radius: 0 !important; } } `; -Styled.Group = styled.section` - ${Styled.Root}[data-orientation="vertical"] & { +const $Group = styled.section` + ${$Root}[data-orientation="vertical"] & { ${layoutMixins.stickyArea0} --stickyArea0-topHeight: 3rem; } @@ -354,7 +376,7 @@ Styled.Group = styled.section` color: var(--color-text-0); `; -Styled.GroupHeader = styled.header` +const $GroupHeader = styled.header` ${layoutMixins.stickyHeader} ${layoutMixins.row} @@ -362,31 +384,16 @@ Styled.GroupHeader = styled.header` font: var(--font-small-medium); `; -Styled.List = styled(List)` - align-self: center; - - &[data-orientation='horizontal'] { - ${layoutMixins.row} - gap: 0.5rem; - align-items: start; - } - - &[data-orientation='vertical'] { - ${layoutMixins.flexColumn} - gap: 0.25rem; - } -`; - -Styled.ListItem = styled(Item)` +const $ListItem = styled(Item)` display: grid; position: relative; - ${Styled.List}[data-orientation="horizontal"] > & { + ${$List}[data-orientation="horizontal"] > & { gap: var(--submenu-side-offset); } `; -Styled.SubMenuTrigger = styled(Trigger)` +const $SubMenuTrigger = styled(Trigger)` border-radius: var(--navigationMenu-item-radius); outline-offset: -2px; @@ -397,7 +404,10 @@ Styled.SubMenuTrigger = styled(Trigger)` } `; -Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>` +type navItemStyleProps = { orientation: 'horizontal' | 'vertical' }; +const NavItemTypeTemp = getSimpleStyledOutputType(NavItem, {} as navItemStyleProps); + +const $NavItem = styled(NavItem)` ${({ subitems }) => subitems?.length ? css` @@ -446,11 +456,11 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>` /* Border-radius! */ - ${Styled.List}[data-orientation="menu"] & { + ${$List}[data-orientation="menu"] & { --item-radius: 0; } - ${Styled.List}[data-orientation="menu"] > ${Styled.ListItem}:first-child > & { + ${$List}[data-orientation="menu"] > ${$ListItem}:first-child > & { border-top-left-radius: var(--popover-radius); &:not([data-state='open']) { @@ -458,7 +468,7 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>` } } - ${Styled.List}[data-orientation="menu"] > ${Styled.ListItem}:last-child > & { + ${$List}[data-orientation="menu"] > ${$ListItem}:last-child > & { border-bottom-left-radius: var(--popover-radius); &:not([data-state='open']) { @@ -466,16 +476,16 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>` } } - ${Styled.List}[data-orientation="menu"] ${Styled.List}[data-orientation="menu"] > ${Styled.ListItem}:first-child > & { + ${$List}[data-orientation="menu"] ${$List}[data-orientation="menu"] > ${$ListItem}:first-child > & { border-top-left-radius: 0; } -`; +` as typeof NavItemTypeTemp; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` font-size: 0.375em; transition: rotate 0.3s var(--ease-out-expo); - ${Styled.List}[data-orientation="menu"] & { + ${$List}[data-orientation="menu"] & { rotate: -0.25turn; } `; diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index bca9dd697..7daac8319 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -1,8 +1,8 @@ import React, { type MouseEvent } from 'react'; + import styled, { css } from 'styled-components'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; - import { NotificationStatus, type Notification as NotificationDataType, @@ -10,11 +10,11 @@ import { import { useNotifications } from '@/hooks/useNotifications'; -import { popoverMixins } from '@/styles/popoverMixins'; import { layoutMixins } from '@/styles/layoutMixins'; +import { popoverMixins } from '@/styles/popoverMixins'; -import { IconButton } from '@/components/IconButton'; import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; import { Output, OutputType } from '@/components/Output'; type ElementProps = { @@ -52,6 +52,7 @@ export const Notification = ({ withClose = !isToast, }: NotificationProps) => { const { markCleared, markSeen } = useNotifications(); + const slotContentOrDescription = slotCustomContent ?? slotDescription; return ( <$Container className={className} isToast={isToast} onClick={onClick}> @@ -73,7 +74,7 @@ export const Notification = ({ iconName={IconName.Close} shape={ButtonShape.Square} size={ButtonSize.XSmall} - onClick={(e: MouseEvent) => { + onClick={(e: React.MouseEvent) => { e.stopPropagation(); if (notification.status < NotificationStatus.Seen) { @@ -87,7 +88,7 @@ export const Notification = ({ )} - <$Description>{slotCustomContent ?? slotDescription} + {slotContentOrDescription && <$Description>{slotContentOrDescription}} {slotAction && <$Action>{slotAction}} ); @@ -121,14 +122,13 @@ const $Container = styled.div<{ isToast?: boolean }>` const $Header = styled.header` ${layoutMixins.row} position: relative; + gap: 0.5rem; `; const $Icon = styled.div` ${layoutMixins.row} float: left; - margin-right: 0.5rem; - line-height: 1; > svg, @@ -152,6 +152,7 @@ const $Description = styled.div` margin-top: 0.5rem; color: var(--color-text-0); font: var(--font-small-book); + white-space: break-spaces; `; const $Action = styled.div` diff --git a/src/components/NumberValue.tsx b/src/components/NumberValue.tsx new file mode 100644 index 000000000..498f96c6c --- /dev/null +++ b/src/components/NumberValue.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +import { formatZeroNumbers } from '@/lib/formatZeroNumbers'; + +export type NumberValueProps = { + value: string; + withSubscript?: boolean; + className?: string; +}; + +export const NumberValue = ({ className, value, withSubscript }: NumberValueProps) => { + const { significantDigits, decimalDigits, zeros, punctuationSymbol } = formatZeroNumbers(value); + + if (withSubscript) { + return ( + + {significantDigits} + {punctuationSymbol} + {Boolean(zeros) && ( + <> + 0<$Sub title={value}>{zeros} + + )} + {decimalDigits} + + ); + } + + return value; +}; +const $Sub = styled.sub` + font-size: 0.85em; +`; diff --git a/src/components/OrderSideTag.stories.tsx b/src/components/OrderSideTag.stories.tsx index 088d456fc..f9f2715b3 100644 --- a/src/components/OrderSideTag.stories.tsx +++ b/src/components/OrderSideTag.stories.tsx @@ -1,5 +1,5 @@ -import type { Story } from '@ladle/react'; import { OrderSide } from '@dydxprotocol/v4-client-js'; +import type { Story } from '@ladle/react'; import { OrderSideTag } from '@/components/OrderSideTag'; diff --git a/src/components/OrderSideTag.tsx b/src/components/OrderSideTag.tsx index 430426b33..c1878e6c2 100644 --- a/src/components/OrderSideTag.tsx +++ b/src/components/OrderSideTag.tsx @@ -1,7 +1,8 @@ import { OrderSide } from '@dydxprotocol/v4-client-js'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { Tag, TagSign, TagSize, TagType } from './Tag'; diff --git a/src/components/Output.tsx b/src/components/Output.tsx index ec55d8f71..0dc17ab31 100644 --- a/src/components/Output.tsx +++ b/src/components/Output.tsx @@ -1,8 +1,9 @@ import { useContext } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; + import BigNumber from 'bignumber.js'; -import { useSelector } from 'react-redux'; import { DateTime } from 'luxon'; +import { useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { LEVERAGE_DECIMALS, @@ -12,22 +13,25 @@ import { TOKEN_DECIMALS, USD_DECIMALS, } from '@/constants/numbers'; - import { UNICODE } from '@/constants/unicode'; -import { useLocaleSeparators, useStringGetter } from '@/hooks'; + +import { LoadingContext } from '@/contexts/LoadingContext'; +import { useLocaleSeparators } from '@/hooks/useLocaleSeparators'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; -import { LoadingOutput } from './Loading/LoadingOutput'; import { RelativeTime } from '@/components/RelativeTime'; import { Tag } from '@/components/Tag'; -import { LoadingContext } from '@/contexts/LoadingContext'; - import { getSelectedLocale } from '@/state/localizationSelectors'; -import { type BigNumberish, MustBigNumber, isNumber } from '@/lib/numbers'; +import { MustBigNumber, isNumber, type BigNumberish } from '@/lib/numbers'; import { getStringsForDateTimeDiff, getTimestamp } from '@/lib/timeUtils'; +import { LoadingOutput } from './Loading/LoadingOutput'; +import { NumberValue } from './NumberValue'; + export enum OutputType { Text = 'Text', CompactNumber = 'CompactNumber', @@ -51,15 +55,25 @@ export enum ShowSign { None = 'None', } -type ElementProps = { +type FormatParams = { type: OutputType; value?: BigNumberish | null; - isLoading?: boolean; + locale?: string; +}; + +type FormatNumberParams = { fractionDigits?: number | null; showSign?: ShowSign; + slotLeft?: React.ReactNode; slotRight?: React.ReactNode; useGrouping?: boolean; roundingMode?: BigNumber.RoundingMode; + localeDecimalSeparator?: string; + localeGroupSeparator?: string; +} & FormatParams; + +type FormatTimestampParams = { + withSubscript?: boolean; relativeTimeFormatOptions?: { format: 'long' | 'short' | 'narrow' | 'singleCharacter'; resolution?: number; @@ -68,9 +82,13 @@ type ElementProps = { timeOptions?: { useUTC?: boolean; }; +} & FormatParams; + +type ElementProps = { + isLoading?: boolean; + slotRight?: React.ReactNode; tag?: React.ReactNode; withParentheses?: boolean; - locale?: string; }; type StyleProps = { @@ -78,27 +96,226 @@ type StyleProps = { withBaseFont?: boolean; }; -export type OutputProps = ElementProps & StyleProps; - -export const Output = ({ - type, - value, - isLoading, - fractionDigits, - showSign = ShowSign.Negative, - slotRight, - useGrouping = true, - roundingMode = BigNumber.ROUND_HALF_UP, - relativeTimeFormatOptions = { - format: 'singleCharacter', - }, - timeOptions, - tag, - withParentheses, - locale = navigator.language || 'en-US', - className, - withBaseFont, -}: OutputProps) => { +export type OutputProps = ElementProps & + StyleProps & + Exclude & + FormatTimestampParams; + +export const formatTimestamp = ( + params: FormatTimestampParams +): { + displayString?: string; + timestamp?: number; + unitStringKey?: string; +} => { + const { + value, + type, + relativeTimeFormatOptions = { + format: 'singleCharacter', + }, + timeOptions, + locale, + } = params; + + switch (type) { + case OutputType.RelativeTime: { + const timestamp = getTimestamp(value); + + if (!timestamp) { + return { + timestamp: undefined, + }; + } + + if (relativeTimeFormatOptions.format === 'singleCharacter') { + const { timeString, unitStringKey } = getStringsForDateTimeDiff( + DateTime.fromMillis(timestamp) + ); + + return { + timestamp, + displayString: timeString, + unitStringKey, + }; + } + + return { + timestamp, + }; + } + case OutputType.Date: + case OutputType.Time: + case OutputType.DateTime: { + if ((typeof value !== 'string' && typeof value !== 'number') || !value) break; + const date = new Date(value); + const dateString = { + [OutputType.Date]: date.toLocaleString(locale, { + dateStyle: 'medium', + timeZone: timeOptions?.useUTC ? 'UTC' : undefined, + }), + [OutputType.DateTime]: date.toLocaleString(locale, { + dateStyle: 'short', + timeStyle: 'short', + timeZone: timeOptions?.useUTC ? 'UTC' : undefined, + }), + [OutputType.Time]: date.toLocaleString(locale, { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZone: timeOptions?.useUTC ? 'UTC' : undefined, + }), + }[type]; + + return { + displayString: dateString, + }; + } + } + + return { + displayString: undefined, + timestamp: undefined, + unitStringKey: undefined, + }; +}; + +export const formatNumber = (params: FormatNumberParams) => { + const { + value, + showSign = ShowSign.Negative, + useGrouping = true, + type, + locale = navigator.language || 'en-US', + fractionDigits, + roundingMode = BigNumber.ROUND_HALF_UP, + localeDecimalSeparator, + localeGroupSeparator, + } = params; + + const format = { + decimalSeparator: localeDecimalSeparator, + ...(useGrouping + ? { + groupSeparator: localeGroupSeparator, + groupSize: 3, + secondaryGroupSize: 0, + fractionGroupSeparator: ' ', + fractionGroupSize: 0, + } + : {}), + }; + + const isNegative = MustBigNumber(value).isNegative(); + const isPositive = MustBigNumber(value).isPositive() && !MustBigNumber(value).isZero(); + + const sign = { + [ShowSign.Both]: isNegative ? UNICODE.MINUS : isPositive ? UNICODE.PLUS : undefined, + [ShowSign.Negative]: isNegative ? UNICODE.MINUS : undefined, + [ShowSign.None]: undefined, + }[showSign]; + + const valueBN = MustBigNumber(value).abs(); + let formattedString: string | undefined = undefined; + + switch (type) { + case OutputType.CompactNumber: + if (!isNumber(value)) { + throw new Error('value must be a number for compact number output'); + } + + formattedString = Intl.NumberFormat(locale, { + style: 'decimal', + notation: 'compact', + maximumSignificantDigits: 3, + }) + .format(Math.abs(value)) + .toLowerCase(); + break; + case OutputType.Number: + formattedString = valueBN.toFormat(fractionDigits ?? 0, roundingMode, { + ...format, + }); + break; + case OutputType.Fiat: + formattedString = valueBN.toFormat(fractionDigits ?? USD_DECIMALS, roundingMode, { + ...format, + prefix: '$', + }); + break; + case OutputType.SmallFiat: + formattedString = valueBN.toFormat(fractionDigits ?? SMALL_USD_DECIMALS, roundingMode, { + ...format, + prefix: '$', + }); + break; + case OutputType.CompactFiat: + if (!isNumber(value)) { + throw new Error('value must be a number for compact fiat output'); + } + formattedString = Intl.NumberFormat(locale, { + style: 'currency', + currency: 'USD', + notation: 'compact', + maximumSignificantDigits: 3, + }) + .format(Math.abs(value)) + .toLowerCase(); + break; + case OutputType.Asset: + formattedString = valueBN.toFormat(fractionDigits ?? TOKEN_DECIMALS, roundingMode, { + ...format, + }); + break; + case OutputType.Percent: + formattedString = valueBN + .times(100) + .toFormat(fractionDigits ?? PERCENT_DECIMALS, roundingMode, { + ...format, + suffix: '%', + }); + break; + case OutputType.SmallPercent: + formattedString = valueBN + .times(100) + .toFormat(fractionDigits ?? SMALL_PERCENT_DECIMALS, roundingMode, { + ...format, + suffix: '%', + }); + break; + case OutputType.Multiple: + formattedString = valueBN.toFormat(fractionDigits ?? LEVERAGE_DECIMALS, roundingMode, { + ...format, + suffix: '×', + }); + break; + } + + return { + sign, + format, + formattedString, + }; +}; + +export const Output = (props: OutputProps) => { + const { + type, + value, + isLoading, + slotLeft, + slotRight, + withSubscript, + relativeTimeFormatOptions = { + format: 'singleCharacter', + }, + tag, + withParentheses, + locale = navigator.language || 'en-US', + className, + withBaseFont, + } = props; const selectedLocale = useSelector(getSelectedLocale); const stringGetter = useStringGetter(); const isDetailsLoading = useContext(LoadingContext); @@ -112,29 +329,26 @@ export const Output = ({ switch (type) { case OutputType.Text: { return ( - + {slotLeft} {value?.toString() ?? null} {tag && {tag}} {slotRight} - + ); } case OutputType.RelativeTime: { - const timestamp = getTimestamp(value); + const { timestamp, displayString, unitStringKey } = formatTimestamp(props); if (!timestamp) return null; - if (relativeTimeFormatOptions.format === 'singleCharacter') { - const { timeString, unitStringKey } = getStringsForDateTimeDiff( - DateTime.fromMillis(timestamp) - ); - + if (displayString && unitStringKey) { return ( - - {timeString} + {displayString} {stringGetter({ key: unitStringKey })} {tag && {tag}} - + ); } return ( - {tag && {tag}} - + ); } case OutputType.Date: case OutputType.Time: case OutputType.DateTime: { if ((typeof value !== 'string' && typeof value !== 'number') || !value) return null; - const date = new Date(value); - const dateString = { - [OutputType.Date]: date.toLocaleString(selectedLocale, { - dateStyle: 'medium', - timeZone: timeOptions?.useUTC ? 'UTC' : undefined, - }), - [OutputType.DateTime]: date.toLocaleString(selectedLocale, { - dateStyle: 'short', - timeStyle: 'short', - timeZone: timeOptions?.useUTC ? 'UTC' : undefined, - }), - [OutputType.Time]: date.toLocaleString(selectedLocale, { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZone: timeOptions?.useUTC ? 'UTC' : undefined, - }), - }[type]; + + const { displayString } = formatTimestamp(props); return ( - - {dateString} - + <$Text key={value} title={`${value ?? ''}${tag ? ` ${tag}` : ''}`} className={className}> + {displayString} + ); } case OutputType.CompactNumber: @@ -208,31 +401,14 @@ export const Output = ({ case OutputType.SmallPercent: case OutputType.Multiple: { const hasValue = value !== null && value !== undefined; - const valueBN = MustBigNumber(value).abs(); - const isNegative = MustBigNumber(value).isNegative(); - const isPositive = MustBigNumber(value).isPositive() && !MustBigNumber(value).isZero(); - - const sign: string | undefined = { - [ShowSign.Both]: isNegative ? UNICODE.MINUS : isPositive ? UNICODE.PLUS : undefined, - [ShowSign.Negative]: isNegative ? UNICODE.MINUS : undefined, - [ShowSign.None]: undefined, - }[showSign]; - - const format = { - decimalSeparator: LOCALE_DECIMAL_SEPARATOR, - ...(useGrouping - ? { - groupSeparator: LOCALE_GROUP_SEPARATOR, - groupSize: 3, - secondaryGroupSize: 0, - fractionGroupSeparator: ' ', - fractionGroupSize: 0, - } - : {}), - }; + const { sign, formattedString } = formatNumber({ + ...props, + localeDecimalSeparator: LOCALE_DECIMAL_SEPARATOR, + localeGroupSeparator: LOCALE_GROUP_SEPARATOR, + }); return ( - - {sign && {sign}} + {slotLeft} + {sign && <$Sign>{sign}} {hasValue && { [OutputType.CompactNumber]: () => { @@ -254,66 +431,40 @@ export const Output = ({ throw new Error('value must be a number for compact number output'); } - return Intl.NumberFormat(locale, { - style: 'decimal', - notation: 'compact', - maximumSignificantDigits: 3, - }) - .format(Math.abs(value)) - .toLowerCase(); + return ; }, - [OutputType.Number]: () => - valueBN.toFormat(fractionDigits ?? 0, roundingMode, { - ...format, - }), - [OutputType.Fiat]: () => - valueBN.toFormat(fractionDigits ?? USD_DECIMALS, roundingMode, { - ...format, - prefix: '$', - }), - [OutputType.SmallFiat]: () => - valueBN.toFormat(fractionDigits ?? SMALL_USD_DECIMALS, roundingMode, { - ...format, - prefix: '$', - }), + [OutputType.Number]: () => ( + + ), + [OutputType.Fiat]: () => ( + + ), + [OutputType.SmallFiat]: () => ( + + ), [OutputType.CompactFiat]: () => { if (!isNumber(value)) { throw new Error('value must be a number for compact fiat output'); } - return Intl.NumberFormat(locale, { - style: 'currency', - currency: 'USD', - notation: 'compact', - maximumSignificantDigits: 3, - }) - .format(Math.abs(value)) - .toLowerCase(); + + return ; }, - [OutputType.Asset]: () => - valueBN.toFormat(fractionDigits ?? TOKEN_DECIMALS, roundingMode, { - ...format, - }), - [OutputType.Percent]: () => - valueBN.times(100).toFormat(fractionDigits ?? PERCENT_DECIMALS, roundingMode, { - ...format, - suffix: '%', - }), - [OutputType.SmallPercent]: () => - valueBN - .times(100) - .toFormat(fractionDigits ?? SMALL_PERCENT_DECIMALS, roundingMode, { - ...format, - suffix: '%', - }), - [OutputType.Multiple]: () => - valueBN.toFormat(fractionDigits ?? LEVERAGE_DECIMALS, roundingMode, { - ...format, - suffix: '×', - }), + [OutputType.Asset]: () => ( + + ), + [OutputType.Percent]: () => ( + + ), + [OutputType.SmallPercent]: () => ( + + ), + [OutputType.Multiple]: () => ( + + ), }[type]()} {slotRight} - {tag && {tag}} - + {tag && <$Tag>{tag}} + ); } default: @@ -321,9 +472,7 @@ export const Output = ({ } }; -const Styled: Record = {}; - -Styled.Output = styled.output<{ withParentheses?: boolean }>` +const _OUTPUT_STYLES = styled.output<{ withParentheses?: boolean }>` --output-beforeString: ''; --output-afterString: ''; --output-sign-color: currentColor; @@ -356,17 +505,19 @@ Styled.Output = styled.output<{ withParentheses?: boolean }>` `} `; -Styled.Tag = styled(Tag)` +const $Output = _OUTPUT_STYLES; + +const $Tag = styled(Tag)` margin-left: 0.5ch; `; -Styled.Sign = styled.span` +const $Sign = styled.span` color: var(--output-sign-color); `; -Styled.Text = styled(Styled.Output)``; +const $Text = styled(_OUTPUT_STYLES)``; -Styled.Number = styled(Styled.Output)<{ withBaseFont?: boolean }>` +const $Number = styled(_OUTPUT_STYLES)<{ withBaseFont?: boolean }>` ${({ withBaseFont }) => !withBaseFont && css` diff --git a/src/components/PageMenu.stories.tsx b/src/components/PageMenu.stories.tsx index 00222d22e..6358d06cb 100644 --- a/src/components/PageMenu.stories.tsx +++ b/src/components/PageMenu.stories.tsx @@ -1,11 +1,11 @@ -import { MemoryRouter } from 'react-router-dom'; import type { Story } from '@ladle/react'; +import { MemoryRouter } from 'react-router-dom'; import type { MenuGroup } from '@/constants/menus'; import { PageMenu } from '@/components/PageMenu'; -import { PageMenuItemType } from './PageMenu/PageMenuItem'; +import { PageMenuItemType } from './PageMenu/PageMenuItem'; import { StoryWrapper } from '.ladle/components'; export const PageMenuStory: Story> = (args) => { diff --git a/src/components/PageMenu.tsx b/src/components/PageMenu.tsx index d505bea0d..af0b0aa8c 100644 --- a/src/components/PageMenu.tsx +++ b/src/components/PageMenu.tsx @@ -1,4 +1,4 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import type { MenuGroup } from '@/constants/menus'; @@ -9,16 +9,13 @@ import { PageMenuItem, type PageMenuItemType } from './PageMenu/PageMenuItem'; export const PageMenu = ({ items, }: MenuGroup) => ( - + <$PageMenu> {items.map((item) => ( ))} - + ); - -const Styled: Record = {}; - -Styled.PageMenu = styled.menu` +const $PageMenu = styled.menu` ${layoutMixins.flexColumn} ${layoutMixins.withInnerHorizontalBorders} diff --git a/src/components/PageMenu/PageMenuItem.tsx b/src/components/PageMenu/PageMenuItem.tsx index 5cafbc6f2..7b58dce2e 100644 --- a/src/components/PageMenu/PageMenuItem.tsx +++ b/src/components/PageMenu/PageMenuItem.tsx @@ -20,7 +20,7 @@ export const PageMenuItem = ({ // TODO: implement toggle item component when needed for notifications settings return null; case PageMenuItemType.RadioGroup: - return ; + return ; default: return null; } diff --git a/src/components/PageMenu/PageMenuNavigationItem.tsx b/src/components/PageMenu/PageMenuNavigationItem.tsx index 0ea452502..63ec896e1 100644 --- a/src/components/PageMenu/PageMenuNavigationItem.tsx +++ b/src/components/PageMenu/PageMenuNavigationItem.tsx @@ -1,13 +1,13 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import { Link } from 'react-router-dom'; +import styled from 'styled-components'; import type { MenuItem } from '@/constants/menus'; -import { Icon, IconName } from '@/components/Icon'; - import { layoutMixins } from '@/styles/layoutMixins'; import { popoverMixins } from '@/styles/popoverMixins'; +import { Icon, IconName } from '@/components/Icon'; + export const PageMenuNavigationItem = < MenuItemValue extends string, PageMenuItemType extends string @@ -17,19 +17,16 @@ export const PageMenuNavigationItem = < labelRight, }: MenuItem) => ( - + <$MenuItem>
    {label}
    - + <$RightRow> {labelRight && {labelRight}} - - -
    + <$Icon iconName={IconName.ChevronRight} /> + + ); - -const Styled: Record = {}; - -Styled.MenuItem = styled.ul` +const $MenuItem = styled.ul` ${popoverMixins.item} --item-padding: 1.25rem 1.625rem; @@ -38,7 +35,7 @@ Styled.MenuItem = styled.ul` ${layoutMixins.spacedRow} `; -Styled.RightRow = styled.div` +const $RightRow = styled.div` ${layoutMixins.row} gap: 1rem; @@ -46,6 +43,6 @@ Styled.RightRow = styled.div` color: var(--color-text-0); `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` color: var(--color-text-0); `; diff --git a/src/components/PageMenu/PageMenuRadioGroup.tsx b/src/components/PageMenu/PageMenuRadioGroup.tsx index 5df5dea23..9c1575178 100644 --- a/src/components/PageMenu/PageMenuRadioGroup.tsx +++ b/src/components/PageMenu/PageMenuRadioGroup.tsx @@ -1,13 +1,13 @@ -import styled, { AnyStyledComponent } from 'styled-components'; -import { Root, Item } from '@radix-ui/react-radio-group'; +import { Item, Root } from '@radix-ui/react-radio-group'; +import styled from 'styled-components'; import type { MenuItem } from '@/constants/menus'; -import { Icon, IconName } from '@/components/Icon'; - import { layoutMixins } from '@/styles/layoutMixins'; import { popoverMixins } from '@/styles/popoverMixins'; +import { Icon, IconName } from '@/components/Icon'; + export const PageMenuRadioGroupItem = < MenuItemValue extends string, PageMenuItemType extends string @@ -17,33 +17,26 @@ export const PageMenuRadioGroupItem = < subitems, }: MenuItem) => subitems?.length ? ( - + <$Root value={curentValue} onValueChange={onSelect}> {subitems.map(({ disabled, value, label, slotBefore }) => ( - + <$MenuItem key={value} value={value} disabled={disabled}>
    - <>{slotBefore} + {slotBefore} {label}
    - {value === curentValue ? ( - - ) : ( - - )} -
    + {value === curentValue ? <$CheckIcon iconName={IconName.Check} /> : <$EmptyIcon />} + ))} -
    + ) : null; - -const Styled: Record = {}; - -Styled.Root = styled(Root)` +const $Root = styled(Root)` ${layoutMixins.flexColumn} ${layoutMixins.withInnerHorizontalBorders} gap: var(--border-width); `; -Styled.MenuItem = styled(Item)<{ $selected: boolean }>` +const $MenuItem = styled(Item)` ${layoutMixins.spacedRow} ${popoverMixins.item} @@ -51,7 +44,7 @@ Styled.MenuItem = styled(Item)<{ $selected: boolean }>` --item-checked-backgroundColor: var(--color-layer-0); `; -Styled.CheckIcon = styled(Icon)` +const $CheckIcon = styled(Icon)` padding: 4px; border-radius: 50%; @@ -60,7 +53,7 @@ Styled.CheckIcon = styled(Icon)` background-color: var(--color-accent); `; -Styled.EmptyIcon = styled.div` +const $EmptyIcon = styled.div` width: 0.9em; height: 0.9em; @@ -69,4 +62,3 @@ Styled.EmptyIcon = styled.div` background-color: var(--color-layer-0); `; - diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 1ea335f16..9d9a0ca54 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -1,10 +1,10 @@ -import styled, { AnyStyledComponent, css } from 'styled-components'; import { Link } from 'react-router-dom'; +import styled, { css } from 'styled-components'; -import { Icon, IconName } from '@/components/Icon'; - -import { layoutMixins } from '@/styles/layoutMixins'; import { breakpoints } from '@/styles'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Icon, IconName } from '@/components/Icon'; type ElementProps = { slotHeaderContent?: React.ReactNode; @@ -34,37 +34,31 @@ export const Panel = ({ hasSeparator, className, }: PanelProps) => ( - - + <$Panel onClick={onClick} className={className}> + <$Left> {href ? ( - {slotHeader ? ( - slotHeader - ) : ( - + {slotHeader ?? ( + <$Header role="button" onClick={onHeaderClick} hasSeparator={hasSeparator}> {slotHeaderContent} - - + <$Icon iconName={IconName.ChevronRight} /> + )} - ) : slotHeader ? ( - slotHeader ) : ( - slotHeaderContent && ( - + slotHeader ?? + (slotHeaderContent && ( + <$Header role="button" onClick={onHeaderClick} hasSeparator={hasSeparator}> {slotHeaderContent} - - ) + + )) )} - {children} - + <$Content>{children} + {slotRight} - + ); - -const Styled: Record = {}; - -Styled.Panel = styled.section<{ onClick?: () => void }>` +const $Panel = styled.section<{ onClick?: () => void }>` --panel-paddingY: 1rem; --panel-paddingX: 1rem; --panel-content-paddingY: var(--panel-paddingY); @@ -95,12 +89,12 @@ Styled.Panel = styled.section<{ onClick?: () => void }>` `} `; -Styled.Left = styled.div` +const $Left = styled.div` ${layoutMixins.flexColumn} width: 100%; `; -Styled.Header = styled.header<{ hasSeparator?: boolean }>` +const $Header = styled.header<{ hasSeparator?: boolean }>` ${layoutMixins.spacedRow} padding: var(--panel-paddingY) var(--panel-paddingX); @@ -112,12 +106,12 @@ Styled.Header = styled.header<{ hasSeparator?: boolean }>` `} `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` color: var(--color-text-0); font-size: 0.625rem; `; -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.scrollArea} ${layoutMixins.stickyArea0} --stickyArea0-background: transparent; diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx index 3f07b7dda..51d6401b5 100644 --- a/src/components/Popover.tsx +++ b/src/components/Popover.tsx @@ -1,7 +1,8 @@ import { useMemo, useState } from 'react'; -import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; -import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover'; + +import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover'; import { useRect } from '@radix-ui/react-use-rect'; +import styled, { css, keyframes } from 'styled-components'; import { popoverMixins } from '@/styles/popoverMixins'; @@ -45,38 +46,35 @@ export const Popover = ({ className, children, }: PopoverProps) => { - const [trigger, setTrigger] = useState(null); + const [trigger, setTrigger] = useState(null); const rect = useRect(trigger); const width = useMemo(() => fullWidth && rect?.width, undefined); const content = ( - { e.preventDefault(); }} - style={{ width }} + style={{ width: width != null && !!width ? width : undefined }} $noBlur={noBlur} className={className} sideOffset={sideOffset} > {children} - + ); return ( - + <$Trigger ref={setTrigger} $noBlur={noBlur} $triggerType={triggerType}> {slotTrigger} - + {slotAnchor} {withPortal ? {content} : content} ); }; - -const Styled: Record = {}; - -Styled.Trigger = styled(Trigger)<{ $noBlur?: boolean; $triggerType: TriggerType }>` +const $Trigger = styled(Trigger)<{ $noBlur?: boolean; $triggerType: TriggerType }>` ${popoverMixins.backdropOverlay} ${popoverMixins.trigger} @@ -100,7 +98,7 @@ Styled.Trigger = styled(Trigger)<{ $noBlur?: boolean; $triggerType: TriggerType --trigger-padding: 0; `; -Styled.Content = styled(Content)<{ $noBlur?: boolean }>` +const $Content = styled(Content)<{ $noBlur?: boolean }>` ${({ $noBlur }) => !$noBlur && css` diff --git a/src/components/PositionSideTag.tsx b/src/components/PositionSideTag.tsx index 710672f70..5a2322196 100644 --- a/src/components/PositionSideTag.tsx +++ b/src/components/PositionSideTag.tsx @@ -1,5 +1,6 @@ import { POSITION_SIDE_STRINGS, PositionSide } from '@/constants/trade'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { Tag, TagSign, TagSize, TagType } from '@/components/Tag'; @@ -21,7 +22,11 @@ export const PositionSideTag = ({ positionSide, size }: ElementProps & StyleProp const stringGetter = useStringGetter(); return ( - + {stringGetter({ key: POSITION_SIDE_STRINGS[positionSide || PositionSide.None] })} ); diff --git a/src/components/PotentialPositionCard.tsx b/src/components/PotentialPositionCard.tsx new file mode 100644 index 000000000..c477f23cc --- /dev/null +++ b/src/components/PotentialPositionCard.tsx @@ -0,0 +1,81 @@ +import styled from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { AssetIcon } from './AssetIcon'; +import { Icon, IconName } from './Icon'; +import { Link } from './Link'; +import { Output, OutputType } from './Output'; + +type PotentialPositionCardProps = { + onViewOrders: (marketId: string) => void; +}; + +export const PotentialPositionCard = ({ onViewOrders }: PotentialPositionCardProps) => { + const stringGetter = useStringGetter(); + + return ( + <$PotentialPositionCard> + <$MarketRow> + Market Name + + <$MarginRow> + <$MarginLabel>{stringGetter({ key: STRING_KEYS.MARGIN })}{' '} + <$Output type={OutputType.Fiat} value={1_000} /> + + <$ActionRow> + <$Link onClick={() => onViewOrders('UNI-USD')}> + {stringGetter({ key: STRING_KEYS.VIEW_ORDERS })} + + + + ); +}; + +const $PotentialPositionCard = styled.div` + ${layoutMixins.flexColumn} + width: 14rem; + min-width: 14rem; + height: 6.5rem; + background-color: var(--color-layer-3); + overflow: hidden; + padding: 0.75rem 0; + border-radius: 0.625rem; +`; +const $MarketRow = styled.div` + ${layoutMixins.row} + gap: 0.5rem; + padding: 0 0.625rem; + font: var(--font-small-book); + + img { + font-size: 1.25rem; // 20px x 20px + } +`; +const $MarginRow = styled.div` + ${layoutMixins.spacedRow} + padding: 0 0.625rem; + margin-top: 0.625rem; +`; +const $MarginLabel = styled.span` + color: var(--color-text-0); + font: var(--font-mini-book); +`; +const $Output = styled(Output)` + font: var(--font-small-book); +`; +const $ActionRow = styled.div` + ${layoutMixins.spacedRow} + border-top: var(--border); + margin-top: 0.5rem; + padding: 0 0.625rem; + padding-top: 0.5rem; +`; +const $Link = styled(Link)` + --link-color: var(--color-accent); + font: var(--font-small-book); +`; diff --git a/src/components/QrCode.tsx b/src/components/QrCode.tsx index fb7938353..c00225b26 100644 --- a/src/components/QrCode.tsx +++ b/src/components/QrCode.tsx @@ -1,10 +1,11 @@ import { useEffect, useRef } from 'react'; -import { useSelector } from 'react-redux'; -import styled, { AnyStyledComponent } from 'styled-components'; + import QRCodeStyling from 'qr-code-styling'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; -import { getAppTheme } from '@/state/configsSelectors'; import { AppTheme } from '@/state/configs'; +import { getAppTheme } from '@/state/configsSelectors'; type ElementProps = { value: string; @@ -75,12 +76,9 @@ export const QrCode = ({ className, value, hasLogo, size = 300 }: ElementProps & } }, [appTheme, hasLogo]); - return ; + return <$QrCode className={className} ref={ref} />; }; - -const Styled: Record = {}; - -Styled.QrCode = styled.div` +const $QrCode = styled.div` width: 100%; cursor: none; border-radius: 0.75em; diff --git a/src/components/RadioButtonCards.stories.tsx b/src/components/RadioButtonCards.stories.tsx new file mode 100644 index 000000000..7ba1a3f53 --- /dev/null +++ b/src/components/RadioButtonCards.stories.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react'; + +import type { Story } from '@ladle/react'; +import styled from 'styled-components'; + +import { RadioButtonCards } from '@/components/RadioButtonCards'; + +import { StoryWrapper } from '.ladle/components'; + +const exampleItems = [ + { + value: '1', + label: 'Item 1', + body: ( +
    +
    Card with body element
    +
    + ), + }, + { + value: '2', + label: 'Item 2', + }, + { + value: '3', + label: 'Item 3', + }, + { + value: '4', + label: 'Item 4', + }, +]; + +export const RadioButtonCardsStory: Story<{ + bgColor?: string; + withSlotTop?: boolean; + withSlotBottom?: boolean; +}> = ({ bgColor, withSlotTop, withSlotBottom }) => { + const [item, setItem] = useState(exampleItems[0].value); + return ( + + setItem(value)} + radioItems={exampleItems} + slotTop={withSlotTop &&

    Radio Button Cards Header

    } + slotBottom={withSlotBottom &&

    Radio Button Cards Footer

    } + /> +
    + ); +}; + +const StyledRadioButtonCards = styled(RadioButtonCards)<{ bgColor?: string }>` + ${({ bgColor }) => bgColor && `background-color: var(${bgColor});`} +`; + +RadioButtonCardsStory.args = { + withSlotTop: true, + withSlotBottom: true, +}; + +RadioButtonCardsStory.argTypes = { + bgColor: { + options: [ + undefined, + '--color-layer-0', + '--color-layer-1', + '--color-layer-2', + '--color-layer-3', + '--color-layer-4', + '--color-layer-5', + '--color-layer-6', + '--color-layer-7', + ], + control: { type: 'select' }, + defaultValue: undefined, + }, +}; diff --git a/src/components/RadioButtonCards.tsx b/src/components/RadioButtonCards.tsx new file mode 100644 index 000000000..3c3ef08db --- /dev/null +++ b/src/components/RadioButtonCards.tsx @@ -0,0 +1,110 @@ +import { Item, Root } from '@radix-ui/react-radio-group'; +import styled from 'styled-components'; + +import { MenuItem } from '@/constants/menus'; + +import { Icon, IconName } from './Icon'; + +type RadioItem = Pick< + MenuItem, + 'value' | 'label' | 'disabled' +> & { + body?: React.ReactNode; +}; + +type RadioButtonCardsProps = { + className?: string; + slotTop?: React.ReactNode; + slotBottom?: React.ReactNode; + radioItems: RadioItem[]; +} & Parameters[0]; + +export const RadioButtonCards = ({ + className, + value, + onValueChange, + radioItems, + slotTop, + slotBottom, +}: RadioButtonCardsProps) => { + return ( + <$Root value={value} onValueChange={onValueChange} className={className}> + {slotTop} + {radioItems.map((item) => ( + <$RadioButtonCard key={item.value} value={item.value} disabled={item.disabled}> + <$CardHeader> + {item.label} + {value === item.value ? <$CheckIcon iconName={IconName.Check} /> : <$EmptyIcon />} + + {item.body} + + ))} + {slotBottom} + + ); +}; +const $Root = styled(Root)` + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + + --radio-button-cards-item-padding: ; + --radio-button-cards-item-gap: ; + --radio-button-cards-item-checked-backgroundColor: ; + --radio-button-cards-item-disabled-backgroundColor: ; + --radio-button-cards-item-backgroundColor: ; +`; + +const $RadioButtonCard = styled(Item)` + display: flex; + flex-direction: column; + border-radius: 0.625rem; + background-color: var(--radio-button-cards-item-backgroundColor, transparent); + border: 1px solid var(--color-layer-6); + padding: var(--radio-button-cards-item-padding, 1rem); + font: var(--font-mini-book); + text-align: left; + gap: var(--radio-button-cards-item-gap, 0.5rem); + + &:disabled { + cursor: default; + background-color: var(--radio-button-cards-item-disabled-backgroundColor, transparent); + } + + &[data-state='checked'] { + background-color: var(--radio-button-cards-item-checked-backgroundColor, var(--color-layer-2)); + } +`; + +const $CardHeader = styled.div` + display: flex; + flex: 1; + align-self: stretch; + align-items: center; + color: var(--color-text-2); + font: var(--font-base-medium); + justify-content: space-between; + gap: 1rem; +`; + +const $CheckIcon = styled(Icon)` + width: 1rem; + height: 1rem; + padding: 0.25rem; + + border-radius: 50%; + + color: var(--color-text-1); + background-color: var(--color-accent); +`; + +const $EmptyIcon = styled.div` + width: 1rem; + height: 1rem; + + box-shadow: 0 0 0 0.1rem var(--color-layer-5); + border-radius: 50%; + + background-color: var(--color-layer-0); +`; diff --git a/src/components/RelativeTime.tsx b/src/components/RelativeTime.tsx index 0939a74fc..e9571a1a8 100644 --- a/src/components/RelativeTime.tsx +++ b/src/components/RelativeTime.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; + import { useSelector } from 'react-redux'; import { getSelectedLocale } from '@/state/localizationSelectors'; diff --git a/src/components/Ring.tsx b/src/components/Ring.tsx index ddfe610df..a88acde63 100644 --- a/src/components/Ring.tsx +++ b/src/components/Ring.tsx @@ -1,4 +1,4 @@ -import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components'; +import styled, { css, keyframes } from 'styled-components'; type ElementProps = { value: number; @@ -15,7 +15,7 @@ export const Ring = ({ className, value, withAnimation }: ElementProps & StylePr const offset = Math.max(circumference - circumference * value, 0); return ( - - + ); }; - -const Styled: Record = {}; - -Styled.Ring = styled.svg<{ withAnimation?: boolean }>` +const $Ring = styled.svg<{ withAnimation?: boolean }>` --ring-color: currentColor; transform: rotate(-90deg); diff --git a/src/components/ScrollAreas.stories.tsx b/src/components/ScrollAreas.stories.tsx index a13d87fc6..f90515a16 100644 --- a/src/components/ScrollAreas.stories.tsx +++ b/src/components/ScrollAreas.stories.tsx @@ -1,164 +1,162 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import type { Story } from '@ladle/react'; +import styled from 'styled-components'; -import { StoryWrapper } from '.ladle/components'; import { layoutMixins } from '@/styles/layoutMixins'; +import { StoryWrapper } from '.ladle/components'; + export const ScrollAreasStory: Story<{}> = (args) => ( - + <$ScrollArea width="500px" height="700px">

    Scroll area. (layoutMixins.scrollArea)

    - + <$PlaceholderContent />

    Basic sticky area:

    - - Sticky header. + <$StickyArea0 topHeight="4rem" bottomHeight="3rem"> + <$StickyHeader>Sticky header.

    Sticky area. (layoutMixins.stickyArea0)

    - + <$PlaceholderContent /> - Sticky footer. -
    + <$StickyFooter>Sticky footer. + - + <$PlaceholderContent />

    Nested sticky area:

    - - Sticky header. + <$StickyArea0 topHeight="4rem" bottomHeight="3rem"> + <$StickyHeader>Sticky header.

    Sticky area. (layoutMixins.stickyArea0)

    - + <$PlaceholderContent /> - - Nested sticky header. + <$StickyArea1 topHeight="3rem" bottomHeight="2rem"> + <$StickyHeader>Nested sticky header.

    Nested sticky area. (layoutMixins.stickyArea1)

    - + <$PlaceholderContent /> - Nested sticky footer. -
    + <$StickyFooter>Nested sticky footer. +

    Sticky area. (layoutMixins.stickyArea1)

    - Sticky footer. -
    + <$StickyFooter>Sticky footer. + - + <$PlaceholderContent />

    Super-nested sticky area:

    - - Sticky header. + <$StickyArea0 topHeight="4rem" bottomHeight="3rem"> + <$StickyHeader>Sticky header.

    Sticky area. (layoutMixins.stickyArea0)

    - - Nested sticky header. + <$StickyArea1 topHeight="3rem" bottomHeight="2rem"> + <$StickyHeader>Nested sticky header. - - Super-nested sticky header. + <$StickyArea2 topHeight="3rem" bottomHeight="2rem"> + <$StickyHeader>Super-nested sticky header.

    Super-nested sticky area. (layoutMixins.stickyArea2)

    - + <$PlaceholderContent /> - Super-nested sticky footer. -
    + <$StickyFooter>Super-nested sticky footer. + - Nested sticky footer. -
    + <$StickyFooter>Nested sticky footer. +

    Sticky area. (layoutMixins.stickyArea1)

    - Sticky footer. -
    + <$StickyFooter>Sticky footer. +

    Nested scroll area:

    - - Sticky header. + <$StickyArea0 topHeight="4rem" bottomHeight="3rem"> + <$StickyHeader>Sticky header.

    Sticky area. (layoutMixins.stickyArea0)

    - - Nested sticky header. + <$StickyArea1 topHeight="3rem" bottomHeight="2rem"> + <$StickyHeader>Nested sticky header. - - Super-nested sticky header. + <$StickyArea2 topHeight="3rem" bottomHeight="2rem"> + <$StickyHeader>Super-nested sticky header.

    Super-nested sticky area. (layoutMixins.stickyArea2)

    - + <$ScrollArea height="300px">

    Nested scroll area. (layoutMixins.scrollArea)

    - - Sticky header. + <$StickyArea0 topHeight="4rem" bottomHeight="3rem"> + <$StickyHeader>Sticky header.

    Sticky area. (layoutMixins.stickyArea0)

    - + <$PlaceholderContent /> - Sticky footer. -
    -
    + <$StickyFooter>Sticky footer. + + - Super-nested sticky footer. -
    + <$StickyFooter>Super-nested sticky footer. + - Nested sticky footer. -
    + <$StickyFooter>Nested sticky footer. +

    Sticky area. (layoutMixins.stickyArea1)

    - Sticky footer. -
    -
    + <$StickyFooter>Sticky footer. + +
    ); - -const Styled: Record = {}; - -Styled.ScrollArea = styled.section<{ width: string, height: string, }>` +const $ScrollArea = styled.section<{ width?: string; height: string }>` ${layoutMixins.container} ${layoutMixins.scrollArea} @@ -179,7 +177,7 @@ Styled.ScrollArea = styled.section<{ width: string, height: string, }>` } `; -Styled.StickyHeader = styled.header<{}>` +const $StickyHeader = styled.header<{}>` ${layoutMixins.stickyHeader} ${layoutMixins.row} @@ -193,7 +191,7 @@ Styled.StickyHeader = styled.header<{}>` border-radius: 0.5rem; `; -Styled.StickyFooter = styled.footer<{}>` +const $StickyFooter = styled.footer<{}>` ${layoutMixins.stickyFooter} ${layoutMixins.row} @@ -205,7 +203,7 @@ Styled.StickyFooter = styled.footer<{}>` border-radius: 0.5rem; `; -Styled.StickyArea0 = styled.section<{ topHeight: string; bottomHeight: string }>` +const $StickyArea0 = styled.section<{ topHeight: string; bottomHeight: string }>` ${layoutMixins.stickyArea0} --stickyArea0-topHeight: ${({ topHeight }) => topHeight}; --stickyArea0-bottomHeight: ${({ bottomHeight }) => bottomHeight}; @@ -220,7 +218,7 @@ Styled.StickyArea0 = styled.section<{ topHeight: string; bottomHeight: string }> border-radius: 1rem; `; -Styled.StickyArea1 = styled.section<{ topHeight: string; bottomHeight: string }>` +const $StickyArea1 = styled.section<{ topHeight: string; bottomHeight: string }>` ${layoutMixins.stickyArea1} --stickyArea1-topHeight: ${({ topHeight }) => topHeight}; --stickyArea1-bottomHeight: ${({ bottomHeight }) => bottomHeight}; @@ -235,7 +233,7 @@ Styled.StickyArea1 = styled.section<{ topHeight: string; bottomHeight: string }> border-radius: 0.5rem; `; -Styled.StickyArea2 = styled.section<{ topHeight: string; bottomHeight: string }>` +const $StickyArea2 = styled.section<{ topHeight: string; bottomHeight: string }>` ${layoutMixins.stickyArea2} --stickyArea2-topHeight: ${({ topHeight }) => topHeight}; --stickyArea2-bottomHeight: ${({ bottomHeight }) => bottomHeight}; @@ -250,7 +248,7 @@ Styled.StickyArea2 = styled.section<{ topHeight: string; bottomHeight: string }> border-radius: 0.5rem; `; -Styled.PlaceholderContent = styled.p` +const $PlaceholderContent = styled.p` opacity: 0.3; &:before { diff --git a/src/components/SearchInput.stories.tsx b/src/components/SearchInput.stories.tsx index 67f06e75e..a2af01362 100644 --- a/src/components/SearchInput.stories.tsx +++ b/src/components/SearchInput.stories.tsx @@ -1,23 +1,21 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import type { Story } from '@ladle/react'; +import styled from 'styled-components'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { SearchInput } from '@/components/SearchInput'; -import { StoryWrapper } from '.ladle/components'; -import { layoutMixins } from '@/styles/layoutMixins'; import { InputType } from './Input'; +import { StoryWrapper } from '.ladle/components'; export const SearchInputStory: Story[0]> = (args) => ( - + <$Container> - + ); - -const Styled: Record = {}; - -Styled.Container = styled.section` +const $Container = styled.section` background: var(--color-layer-3); ${layoutMixins.container} diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx index d9526b952..00f3214a9 100644 --- a/src/components/SearchInput.tsx +++ b/src/components/SearchInput.tsx @@ -1,100 +1,75 @@ -import { useEffect, useRef, useState } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; +import { useRef, useState } from 'react'; + +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; -import { IconName } from '@/components/Icon'; +import { Icon, IconName } from '@/components/Icon'; import { IconButton } from '@/components/IconButton'; -import { Input, type InputProps } from '@/components/Input'; +import { Input, InputType, type InputProps } from '@/components/Input'; type ElementProps = { - onOpenChange?: (isOpen: boolean) => void; onTextChange?: (value: string) => void; }; export type SearchInputProps = ElementProps & InputProps; -export const SearchInput = ({ - type, - placeholder, - onOpenChange, - onTextChange, -}: SearchInputProps) => { - const [isOpen, setIsOpen] = useState(false); +export const SearchInput = ({ placeholder, onTextChange }: SearchInputProps) => { const [value, setValue] = useState(''); const inputRef = useRef(null); - useEffect(() => { - if (isOpen) inputRef?.current?.focus(); - }, [inputRef, isOpen]); - return ( - - { - setIsOpen(isPressed); - onOpenChange?.(isPressed); - - if (!isPressed) { - onTextChange?.(''); - setValue(''); - } - }} - /> - + <$Icon iconName={IconName.Search} /> + <$Input autoFocus ref={inputRef} value={value} - isOpen={isOpen} - type={type} + type={InputType.Search} onChange={(e: React.ChangeEvent) => { setValue(e.target.value); onTextChange?.(e.target.value); }} - disabled={!isOpen} placeholder={placeholder} /> - + {value.length > 0 && ( + <$IconButton + iconName={IconName.Close} + onClick={() => { + setValue(''); + }} + /> + )} + ); }; - -const Styled: Record = {}; - -Styled.Search = styled.div` +const $Search = styled.div` ${layoutMixins.row} - + width: auto; + height: 2rem; + background-color: var(--color-layer-3); + color: ${({ theme }) => theme.textTertiary}; + border-radius: 2.5rem; + border: solid var(--border-width) var(--color-layer-6); + padding: 0 1rem; + gap: 0.375rem; justify-content: end; - width: 100%; - height: 100%; `; -Styled.Input = styled(Input)<{ isOpen: boolean }>` - max-width: 0; - - @media (prefers-reduced-motion: no-preference) { - transition: max-width 0.45s var(--ease-out-expo); - } - - ${({ isOpen }) => - isOpen && - css` - padding-left: 0.5rem; - max-width: 100%; - `} +const $Input = styled(Input)` + max-width: 100%; + border-radius: 0; `; -Styled.IconButton = styled(IconButton)<{ iconName: IconName.Close | IconName.Search }>` - --button-toggle-on-backgroundColor: var(--color-layer-3); - --button-toggle-on-textColor: var(--color-text-0); +const $IconButton = styled(IconButton)` + --button-icon-size: 0.5rem; + --button-border: none; + --button-backgroundColor: transparent; + color: ${({ theme }) => theme.textSecondary}; + width: 1.5rem; + height: 1.5rem; +`; - ${({ iconName }) => - iconName === IconName.Close && - css` - svg { - height: 0.875em; - } - `} +const $Icon = styled(Icon)` + color: ${({ theme }) => theme.textSecondary}; `; diff --git a/src/components/SearchSelectMenu.stories.tsx b/src/components/SearchSelectMenu.stories.tsx index 97ab8c72a..e180f0b3b 100644 --- a/src/components/SearchSelectMenu.stories.tsx +++ b/src/components/SearchSelectMenu.stories.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; + import type { Story } from '@ladle/react'; import styled from 'styled-components'; diff --git a/src/components/SearchSelectMenu.tsx b/src/components/SearchSelectMenu.tsx index ac6f6201f..1fd5ef3d7 100644 --- a/src/components/SearchSelectMenu.tsx +++ b/src/components/SearchSelectMenu.tsx @@ -1,8 +1,14 @@ -import { type ReactNode, useState, useRef } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; +import { useRef, useState, type ReactNode } from 'react'; + +import styled, { css } from 'styled-components'; import { type MenuConfig } from '@/constants/menus'; -import { useOnClickOutside } from '@/hooks'; + +import { useOnClickOutside } from '@/hooks/useOnClickOutside'; + +import breakpoints from '@/styles/breakpoints'; +import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { ComboboxMenu } from '@/components/ComboboxMenu'; import { type DetailsItem } from '@/components/Details'; @@ -11,9 +17,7 @@ import { Popover, TriggerType } from '@/components/Popover'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; import { WithLabel } from '@/components/WithLabel'; -import { layoutMixins } from '@/styles/layoutMixins'; -import { formMixins } from '@/styles/formMixins'; -import breakpoints from '@/styles/breakpoints'; +import { getSimpleStyledOutputType } from '@/lib/genericFunctionalComponentUtils'; type ElementProps = { asChild?: boolean; @@ -36,7 +40,6 @@ export const SearchSelectMenu = ({ asChild, children, className, - disabled, label, items, withSearch = true, @@ -55,16 +58,16 @@ export const SearchSelectMenu = ({ const Trigger = asChild ? ( children ) : ( - - {label ? {children} : children} - - + <$MenuTrigger> + {label ? <$WithLabel label={label}>{children} : children} + <$TriggerIcon iconName={IconName.Triangle} open={open} /> + ); return ( - - - + <$WithDetailsReceipt detailItems={withReceiptItems} side="bottom"> + <$Popover open={open} onOpenChange={setOpen} slotTrigger={Trigger} @@ -72,26 +75,23 @@ export const SearchSelectMenu = ({ fullWidth noBlur > - setOpen(false)} withStickyLayout $withSearch={withSearch} /> - - - + + + ); }; - -const Styled: Record = {}; - -Styled.SearchSelectMenu = styled.div` +const $SearchSelectMenu = styled.div` ${layoutMixins.column} `; -Styled.MenuTrigger = styled.div` +const $MenuTrigger = styled.div` height: var(--form-input-height); ${layoutMixins.spacedRow} @@ -103,7 +103,7 @@ Styled.MenuTrigger = styled.div` } `; -Styled.WithLabel = styled(WithLabel)` +const $WithLabel = styled(WithLabel)` ${formMixins.inputLabel} label { @@ -111,7 +111,7 @@ Styled.WithLabel = styled(WithLabel)` } `; -Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` +const $WithDetailsReceipt = styled(WithDetailsReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); abbr { @@ -119,7 +119,7 @@ Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` } `; -Styled.Popover = styled(Popover)` +const $Popover = styled(Popover)` max-height: 30vh; margin-top: 1rem; border: var(--border-width) solid var(--color-layer-6); @@ -128,7 +128,10 @@ Styled.Popover = styled(Popover)` box-shadow: none; `; -Styled.ComboboxMenu = styled(ComboboxMenu)<{ $withSearch?: boolean }>` +type comboboxMenuStyleProps = { $withSearch?: boolean }; +const ComboboxMenuStyleType = getSimpleStyledOutputType(ComboboxMenu, {} as comboboxMenuStyleProps); + +const $ComboboxMenu = styled(ComboboxMenu)` ${layoutMixins.withInnerHorizontalBorders} --comboboxMenu-backgroundColor: var(--color-layer-4); @@ -151,9 +154,9 @@ Styled.ComboboxMenu = styled(ComboboxMenu)<{ $withSearch?: boolean }>` border-radius: 0.5rem; max-height: 30vh; overflow: auto; -`; +` as typeof ComboboxMenuStyleType; -Styled.TriggerIcon = styled(Icon)<{ open?: boolean }>` +const $TriggerIcon = styled(Icon)<{ open?: boolean }>` width: 0.625rem; height: 0.375rem; color: var(--color-text-0); diff --git a/src/components/SelectMenu.stories.tsx b/src/components/SelectMenu.stories.tsx index 168b9e8fa..70e6a9970 100644 --- a/src/components/SelectMenu.stories.tsx +++ b/src/components/SelectMenu.stories.tsx @@ -1,11 +1,13 @@ import { useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + import type { Story } from '@ladle/react'; +import styled from 'styled-components'; + +import { layoutMixins } from '@/styles/layoutMixins'; -import { SelectMenu, SelectItem } from '@/components/SelectMenu'; +import { SelectItem, SelectMenu } from '@/components/SelectMenu'; import { StoryWrapper } from '.ladle/components'; -import { layoutMixins } from '@/styles/layoutMixins'; const exampleItems: { value: string; label: string }[] = [ { @@ -32,7 +34,7 @@ export const SelectMenuStory: Story[0]> = (args) = return ( - + <$Container> {exampleItems.map(({ value, label }) => ( @@ -44,14 +46,11 @@ export const SelectMenuStory: Story[0]> = (args) = ))} - + ); }; - -const Styled: Record = {}; - -Styled.Container = styled.section` +const $Container = styled.section` background: var(--color-layer-3); ${layoutMixins.container} diff --git a/src/components/SelectMenu.tsx b/src/components/SelectMenu.tsx index 2ef14ae5b..9fe82c11b 100644 --- a/src/components/SelectMenu.tsx +++ b/src/components/SelectMenu.tsx @@ -1,24 +1,24 @@ import React from 'react'; -import styled, { css, type AnyStyledComponent } from 'styled-components'; +import { CheckIcon } from '@radix-ui/react-icons'; import { - Root, - Value, - Viewport, - Trigger, Content, Item, - ItemText, ItemIndicator, + ItemText, Portal, - Icon as SelectIcon, + Root, + Trigger, + Value, + Viewport, } from '@radix-ui/react-select'; -import { CheckIcon } from '@radix-ui/react-icons'; +import styled from 'styled-components'; -import { popoverMixins } from '@/styles/popoverMixins'; import { formMixins } from '@/styles/formMixins'; +import { popoverMixins } from '@/styles/popoverMixins'; import { WithLabel } from '@/components/WithLabel'; + import { Icon, IconName } from './Icon'; export const SelectMenu = ({ @@ -38,24 +38,24 @@ export const SelectMenu = ({ }) => { return ( - + <$Trigger className={className} $withBlur={withBlur}> {label ? ( - + <$WithLabel label={label}> - + ) : ( )} {React.Children.toArray(children).length > 1 && ( - + <$TriggerIcon iconName={IconName.Triangle} /> )} - + - + <$Content className={className}> {/* */} {children} {/* */} - + ); @@ -68,19 +68,16 @@ export const SelectItem = ({ }: { className?: string; value: T; - label: string; + label: React.ReactNode; }) => ( - + <$Item className={className} value={value}> {label} - + <$ItemIndicator> - - + + ); - -const Styled: Record = {}; - -Styled.Trigger = styled(Trigger)<{ $withBlur?: boolean }>` +const $Trigger = styled(Trigger)<{ $withBlur?: boolean }>` --select-menu-trigger-maxWidth: ; max-width: var(--select-menu-trigger-maxWidth); ${popoverMixins.trigger} @@ -94,7 +91,7 @@ Styled.Trigger = styled(Trigger)<{ $withBlur?: boolean }>` } `; -Styled.Content = styled(Content)` +const $Content = styled(Content)` --select-menu-content-maxWidth: ; max-width: var(--select-menu-content-maxWidth); @@ -102,22 +99,22 @@ Styled.Content = styled(Content)` ${popoverMixins.popoverAnimation} `; -Styled.Item = styled(Item)` +const $Item = styled(Item)` ${popoverMixins.item} `; -Styled.ItemIndicator = styled(ItemIndicator)` +const $ItemIndicator = styled(ItemIndicator)` margin-left: auto; display: inline-flex; transition: transform 0.3s var(--ease-out-expo); `; -Styled.WithLabel = styled(WithLabel)` +const $WithLabel = styled(WithLabel)` ${formMixins.inputLabel} border-radius: 0; `; -Styled.TriggerIcon = styled(Icon)` +const $TriggerIcon = styled(Icon)` width: 0.625rem; height: 0.375rem; color: var(--color-text-0); diff --git a/src/components/Separator.tsx b/src/components/Separator.tsx index 64c6e6c37..a8f23e121 100644 --- a/src/components/Separator.tsx +++ b/src/components/Separator.tsx @@ -1,7 +1,7 @@ import { Fragment } from 'react'; -import styled, { css } from 'styled-components'; import { Separator } from '@radix-ui/react-separator'; +import styled, { css } from 'styled-components'; const StyledSeparator = styled(Separator)` flex: 0 !important; diff --git a/src/components/Slider.tsx b/src/components/Slider.tsx new file mode 100644 index 000000000..e77db2086 --- /dev/null +++ b/src/components/Slider.tsx @@ -0,0 +1,103 @@ +import { Root, Thumb, Track } from '@radix-ui/react-slider'; +import styled from 'styled-components'; + +type ElementProps = { + value: number; + label?: string; + onSliderDrag: ([value]: number[]) => void; + onValueCommit: ([value]: number[]) => void; + min?: number; + max?: number; + step?: number; +}; + +type StyleProps = { className?: string }; + +export const Slider = ({ + className, + label = 'slider', + value, + onSliderDrag, + onValueCommit, + min, + max, + step = 0.1, +}: ElementProps & StyleProps) => ( + <$Root + aria-label={label} + className={className} + min={min} + max={max} + step={step} + value={[value]} + onValueChange={onSliderDrag} + onValueCommit={onValueCommit} + > + <$Track /> + <$Thumb /> + +); +const $Root = styled(Root)` + // make thumb covers the start of the track + --radix-slider-thumb-transform: translateX(-65%) !important; + --slider-track-background: ; + --slider-track-backgroundColor: var(--color-layer-4); + + position: relative; + + display: flex; + align-items: center; + + user-select: none; + + height: 100%; +`; + +const $Track = styled(Track)` + position: relative; + + display: flex; + flex-grow: 1; + align-items: center; + + height: 0.5rem; + margin-right: 0.25rem; // make thumb covers the end of the track + + cursor: pointer; + background: var(--slider-track-background); + + &:before { + content: ''; + width: 100%; + height: 100%; + + background: linear-gradient( + 90deg, + transparent, + transparent 15%, + var(--slider-track-backgroundColor) 15%, + var(--slider-track-backgroundColor) 50%, + transparent 50%, + transparent 65%, + var(--slider-track-backgroundColor) 65% + ) + 0 0 / 0.6rem; + } +`; + +const $Thumb = styled(Thumb)` + height: 1.375rem; + width: 1.375rem; + + display: flex; + justify-content: center; + align-items: center; + + background-color: var(--color-layer-6); + opacity: 0.8; + + border: 1.5px solid var(--color-layer-7); + border-radius: 50%; + + cursor: grab; +`; diff --git a/src/components/StepIndicator.stories.tsx b/src/components/StepIndicator.stories.tsx index bca960b05..13a159675 100644 --- a/src/components/StepIndicator.stories.tsx +++ b/src/components/StepIndicator.stories.tsx @@ -1,8 +1,7 @@ import type { Story } from '@ladle/react'; -import { StoryWrapper } from '.ladle/components'; - import { StepIndicator, type StepIndicatorProps } from './StepIndicator'; +import { StoryWrapper } from '.ladle/components'; export const StepIndicatorStory: Story = (args) => ( diff --git a/src/components/StepIndicator.tsx b/src/components/StepIndicator.tsx index c83618fa2..2d4ab2da6 100644 --- a/src/components/StepIndicator.tsx +++ b/src/components/StepIndicator.tsx @@ -1,4 +1,4 @@ -import styled, { css, type AnyStyledComponent } from 'styled-components'; +import styled, { css } from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -14,23 +14,16 @@ type StyleProps = { export type StepIndicatorProps = ElementProps & StyleProps; export const StepIndicator = ({ className, currentStepIndex, totalSteps }: StepIndicatorProps) => ( - {[...Array(totalSteps)].map((_, i) => ( - + <$Step key={i} isActive={i === currentStepIndex - 1} isFilled={i <= currentStepIndex - 1} /> ))} - + ); - -const Styled: Record = {}; - -Styled.StepIndicator = styled.div<{ progress: number }>` +const $StepIndicator = styled.div<{ progress: number }>` --stepIndicator-line-backgroundColor: var(--color-layer-4); --stepIndicator-step-backgroundColor: var(--color-layer-4); --stepIndicator-active-step-boxShadowColor: hsla(240, 32%, 21%, 1); @@ -65,7 +58,7 @@ Styled.StepIndicator = styled.div<{ progress: number }>` } `; -Styled.Step = styled.div<{ isActive?: boolean; isFilled?: boolean }>` +const $Step = styled.div<{ isActive?: boolean; isFilled?: boolean }>` width: 0.5em; height: 0.5em; border-radius: 50%; diff --git a/src/components/Switch.stories.tsx b/src/components/Switch.stories.tsx index a36602575..ea87e6e54 100644 --- a/src/components/Switch.stories.tsx +++ b/src/components/Switch.stories.tsx @@ -1,19 +1,18 @@ import { useState } from 'react'; + import type { Story } from '@ladle/react'; import styled from 'styled-components'; -import { StoryWrapper } from '.ladle/components'; - import { Switch } from '@/components/Switch'; -type SwitchProps = Parameters; +import { StoryWrapper } from '.ladle/components'; -export const SwitchStory: Story = (args: SwitchProps) => { +export const SwitchStory: Story[0]> = (args) => { const [checked, setChecked] = useState(false); return ( - - + + ); }; diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index 103f93335..1f56c9afa 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -1,6 +1,5 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; - import { Root, Thumb } from '@radix-ui/react-switch'; +import styled from 'styled-components'; type ElementProps = { checked: boolean; @@ -24,7 +23,7 @@ export const Switch = ({ required, value, }: ElementProps & StyleProps) => ( - - - + <$Thumb /> + ); - -const Styled: Record = {}; - -Styled.Root = styled(Root)` +const $Root = styled(Root)` --switch-width: 2.625em; --switch-height: 1.5em; --switch-backgroundColor: var(--color-layer-0); @@ -67,7 +63,7 @@ Styled.Root = styled(Root)` } `; -Styled.Thumb = styled(Thumb)` +const $Thumb = styled(Thumb)` width: calc(var(--switch-width) / 2); height: calc(var(--switch-height) - 0.1875em); diff --git a/src/components/Table.tsx b/src/components/Table.tsx index d9dc80a70..e2bd20dcc 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,62 +1,74 @@ -import React, { Fragment, Key, useCallback, useEffect, useMemo, useState } from 'react'; -import styled, { css, type AnyStyledComponent } from 'styled-components'; +import React, { Key, useCallback, useEffect, useState } from 'react'; import { - useTable, - useTableCell, - useTableColumnHeader, - useTableRow, - useTableHeaderRow, - useTableRowGroup, - useTableSelectAllCheckbox, - useTableSelectionCheckbox, - mergeProps, - useFocusRing, - useCollator, -} from 'react-aria'; - -import { type ColumnSize, type TableCollection } from '@react-types/table'; -import { type GridNode } from '@react-types/grid'; - -import type { Node, SortDescriptor, SortDirection, CollectionChildren } from '@react-types/shared'; - -import { - Cell, - // CollectionBuilderContext, + Cell, // CollectionBuilderContext, Column, Row, TableBody, TableHeader, - type TableState, useTableState, + type TableState, } from '@react-stately/table'; - +import { type GridNode } from '@react-types/grid'; +import type { CollectionChildren, Node, SortDescriptor, SortDirection } from '@react-types/shared'; +import { type ColumnSize, type TableCollection } from '@react-types/table'; +import { isFunction } from 'lodash'; +import { + mergeProps, + useCollator, + useFocusRing, + useTable, + useTableCell, + useTableColumnHeader, + useTableHeaderRow, + useTableRow, + useTableRowGroup, +} from 'react-aria'; import { useAsyncList } from 'react-stately'; +import styled, { css } from 'styled-components'; -import { useBreakpoints, useStringGetter } from '@/hooks'; -import { MediaQueryKeys } from '@/hooks/useBreakpoints'; +import { MediaQueryKeys, useBreakpoints } from '@/hooks/useBreakpoints'; +import { useTablePagination } from '@/hooks/useTablePagination'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; -import { CaretIcon } from '@/icons'; - -import { STRING_KEYS } from '@/constants/localization'; import { MustBigNumber } from '@/lib/numbers'; import { Icon, IconName } from './Icon'; +import { PAGE_SIZES, PageSize, TablePaginationRow } from './Table/TablePaginationRow'; import { Tag } from './Tag'; -import { Button } from './Button'; +export { ActionsTableCell } from './Table/ActionsTableCell'; + +// TODO: fix circular dependencies +// eslint-disable-next-line import/no-cycle +export { AssetTableCell } from './Table/AssetTableCell'; + +// TODO: remove barrel files: https://www.npmjs.com/package/eslint-plugin-no-barrel-files +// Reasoning why: https://dev.to/tassiofront/barrel-files-and-why-you-should-stop-using-them-now-bc4 +// eslint-disable-next-line import/no-cycle +export { MarketTableCell } from './Table/MarketTableCell'; export { TableCell } from './Table/TableCell'; export { TableColumnHeader } from './Table/TableColumnHeader'; -export { MarketTableCell } from './Table/MarketTableCell'; export type CustomRowConfig = { key: string; slotCustomRow: (..._: Parameters) => React.ReactNode; }; +function isCustomRow( + v: TableRowData | CustomRowConfig +): v is CustomRowConfig { + return (v as any).slotCustomRow != null && isFunction((v as any).slotCustomRow); +} + +function isTableRowData( + v: TableRowData | CustomRowConfig +): v is TableRowData { + return !isCustomRow(v); +} + export type TableItem = { value: TableRowData; @@ -68,7 +80,9 @@ export type TableItem = { onSelect?: (key: TableRowData) => void; }; -export type ColumnDef = { +export type BaseTableRowData = {}; + +export type ColumnDef = { columnKey: string; label: React.ReactNode; tag?: React.ReactNode; @@ -83,29 +97,24 @@ export type ColumnDef = { width?: ColumnSize; }; -export type ElementProps = { +export type TableElementProps = { label?: string; columns: ColumnDef[]; - data: TableRowData[]; - getRowKey: (rowData: TableRowData, rowIndex?: number) => TableRowKey; + data: Array; + getRowKey: (rowData: TableRowData, rowIndex?: number) => Key; getRowAttributes?: (rowData: TableRowData, rowIndex?: number) => Record; - // shouldRowRender?: (prevRowData: object, currentRowData: object) => boolean; defaultSortDescriptor?: SortDescriptor; selectionMode?: 'multiple' | 'single'; selectionBehavior?: 'replace' | 'toggle'; - onRowAction?: (key: TableRowKey, row: TableRowData) => void; + onRowAction?: (key: Key, row: TableRowData) => void; slotEmpty?: React.ReactNode; - viewMoreConfig?: { - initialNumRowsToShow: number; - numRowsPerPage?: number; - }; - // collection: TableCollection; - // children: React.ReactNode; + initialPageSize?: PageSize; + paginationBehavior?: 'paginate' | 'showAll'; }; -type StyleProps = { +export type TableStyleProps = { hideHeader?: boolean; - withGradientCardRows?: boolean; + withGradientCardRows?: boolean; // TODO: CT-662 withFocusStickyRows?: boolean; withOuterBorder?: boolean; withInnerBorders?: boolean; @@ -117,7 +126,10 @@ type StyleProps = { export type TableConfig = TableItem[]; -export const Table = ({ +export type AllTableProps = + TableElementProps & TableStyleProps & { style?: { [customProp: string]: number } }; + +export const Table = ({ label = '', columns, data = [], @@ -128,11 +140,8 @@ export const Table = ({ selectionMode = 'single', selectionBehavior = 'toggle', slotEmpty, - viewMoreConfig, - // shouldRowRender, - - // collection, - // children, + initialPageSize = 10, + paginationBehavior = 'paginate', hideHeader = false, withGradientCardRows = false, withFocusStickyRows = false, @@ -142,20 +151,13 @@ export const Table = ({ withScrollSnapRows = false, className, style, -}: ElementProps & StyleProps) => { - const [selectedKeys, setSelectedKeys] = useState(new Set()); - const [numRowsToShow, setNumRowsToShow] = useState(viewMoreConfig?.initialNumRowsToShow); - const enableViewMore = viewMoreConfig !== undefined; - - const onViewMoreClick = () => { - if (!viewMoreConfig) return; - const { numRowsPerPage } = viewMoreConfig; - if (numRowsPerPage) { - setNumRowsToShow((prev) => (prev ?? 0) + numRowsPerPage); - } else { - setNumRowsToShow(data.length); - } - }; +}: AllTableProps) => { + const [selectedKeys, setSelectedKeys] = useState(new Set()); + + const { currentPage, pageSize, pages, setCurrentPage, setPageSize } = useTablePagination({ + initialPageSize, + totalRows: data.length, + }); const currentBreakpoints = useBreakpoints(); const shownColumns = columns.filter( @@ -165,22 +167,22 @@ export const Table = ({ const collator = useCollator(); const sortFn = ( - a: TableRowData, - b: TableRowData, + a: TableRowData | CustomRowConfig, + b: TableRowData | CustomRowConfig, sortColumn?: Key, sortDirection?: SortDirection ) => { if (!sortColumn) return 0; - const column = columns.find((column) => column.columnKey === sortColumn); - const first = column?.getCellValue(a); - const second = column?.getCellValue(b); + const column = columns.find((c) => c.columnKey === sortColumn); + const first = isCustomRow(a) ? 0 : column?.getCellValue(a); + const second = isCustomRow(b) ? 0 : column?.getCellValue(b); return ( // Compare the items by the sorted column - (isNaN(first as number) + (Number.isNaN(Number(first)) ? // String - collator.compare(first as string, second as string) + collator.compare(String(first), String(second)) : // Number MustBigNumber(first).comparedTo(MustBigNumber(second))) * // Flip the direction if descending order is specified. @@ -188,8 +190,15 @@ export const Table = ({ ); }; - const list = useAsyncList({ - getKey: getRowKey, + const internalGetRowKey = useCallback( + (row: TableRowData | CustomRowConfig) => { + return isCustomRow(row) ? row.key : getRowKey(row); + }, + [getRowKey] + ); + + const list = useAsyncList({ + getKey: internalGetRowKey, load: async ({ sortDescriptor }) => ({ items: sortDescriptor?.column ? data.sort((a, b) => sortFn(a, b, sortDescriptor?.column, sortDescriptor?.direction)) @@ -203,12 +212,15 @@ export const Table = ({ }), }); + // FIX: refactor table so we don't have to manually reload + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => list.reload(), [data]); const isEmpty = data.length === 0; + const shouldPaginate = paginationBehavior === 'paginate' && data.length > Math.min(...PAGE_SIZES); return ( - ({ getRowAttributes={getRowAttributes} onRowAction={ onRowAction && - ((key: TableRowKey) => onRowAction(key, data.find((row) => getRowKey(row) === key)!)) + ((key: Key) => + onRowAction( + key, + data.filter(isTableRowData).find((row) => internalGetRowKey(row) === key)! + )) } - numColumns={shownColumns.length} - onViewMoreClick={ - enableViewMore && numRowsToShow! < data.length ? onViewMoreClick : undefined - } - // shouldRowRender={shouldRowRender} hideHeader={hideHeader} withGradientCardRows={withGradientCardRows} withFocusStickyRows={withFocusStickyRows} @@ -241,6 +252,19 @@ export const Table = ({ withInnerBorders={withInnerBorders} withScrollSnapColumns={withScrollSnapColumns} withScrollSnapRows={withScrollSnapRows} + numColumns={shownColumns.length} + paginationRow={ + shouldPaginate ? ( + + ) : undefined + } > {(column) => ( @@ -257,12 +281,19 @@ export const Table = ({ )} - + pageSize + ? list.items.slice(currentPage * pageSize, (currentPage + 1) * pageSize) + : list.items + } + > {(item) => ( - + {(columnKey) => ( - - {columns.find((column) => column.columnKey === columnKey)?.renderCell?.(item)} + + {isTableRowData(item) && + columns.find((column) => column.columnKey === columnKey)?.renderCell?.(item)} )} @@ -270,29 +301,30 @@ export const Table = ({ ) : ( - {slotEmpty} + <$Empty withOuterBorder={withOuterBorder}>{slotEmpty} )} - + ); }; -const TableRoot = (props: { +// TODO: remove useless extends +const TableRoot = (props: { 'aria-label'?: string; sortDescriptor?: SortDescriptor; onSortChange?: (descriptor: SortDescriptor) => void; selectionMode: 'multiple' | 'single'; selectionBehavior: 'replace' | 'toggle'; - selectedKeys: Set; - setSelectedKeys: (selectedKeys: Set) => void; + selectedKeys: Set; + setSelectedKeys: (selectedKeys: Set) => void; getRowAttributes?: ( rowData: TableRowData, rowIndex?: number ) => Record>; - onRowAction?: (key: TableRowKey) => void; - // shouldRowRender?: (prevRowData: object, currentRowData: object) => boolean; + onRowAction?: (key: Key) => void; children: CollectionChildren; + numColumns: number; - onViewMoreClick?: () => void; + paginationRow?: React.ReactNode; hideHeader?: boolean; withGradientCardRows?: boolean; @@ -302,7 +334,22 @@ const TableRoot = { - const { selectionMode, selectionBehavior, numColumns, onViewMoreClick } = props; + const { + 'aria-label': ariaLabel, + selectionMode, + selectionBehavior, + getRowAttributes, + onRowAction, + numColumns, + paginationRow, + hideHeader, + withGradientCardRows, + withFocusStickyRows, + withOuterBorder, + withInnerBorders, + withScrollSnapColumns, + withScrollSnapRows, + } = props; const state = useTableState({ ...props, @@ -311,47 +358,44 @@ const TableRoot = (null); const { collection } = state; + const { gridProps } = useTable( { - 'aria-label': props['aria-label'], - onRowAction: props.onRowAction as (key: Key) => void, + 'aria-label': ariaLabel, + onRowAction, }, state, ref ); return ( - + {paginationRow && ( + <$Tfoot> + <$PaginationTr key="pagination"> + e.preventDefault()} + onPointerDown={(e) => e.preventDefault()} + > + {paginationRow} + + + + )} + ); }; @@ -430,14 +479,14 @@ const TableHeadRowGroup = ({ const { rowGroupProps } = useTableRowGroup(); return ( - + ); }; @@ -446,22 +495,22 @@ const TableBodyRowGroup = ({ withGradientCardRows, withInnerBorders, withOuterBorder, -}: { children: React.ReactNode } & StyleProps) => { +}: { children: React.ReactNode } & TableStyleProps) => { const { rowGroupProps } = useTableRowGroup(); return ( - {children} - + ); }; -const TableHeaderRow = ({ +const TableHeaderRow = ({ item, state, children, @@ -476,13 +525,13 @@ const TableHeaderRow = ({ const { rowProps } = useTableHeaderRow({ node: item }, state, ref); return ( - + <$Tr ref={ref} {...rowProps} withScrollSnapRows={withScrollSnapRows}> {children} - + ); }; -const TableColumnHeader = ({ +const TableColumnHeader = ({ column, state, withScrollSnapColumns, @@ -493,56 +542,40 @@ const TableColumnHeader = ({ }) => { const ref = React.useRef(null); const { columnHeaderProps } = useTableColumnHeader({ node: column }, state, ref); - const { isFocusVisible, focusProps } = useFocusRing(); + const { focusProps } = useFocusRing(); return ( - - + <$Row> {column.rendered} {column.props.allowsSorting && ( - + )} - - - ); -}; - -export const ViewMoreRow = ({ colSpan, onClick }: { colSpan: number; onClick: () => void }) => { - const stringGetter = useStringGetter(); - return ( - - e.preventDefault()} - onPointerDown={(e: MouseEvent) => e.preventDefault()} - > - } onClick={onClick}> - {stringGetter({ key: STRING_KEYS.VIEW_MORE })} - - - + + ); }; -export const TableRow = ({ +export const TableRow = ({ item, children, state, - // shouldRowRender, + hasRowAction, withGradientCardRows, withFocusStickyRows, withScrollSnapRows, @@ -551,7 +584,7 @@ export const TableRow = ({ item: TableCollection['rows'][number]; children: React.ReactNode; state: TableState; - // shouldRowRender?: (prevRowData: TableRowData, currentRowData: TableRowData) => boolean; + hasRowAction?: boolean; withGradientCardRows?: boolean; withFocusStickyRows?: boolean; withScrollSnapRows?: boolean; @@ -559,7 +592,7 @@ export const TableRow = ({ const ref = React.useRef(null); const selectionManager = state.selectionManager; const isSelected = selectionManager.isSelected(item.key); - const isClickable = selectionManager.selectionBehavior === 'toggle'; + const isClickable = selectionManager.selectionBehavior === 'toggle' && hasRowAction; const { rowProps, isPressed } = useTableRow( { @@ -569,32 +602,25 @@ export const TableRow = ({ ref ); - const { isFocusVisible, focusProps } = useFocusRing(); + const { focusProps } = useFocusRing(); return ( - {children} - + ); }; -// const TableRowMemo = React.memo( -// TableRow, -// (a, b) => !!b.shouldRowRender?.(a.item.value, b.item.value) -// ); - -const TableCell = ({ +const TableCell = ({ cell, state, isActionable, @@ -605,10 +631,10 @@ const TableCell = ({ }) => { const ref = React.useRef(null); const { gridCellProps } = useTableCell({ node: cell }, state, ref); - const { isFocusVisible, focusProps } = useFocusRing(); + const { focusProps } = useFocusRing(); return ( - ({ {/* */} {cell.rendered} {/* */} - + ); }; -// const TableSelectAllCell = ({ column, state }) => { -// const ref = React.useRef(null); -// const isSingleSelectionMode = state.selectionManager.selectionMode === 'single'; -// const { columnHeaderProps } = useTableColumnHeader({ node: column }, state, ref); -// const { checkboxProps } = useTableSelectAllCheckbox(state); - -// return ( -// -// {state.selectionManager.selectionMode === 'single' ? ( -// {inputProps['aria-label']} -// ) : ( -// -// )} -// -// ); -// }; - -// const TableCheckboxCell = ({ cell, state }: { cell; state }) => { -// const ref = React.useRef(null); -// const { gridCellProps } = useTableCell({ node: cell }, state, ref); -// const { checkboxProps } = useTableSelectionCheckbox({ key: cell.parentKey }, state); - -// return ( -// -// -// -// ); -// }; - -const Styled: Record = {}; - -Styled.TableWrapper = styled.div<{ +const $TableWrapper = styled.div<{ isEmpty: boolean; withGradientCardRows?: boolean; withOuterBorder: boolean; }>` // Params - --tableHeader-textColor: var(--color-text-0, inherit); - --tableHeader-backgroundColor: inherit; + --tableStickyRow-textColor: var(--color-text-0, inherit); + --tableStickyRow-backgroundColor: inherit; --table-header-height: 2rem; --tableRow-hover-backgroundColor: var(--color-layer-3); @@ -684,8 +673,6 @@ Styled.TableWrapper = styled.div<{ --table-lastColumn-cell-align: end; // start | center | end | var(--table-cell-align) --tableCell-padding: 0 1rem; - --tableViewMore-borderColor: inherit; - // Rules flex: 1; @@ -705,7 +692,7 @@ Styled.TableWrapper = styled.div<{ `} `; -Styled.Empty = styled.div<{ withOuterBorder: boolean }>` +const $Empty = styled.div<{ withOuterBorder: boolean }>` ${layoutMixins.column} height: 100%; @@ -718,18 +705,22 @@ Styled.Empty = styled.div<{ withOuterBorder: boolean }>` font: var(--font-base-book); `; -Styled.Table = styled.table<{ +type StyledTableStyleProps = { hideHeader?: boolean; - withGradientCardRows: boolean; - withOuterBorder: boolean; - withInnerBorders: boolean; - withSolidHeader: boolean; -}>` + withGradientCardRows?: boolean; + withOuterBorder?: boolean; + withInnerBorders?: boolean; + withSolidHeader?: boolean; +}; + +const $Table = styled.table` align-self: start; ${layoutMixins.stickyArea1} --stickyArea1-background: var(--color-layer-2); --stickyArea1-topHeight: var(--table-header-height); + --stickyArea1-bottomHeight: var(--table-header-height); + ${({ hideHeader }) => hideHeader && css` @@ -775,10 +766,10 @@ Styled.Table = styled.table<{ } `; -Styled.Tr = styled.tr<{ +const $Tr = styled.tr<{ isClickable?: boolean; withFocusStickyRows?: boolean; - withScrollSnapRows: boolean; + withScrollSnapRows?: boolean; }>` /* Computed */ --tableRow-currentBackgroundColor: var(--tableRow-backgroundColor); @@ -816,7 +807,7 @@ Styled.Tr = styled.tr<{ `} `; -Styled.Th = styled.th<{ withScrollSnapColumns: boolean }>` +const $Th = styled.th<{ withScrollSnapColumns?: boolean }>` // Computed --table-cell-currentAlign: var(--table-cell-align); @@ -838,7 +829,7 @@ Styled.Th = styled.th<{ withScrollSnapColumns: boolean }>` text-align: var(--table-cell-currentAlign); `; -Styled.Td = styled.td` +const $Td = styled.td` // Computed --table-cell-currentAlign: var(--table-cell-align); @@ -859,7 +850,7 @@ Styled.Td = styled.td` } `; -Styled.SortArrow = styled.span<{ sortDirection: 'ascending' | 'descending' }>` +const $SortArrow = styled.span<{ sortDirection?: 'ascending' | 'descending' }>` float: right; margin-left: auto; @@ -868,16 +859,16 @@ Styled.SortArrow = styled.span<{ sortDirection: 'ascending' | 'descending' }>` font-size: 0.375em; - ${Styled.Th}[aria-sort="none"] & { + ${$Th}[aria-sort="none"] & { visibility: hidden; } - ${Styled.Th}[aria-sort="ascending"] & { + ${$Th}[aria-sort="ascending"] & { transform: scaleY(-1); } `; -Styled.Thead = styled.thead` +const $Thead = styled.thead` ${layoutMixins.stickyHeader} scroll-snap-align: none; font: var(--font-mini-book); @@ -886,8 +877,8 @@ Styled.Thead = styled.thead` height: var(--stickyArea-topHeight); } - color: var(--tableHeader-textColor); - background-color: var(--tableHeader-backgroundColor); + color: var(--tableStickyRow-textColor); + background-color: var(--tableStickyRow-backgroundColor); ${({ withInnerBorders, withGradientCardRows }) => withInnerBorders && @@ -897,13 +888,25 @@ Styled.Thead = styled.thead` `} `; -Styled.Tbody = styled.tbody` +const $Tfoot = styled.tfoot` + ${layoutMixins.stickyFooter} + scroll-snap-align: none; + font: var(--font-mini-book); + + > * { + height: var(--stickyArea-bottomHeight); + } + + color: var(--tableStickyRow-textColor); + background-color: var(--tableStickyRow-backgroundColor); +`; + +const $Tbody = styled.tbody` ${layoutMixins.stickyArea2} font: var(--font-small-book); // If height is fixed with not enough rows to overflow, vertically center the rows - &:before, - &:after { + &:before { content: ''; display: table-row; } @@ -981,23 +984,11 @@ Styled.Tbody = styled.tbody` `} `; -Styled.Row = styled.div` +const $Row = styled.div` ${layoutMixins.inlineRow} padding: var(--tableCell-padding); `; -Styled.ViewMoreButton = styled(Button)` - --button-backgroundColor: var(--color-layer-2); - --button-textColor: var(--color-text-1); - - width: 100%; - - svg { - width: 0.675rem; - margin-left: 0.5ch; - } -`; - -Styled.ViewMoreTr = styled(Styled.Tr)` - --border-color: var(--tableViewMore-borderColor); +const $PaginationTr = styled.tr` + box-shadow: 0 calc(-1 * var(--border-width)) 0 0 var(--border-color); `; diff --git a/src/components/Table/ActionsTableCell.tsx b/src/components/Table/ActionsTableCell.tsx new file mode 100644 index 000000000..69236d4a8 --- /dev/null +++ b/src/components/Table/ActionsTableCell.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import styled, { css } from 'styled-components'; + +import { Toolbar } from '@/components/Toolbar'; + +type ElementProps = { + children: React.ReactNode; +}; + +export const ActionsTableCell = ({ children }: ElementProps) => ( + <$ActionsCell> + <$Toolbar $numChildren={React.Children.toArray(children).length}>{children} + +); +const $ActionsCell = styled.div` + display: flex; + justify-content: flex-end; +`; + +const $Toolbar = styled(Toolbar)<{ $numChildren: number }>` + ${({ $numChildren }) => + $numChildren && + css` + width: calc(${$numChildren} * 2rem + (${$numChildren} - 1) * 0.5rem); + `} + + display: flex; + justify-content: flex-end; + padding: 0; + + > *:not(:last-child) { + margin-right: 0.5rem; + } +`; diff --git a/src/components/Table/AssetTableCell.tsx b/src/components/Table/AssetTableCell.tsx new file mode 100644 index 000000000..aab160df0 --- /dev/null +++ b/src/components/Table/AssetTableCell.tsx @@ -0,0 +1,52 @@ +import styled from 'styled-components'; + +import type { Asset } from '@/constants/abacus'; + +import { breakpoints } from '@/styles'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { TableCell } from '@/components/Table'; +import { Tag } from '@/components/Tag'; + +interface AssetTableCellProps { + asset?: Asset; + className?: string; + stacked?: boolean; +} + +export const AssetTableCell = (props: AssetTableCellProps) => { + const { asset, stacked, className } = props; + + return ( + }> + <$TableCellContent stacked={stacked}> + <$Asset stacked={stacked}>{asset?.name} + {stacked ? <$AssetID>{asset?.id} : {asset?.id}} + + + ); +}; +const $TableCellContent = styled.div<{ stacked?: boolean }>` + gap: ${({ stacked }) => (stacked ? '0.125rem' : '0.75rem')}; + display: flex; + flex-direction: ${({ stacked }) => (stacked ? 'column' : 'row')}; + align-items: ${({ stacked }) => (stacked ? 'flex-start' : 'center')}; +`; + +const $AssetIcon = styled(AssetIcon)<{ stacked?: boolean }>` + font-size: ${({ stacked }) => (stacked ? '1.5rem' : '2rem')}; + + @media ${breakpoints.tablet} { + font-size: ${({ stacked }) => (stacked ? '1.5rem' : '2.25rem')}; + } +`; + +const $AssetID = styled.span` + color: var(--color-text-0); + font: var(--font-mini-medium); +`; + +const $Asset = styled.span<{ stacked?: boolean }>` + color: var(--color-text-1); + font: ${({ stacked }) => (stacked ? 'var(--font-small-medium)' : 'var(--font-medium-medium)')}; +`; diff --git a/src/components/Table/MarketTableCell.tsx b/src/components/Table/MarketTableCell.tsx index 4c6bd22ce..1354c5c60 100644 --- a/src/components/Table/MarketTableCell.tsx +++ b/src/components/Table/MarketTableCell.tsx @@ -1,41 +1,55 @@ -import styled, { type AnyStyledComponent, css } from 'styled-components'; +import styled from 'styled-components'; import type { Asset } from '@/constants/abacus'; + import { breakpoints } from '@/styles'; import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; import { TableCell } from '@/components/Table'; +import { Output, OutputType, ShowSign } from '../Output'; + export const MarketTableCell = ({ asset, marketId, + leverage, showFavorite, + isHighlighted, className, }: { asset?: Asset; marketId: string; + leverage?: number; showFavorite?: boolean; + isHighlighted?: boolean; className?: string; }) => ( {showFavorite && } - + <$AssetIcon symbol={asset?.id} /> } > - {asset?.name} - {marketId} + {leverage ? ( + <> + {marketId} + + + ) : ( + <> + <$Asset>{asset?.name} + {marketId} + + )} ); - -const Styled: Record = {}; - -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` font-size: 1.25rem; @media ${breakpoints.tablet} { @@ -43,7 +57,7 @@ Styled.AssetIcon = styled(AssetIcon)` } `; -Styled.Asset = styled.span` +const $Asset = styled.span` @media ${breakpoints.tablet} { color: var(--color-text-2); } diff --git a/src/components/Table/TableCell.tsx b/src/components/Table/TableCell.tsx index 7aa859676..f1b943148 100644 --- a/src/components/Table/TableCell.tsx +++ b/src/components/Table/TableCell.tsx @@ -1,6 +1,5 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled, { css } from 'styled-components'; -import { layoutMixins } from '@/styles/layoutMixins'; import { tableMixins } from '@/styles/tableMixins'; export const TableCell = ({ @@ -9,26 +8,41 @@ export const TableCell = ({ slotLeft, slotRight, stacked, + stackedWithSecondaryStyling = stacked, + isHighlighted, }: { className?: string; children?: React.ReactNode; slotLeft?: React.ReactNode; slotRight?: React.ReactNode; stacked?: boolean; + isHighlighted?: boolean; + stackedWithSecondaryStyling?: boolean; }) => ( - + <$CellContent isHighlighted={isHighlighted} className={className}> {slotLeft} - {stacked ? {children} : children} + {stacked || stackedWithSecondaryStyling ? ( + <$Column stackedWithSecondaryStyling={stackedWithSecondaryStyling}>{children} + ) : ( + children + )} {slotRight} - + ); - -const Styled: Record = {}; - -Styled.CellContent = styled.div` +const $CellContent = styled.div<{ isHighlighted?: boolean }>` ${tableMixins.cellContent} + + ${({ isHighlighted }) => + isHighlighted && + css` + --primary-content-color: var(--color-text-2); + --secondary-content-color: var(--color-text-1); + `} `; -Styled.Column = styled.div` - ${tableMixins.cellContentColumn} +const $Column = styled.div<{ stackedWithSecondaryStyling?: boolean }>` + ${({ stackedWithSecondaryStyling }) => + stackedWithSecondaryStyling + ? tableMixins.cellContentColumnSecondary + : tableMixins.cellContentColumn} `; diff --git a/src/components/Table/TableColumnHeader.tsx b/src/components/Table/TableColumnHeader.tsx index 152cfa112..2bf0fefeb 100644 --- a/src/components/Table/TableColumnHeader.tsx +++ b/src/components/Table/TableColumnHeader.tsx @@ -1,4 +1,4 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { tableMixins } from '@/styles/tableMixins'; @@ -8,10 +8,7 @@ export const TableColumnHeader = ({ }: { className?: string; children?: React.ReactNode; -}) => {children}; - -const Styled: Record = {}; - -Styled.HeaderCellContent = styled.div` +}) => <$HeaderCellContent className={className}>{children}; +const $HeaderCellContent = styled.div` ${tableMixins.headerCellContent} `; diff --git a/src/components/Table/TablePaginationRow.tsx b/src/components/Table/TablePaginationRow.tsx new file mode 100644 index 000000000..fd023bdc4 --- /dev/null +++ b/src/components/Table/TablePaginationRow.tsx @@ -0,0 +1,128 @@ +import { Dispatch, SetStateAction } from 'react'; + +import styled from 'styled-components'; + +import { ButtonAction, ButtonShape, ButtonSize } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { type MenuItem } from '@/constants/menus'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { DropdownSelectMenu } from '@/components/DropdownSelectMenu'; +import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { ToggleGroup } from '@/components/ToggleGroup'; + +export const PAGE_SIZES = [5, 10, 15, 20] as const; +export type PageSize = (typeof PAGE_SIZES)[number]; + +type ElementProps = { + currentPage: number; + pageSize: PageSize; + pages: MenuItem[]; + totalRows: number; + setCurrentPage: Dispatch>; + setPageSize: Dispatch>; +}; + +export const TablePaginationRow = ({ + currentPage, + pageSize, + pages, + totalRows, + setCurrentPage, + setPageSize, +}: ElementProps) => { + const stringGetter = useStringGetter(); + + const pageNumberToDisplay = (pageNumber: number) => String(pageNumber + 1); + const pageToggles = () => { + const buttonProps = { + action: ButtonAction.Navigation, + shape: ButtonShape.Square, + size: ButtonSize.XSmall, + }; + + return ( + <$InlineRow> + setCurrentPage(currentPage - 1)} + state={{ isDisabled: currentPage === 0 }} + /> + <$ToggleGroup + {...buttonProps} + value={pageNumberToDisplay(currentPage)} + items={pages} + onValueChange={(pageNumber: string) => setCurrentPage(Number(pageNumber) - 1)} + /> + setCurrentPage(currentPage + 1)} + state={{ + isDisabled: pageNumberToDisplay(currentPage) === pages[pages.length - 1]?.value, + }} + /> + + ); + }; + + const pageSizeSelector = () => ( + <$InlineRow> + {stringGetter({ + key: STRING_KEYS.SHOW, + params: { + NUMBER: ( + <$DropdownSelectMenu + value={String(pageSize)} + items={PAGE_SIZES.map((size) => ({ + label: String(size), + value: String(size), + }))} + onValueChange={(value: String) => setPageSize(Number(value) as PageSize)} + /> + ), + }, + })} + + ); + + return ( + <$PaginationRow> + {stringGetter({ + key: STRING_KEYS.SHOWING_NUM_OUT_OF_TOTAL, + params: { + START: currentPage * pageSize + 1, + END: Math.min((currentPage + 1) * pageSize, totalRows), + TOTAL: totalRows, + }, + })} + {pageToggles()} + {pageSizeSelector()} + + ); +}; + +const $InlineRow = styled.div` + ${layoutMixins.inlineRow} +`; + +const $PaginationRow = styled.div` + ${layoutMixins.spacedRow} + padding: var(--tableCell-padding) +`; + +const $ToggleGroup = styled(ToggleGroup)` + [data-disabled] { + border: none; + background-color: transparent; + } +`; + +const $DropdownSelectMenu = styled(DropdownSelectMenu)` + --dropdownSelectMenu-item-font-size: var(--fontSize-mini); +`; diff --git a/src/components/Tabs.stories.tsx b/src/components/Tabs.stories.tsx index 68fcf562a..5a8f85301 100644 --- a/src/components/Tabs.stories.tsx +++ b/src/components/Tabs.stories.tsx @@ -1,5 +1,5 @@ import type { Story } from '@ladle/react'; -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -16,9 +16,9 @@ enum TabItem { export const TabsStory: Story[0]> = (args) => { return ( - + <$Container> - + ); }; @@ -51,10 +51,7 @@ TabsStory.argTypes = { defaultValue: TabItem.Item1, }, }; - -const Styled: Record = {}; - -Styled.Container = styled.section` +const $Container = styled.section` background: var(--color-layer-3); width: 400px; diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 9012991d2..fe0ac7e25 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -1,12 +1,13 @@ import { type ReactNode } from 'react'; -import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; + import { Content, List, Root, Trigger } from '@radix-ui/react-tabs'; +import styled, { css, keyframes } from 'styled-components'; + +import { type MenuItem } from '@/constants/menus'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; -import { type MenuItem } from '@/constants/menus'; - import { DropdownSelectMenu } from '@/components/DropdownSelectMenu'; import { Tag } from '@/components/Tag'; import { Toolbar } from '@/components/Toolbar'; @@ -31,7 +32,7 @@ type ElementProps = { slotToolbar?: ReactNode; sharedContent?: ReactNode; onValueChange?: (value: TabItemsValue) => void; - onWheel?: (event: WheelEvent) => void; + onWheel?: (event: React.WheelEvent) => void; }; type StyleProps = { @@ -62,74 +63,71 @@ export const Tabs = ({ const triggers = ( <> - + <$List $fullWidthTabs={fullWidthTabs} $withBorders={withBorders}> {items.map((item) => !item.subitems ? ( item.customTrigger ?? ( - + <$Trigger key={item.value} value={item.value} $withBorders={withBorders}> {item.label} {item.tag && {item.tag}} {item.slotRight} - + ) ) : ( - []} value={value} onValueChange={onValueChange} align="end" $isActive={item.subitems.some((subitem) => subitem.value === value)} - slotTrigger={} + slotTrigger={<$DropdownTabTrigger value={value ?? ''} />} > {item.label} - + ) )} - + - {(currentItem?.slotToolbar || slotToolbar) && ( - {currentItem?.slotToolbar || slotToolbar} + {(currentItem?.slotToolbar ?? slotToolbar) && ( + {currentItem?.slotToolbar ?? slotToolbar} )} ); return ( - onValueChange(val as TabItemsValue) : undefined + } onWheel={onWheel} $side={side} $withInnerBorder={withBorders} > - {triggers} + <$Header $side={side}>{triggers} - {sharedContent ? ( - sharedContent - ) : ( - + {sharedContent ?? ( + <$Stack> {items.map(({ asChild, value, content, forceMount }) => ( - {content} - + ))} - + )} - + ); }; - -const Styled: Record = {}; - const tabTriggerStyle = css` ${layoutMixins.row} justify-content: center; @@ -148,7 +146,7 @@ const tabTriggerStyle = css` } `; -Styled.Root = styled(Root)<{ $side: 'top' | 'bottom'; $withInnerBorder?: boolean }>` +const $Root = styled(Root)<{ $side: 'top' | 'bottom'; $withInnerBorder?: boolean }>` /* Overrides */ --trigger-backgroundColor: var(--color-layer-2); --trigger-textColor: var(--color-text-0); @@ -196,7 +194,7 @@ Styled.Root = styled(Root)<{ $side: 'top' | 'bottom'; $withInnerBorder?: boolean } `; -Styled.Header = styled.header<{ $side: 'top' | 'bottom' }>` +const $Header = styled.header<{ $side: 'top' | 'bottom' }>` ${layoutMixins.contentSectionDetachedScrollable} ${({ $side }) => @@ -214,7 +212,7 @@ Styled.Header = styled.header<{ $side: 'top' | 'bottom' }>` justify-content: space-between; `; -Styled.List = styled(List)<{ $fullWidthTabs?: boolean; $withBorders?: boolean }>` +const $List = styled(List)<{ $fullWidthTabs?: boolean; $withBorders?: boolean }>` align-self: stretch; ${({ $withBorders }) => @@ -234,7 +232,7 @@ Styled.List = styled(List)<{ $fullWidthTabs?: boolean; $withBorders?: boolean }> `} `; -Styled.Trigger = styled(Trigger)<{ $withBorders?: boolean }>` +const $Trigger = styled(Trigger)<{ $withBorders?: boolean }>` ${({ $withBorders }) => $withBorders && css` @@ -244,13 +242,13 @@ Styled.Trigger = styled(Trigger)<{ $withBorders?: boolean }>` ${tabTriggerStyle} `; -Styled.Stack = styled.div` +const $Stack = styled.div` ${layoutMixins.stack} box-shadow: none; `; -Styled.Content = styled(Content)<{ $hide?: boolean; $withTransitions: boolean }>` +const $Content = styled(Content)<{ $hide?: boolean; $withTransitions: boolean }>` ${layoutMixins.flexColumn} outline: none; box-shadow: none; @@ -294,7 +292,7 @@ Styled.Content = styled(Content)<{ $hide?: boolean; $withTransitions: boolean }> } `; -Styled.DropdownTabTrigger = styled(Trigger)` +const $DropdownTabTrigger = styled(Trigger)` ${tabTriggerStyle} gap: 1ch; @@ -302,7 +300,7 @@ Styled.DropdownTabTrigger = styled(Trigger)` width: 100%; `; -Styled.DropdownSelectMenu = styled(DropdownSelectMenu)<{ $isActive?: boolean }>` +const $DropdownSelectMenu = styled(DropdownSelectMenu)<{ $isActive?: boolean }>` --trigger-radius: 0; ${({ $isActive }) => @@ -311,12 +309,14 @@ Styled.DropdownSelectMenu = styled(DropdownSelectMenu)<{ $isActive?: boolean }>` --trigger-textColor: var(--trigger-active-textColor); --trigger-backgroundColor: var(--trigger-active-backgroundColor); `} -`; +` as ( + props: { $isActive?: boolean } & React.ComponentProps> +) => ReactNode; export const MobileTabs = styled(Tabs)` --trigger-backgroundColor: transparent; --trigger-active-backgroundColor: transparent; - --tableHeader-backgroundColor: var(--color-layer-2); + --tableStickyRow-backgroundColor: var(--color-layer-2); --trigger-font: var(--font-extra-book); padding-bottom: 1rem; diff --git a/src/components/TimeoutButton.tsx b/src/components/TimeoutButton.tsx index 1f59548ed..8ffc0d270 100644 --- a/src/components/TimeoutButton.tsx +++ b/src/components/TimeoutButton.tsx @@ -1,10 +1,12 @@ -import { type ReactNode, useState, useEffect } from 'react'; +import { useEffect, useState, type ReactNode } from 'react'; import { ButtonAction, ButtonState } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { useNow, useStringGetter } from '@/hooks'; -import { Button, type ButtonStateConfig, type ButtonProps } from '@/components/Button'; +import { useNow } from '@/hooks/useNow'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Button, type ButtonProps, type ButtonStateConfig } from '@/components/Button'; type ElementProps = { timeoutInSeconds: number; diff --git a/src/components/TimoutButton.stories.tsx b/src/components/TimoutButton.stories.tsx index a8dbbc889..1fd9b14a2 100644 --- a/src/components/TimoutButton.stories.tsx +++ b/src/components/TimoutButton.stories.tsx @@ -1,15 +1,12 @@ import type { Story } from '@ladle/react'; -import { StoryWrapper } from '.ladle/components'; import { TimeoutButton, type TimeoutButtonProps } from './TimeoutButton'; +import { StoryWrapper } from '.ladle/components'; export const TimeoutButtonStory: Story = (args) => { return ( - alert('Timeout button clicked!')} - > + alert('Timeout button clicked!')}> Continue diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 94dfe800e..39aa20abb 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -1,15 +1,17 @@ -import { type MouseEvent, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, type MouseEvent } from 'react'; + +import { Action, Close, Root } from '@radix-ui/react-toast'; import styled, { keyframes } from 'styled-components'; -import { Root, Action, Close } from '@radix-ui/react-toast'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; -import { popoverMixins } from '@/styles/popoverMixins'; + import { breakpoints } from '@/styles'; +import { popoverMixins } from '@/styles/popoverMixins'; import { Notification, type NotificationProps } from '@/components/Notification'; -import { IconButton } from './IconButton'; import { IconName } from './Icon'; +import { IconButton } from './IconButton'; type ElementProps = { isOpen?: boolean; @@ -79,7 +81,7 @@ export const Toast = ({ iconName={IconName.Close} shape={ButtonShape.Circle} size={ButtonSize.XSmall} - onClick={(e: MouseEvent) => e.stopPropagation()} + onClick={(e: React.MouseEvent) => e.stopPropagation()} /> @@ -93,10 +95,10 @@ export const Toast = ({ slotTitleRight={slotTitleRight} slotDescription={slotDescription} slotAction={ - actionDescription && ( - <$Action asChild altText={actionAltText}> + Boolean(slotAction && actionAltText) && ( + {slotAction} - + ) } /> @@ -107,7 +109,7 @@ export const Toast = ({ const $Root = styled(Root)` // Params - --toast-transition-duration: 0.5s; + --toast-transition-duration: 0.3s; // Computed --x: var(--radix-toast-swipe-move-x, 0px); @@ -152,7 +154,7 @@ const $Root = styled(Root)` 33% { /* scale: 1.05; */ /* filter: brightness(120%); */ - filter: drop-shadow(0 0 var(--color-text-1)); + filter: drop-shadow(0 0 var(--color-text-0)); } `} calc(var(--toast-transition-duration) * 3) 0.1s; } @@ -261,7 +263,3 @@ const $CloseButton = styled(IconButton)` z-index: 2; } `; - -const $Action = styled(Action)` - margin-top: 0.5rem; -`; diff --git a/src/components/ToastArea.tsx b/src/components/ToastArea.tsx index 5aa9e980b..075d8fbf7 100644 --- a/src/components/ToastArea.tsx +++ b/src/components/ToastArea.tsx @@ -1,8 +1,5 @@ -import styled from 'styled-components'; - import { Provider, Viewport } from '@radix-ui/react-toast'; - -import { layoutMixins } from '@/styles/layoutMixins'; +import styled from 'styled-components'; type ElementProps = { swipeDirection: 'up' | 'down' | 'left' | 'right'; @@ -27,7 +24,7 @@ export const ToastArea = ({ swipeDirection, children, className }: ToastAreaProp const $ToastArea = styled.aside` // Params --toasts-gap: 0.5rem; - + // Rules z-index: 1; diff --git a/src/components/ToggleButton.stories.tsx b/src/components/ToggleButton.stories.tsx index 339b3d1c2..11b0c8b4e 100644 --- a/src/components/ToggleButton.stories.tsx +++ b/src/components/ToggleButton.stories.tsx @@ -2,8 +2,8 @@ import type { Story } from '@ladle/react'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; -import { StoryWrapper } from '.ladle/components'; import { ToggleButton } from './ToggleButton'; +import { StoryWrapper } from '.ladle/components'; export const ToggleButtonStory: Story[0]> = (args) => ( diff --git a/src/components/ToggleButton.tsx b/src/components/ToggleButton.tsx index c7f425408..98c994606 100644 --- a/src/components/ToggleButton.tsx +++ b/src/components/ToggleButton.tsx @@ -1,6 +1,7 @@ import { forwardRef } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; + import { Root } from '@radix-ui/react-toggle'; +import styled from 'styled-components'; import { BaseButton, type BaseButtonProps } from '@/components/BaseButton'; @@ -37,19 +38,16 @@ export const ToggleButton = forwardRef { return ( - + <$BaseButton ref={ref} disabled={disabled} {...buttonProps}> {slotLeft} {children} {slotRight} - + ); } ); - -const Styled: Record = {}; - -Styled.BaseButton = styled(BaseButton)` +const $BaseButton = styled(BaseButton)` --button-toggle-off-backgroundColor: var(--color-layer-3); --button-toggle-off-textColor: var(--color-text-0); --button-toggle-off-border: solid var(--border-width) var(--border-color); diff --git a/src/components/ToggleGroup.stories.tsx b/src/components/ToggleGroup.stories.tsx index 71ab558c5..7ca4e5e81 100644 --- a/src/components/ToggleGroup.stories.tsx +++ b/src/components/ToggleGroup.stories.tsx @@ -1,10 +1,11 @@ import { useState } from 'react'; + import type { Story } from '@ladle/react'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; -import { StoryWrapper } from '.ladle/components'; import { ToggleGroup } from './ToggleGroup'; +import { StoryWrapper } from '.ladle/components'; const ToggleGroupItems = [ { diff --git a/src/components/ToggleGroup.tsx b/src/components/ToggleGroup.tsx index 246515e1b..175a2ba04 100644 --- a/src/components/ToggleGroup.tsx +++ b/src/components/ToggleGroup.tsx @@ -1,19 +1,24 @@ -import { forwardRef, type Ref } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; -import { Root, Item } from '@radix-ui/react-toggle-group'; +import { type Ref } from 'react'; + +import { Item, Root } from '@radix-ui/react-toggle-group'; +import styled from 'styled-components'; -import { type MenuItem } from '@/constants/menus'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; -import { useBreakpoints } from '@/hooks'; +import { type MenuItem } from '@/constants/menus'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; + import { layoutMixins } from '@/styles/layoutMixins'; import { type BaseButtonProps } from '@/components/BaseButton'; import { ToggleButton } from '@/components/ToggleButton'; +import { forwardRefFn } from '@/lib/genericFunctionalComponentUtils'; + type ElementProps = { items: MenuItem[]; value: MenuItemValue; - onValueChange: (value: any) => void; + onValueChange: (value: MenuItemValue) => void; onInteraction?: () => void; ensureSelected?: boolean; }; @@ -22,7 +27,7 @@ type StyleProps = { className?: string; }; -export const ToggleGroup = forwardRef( +export const ToggleGroup = forwardRefFn( ( { items, @@ -42,7 +47,7 @@ export const ToggleGroup = forwardRef( const { isTablet } = useBreakpoints(); return ( - {items.map((item) => ( - + {item.slotBefore} @@ -67,14 +73,11 @@ export const ToggleGroup = forwardRef( ))} - + ); } ); - -const Styled: Record = {}; - -Styled.Root = styled(Root)` +const $Root = styled(Root)` ${layoutMixins.row} gap: 0.33em; `; diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 1da0ec510..978296c9d 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -1,9 +1,10 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import { Root } from '@radix-ui/react-toolbar'; +import styled from 'styled-components'; -import { Root, Button, Separator, Link, ToggleGroup, ToggleItem } from '@radix-ui/react-toolbar'; -import { WithSeparators } from './Separator'; import { layoutMixins } from '@/styles/layoutMixins'; +import { WithSeparators } from './Separator'; + type ElementProps = { children: React.ReactNode; }; @@ -20,7 +21,7 @@ export const Toolbar = ({ withSeparators = false, className, }: ElementProps & StyleProps) => ( - + <$Root className={className} layout={layout}> {children} @@ -34,12 +35,9 @@ export const Toolbar = ({ ))} */} - + ); - -const Styled: Record = {}; - -Styled.Root = styled(Root)<{ layout?: 'column' | 'row' }>` +const $Root = styled(Root)<{ layout?: 'column' | 'row' }>` ${({ layout }) => layout && { diff --git a/src/components/TriangleIndicator.stories.tsx b/src/components/TriangleIndicator.stories.tsx index 9e1f44f7e..2c77567ff 100644 --- a/src/components/TriangleIndicator.stories.tsx +++ b/src/components/TriangleIndicator.stories.tsx @@ -1,22 +1,24 @@ -import { useEffect, useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; +import { useState } from 'react'; + import type { Story } from '@ladle/react'; +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; -import { TriangleIndicator, TriangleIndicatorProps } from '@/components/TriangleIndicator'; +import { TriangleIndicator } from '@/components/TriangleIndicator'; -import { StoryWrapper } from '.ladle/components'; import { MustBigNumber } from '@/lib/numbers'; +import { StoryWrapper } from '.ladle/components'; + export const TriangleIndicatorStory: Story<{ value: number }> = (args) => { const [valueBN] = useState(MustBigNumber(args.value)); return ( - + <$Container> - + ); }; @@ -24,10 +26,7 @@ export const TriangleIndicatorStory: Story<{ value: number }> = (args) => { TriangleIndicatorStory.args = { value: 0, }; - -const Styled: Record = {}; - -Styled.Container = styled.section` +const $Container = styled.section` background: var(--color-layer-3); ${layoutMixins.container} diff --git a/src/components/TriangleIndicator.tsx b/src/components/TriangleIndicator.tsx index d3fd1221d..49560644b 100644 --- a/src/components/TriangleIndicator.tsx +++ b/src/components/TriangleIndicator.tsx @@ -1,5 +1,5 @@ -import styled, { AnyStyledComponent, css } from 'styled-components'; import BigNumber from 'bignumber.js'; +import styled, { AnyStyledComponent, css } from 'styled-components'; import { NumberSign } from '@/constants/numbers'; @@ -20,15 +20,12 @@ const getSign = (num: BigNumber) => export const TriangleIndicator = ({ className, value }: TriangleIndicatorProps) => { return ( - + <$TriangleIndicator className={className} sign={getSign(value)}> - + ); }; - -const Styled: Record = {}; - -Styled.TriangleIndicator = styled.div<{ sign: NumberSign }>` +const $TriangleIndicator = styled.div<{ sign: NumberSign }>` display: flex; align-items: center; height: 100%; diff --git a/src/components/UsageBars.stories.tsx b/src/components/UsageBars.stories.tsx index 0b2e32417..fe399bf1c 100644 --- a/src/components/UsageBars.stories.tsx +++ b/src/components/UsageBars.stories.tsx @@ -1,5 +1,5 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import type { Story } from '@ladle/react'; +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -9,19 +9,16 @@ import { StoryWrapper } from '.ladle/components'; export const UsageBarsStory: Story<{ value: number }> = (args) => ( - + <$Container> - + ); UsageBarsStory.args = { value: 0, }; - -const Styled: Record = {}; - -Styled.Container = styled.section` +const $Container = styled.section` ${layoutMixins.container} background: var(--color-layer-3); diff --git a/src/components/UsageBars.tsx b/src/components/UsageBars.tsx index 3b77a1700..a645b846a 100644 --- a/src/components/UsageBars.tsx +++ b/src/components/UsageBars.tsx @@ -1,4 +1,4 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { type RiskLevels } from '@/constants/abacus'; @@ -14,9 +14,9 @@ type StyleProps = { }; export const UsageBars = ({ value, className }: ElementProps & StyleProps) => ( - + <$UsageBars className={className} riskLevel={abacusHelper.leverageRiskLevel(value ?? 0)}> {Array.from({ length: 3 }, (_, i) => ( - ( active={i <= abacusHelper.leverageRiskLevel(value ?? 0).ordinal} /> ))} - + ); - -const Styled: Record = {}; - -Styled.UsageBars = styled.div<{ riskLevel: RiskLevels }>` +const $UsageBars = styled.div<{ riskLevel: RiskLevels }>` ${({ riskLevel }) => UsageColorFromRiskLevel(riskLevel)} width: 0.875rem; @@ -40,7 +37,7 @@ Styled.UsageBars = styled.div<{ riskLevel: RiskLevels }>` justify-content: space-between; `; -Styled.Bar = styled.div<{ active: boolean }>` +const $Bar = styled.div<{ active: boolean; style?: { [custom: string]: string | number } }>` --active-delay: calc(0.2s * calc(var(--i) + 1)); max-width: 3px; diff --git a/src/components/WithConfirmationPopover.stories.tsx b/src/components/WithConfirmationPopover.stories.tsx index 2032932ab..c43814b9c 100644 --- a/src/components/WithConfirmationPopover.stories.tsx +++ b/src/components/WithConfirmationPopover.stories.tsx @@ -1,4 +1,5 @@ -import { type ChangeEvent, useState } from 'react'; +import { useState, type ChangeEvent } from 'react'; + import type { Story } from '@ladle/react'; import { @@ -6,8 +7,8 @@ import { WithConfirmationPopoverProps, } from '@/components/WithConfirmationPopover'; -import { StoryWrapper } from '.ladle/components'; import { Input, InputType } from './Input'; +import { StoryWrapper } from '.ladle/components'; export const WithConfirmationPopoverStory: Story = (args) => { const [textValue, setTextValue] = useState(''); diff --git a/src/components/WithConfirmationPopover.tsx b/src/components/WithConfirmationPopover.tsx index eaae6108b..d20c351b1 100644 --- a/src/components/WithConfirmationPopover.tsx +++ b/src/components/WithConfirmationPopover.tsx @@ -2,15 +2,15 @@ import { forwardRef, type FormEvent, type FormEventHandler, - type MouseEventHandler, type ReactElement, type Ref, } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; import { Anchor, Content, Portal, Root, Trigger } from '@radix-ui/react-popover'; +import styled from 'styled-components'; import { ButtonType } from '@/constants/buttons'; + import { layoutMixins } from '@/styles/layoutMixins'; import { IconName } from '@/components/Icon'; @@ -19,7 +19,7 @@ import { IconButton } from '@/components/IconButton'; type ElementProps = { children?: ReactElement; asChild?: boolean; - onCancel?: MouseEventHandler | MouseEventHandler; + onCancel?: () => void; onConfirm?: FormEventHandler; open?: boolean; onOpenChange?: (open: boolean) => void; @@ -57,14 +57,14 @@ export const WithConfirmationPopover = forwardRef( - e.preventDefault()} + onOpenAutoFocus={(e: Event) => e.preventDefault()} > - { e.preventDefault(); e.stopPropagation(); @@ -73,22 +73,17 @@ export const WithConfirmationPopover = forwardRef( }} > {children} - - {onCancel && } - {onConfirm && ( - - )} - - - + <$ConfirmationButtons> + {onCancel && <$CancelButton iconName={IconName.Close} onClick={onCancel} />} + {onConfirm && <$ConfirmButton iconName={IconName.Check} type={ButtonType.Submit} />} + + + ) ); - -const Styled: Record = {}; - -Styled.Content = styled(Content)` +const $Content = styled(Content)` z-index: 1; &:focus-visible { @@ -96,24 +91,24 @@ Styled.Content = styled(Content)` } `; -Styled.Form = styled.form` +const $Form = styled.form` ${layoutMixins.column} gap: 0.25rem; `; -Styled.ConfirmationButtons = styled.div` +const $ConfirmationButtons = styled.div` ${layoutMixins.row}; justify-content: flex-end; gap: 0.25rem; `; -Styled.IconButton = styled(IconButton)` +const $IconButton = styled(IconButton)` --button-height: 1.25rem; --button-font: var(--font-tiny-book); `; -Styled.ConfirmButton = styled(Styled.IconButton)` +const $ConfirmButton = styled($IconButton)` --button-backgroundColor: hsla(203, 25%, 19%, 1); svg { @@ -121,7 +116,7 @@ Styled.ConfirmButton = styled(Styled.IconButton)` } `; -Styled.CancelButton = styled(Styled.IconButton)` +const $CancelButton = styled($IconButton)` --button-backgroundColor: hsla(296, 16%, 18%, 1); svg { diff --git a/src/components/WithDetailsReceipt.stories.tsx b/src/components/WithDetailsReceipt.stories.tsx index 730a78a81..43780c985 100644 --- a/src/components/WithDetailsReceipt.stories.tsx +++ b/src/components/WithDetailsReceipt.stories.tsx @@ -1,10 +1,9 @@ import type { Story } from '@ladle/react'; import { Button } from '@/components/Button'; - import { WithDetailsReceipt, WithDetailsReceiptProps } from '@/components/WithDetailsReceipt'; -import { type DetailsItem } from './Details'; +import { type DetailsItem } from './Details'; import { StoryWrapper } from '.ladle/components'; const detailItems: DetailsItem[] = [ diff --git a/src/components/WithDetailsReceipt.tsx b/src/components/WithDetailsReceipt.tsx index 44fb64eeb..ca5b1ad78 100644 --- a/src/components/WithDetailsReceipt.tsx +++ b/src/components/WithDetailsReceipt.tsx @@ -1,4 +1,4 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { Details, type DetailsItem } from '@/components/Details'; import { WithReceipt } from '@/components/WithReceipt'; @@ -27,15 +27,12 @@ export const WithDetailsReceipt = ({ className={className} hideReceipt={hideReceipt} side={side} - slotReceipt={detailItems && } + slotReceipt={detailItems && <$Details items={detailItems} />} > {children} ); - -const Styled: Record = {}; - -Styled.Details = styled(Details)` +const $Details = styled(Details)` --details-item-backgroundColor: var(--withReceipt-backgroundColor); padding: 0.375rem 0.75rem 0.25rem; diff --git a/src/components/WithHovercard.stories.tsx b/src/components/WithHovercard.stories.tsx new file mode 100644 index 000000000..5e2d506c0 --- /dev/null +++ b/src/components/WithHovercard.stories.tsx @@ -0,0 +1,40 @@ +import type { Story } from '@ladle/react'; + +import { tooltipStrings } from '@/constants/tooltips'; + +import { Button } from '@/components/Button'; +import { WithHovercard } from '@/components/WithHovercard'; + +import { StoryWrapper } from '.ladle/components'; + +export const Hovercard: Story[0]> = (args) => { + return ( + + Trigger} + slotButton={} + /> + + ); +}; + +Hovercard.args = {}; + +Hovercard.argTypes = { + align: { + options: ['start', 'center', 'end'], + control: { type: 'select' }, + defaultValue: 'start', + }, + side: { + options: ['top', 'bottom', 'left', 'right'], + control: { type: 'select' }, + defaultValue: 'top', + }, + hovercard: { + options: Object.keys(tooltipStrings), + control: { type: 'select' }, + defaultValue: Object.keys(tooltipStrings)[0], + }, +}; diff --git a/src/components/WithHovercard.tsx b/src/components/WithHovercard.tsx new file mode 100644 index 000000000..978fd3c06 --- /dev/null +++ b/src/components/WithHovercard.tsx @@ -0,0 +1,82 @@ +import type { ReactNode } from 'react'; + +import { Content, Portal, Root, Trigger } from '@radix-ui/react-hover-card'; +import styled from 'styled-components'; + +import { tooltipStrings } from '@/constants/tooltips'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { popoverMixins } from '@/styles/popoverMixins'; + +type ElementProps = { + hovercard?: keyof typeof tooltipStrings; + stringParams?: Record; + slotTrigger?: ReactNode; + slotButton?: ReactNode; +}; + +type StyleProps = { + className?: string; + align?: 'start' | 'center' | 'end'; + side?: 'top' | 'right' | 'bottom' | 'left'; +}; + +export const WithHovercard = ({ + hovercard, + stringParams, + slotTrigger, + slotButton, + className, + align, + side, +}: ElementProps & StyleProps) => { + const stringGetter = useStringGetter(); + + const getHovercardStrings = hovercard && tooltipStrings[hovercard]; + + let hovercardTitle; + let hovercardBody; + + if (getHovercardStrings) { + const { title, body } = getHovercardStrings({ + stringGetter, + stringParams, + }); + hovercardTitle = title; + hovercardBody = body; + } + + return ( + + {slotTrigger && {slotTrigger}} + + <$Content className={className} align={align} alignOffset={-16} side={side} sideOffset={8}> + {hovercardTitle && <$Title>{hovercardTitle}} + {hovercardBody &&

    {hovercardBody}

    } + {slotButton} + +
    +
    + ); +}; +const $Content = styled(Content)` + ${popoverMixins.popover} + --popover-backgroundColor: var(--color-layer-6); + + ${popoverMixins.popoverAnimation} + --popover-closed-height: auto; + + display: grid; + max-width: 30ch; + gap: 0.5rem; + padding: 0.75em; + + font-size: 0.8125em; + border-radius: 0.33rem; +`; + +const $Title = styled.h3` + font: var(--font-small-bold); + color: var(--color-text-2); +`; diff --git a/src/components/WithLabel.stories.tsx b/src/components/WithLabel.stories.tsx index 9e819bf0e..16db7ff02 100644 --- a/src/components/WithLabel.stories.tsx +++ b/src/components/WithLabel.stories.tsx @@ -1,21 +1,22 @@ import { useState } from 'react'; + import type { Story } from '@ladle/react'; +import styled from 'styled-components'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { Input, InputType } from '@/components/Input'; import { WithLabel } from '@/components/WithLabel'; import { StoryWrapper } from '.ladle/components'; -import styled, { type AnyStyledComponent } from 'styled-components'; -import { layoutMixins } from '@/styles/layoutMixins'; - export const WithLabelStory: Story[0]> = (args) => { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); return ( - + <$Column> ) => setFirstName(e.target.value)} @@ -32,7 +33,7 @@ export const WithLabelStory: Story[0]> = (args) => value={lastName} /> - + ); }; @@ -40,10 +41,7 @@ export const WithLabelStory: Story[0]> = (args) => WithLabelStory.args = { label: 'First Name', }; - -const Styled: Record = {}; - -Styled.Column = styled.div` +const $Column = styled.div` ${layoutMixins.column} gap: 1rem; `; diff --git a/src/components/WithLabel.tsx b/src/components/WithLabel.tsx index 83e5f4bd1..96d0ef012 100644 --- a/src/components/WithLabel.tsx +++ b/src/components/WithLabel.tsx @@ -1,4 +1,4 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -13,22 +13,19 @@ type StyleProps = { }; export const WithLabel = ({ label, inputID, children, className }: ElementProps & StyleProps) => ( - - {label} + <$WithLabel className={className}> + <$Label htmlFor={inputID}>{label} {children} - + ); - -const Styled: Record = {}; - -Styled.WithLabel = styled.div` +const $WithLabel = styled.div` --label-textColor: var(--color-text-1); display: grid; gap: 0.5rem; `; -Styled.Label = styled.label` +const $Label = styled.label` ${layoutMixins.inlineRow} font: var(--font-mini-book); color: var(--label-textColor); diff --git a/src/components/WithReceipt.stories.tsx b/src/components/WithReceipt.stories.tsx index e38f7c898..600f23724 100644 --- a/src/components/WithReceipt.stories.tsx +++ b/src/components/WithReceipt.stories.tsx @@ -1,7 +1,6 @@ import type { Story } from '@ladle/react'; import { Button } from '@/components/Button'; - import { WithReceipt } from '@/components/WithReceipt'; import { StoryWrapper } from '.ladle/components'; diff --git a/src/components/WithReceipt.tsx b/src/components/WithReceipt.tsx index 8b3c405b5..1794d3ae9 100644 --- a/src/components/WithReceipt.tsx +++ b/src/components/WithReceipt.tsx @@ -1,5 +1,6 @@ import { type ReactNode } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; + +import styled, { css } from 'styled-components'; type ElementProps = { slotReceipt?: ReactNode; @@ -23,23 +24,17 @@ export const WithReceipt = ({ return <>{children}; } - const receipt = {slotReceipt}; + const receipt = <$SlotReceipt>{slotReceipt}; return ( - + <$WithReceipt className={className} hideReceipt={hideReceipt}> {side === 'top' && receipt} {children} {side === 'bottom' && receipt} - + ); }; - -const Styled: Record = {}; - -Styled.WithReceipt = styled.div<{ hideReceipt?: boolean }>` +const $WithReceipt = styled.div<{ hideReceipt?: boolean }>` --withReceipt-backgroundColor: var(--color-layer-1); display: grid; @@ -51,11 +46,11 @@ Styled.WithReceipt = styled.div<{ hideReceipt?: boolean }>` css` background-color: transparent; - ${Styled.SlotReceipt} { + ${$SlotReceipt} { height: 0; opacity: 0; } `} `; -Styled.SlotReceipt = styled.div``; +const $SlotReceipt = styled.div``; diff --git a/src/components/WithSidebar.tsx b/src/components/WithSidebar.tsx index 676c63444..56dd24c63 100644 --- a/src/components/WithSidebar.tsx +++ b/src/components/WithSidebar.tsx @@ -1,16 +1,18 @@ import React from 'react'; + import { useDispatch, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent, keyframes } from 'styled-components'; +import styled, { keyframes } from 'styled-components'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; -import { ToggleButton } from '@/components/ToggleButton'; import { Icon, IconName } from '@/components/Icon'; +import { ToggleButton } from '@/components/ToggleButton'; import { setIsSidebarOpen } from '@/state/layout'; import { getIsSidebarOpen } from '@/state/layoutSelectors'; @@ -28,10 +30,10 @@ export const WithSidebar: React.FC = ({ children, sidebar }) = const stringGetter = useStringGetter(); return ( - + <$Container data-state={!sidebar ? 'none' : isSidebarOpen ? 'open' : 'closed'}> {sidebar && ( - - + <$TriggerButton shape={ButtonShape.Pill} size={isSidebarOpen ? ButtonSize.XSmall : ButtonSize.Base} isPressed={!isSidebarOpen} @@ -42,20 +44,17 @@ export const WithSidebar: React.FC = ({ children, sidebar }) = ) : ( )} - + - {sidebar} - + <$Sidebar data-state={isSidebarOpen ? 'open' : 'closed'}>{sidebar} + )} - {children} - + <$Content>{children} + ); }; - -const Styled: Record = {}; - -Styled.Container = styled.div` +const $Container = styled.div` /* Params */ --withSidebar-containerWidth: 100vw; --withSidebar-open-sidebarWidth: var(--sidebar-width); @@ -110,7 +109,7 @@ Styled.Container = styled.div` grid-template: var(--withSidebar-gridTemplate); `; -Styled.Side = styled.aside` +const $Side = styled.aside` grid-area: Side; ${layoutMixins.container} @@ -123,7 +122,7 @@ Styled.Side = styled.aside` ${layoutMixins.stack} `; -Styled.Sidebar = styled.div` +const $Sidebar = styled.div` --current-sidebar-width: var(--sidebar-width); ${layoutMixins.scrollArea} @@ -147,7 +146,7 @@ Styled.Sidebar = styled.div` } `; -Styled.TriggerButton = styled(ToggleButton)` +const $TriggerButton = styled(ToggleButton)` --button-toggle-on-backgroundColor: transparent; place-self: start end; @@ -191,7 +190,7 @@ Styled.TriggerButton = styled(ToggleButton)` } `; -Styled.Content = styled.article` +const $Content = styled.article` grid-area: Content; ${layoutMixins.contentContainerPage} diff --git a/src/components/WithTooltip.stories.tsx b/src/components/WithTooltip.stories.tsx index 5058d2c6b..38f6bacbd 100644 --- a/src/components/WithTooltip.stories.tsx +++ b/src/components/WithTooltip.stories.tsx @@ -1,9 +1,9 @@ import type { Story } from '@ladle/react'; -import { WithTooltip } from '@/components/WithTooltip'; - import { tooltipStrings } from '@/constants/tooltips'; +import { WithTooltip } from '@/components/WithTooltip'; + import { StoryWrapper } from '.ladle/components'; export const Tooltip: Story[0]> = (args) => { diff --git a/src/components/WithTooltip.tsx b/src/components/WithTooltip.tsx index a817f3a5b..d7a6c7038 100644 --- a/src/components/WithTooltip.tsx +++ b/src/components/WithTooltip.tsx @@ -1,22 +1,24 @@ import type { ReactNode } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; -import { Content, Portal, Provider, Root, Trigger, Arrow } from '@radix-ui/react-tooltip'; + +import { Arrow, Content, Portal, Provider, Root, Trigger } from '@radix-ui/react-tooltip'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { tooltipStrings } from '@/constants/tooltips'; -import { useStringGetter, useURLConfigs } from '@/hooks'; - -import { Icon, IconName } from '@/components/Icon'; -import { Link } from '@/components/Link'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; import { layoutMixins } from '@/styles/layoutMixins'; import { popoverMixins } from '@/styles/popoverMixins'; +import { Icon, IconName } from '@/components/Icon'; +import { Link } from '@/components/Link'; + type ElementProps = { tooltip?: keyof typeof tooltipStrings; tooltipString?: string; - stringParams?: Record; + stringParams?: Record; withIcon?: boolean; children?: ReactNode; slotTooltip?: ReactNode; @@ -66,38 +68,35 @@ export const WithTooltip = ({ - + <$Abbr> {children} - {withIcon && } - + {withIcon && <$Icon iconName={IconName.HelpCircle} />} + - + <$Content sideOffset={8} side={side} align={align} className={className} asChild> {slotTooltip ?? (
    {tooltipTitle &&
    {tooltipTitle}
    } {tooltipBody &&
    {tooltipBody}
    } {tooltipLearnMore && (
    - + <$LearnMore href={tooltipLearnMore}> {stringGetter({ key: STRING_KEYS.LEARN_MORE })} → - +
    )} - + <$Arrow />
    )} -
    +
    ); }; - -const Styled: Record = {}; - -Styled.Abbr = styled.abbr` +const $Abbr = styled.abbr` ${layoutMixins.inlineRow} text-decoration: underline dashed 0px; @@ -108,7 +107,7 @@ Styled.Abbr = styled.abbr` cursor: help; `; -Styled.Content = styled(Content)` +const $Content = styled(Content)` --tooltip-backgroundColor: var(--color-layer-4); --tooltip-backgroundColor: ${({ theme }) => theme.tooltipBackground}; @@ -138,7 +137,7 @@ Styled.Content = styled(Content)` } `; -Styled.Arrow = styled(Arrow)` +const $Arrow = styled(Arrow)` width: 0.75rem; height: 0.375rem; @@ -147,10 +146,10 @@ Styled.Arrow = styled(Arrow)` } `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` color: var(--color-text-0); `; -Styled.LearnMore = styled(Link)` +const $LearnMore = styled(Link)` --link-color: var(--color-accent); `; diff --git a/src/components/visx/AxisLabelOutput.tsx b/src/components/visx/AxisLabelOutput.tsx index fe0bd6cf7..654dd5764 100644 --- a/src/components/visx/AxisLabelOutput.tsx +++ b/src/components/visx/AxisLabelOutput.tsx @@ -1,24 +1,17 @@ -import styled, { css, type AnyStyledComponent } from 'styled-components'; +import styled, { css } from 'styled-components'; import { Output } from '../Output'; -type ElementProps = { - children: React.ReactNode; -} & Parameters[0]; +type ElementProps = Parameters[0]; type StyleProps = { accentColor?: string; }; -export const AxisLabelOutput = ({ children, accentColor, ...props }: ElementProps & StyleProps) => ( - - {children} - +export const AxisLabelOutput = ({ accentColor, ...props }: ElementProps & StyleProps) => ( + <$AxisLabelOutput accentColor={accentColor} {...props} /> ); - -const Styled: Record = {}; - -Styled.AxisLabelOutput = styled(Output)<{ accentColor?: string }>` +const $AxisLabelOutput = styled(Output)<{ accentColor?: string }>` --accent-color: var(--color-layer-6); ${({ accentColor }) => diff --git a/src/components/visx/SparklineChart.tsx b/src/components/visx/SparklineChart.tsx new file mode 100644 index 000000000..2fa8b7c7c --- /dev/null +++ b/src/components/visx/SparklineChart.tsx @@ -0,0 +1,79 @@ +import { curveNatural } from '@visx/curve'; +import { LinearGradient } from '@visx/gradient'; +import { ParentSize } from '@visx/responsive'; +import { Axis, LineSeries, XYChart, buildChartTheme, darkTheme } from '@visx/xychart'; +import styled from 'styled-components'; + +interface SparklineChartProps { + data: Datum[]; + positive: boolean; + xAccessor: (_: Datum) => number; + yAccessor: (_: Datum) => number; +} + +const theme = buildChartTheme({ + ...darkTheme, + colors: ['var(--color-positive)', 'var(--color-negative)'], + tickLength: 0, + gridColor: 'transparent', + gridColorDark: 'transparent', +}); + +export const SparklineChart = (props: SparklineChartProps) => { + const { data, positive, xAccessor, yAccessor } = props; + + return ( + <$ParentSize> + {({ height, width }: { width: number; height: number }) => ( + + + + + + + + )} + + ); +}; +const $ParentSize = styled(ParentSize)` + & > svg { + overflow: visible; + } +`; diff --git a/src/components/visx/TimeSeriesChart.tsx b/src/components/visx/TimeSeriesChart.tsx index 43adba270..d03d2c121 100644 --- a/src/components/visx/TimeSeriesChart.tsx +++ b/src/components/visx/TimeSeriesChart.tsx @@ -1,31 +1,34 @@ import React, { useEffect, useMemo, useState } from 'react'; -import styled, { AnyStyledComponent, keyframes } from 'styled-components'; - -import { allTimeUnits } from '@/constants/time'; -import { useBreakpoints } from '@/hooks'; -import { useAnimationFrame } from '@/hooks/useAnimationFrame'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { LinearGradient } from '@visx/gradient'; import { ParentSize } from '@visx/responsive'; +import type { ScaleConfig } from '@visx/scale'; import { - XYChart, Axis, - Grid, DataProvider, EventEmitterProvider, - LineSeries, GlyphSeries, - type Margin, + Grid, + LineSeries, + XYChart, type AxisScale, + type Margin, type TooltipContextType, } from '@visx/xychart'; -import type { ScaleConfig } from '@visx/scale'; -import { LinearGradient } from '@visx/gradient'; -import Tooltip from '@/components/visx/XYChartTooltipWithBounds'; import { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'; +import styled, { keyframes } from 'styled-components'; + +import { allTimeUnits } from '@/constants/time'; + +import { useAnimationFrame } from '@/hooks/useAnimationFrame'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import Tooltip from '@/components/visx/XYChartTooltipWithBounds'; -import { clamp, lerp, map } from '@/lib/math'; import { formatAbsoluteTime } from '@/lib/dateTime'; +import { clamp, lerp, map } from '@/lib/math'; import { objectEntries } from '@/lib/objectEntries'; import { XYChartThreshold, type Threshold } from './XYChartThreshold'; @@ -155,11 +158,17 @@ export const TimeSeriesChart = ({ useAnimationFrame( (elapsedMilliseconds) => { if (zoomDomainAnimateTo) { - setZoomDomain( - (zoomDomain) => - zoomDomain && - zoomDomain * (zoomDomainAnimateTo / zoomDomain) ** (elapsedMilliseconds * 0.0166) - ); + setZoomDomain((zoomDomain) => { + if (!zoomDomain) return zoomDomain; + + const newZoomDomain = + zoomDomain * (zoomDomainAnimateTo / zoomDomain) ** (elapsedMilliseconds * 0.01); + + // clamp according to direction + return zoomDomainAnimateTo > zoomDomain + ? Math.min(newZoomDomain, zoomDomainAnimateTo) + : Math.max(newZoomDomain, zoomDomainAnimateTo); + }); } }, [zoomDomainAnimateTo] @@ -167,7 +176,13 @@ export const TimeSeriesChart = ({ // Computations const { zoom, domain, range, visibleData } = useMemo(() => { - if (!zoomDomain) return {}; + if (!zoomDomain) + return { + zoom: 1, + domain: [0, 1] as [number, number], + range: [0, 1] as [number, number], + visibleData: data, + }; const zoom = zoomDomain / minZoomDomain; @@ -181,7 +196,6 @@ export const TimeSeriesChart = ({ ); const range = visibleData - .filter((datum) => xAccessor(datum) >= domain[0] && xAccessor(datum) <= domain[1]) .map((datum) => yAccessor(datum)) .reduce((range, y) => [Math.min(range[0], y), Math.max(range[1], y)] as const, [ Infinity, @@ -198,7 +212,9 @@ export const TimeSeriesChart = ({ }, [visibleData]); // Events - const onWheel = ({ deltaX, deltaY }: WheelEvent) => { + const onWheel = ({ deltaX, deltaY }: React.WheelEvent) => { + if (!zoomDomain) return; + setZoomDomain( clamp( Math.max(1e-320, Math.min(Number.MAX_SAFE_INTEGER, zoomDomain * Math.exp(deltaY / 1000))), @@ -213,7 +229,7 @@ export const TimeSeriesChart = ({ }; return ( - + <$Container onWheel={onWheel} className={className}> {data.length && zoomDomain ? ( ({ }} > - + <$ParentSize> {({ width, height }: { width: number; height: number }) => { const numTicksX = (width - (margin?.left ?? 0) - (margin?.right ?? 0)) / tickSpacingX; @@ -334,7 +350,7 @@ export const TimeSeriesChart = ({ {!isMobile && ( <> {margin?.left && margin.left > 0 && ( - + <$YAxisBackground x="0" y="0" width={margin.left} height="100%" /> )} ({ ); }} - + ) : ( @@ -404,13 +420,11 @@ export const TimeSeriesChart = ({ )} {children} - + ); }; -const Styled: Record = {}; - -Styled.Container = styled.div` +const $Container = styled.div` ${layoutMixins.stack} width: 0; min-width: 100%; @@ -449,7 +463,7 @@ Styled.Container = styled.div` } `; -Styled.ParentSize = styled(ParentSize)` +const $ParentSize = styled(ParentSize)` min-height: 0; display: grid; @@ -457,7 +471,7 @@ Styled.ParentSize = styled(ParentSize)` overscroll-behavior: contain; `; -Styled.YAxisBackground = styled.foreignObject` +const $YAxisBackground = styled.foreignObject` background: var(--stickyArea-background); /* Safari */ diff --git a/src/components/visx/TooltipContent.tsx b/src/components/visx/TooltipContent.tsx index 7b594f14d..d65728b10 100644 --- a/src/components/visx/TooltipContent.tsx +++ b/src/components/visx/TooltipContent.tsx @@ -1,4 +1,4 @@ -import styled, { css, type AnyStyledComponent } from 'styled-components'; +import styled, { css } from 'styled-components'; import { popoverMixins } from '@/styles/popoverMixins'; @@ -11,12 +11,9 @@ type StyleProps = { }; export const TooltipContent = ({ children, accentColor }: ElementProps & StyleProps) => ( - {children} + <$TooltipContent accentColor={accentColor}>{children} ); - -const Styled: Record = {}; - -Styled.TooltipContent = styled.aside<{ accentColor?: string }>` +const $TooltipContent = styled.aside<{ accentColor?: string }>` --accent-color: currentColor; ${({ accentColor }) => diff --git a/src/components/visx/XYChartThreshold.tsx b/src/components/visx/XYChartThreshold.tsx index c360b6ecc..e45050d1a 100644 --- a/src/components/visx/XYChartThreshold.tsx +++ b/src/components/visx/XYChartThreshold.tsx @@ -1,6 +1,7 @@ +import { useContext } from 'react'; + import { Threshold } from '@visx/threshold'; import { DataContext } from '@visx/xychart'; -import { useContext } from 'react'; /** A visx that scales based on the nearest . Use inside . */ export const XYChartThreshold = ({ @@ -12,14 +13,18 @@ export const XYChartThreshold = ({ const { xScale, yScale } = useContext(DataContext); return xScale && yScale ? ( - <> - - x={(datum, index, data) => xScale(typeof x === 'function' ? x(datum, index, data) : x) as number} - y0={(datum, index, data) => yScale(typeof y0 === 'function' ? y0(datum, index, data) : y0) as number} - y1={(datum, index, data) => yScale(typeof y1 === 'function' ? y1(datum, index, data) : y1) as number} - {...props} - /> - + + x={(datum, index, data) => + xScale(typeof x === 'function' ? x(datum, index, data) : x) as number + } + y0={(datum, index, data) => + yScale(typeof y0 === 'function' ? y0(datum, index, data) : y0) as number + } + y1={(datum, index, data) => + yScale(typeof y1 === 'function' ? y1(datum, index, data) : y1) as number + } + {...props} + /> ) : null; }; diff --git a/src/components/visx/XYChartTooltipWithBounds.tsx b/src/components/visx/XYChartTooltipWithBounds.tsx index c9633b25f..952be1633 100644 --- a/src/components/visx/XYChartTooltipWithBounds.tsx +++ b/src/components/visx/XYChartTooltipWithBounds.tsx @@ -1,16 +1,14 @@ // Forked from original XYChart Tooltip to use TooltipWithBounds instead of TooltipInPortal: // https://github.com/airbnb/visx/blob/master/packages/visx-xychart/src/components/Tooltip.tsx - import React, { Fragment, useCallback, useContext, useEffect } from 'react'; +import { Group } from '@visx/group'; +import { PickD3Scale } from '@visx/scale'; import { TooltipWithBounds } from '@visx/tooltip'; import type { TooltipProps as BaseTooltipProps } from '@visx/tooltip/lib/tooltips/Tooltip'; -import { PickD3Scale } from '@visx/scale'; -import { Group } from '@visx/group'; - import { - TooltipContext, DataContext, + TooltipContext, type GlyphProps as RenderGlyphProps, type TooltipContextType, } from '@visx/xychart'; @@ -66,7 +64,7 @@ export type TooltipProps = { onTooltipContext?: (tooltipContext: TooltipContextType) => void; }; -function DefaultGlyph(props: RenderTooltipGlyphProps) { +const DefaultGlyph = (props: RenderTooltipGlyphProps) => { const { theme } = useContext(DataContext) || {}; return ( @@ -81,13 +79,13 @@ function DefaultGlyph(props: RenderTooltipGlyphProps ); -} +}; function defaultRenderGlyph(props: RenderTooltipGlyphProps) { return ; } -function TooltipInner({ +const TooltipInner = ({ horizontalCrosshairStyle, glyphStyle, onTooltipContext, @@ -105,7 +103,7 @@ function TooltipInner({ snapCrosshairToDatumY = true, verticalCrosshairStyle, ...tooltipProps -}: TooltipProps) { +}: TooltipProps) => { const { colorScale, theme, @@ -293,7 +291,7 @@ function TooltipInner({ ) : null; -} +}; /** * This is a wrapper component which bails early if tooltip is not visible. diff --git a/src/components/visx/XYChartWithPointerEvents.tsx b/src/components/visx/XYChartWithPointerEvents.tsx index a35d2a3e5..14fcd5a6e 100644 --- a/src/components/visx/XYChartWithPointerEvents.tsx +++ b/src/components/visx/XYChartWithPointerEvents.tsx @@ -1,8 +1,9 @@ import React, { PropsWithChildren, useContext, useState } from 'react'; -import { Point } from '@visx/point'; import { localPoint } from '@visx/event'; -import { XYChart, DataContext, type EventHandlerParams } from '@visx/xychart'; +import { Point } from '@visx/point'; +import { DataContext, XYChart, type EventHandlerParams } from '@visx/xychart'; + import { getScaleBandwidth } from '@/components/visx/getScaleBandwidth'; export const XYChartWithPointerEvents = ({ diff --git a/src/components/visx/getScaleBandwidth.ts b/src/components/visx/getScaleBandwidth.ts index 700e234d2..1e95aee61 100644 --- a/src/components/visx/getScaleBandwidth.ts +++ b/src/components/visx/getScaleBandwidth.ts @@ -1,5 +1,4 @@ // https://github.com/airbnb/visx/blob/master/packages/visx-xychart/src/typeguards/isValidNumber.ts - import { AxisScale } from '@visx/axis'; export function getScaleBandwidth(scale?: Scale) { diff --git a/src/constants/abacus.ts b/src/constants/abacus.ts index ac44cf3fc..46154ddff 100644 --- a/src/constants/abacus.ts +++ b/src/constants/abacus.ts @@ -1,7 +1,8 @@ import Abacus, { kollections } from '@dydxprotocol/v4-abacus'; import { OrderSide } from '@dydxprotocol/v4-client-js'; -import { PositionSide, TradeTypes } from './trade'; + import { STRING_KEYS } from './localization'; +import { PositionSide, TradeTypes } from './trade'; export type Nullable = T | null | undefined; @@ -45,6 +46,10 @@ export type AbacusTrackingProtocol = Omit< Abacus.exchange.dydx.abacus.protocols.TrackingProtocol, '__doNotUseOrImplementIt' >; +export type AbacusLoggingProtocol = Omit< + Abacus.exchange.dydx.abacus.protocols.LoggingProtocol, + '__doNotUseOrImplementIt' +>; export type FileLocation = Abacus.exchange.dydx.abacus.protocols.FileLocation; export type ThreadingType = Abacus.exchange.dydx.abacus.protocols.ThreadingType; @@ -107,6 +112,7 @@ export type TradeInputs = Abacus.exchange.dydx.abacus.output.input.TradeInput; export type ClosePositionInputs = Abacus.exchange.dydx.abacus.output.input.ClosePositionInput; export type TradeInputSummary = Abacus.exchange.dydx.abacus.output.input.TradeInputSummary; export type TransferInputs = Abacus.exchange.dydx.abacus.output.input.TransferInput; +export type TriggerOrdersInputs = Abacus.exchange.dydx.abacus.output.input.TriggerOrdersInput; export type InputError = Abacus.exchange.dydx.abacus.output.input.ValidationError; export type TransferInputTokenResource = Abacus.exchange.dydx.abacus.output.input.TransferInputTokenResource; @@ -144,7 +150,7 @@ export type SubaccountTransfers = Abacus.exchange.dydx.abacus.output.SubaccountT // ------ Historical PnL ------ // export type SubAccountHistoricalPNL = Abacus.exchange.dydx.abacus.output.SubaccountHistoricalPNL; export type SubAccountHistoricalPNLs = Abacus.exchange.dydx.abacus.output.SubaccountHistoricalPNL[]; -export const HistoricalPnlPeriod = Abacus.exchange.dydx.abacus.protocols.HistoricalPnlPeriod; +export const HistoricalPnlPeriod = Abacus.exchange.dydx.abacus.state.manager.HistoricalPnlPeriod; const historicalPnlPeriod = [...HistoricalPnlPeriod.values()] as const; export type HistoricalPnlPeriods = (typeof historicalPnlPeriod)[number]; @@ -174,20 +180,34 @@ export const ClosePositionInputField = const closePositionInputFields = [...ClosePositionInputField.values()] as const; export type ClosePositionInputFields = (typeof closePositionInputFields)[number]; +// ------ Trigger Order Items ------ // +export const TriggerOrdersInputField = + Abacus.exchange.dydx.abacus.state.model.TriggerOrdersInputField; +const triggerOrdersInputFields = [...TriggerOrdersInputField.values()] as const; +export type TriggerOrdersInputFields = (typeof triggerOrdersInputFields)[number]; +export type TriggerOrdersInputPrice = Abacus.exchange.dydx.abacus.output.input.TriggerPrice; +export type TriggerOrdersTriggerOrder = Abacus.exchange.dydx.abacus.output.input.TriggerOrder; + export type ValidationError = Abacus.exchange.dydx.abacus.output.input.ValidationError; export const TradeInputErrorAction = Abacus.exchange.dydx.abacus.output.input.ErrorAction; export type AbacusOrderTypes = Abacus.exchange.dydx.abacus.output.input.OrderType; export type AbacusOrderSides = Abacus.exchange.dydx.abacus.output.input.OrderSide; +export type AbacusOrderTimeInForces = Abacus.exchange.dydx.abacus.output.input.OrderTimeInForce; export const AbacusOrderType = Abacus.exchange.dydx.abacus.output.input.OrderType; export const AbacusOrderSide = Abacus.exchange.dydx.abacus.output.input.OrderSide; +export const AbacusOrderTimeInForce = Abacus.exchange.dydx.abacus.output.input.OrderTimeInForce; export const AbacusPositionSide = Abacus.exchange.dydx.abacus.output.PositionSide; export type AbacusPositionSides = Abacus.exchange.dydx.abacus.output.PositionSide; +export const AbacusMarginMode = Abacus.exchange.dydx.abacus.output.input.MarginMode; + export type HumanReadablePlaceOrderPayload = Abacus.exchange.dydx.abacus.state.manager.HumanReadablePlaceOrderPayload; export type HumanReadableCancelOrderPayload = Abacus.exchange.dydx.abacus.state.manager.HumanReadableCancelOrderPayload; +export type HumanReadableTriggerOrdersPayload = + Abacus.exchange.dydx.abacus.state.manager.HumanReadableTriggerOrdersPayload; export type HumanReadableWithdrawPayload = Abacus.exchange.dydx.abacus.state.manager.HumanReadableWithdrawPayload; export type HumanReadableTransferPayload = @@ -209,6 +229,11 @@ export const RestrictionType = Abacus.exchange.dydx.abacus.output.Restriction; const restrictionTypes = [...RestrictionType.values()] as const; export type RestrictionTypes = (typeof restrictionTypes)[number]; +// ------ Compliance ------ // +export const ComplianceStatus = Abacus.exchange.dydx.abacus.output.ComplianceStatus; +export const ComplianceAction = Abacus.exchange.dydx.abacus.output.ComplianceAction; +export type Compliance = Abacus.exchange.dydx.abacus.output.Compliance; + // ------ Api data ------ // export const ApiData = Abacus.exchange.dydx.abacus.state.manager.ApiData; @@ -274,8 +299,8 @@ export const ORDER_STATUS_STRINGS: Record = { + [AbacusMarginMode.cross.name]: STRING_KEYS.CROSS, + [AbacusMarginMode.cross.rawValue]: STRING_KEYS.CROSS, + [AbacusMarginMode.isolated.name]: STRING_KEYS.ISOLATED, + [AbacusMarginMode.isolated.rawValue]: STRING_KEYS.ISOLATED, +}; + // Custom types involving Abacus export type NetworkConfig = Partial<{ diff --git a/src/constants/account.ts b/src/constants/account.ts index c353fe9c0..3ec81060f 100644 --- a/src/constants/account.ts +++ b/src/constants/account.ts @@ -44,4 +44,10 @@ export type EvmDerivedAddresses = { }; }; +export type Hdkey = { + mnemonic: string; + privateKey: Uint8Array | null; + publicKey: Uint8Array | null; +}; + export const AMOUNT_RESERVED_FOR_GAS_USDC = 0.1; diff --git a/src/constants/analytics.ts b/src/constants/analytics.ts index 1392cfdbb..8e47dc0cf 100644 --- a/src/constants/analytics.ts +++ b/src/constants/analytics.ts @@ -1,16 +1,18 @@ -import type { SupportedLocales } from './localization'; -import type { DydxNetwork } from './networks'; +import type { AbacusApiStatus, HumanReadablePlaceOrderPayload } from './abacus'; import type { OnboardingState, OnboardingSteps } from './account'; -import type { DydxAddress, WalletType, WalletConnectionType, EvmAddress } from './wallets'; import type { DialogTypes } from './dialogs'; +import type { SupportedLocales } from './localization'; +import type { DydxNetwork } from './networks'; +import { TransferNotificationTypes } from './notifications'; import type { TradeTypes } from './trade'; -import type { AbacusApiStatus, HumanReadablePlaceOrderPayload } from './abacus'; +import type { DydxAddress, EvmAddress, WalletConnectionType, WalletType } from './wallets'; // User properties export enum AnalyticsUserProperty { // Environment Locale = 'selectedLocale', Breakpoint = 'breakpoint', + Version = 'version', // Network Network = 'network', @@ -30,23 +32,25 @@ export type AnalyticsUserPropertyValue = T extends AnalyticsUserProperty.Breakpoint ? 'MOBILE' | 'TABLET' | 'DESKTOP_SMALL' | 'DESKTOP_MEDIUM' | 'DESKTOP_LARGE' | 'UNSUPPORTED' : T extends AnalyticsUserProperty.Locale - ? SupportedLocales - : // Network - T extends AnalyticsUserProperty.Network - ? DydxNetwork - : // Wallet - T extends AnalyticsUserProperty.WalletType - ? WalletType | undefined - : T extends AnalyticsUserProperty.WalletConnectionType - ? WalletConnectionType | undefined - : T extends AnalyticsUserProperty.WalletAddress - ? EvmAddress | DydxAddress | undefined - : // Account - T extends AnalyticsUserProperty.DydxAddress - ? DydxAddress | undefined - : T extends AnalyticsUserProperty.SubaccountNumber - ? number | undefined - : undefined; + ? SupportedLocales + : T extends AnalyticsUserProperty.Version + ? string | undefined + : // Network + T extends AnalyticsUserProperty.Network + ? DydxNetwork + : // Wallet + T extends AnalyticsUserProperty.WalletType + ? WalletType | undefined + : T extends AnalyticsUserProperty.WalletConnectionType + ? WalletConnectionType | undefined + : T extends AnalyticsUserProperty.WalletAddress + ? EvmAddress | DydxAddress | undefined + : // Account + T extends AnalyticsUserProperty.DydxAddress + ? DydxAddress | undefined + : T extends AnalyticsUserProperty.SubaccountNumber + ? number | undefined + : undefined; // Events export enum AnalyticsEvent { @@ -74,6 +78,7 @@ export enum AnalyticsEvent { TransferFaucetConfirmed = 'TransferFaucetConfirmed', TransferDeposit = 'TransferDeposit', TransferWithdraw = 'TransferWithdraw', + TransferNotification = 'TransferNotification', // Trading TradeOrderTypeSelected = 'TradeOrderTypeSelected', @@ -82,6 +87,12 @@ export enum AnalyticsEvent { TradeCancelOrder = 'TradeCancelOrder', TradeCancelOrderConfirmed = 'TradeCancelOrderConfirmed', + // Export CSV + ExportButtonClick = 'ExportCSVClick', + ExportTradesCheckboxClick = 'ExportTradesCheckboxClick', + ExportTransfersCheckboxClick = 'ExportTransfersCheckboxClick', + ExportDownloadClick = 'ExportDownloadClick', + // Notification NotificationAction = 'NotificationAction', } @@ -91,104 +102,145 @@ export type AnalyticsEventData = T extends AnalyticsEvent.AppStart ? {} : T extends AnalyticsEvent.NetworkStatus - ? { - status: (typeof AbacusApiStatus)['name']; - /** Last time indexer node was queried successfully */ - lastSuccessfulIndexerRpcQuery?: number; - /** Time elapsed since indexer node was queried successfully */ - elapsedTime?: number; - blockHeight?: number; - indexerBlockHeight?: number; - trailingBlocks?: number; - } - : // Navigation - T extends AnalyticsEvent.NavigatePage - ? { - path: string; - } - : T extends AnalyticsEvent.NavigateDialog - ? { - type: DialogTypes; - } - : T extends AnalyticsEvent.NavigateDialogClose - ? { - type: DialogTypes; - } - : T extends AnalyticsEvent.NavigateExternal - ? { - link: string; - } - : // Wallet - T extends AnalyticsEvent.ConnectWallet - ? { - walletType: WalletType; - walletConnectionType: WalletConnectionType; - } - : T extends AnalyticsEvent.DisconnectWallet - ? {} - : // Onboarding - T extends AnalyticsEvent.OnboardingStepChanged - ? { - state: OnboardingState; - step?: OnboardingSteps; - } - : T extends AnalyticsEvent.OnboardingAccountDerived - ? { - hasPreviousTransactions: boolean; - } - : // Transfers - T extends AnalyticsEvent.TransferFaucet - ? {} - : T extends AnalyticsEvent.TransferFaucetConfirmed - ? { - /** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */ - roundtripMs: number; - /** URL/IP of node the order was sent to */ - validatorUrl: string; - } - : T extends AnalyticsEvent.TransferDeposit - ? { - chainId?: string; - tokenAddress?: string; - tokenSymbol?: string; - } - : T extends AnalyticsEvent.TransferWithdraw - ? { - chainId?: string; - tokenAddress?: string; - tokenSymbol?: string; - } - : // Trading - T extends AnalyticsEvent.TradeOrderTypeSelected - ? { - type: TradeTypes; - } - : T extends AnalyticsEvent.TradePlaceOrder - ? HumanReadablePlaceOrderPayload & { - isClosePosition: boolean; - } - : T extends AnalyticsEvent.TradePlaceOrderConfirmed - ? { - /** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */ - roundtripMs: number; - /** URL/IP of node the order was sent to */ - validatorUrl: string; - } - : T extends AnalyticsEvent.TradeCancelOrder - ? {} - : T extends AnalyticsEvent.TradeCancelOrderConfirmed - ? { - /** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */ - roundtripMs: number; - /** URL/IP of node the order was sent to */ - validatorUrl: string; - } - : // Notifcation - T extends AnalyticsEvent.NotificationAction - ? { - type: string; - id: string; - } - : never; + ? { + status: (typeof AbacusApiStatus)['name']; + /** Last time indexer node was queried successfully */ + lastSuccessfulIndexerRpcQuery?: number; + /** Time elapsed since indexer node was queried successfully */ + elapsedTime?: number; + blockHeight?: number; + indexerBlockHeight?: number; + trailingBlocks?: number; + } + : // Navigation + T extends AnalyticsEvent.NavigatePage + ? { + path: string; + } + : T extends AnalyticsEvent.NavigateDialog + ? { + type: DialogTypes; + } + : T extends AnalyticsEvent.NavigateDialogClose + ? { + type: DialogTypes; + } + : T extends AnalyticsEvent.NavigateExternal + ? { + link: string; + } + : // Wallet + T extends AnalyticsEvent.ConnectWallet + ? { + walletType: WalletType; + walletConnectionType: WalletConnectionType; + } + : T extends AnalyticsEvent.DisconnectWallet + ? {} + : // Onboarding + T extends AnalyticsEvent.OnboardingStepChanged + ? { + state: OnboardingState; + step?: OnboardingSteps; + } + : T extends AnalyticsEvent.OnboardingAccountDerived + ? { + hasPreviousTransactions: boolean; + } + : // Transfers + T extends AnalyticsEvent.TransferFaucet + ? {} + : T extends AnalyticsEvent.TransferFaucetConfirmed + ? { + /** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */ + roundtripMs: number; + /** URL/IP of node the order was sent to */ + validatorUrl: string; + } + : T extends AnalyticsEvent.TransferDeposit + ? { + chainId?: string; + tokenAddress?: string; + tokenSymbol?: string; + slippage?: number; + gasFee?: number; + bridgeFee?: number; + exchangeRate?: number; + estimatedRouteDuration?: number; + toAmount?: number; + toAmountMin?: number; + } + : T extends AnalyticsEvent.TransferWithdraw + ? { + chainId?: string; + tokenAddress?: string; + tokenSymbol?: string; + slippage?: number; + gasFee?: number; + bridgeFee?: number; + exchangeRate?: number; + estimatedRouteDuration?: number; + toAmount?: number; + toAmountMin?: number; + } + : // Trading + T extends AnalyticsEvent.TradeOrderTypeSelected + ? { + type: TradeTypes; + } + : T extends AnalyticsEvent.TradePlaceOrder + ? HumanReadablePlaceOrderPayload & { + isClosePosition: boolean; + } + : T extends AnalyticsEvent.TradePlaceOrderConfirmed + ? { + /** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */ + roundtripMs: number; + /** URL/IP of node the order was sent to */ + validatorUrl: string; + } + : T extends AnalyticsEvent.TradeCancelOrder + ? {} + : T extends AnalyticsEvent.TradeCancelOrderConfirmed + ? { + /** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */ + roundtripMs: number; + /** URL/IP of node the order was sent to */ + validatorUrl: string; + } + : // Notifcation + T extends AnalyticsEvent.NotificationAction + ? { + type: string; + id: string; + } + : T extends AnalyticsEvent.TransferNotification + ? { + type: TransferNotificationTypes | undefined; + toAmount: number | undefined; + timeSpent: + | Record + | number + | undefined; + txHash: string; + status: 'new' | 'success' | 'error'; + triggeredAt: number | undefined; + } + : T extends AnalyticsEvent.ExportDownloadClick + ? { + trades: boolean; + transfers: boolean; + } + : T extends AnalyticsEvent.ExportTradesCheckboxClick + ? { + value: boolean; + } + : T extends AnalyticsEvent.ExportTransfersCheckboxClick + ? { + value: boolean; + } + : never; export const DEFAULT_TRANSACTION_MEMO = 'dYdX Frontend (web)'; +export const lastSuccessfulRestRequestByOrigin: Record = {}; +export const lastSuccessfulWebsocketRequestByOrigin: Record = {}; diff --git a/src/constants/candles.ts b/src/constants/candles.ts index 58447541e..5ab626376 100644 --- a/src/constants/candles.ts +++ b/src/constants/candles.ts @@ -1,4 +1,5 @@ import { ResolutionString } from 'public/tradingview/charting_library'; + import { timeUnits } from './time'; export interface Candle { diff --git a/src/constants/cctp.ts b/src/constants/cctp.ts new file mode 100644 index 000000000..84401061a --- /dev/null +++ b/src/constants/cctp.ts @@ -0,0 +1,11 @@ +import cctpTokens from '../../public/configs/cctp.json'; + +const CCTP_MAINNET_CHAINS = cctpTokens.filter((token) => !token.isTestnet); +const CCTP_MAINNET_CHAINS_NAMES_LOWER_CASE = CCTP_MAINNET_CHAINS.map((token) => + token.name.toLowerCase() +); + +// TODO: make a general capitalize util fn +export const CCTP_MAINNET_CHAIN_NAMES_CAPITALIZED = CCTP_MAINNET_CHAINS_NAMES_LOWER_CASE.map( + (tokenName) => tokenName[0].toUpperCase() + tokenName.slice(1) +); diff --git a/src/constants/charts.ts b/src/constants/charts.ts index 0f1c1a1a6..1ead9ddf3 100644 --- a/src/constants/charts.ts +++ b/src/constants/charts.ts @@ -1,4 +1,5 @@ import { OrderSide } from '@dydxprotocol/v4-client-js'; + import { FundingDirection } from './markets'; // ------ Depth Chart ------ // diff --git a/src/constants/compliance.ts b/src/constants/compliance.ts new file mode 100644 index 000000000..d7b3c1182 --- /dev/null +++ b/src/constants/compliance.ts @@ -0,0 +1,15 @@ +export enum ComplianceReason { + MANUAL = 'MANUAL', + US_GEO = 'US_GEO', + CA_GEO = 'CA_GEO', + SANCTIONED_GEO = 'SANCTIONED_GEO', + COMPLIANCE_PROVIDER = 'COMPLIANCE_PROVIDER', +} + +export enum ComplianceStates { + FULL_ACCESS = 'FUll_ACCESS', + READ_ONLY = 'READ_ONLY', + CLOSE_ONLY = 'CLOSE_ONLY', +} + +export const CLOSE_ONLY_GRACE_PERIOD = 7; diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index c624182cd..b43cde20e 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -1,5 +1,8 @@ export enum DialogTypes { + AdjustIsolatedMargin = 'AdjustIsolatedMargin', + AdjustTargetLeverage = 'AdjustTargetLeverage', ClosePosition = 'ClosePosition', + ComplianceConfig = 'ComplianceConfig', Deposit = 'Deposit', DisconnectWallet = 'DisconnectWallet', DisplaySettings = 'DisplaySettings', @@ -7,25 +10,30 @@ export enum DialogTypes { ExternalLink = 'ExternalLink', ExternalNavStride = 'ExternalNavStride', FillDetails = 'FillDetails', + GeoCompliance = 'GeoCompliance', Help = 'Help', ExternalNavKeplr = 'ExternalNavKeplr', + ManageFunds = 'ManageFunds', MnemonicExport = 'MnemonicExport', - MobileSignIn = 'MobileSignIn', MobileDownload = 'MobileDownload', + MobileSignIn = 'MobileSignIn', + NewMarketAgreement = 'NewMarketAgreement', + NewMarketMessageDetails = 'NewMarketMessageDetails', Onboarding = 'Onboarding', OrderDetails = 'OrderDetails', Preferences = 'Preferences', RateLimit = 'RateLimit', RestrictedGeo = 'RestrictedGeo', RestrictedWallet = 'RestrictedWallet', + SelectMarginMode = 'SelectMarginMode', Trade = 'Trade', + Triggers = 'Triggers', Transfer = 'Transfer', Withdraw = 'Withdraw', - ManageFunds = 'ManageFunds', - NewMarketMessageDetails = 'NewMarketMessageDetails', - NewMarketAgreement = 'NewMarketAgreement', + WithdrawalGated = 'WithdrawalGated', } export enum TradeBoxDialogTypes { ClosePosition = 'ClosePosition', + SelectMarginMode = 'SelectMarginMode', } diff --git a/src/constants/geo.ts b/src/constants/geo.ts new file mode 100644 index 000000000..6e7c951c0 --- /dev/null +++ b/src/constants/geo.ts @@ -0,0 +1,561 @@ +export enum CountryCodes { + AD = 'AD', + AE = 'AE', + AF = 'AF', + AG = 'AG', + AI = 'AI', + AL = 'AL', + AM = 'AM', + AO = 'AO', + AQ = 'AQ', + AR = 'AR', + AS = 'AS', + AT = 'AT', + AU = 'AU', + AW = 'AW', + AZ = 'AZ', + BA = 'BA', + BB = 'BB', + BD = 'BD', + BE = 'BE', + BF = 'BF', + BG = 'BG', + BH = 'BH', + BI = 'BI', + BJ = 'BJ', + BL = 'BL', + BM = 'BM', + BN = 'BN', + BO = 'BO', + BR = 'BR', + BS = 'BS', + BT = 'BT', + BV = 'BV', + BW = 'BW', + BY = 'BY', + BZ = 'BZ', + CA = 'CA', + CC = 'CC', + CD = 'CD', + CF = 'CF', + CG = 'CG', + CH = 'CH', + CI = 'CI', + CK = 'CK', + CL = 'CL', + CM = 'CM', + CN = 'CN', + CO = 'CO', + CR = 'CR', + CU = 'CU', + CV = 'CV', + CW = 'CW', + CX = 'CX', + CY = 'CY', + CZ = 'CZ', + DE = 'DE', + DJ = 'DJ', + DK = 'DK', + DM = 'DM', + DO = 'DO', + DZ = 'DZ', + EC = 'EC', + EE = 'EE', + EG = 'EG', + EH = 'EH', + ER = 'ER', + ES = 'ES', + ET = 'ET', + FI = 'FI', + FJ = 'FJ', + FK = 'FK', + FM = 'FM', + FO = 'FO', + FR = 'FR', + GA = 'GA', + GB = 'GB', + GD = 'GD', + GE = 'GE', + GG = 'GG', + GH = 'GH', + GI = 'GI', + GL = 'GL', + GM = 'GM', + GN = 'GN', + GQ = 'GQ', + GR = 'GR', + GS = 'GS', + GT = 'GT', + GU = 'GU', + GW = 'GW', + GY = 'GY', + HK = 'HK', + HM = 'HM', + HN = 'HN', + HR = 'HR', + HT = 'HT', + HU = 'HU', + ID = 'ID', + IE = 'IE', + IL = 'IL', + IM = 'IM', + IN = 'IN', + IO = 'IO', + IQ = 'IQ', + IR = 'IR', + IS = 'IS', + IT = 'IT', + JE = 'JE', + JM = 'JM', + JO = 'JO', + JP = 'JP', + KE = 'KE', + KG = 'KG', + KH = 'KH', + KI = 'KI', + KM = 'KM', + KN = 'KN', + KP = 'KP', + KR = 'KR', + KW = 'KW', + KY = 'KY', + KZ = 'KZ', + LA = 'LA', + LB = 'LB', + LC = 'LC', + LI = 'LI', + LK = 'LK', + LR = 'LR', + LS = 'LS', + LT = 'LT', + LU = 'LU', + LV = 'LV', + LY = 'LY', + MA = 'MA', + MC = 'MC', + MD = 'MD', + ME = 'ME', + MF = 'MF', + MG = 'MG', + MH = 'MH', + MK = 'MK', + ML = 'ML', + MM = 'MM', + MN = 'MN', + MO = 'MO', + MP = 'MP', + MR = 'MR', + MS = 'MS', + MT = 'MT', + MU = 'MU', + MV = 'MV', + MW = 'MW', + MX = 'MX', + MY = 'MY', + MZ = 'MZ', + NA = 'NA', + NC = 'NC', + NE = 'NE', + NF = 'NF', + NG = 'NG', + NI = 'NI', + NL = 'NL', + NO = 'NO', + NP = 'NP', + NR = 'NR', + NU = 'NU', + NZ = 'NZ', + OM = 'OM', + PA = 'PA', + PE = 'PE', + PF = 'PF', + PG = 'PG', + PH = 'PH', + PK = 'PK', + PL = 'PL', + PM = 'PM', + PN = 'PN', + PR = 'PR', + PT = 'PT', + PW = 'PW', + PY = 'PY', + QA = 'QA', + RO = 'RO', + RS = 'RS', + RU = 'RU', + RW = 'RW', + SA = 'SA', + SB = 'SB', + SC = 'SC', + SD = 'SD', + SE = 'SE', + SG = 'SG', + SH = 'SH', + SI = 'SI', + SJ = 'SJ', + SK = 'SK', + SL = 'SL', + SM = 'SM', + SN = 'SN', + SO = 'SO', + SR = 'SR', + SS = 'SS', + ST = 'ST', + SV = 'SV', + SX = 'SX', + SY = 'SY', + SZ = 'SZ', + TC = 'TC', + TD = 'TD', + TF = 'TF', + TG = 'TG', + TH = 'TH', + TJ = 'TJ', + TK = 'TK', + TL = 'TL', + TM = 'TM', + TN = 'TN', + TO = 'TO', + TR = 'TR', + TT = 'TT', + TV = 'TV', + TW = 'TW', + TZ = 'TZ', + UA = 'UA', + UG = 'UG', + UM = 'UM', + US = 'US', + UY = 'UY', + UZ = 'UZ', + VA = 'VA', + VC = 'VC', + VE = 'VE', + VG = 'VG', + VI = 'VI', + VN = 'VN', + VU = 'VU', + WF = 'WF', + WS = 'WS', + XK = 'XK', + YE = 'YE', + ZA = 'ZA', + ZM = 'ZM', + ZW = 'ZW', +} + +export const RESTRICTED_COUNTRIES = { + USA: CountryCodes.US, + CANADA: CountryCodes.CA, + + // OFAC Sanctioned + AFGHANISTAN: CountryCodes.AF, + BELARUS: CountryCodes.BY, + CENTRAL_AFRICAN_REPUBLIC: CountryCodes.CF, + CUBA: CountryCodes.CU, + DEM_REPUBLIC_CONGO: CountryCodes.CD, // Democratic Republic of Congo + ERITREA: CountryCodes.ER, // Eritrea + IRAN: CountryCodes.IR, + IRAQ: CountryCodes.IQ, + IVORY_COAST: CountryCodes.CI, // Cote D'Ivoire + LEBANON: CountryCodes.LB, + LIBERIA: CountryCodes.LR, + LIBYA: CountryCodes.LY, + MALI: CountryCodes.ML, + MYANMAR: CountryCodes.MM, // Burma + NICARAGUA: CountryCodes.NI, + NORTH_KOREA: CountryCodes.KP, // Democratic People's Republic of Korea + SOMALIA: CountryCodes.SO, + SOUTH_SUDAN: CountryCodes.SS, + SUDAN: CountryCodes.SD, // The Republic of the Sudan + SYRIA: CountryCodes.SY, // The Syrian Arab Republic + VENEZUELA: CountryCodes.VE, + ZIMBABWE: CountryCodes.ZW, +}; + +export const SOFT_BLOCKED_COUNTRIES = []; + +export const BLOCKED_COUNTRIES = [RESTRICTED_COUNTRIES.USA, RESTRICTED_COUNTRIES.CANADA]; + +export const OFAC_SANCTIONED_COUNTRIES = [ + RESTRICTED_COUNTRIES.AFGHANISTAN, + RESTRICTED_COUNTRIES.BELARUS, + RESTRICTED_COUNTRIES.CENTRAL_AFRICAN_REPUBLIC, + RESTRICTED_COUNTRIES.CUBA, + RESTRICTED_COUNTRIES.DEM_REPUBLIC_CONGO, + RESTRICTED_COUNTRIES.ERITREA, + RESTRICTED_COUNTRIES.IRAN, + RESTRICTED_COUNTRIES.IRAQ, + RESTRICTED_COUNTRIES.IVORY_COAST, + RESTRICTED_COUNTRIES.LEBANON, + RESTRICTED_COUNTRIES.LIBERIA, + RESTRICTED_COUNTRIES.LIBYA, + RESTRICTED_COUNTRIES.MALI, + RESTRICTED_COUNTRIES.MYANMAR, + RESTRICTED_COUNTRIES.NICARAGUA, + RESTRICTED_COUNTRIES.NORTH_KOREA, + RESTRICTED_COUNTRIES.SOMALIA, + RESTRICTED_COUNTRIES.SOUTH_SUDAN, + RESTRICTED_COUNTRIES.SUDAN, + RESTRICTED_COUNTRIES.SYRIA, + RESTRICTED_COUNTRIES.VENEZUELA, + RESTRICTED_COUNTRIES.ZIMBABWE, +]; + +export const COUNTRIES_MAP: { [country: string]: string } = { + 'Afghanistan (‫افغانستان‬‎)': CountryCodes.AF, + Akrotiri: CountryCodes.GB, + 'Albania (Shqipëri)': CountryCodes.AL, + 'Algeria (‫الجزائر‬‎)': CountryCodes.DZ, + 'American Samoa': CountryCodes.AS, + Andorra: CountryCodes.AD, + Angola: CountryCodes.AO, + Anguilla: CountryCodes.AI, + Antarctica: CountryCodes.AQ, + 'Antigua & Barbuda': CountryCodes.AG, + Argentina: CountryCodes.AR, + 'Armenia (Հայաստան)': CountryCodes.AM, + Aruba: CountryCodes.AW, + 'Ashmore and Cartier Islands': CountryCodes.AU, + Australia: CountryCodes.AU, + 'Austria (Österreich)': CountryCodes.AT, + 'Azerbaijan (Azərbaycan)': CountryCodes.AZ, + Bahamas: CountryCodes.BS, + 'Bahrain (‫البحرين‬‎)': CountryCodes.BH, + 'Baker Island': CountryCodes.UM, + 'Bangladesh (বাংলাদেশ)': CountryCodes.BD, + Barbados: CountryCodes.BB, + Belarus: CountryCodes.BY, + 'Belgium (België)': CountryCodes.BE, + Belize: CountryCodes.BZ, + 'Benin (Bénin)': CountryCodes.BJ, + Bermuda: CountryCodes.BM, + 'Bhutan (འབྲུག)': CountryCodes.BT, + Bolivia: CountryCodes.BO, + 'Bosnia-Herzegovina (Босна и Херцеговина)': CountryCodes.BA, + Botswana: CountryCodes.BW, + 'Bouvet Island': CountryCodes.BV, + 'Brazil (Brasil)': CountryCodes.BR, + 'British Indian Ocean Territory': CountryCodes.IO, + 'British Virgin Islands': CountryCodes.VG, + Brunei: CountryCodes.BN, + 'Bulgaria (България)': CountryCodes.BG, + 'Burkina Faso': CountryCodes.BF, + Burma: CountryCodes.MM, + 'Burundi (Uburundi)': CountryCodes.BI, + 'Cambodia (កម្ពុជា)': CountryCodes.KH, + 'Cameroon (Cameroun)': CountryCodes.CM, + Canada: CountryCodes.CA, + 'Cape Verde (Kabu Verdi)': CountryCodes.CV, + 'Cayman Islands': CountryCodes.KY, + 'Central African Republic (République centrafricaine)': CountryCodes.CF, + 'Chad (Tchad)': CountryCodes.TD, + Chile: CountryCodes.CL, + 'China (中国)': CountryCodes.CN, + 'Christmas Island': CountryCodes.CX, + 'Clipperton Island': CountryCodes.FR, + 'Cocos (Keeling) Islands': CountryCodes.CC, + Colombia: CountryCodes.CO, + 'Comoros (‫جزر القمر‬‎)': CountryCodes.KM, + 'Congo (Brazzaville)': CountryCodes.CG, + 'Congo (Kinshasa)': CountryCodes.CD, + 'Cook Islands': CountryCodes.CK, + 'Coral Sea Islands': CountryCodes.AU, + 'Costa Rica': CountryCodes.CR, + "Cote D'Ivoire (Ivory Coast)": CountryCodes.CI, + 'Croatia (Hrvatska)': CountryCodes.HR, + Cuba: CountryCodes.CU, + Curacao: CountryCodes.CW, + 'Cyprus (Κύπρος)': CountryCodes.CY, + 'Czech Republic (Česká republika)': CountryCodes.CZ, + 'Denmark (Danmark)': CountryCodes.DK, + Dhekelia: CountryCodes.GB, + Djibouti: CountryCodes.DJ, + Dominica: CountryCodes.DM, + 'Dominican Republic (República Dominicana)': CountryCodes.DO, + 'East Timor': CountryCodes.TL, + Ecuador: CountryCodes.EC, + 'Egypt (‫مصر‬‎)': CountryCodes.EG, + 'El Salvador': CountryCodes.SV, + 'Equatorial Guinea (Guinea Ecuatorial)': CountryCodes.GQ, + Eritrea: CountryCodes.ER, + 'Estonia (Eesti)': CountryCodes.EE, + Ethiopia: CountryCodes.ET, + 'Falkland Islands (Islas Malvinas)': CountryCodes.FK, + 'Faroe Islands (Føroyar)': CountryCodes.FO, + 'Federated States of Micronesia': CountryCodes.FM, + Fiji: CountryCodes.FJ, + 'Finland (Suomi)': CountryCodes.FI, + France: CountryCodes.FR, + 'French Polynesia (Polynésie française)': CountryCodes.PF, + 'French Southern and Antarctic Lands': CountryCodes.TF, + Gabon: CountryCodes.GA, + 'The Gambia': CountryCodes.GM, + 'Georgia (საქართველო)': CountryCodes.GE, + 'Germany (Deutschland)': CountryCodes.DE, + 'Ghana (Gaana)': CountryCodes.GH, + Gibraltar: CountryCodes.GI, + 'Greece (Ελλάδα)': CountryCodes.GR, + 'Greenland (Kalaallit Nunaat)': CountryCodes.GL, + Grenada: CountryCodes.GD, + Guam: CountryCodes.GU, + Guatemala: CountryCodes.GT, + Guernsey: CountryCodes.GG, + 'Guinea (Guinée)': CountryCodes.GN, + 'Guinea-Bissau (Guiné Bissau)': CountryCodes.GW, + Guyana: CountryCodes.GY, + Haiti: CountryCodes.HT, + 'Heard Island and McDonald Islands': CountryCodes.HM, + 'Holy See': CountryCodes.VA, + Honduras: CountryCodes.HN, + 'Hong Kong (香港)': CountryCodes.HK, + 'Howland Island': CountryCodes.UM, + 'Hungary (Magyarország)': CountryCodes.HU, + 'Iceland (Ísland)': CountryCodes.IS, + 'India (भारत)': CountryCodes.IN, + Indonesia: CountryCodes.ID, + Iran: CountryCodes.IR, + Iraq: CountryCodes.IQ, + Ireland: CountryCodes.IE, + 'Israel (‫ישראל‬‎)': CountryCodes.IL, + 'Italy (Italia)': CountryCodes.IT, + Jamaica: CountryCodes.JM, + 'Jan Mayen': CountryCodes.SJ, + 'Japan (日本)': CountryCodes.JP, + 'Jarvis Island': CountryCodes.UM, + Jersey: CountryCodes.JE, + 'Johnston Atoll': CountryCodes.UM, + 'Jordan (‫الأردن‬‎)': CountryCodes.JO, + 'Kazakhstan (Казахстан)': CountryCodes.KZ, + Kenya: CountryCodes.KE, + 'Kingman Reef': CountryCodes.UM, + Kiribati: CountryCodes.KI, + "Korea, Democratic People's Republic of (North)": CountryCodes.KP, + 'Korea, Republic of (South) (대한민국)': CountryCodes.KR, + Kosovo: CountryCodes.XK, + 'Kuwait (‫الكويت‬‎)': CountryCodes.KW, + 'Kyrgyzstan (Кыргызстан)': CountryCodes.KG, + 'Laos (ລາວ)': CountryCodes.LA, + 'Latvia (Latvija)': CountryCodes.LV, + 'Lebanon (‫لبنان‬‎)': CountryCodes.LB, + Lesotho: CountryCodes.LS, + Liberia: CountryCodes.LR, + 'Libya (‫ليبيا‬‎)': CountryCodes.LY, + Liechtenstein: CountryCodes.LI, + 'Lithuania (Lietuva)': CountryCodes.LT, + Luxembourg: CountryCodes.LU, + 'Macau (澳門)': CountryCodes.MO, + 'Macedonia (FYROM) (Македонија)': CountryCodes.MK, + 'Madagascar (Madagasikara)': CountryCodes.MG, + Malawi: CountryCodes.MW, + Malaysia: CountryCodes.MY, + Maldives: CountryCodes.MV, + Mali: CountryCodes.ML, + Malta: CountryCodes.MT, + 'Man, Isle of': CountryCodes.IM, + 'Marshall Islands': CountryCodes.MH, + 'Mauritania (‫موريتانيا‬‎)': CountryCodes.MR, + 'Mauritius (Moris)': CountryCodes.MU, + 'Mexico (México)': CountryCodes.MX, + 'Midway Islands': CountryCodes.UM, + 'Moldova (Republica Moldova)': CountryCodes.MD, + Monaco: CountryCodes.MC, + 'Mongolia (Монгол)': CountryCodes.MN, + 'Montenegro (Crna Gora)': CountryCodes.ME, + Montserrat: CountryCodes.MS, + 'Morocco (‫المغرب‬‎)': CountryCodes.MA, + 'Mozambique (Moçambique)': CountryCodes.MZ, + 'Namibia (Namibië)': CountryCodes.NA, + Nauru: CountryCodes.NR, + 'Navassa Island': CountryCodes.UM, + 'Nepal (नेपाल)': CountryCodes.NP, + 'Netherlands (Nederland)': CountryCodes.NL, + 'New Caledonia (Nouvelle-Calédonie)': CountryCodes.NC, + 'New Zealand': CountryCodes.NZ, + Nicaragua: CountryCodes.NI, + 'Niger (Nijar)': CountryCodes.NE, + Nigeria: CountryCodes.NG, + Niue: CountryCodes.NU, + 'Norfolk Island': CountryCodes.NF, + 'Northern Mariana Islands': CountryCodes.MP, + 'Norway (Norge)': CountryCodes.NO, + 'Oman (‫عُمان‬‎)': CountryCodes.OM, + 'Pakistan (‫پاکستان‬‎)': CountryCodes.PK, + Palau: CountryCodes.PW, + 'Palmyra Atoll': CountryCodes.UM, + 'Panama (Panamá)': CountryCodes.PA, + 'Papua-New Guinea': CountryCodes.PG, + Paraguay: CountryCodes.PY, + 'Peru (Perú)': CountryCodes.PE, + Philippines: CountryCodes.PH, + 'Pitcairn Islands': CountryCodes.PN, + 'Poland (Polska)': CountryCodes.PL, + Portugal: CountryCodes.PT, + 'Puerto Rico': CountryCodes.PR, + 'Qatar (‫قطر‬‎)': CountryCodes.QA, + 'Romania (România)': CountryCodes.RO, + 'Russia (Россия)': CountryCodes.RU, + Rwanda: CountryCodes.RW, + 'Saint Barthelemy': CountryCodes.BL, + 'Saint Martin (Saint-Martin (partie française))': CountryCodes.MF, + Samoa: CountryCodes.WS, + 'San Marino': CountryCodes.SM, + 'Sao Tome and Principe (São Tomé e Príncipe)': CountryCodes.ST, + 'Saudi Arabia (‫المملكة العربية السعودية‬‎)': CountryCodes.SA, + 'Senegal (Sénégal)': CountryCodes.SN, + 'Serbia (Србија)': CountryCodes.RS, + Seychelles: CountryCodes.SC, + 'Sierra Leone': CountryCodes.SL, + Singapore: CountryCodes.SG, + 'Sint Maarten': CountryCodes.SX, + 'Slovakia (Slovensko)': CountryCodes.SK, + 'Slovenia (Slovenija)': CountryCodes.SI, + 'Solomon Islands': CountryCodes.SB, + 'Somalia (Soomaaliya)': CountryCodes.SO, + 'South Africa': CountryCodes.ZA, + 'South Georgia and the South Sandwich Islands': CountryCodes.GS, + 'South Sudan': CountryCodes.SS, + 'Spain (España)': CountryCodes.ES, + 'Sri Lanka (ශ්‍රී ලංකාව)': CountryCodes.LK, + 'St. Helena': CountryCodes.SH, + 'St. Kitts and Nevis': CountryCodes.KN, + 'St. Lucia Island': CountryCodes.LC, + 'St. Pierre and Miquelon (Saint-Pierre-et-Miquelon)': CountryCodes.PM, + 'St. Vincent and the Grenadines': CountryCodes.VC, + Sudan: CountryCodes.SD, + Suriname: CountryCodes.SR, + Svalbard: CountryCodes.SJ, + Swaziland: CountryCodes.SZ, + 'Sweden (Sverige)': CountryCodes.SE, + 'Switzerland (Schweiz)': CountryCodes.CH, + Syria: CountryCodes.SY, + 'Taiwan (台灣)': CountryCodes.TW, + Tajikistan: CountryCodes.TJ, + Tanzania: CountryCodes.TZ, + 'Thailand (ไทย)': CountryCodes.TH, + Togo: CountryCodes.TG, + Tokelau: CountryCodes.TK, + Tonga: CountryCodes.TO, + 'Trinidad and Tobago': CountryCodes.TT, + 'Tunisia (‫تونس‬‎)': CountryCodes.TN, + 'Turkey (Türkiye)': CountryCodes.TR, + Turkmenistan: CountryCodes.TM, + 'Turks and Caicos Islands': CountryCodes.TC, + Tuvalu: CountryCodes.TV, + Uganda: CountryCodes.UG, + 'Ukraine (Україна)': CountryCodes.UA, + 'United Arab Emirates (‫الإمارات العربية المتحدة‬‎)': CountryCodes.AE, + 'United Kingdom (England, Northern Ireland, Scotland, and Wales)': CountryCodes.GB, + 'United States of America': CountryCodes.US, + Uruguay: CountryCodes.UY, + 'Uzbekistan (Oʻzbekiston)': CountryCodes.UZ, + Vanuatu: CountryCodes.VU, + Venezuela: CountryCodes.VE, + 'Vietnam (Việt Nam)': CountryCodes.VN, + 'Virgin Islands': CountryCodes.VI, + 'Wake Island': CountryCodes.UM, + 'Wallis and Futuna (Wallis-et-Futuna)': CountryCodes.WF, + 'Western Sahara (‫الصحراء الغربية‬‎)': CountryCodes.EH, + 'Yemen (Aden) (‫اليمن‬‎)': CountryCodes.YE, + Zambia: CountryCodes.ZM, + Zimbabwe: CountryCodes.ZW, +}; diff --git a/src/constants/indexer.ts b/src/constants/indexer.ts index 12c2f029b..5191381d8 100644 --- a/src/constants/indexer.ts +++ b/src/constants/indexer.ts @@ -21,3 +21,7 @@ export type PerpetualMarketResponse = { stepBaseQuantums: number; subticksPerTick: number; }; + +export type PerpetualMarketSparklineResponse = { + [key: string]: number[]; +}; diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts index 5ac32cb15..311bec9a3 100644 --- a/src/constants/localStorage.ts +++ b/src/constants/localStorage.ts @@ -7,6 +7,9 @@ export enum LocalStorageKey { OnboardingHasAcknowledgedTerms = 'dydx.OnboardingHasAcknowledgedTerms', EvmDerivedAddresses = 'dydx.EvmDerivedAddresses', + // Gas + SelectedGasDenom = 'dydx.SelectedGasDenom', + // Notifications Notifications = 'dydx.Notifications', NotificationsLastUpdated = 'dydx.NotificationsLastUpdated', @@ -24,11 +27,12 @@ export enum LocalStorageKey { SelectedTradeLayout = 'dydx.SelectedTradeLayout', TradingViewChartConfig = 'dydx.TradingViewChartConfig', HasSeenLaunchIncentives = 'dydx.HasSeenLaunchIncentives', + DefaultToAllMarketsInPositionsOrdersFills = 'dydx.DefaultToAllMarketsInPositionsOrdersFills', } export const LOCAL_STORAGE_VERSIONS = { [LocalStorageKey.EvmDerivedAddresses]: 'v2', - [LocalStorageKey.NotificationPreferences]: 'v1', + [LocalStorageKey.NotificationPreferences]: 'v2', [LocalStorageKey.TransferNotifications]: 'v1', [LocalStorageKey.Notifications]: 'v1', // TODO: version all localStorage keys diff --git a/src/constants/localization.ts b/src/constants/localization.ts index 3c77561fb..a8ae8f533 100644 --- a/src/constants/localization.ts +++ b/src/constants/localization.ts @@ -1,17 +1,17 @@ -import { ReactNode } from 'react'; - import { APP_STRING_KEYS, ERRORS_STRING_KEYS, LOCALE_DATA, + NOTIFICATIONS, + NOTIFICATIONS_STRING_KEYS, TOOLTIPS, WARNINGS_STRING_KEYS, - NOTIFICATIONS_STRING_KEYS, - NOTIFICATIONS, } from '@dydxprotocol/v4-localization'; import { type LinksConfigs } from '@/hooks/useURLConfigs'; +import formatString from '@/lib/formatString'; + export { TOOLTIP_STRING_KEYS } from '@dydxprotocol/v4-localization'; export enum SupportedLocales { @@ -48,12 +48,19 @@ export type StringKey = keyof typeof STRING_KEYS; export type LocaleData = typeof EN_LOCALE_DATA; -export type StringGetterFunction = (a: { +export type StringGetterParams = Record; + +export type StringGetterFunction = ({ + key, + params, +}: { key: string; - params?: { - [key: string]: ReactNode; - }; -}) => string; + params?: T; +}) => T extends { + [K in keyof T]: T[K] extends string | number ? any : T[K] extends JSX.Element ? any : never; +} + ? string + : ReturnType; export const SUPPORTED_LOCALE_STRING_LABELS: { [key in SupportedLocales]: string } = { [SupportedLocales.EN]: 'English', @@ -95,8 +102,8 @@ export type TooltipStrings = { stringParams?: any; urlConfigs?: LinksConfigs; }) => { - title?: string; - body: string; + title?: React.ReactNode; + body: React.ReactNode; learnMoreLink?: string; }; }; diff --git a/src/constants/markets.ts b/src/constants/markets.ts index 0ddc47ded..4e519ae9b 100644 --- a/src/constants/markets.ts +++ b/src/constants/markets.ts @@ -4,20 +4,40 @@ import { STRING_KEYS } from '@/constants/localization'; export type MarketData = { asset: Asset; tickSizeDecimals: number; + oneDaySparkline?: number[]; + isNew?: boolean; + listingDate?: Date; } & PerpetualMarket & PerpetualMarket['perpetual'] & PerpetualMarket['configs']; +export enum MarketSorting { + GAINERS = 'gainers', + LOSERS = 'losers', +} + export enum MarketFilters { ALL = 'all', + NEW = 'new', LAYER_1 = 'Layer 1', + LAYER_2 = 'Layer 2', DEFI = 'Defi', + AI = 'AI', + NFT = 'NFT', + GAMING = 'Gaming', + MEME = 'Meme', } export const MARKET_FILTER_LABELS = { [MarketFilters.ALL]: STRING_KEYS.ALL, + [MarketFilters.NEW]: STRING_KEYS.NEW, [MarketFilters.LAYER_1]: STRING_KEYS.LAYER_1, + [MarketFilters.LAYER_2]: STRING_KEYS.LAYER_2, [MarketFilters.DEFI]: STRING_KEYS.DEFI, + [MarketFilters.AI]: STRING_KEYS.AI, + [MarketFilters.NFT]: STRING_KEYS.NFT, + [MarketFilters.GAMING]: STRING_KEYS.GAMING, + [MarketFilters.MEME]: STRING_KEYS.MEME, }; export const DEFAULT_MARKETID = 'ETH-USD'; diff --git a/src/constants/mockData.ts b/src/constants/mockData.ts index 2e07d40a6..23a169087 100644 --- a/src/constants/mockData.ts +++ b/src/constants/mockData.ts @@ -1,5 +1,4 @@ // Mock data for offline testing - import type { MarketHistoricalFunding } from './abacus'; import { timeUnits } from './time'; diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index 6736199f6..79d5562d0 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -4,9 +4,39 @@ import { StatusResponse } from '@0xsquid/sdk'; export enum NotificationType { AbacusGenerated = 'AbacusGenerated', SquidTransfer = 'SquidTransfer', + TriggerOrder = 'TriggerOrder', ReleaseUpdates = 'ReleaseUpdates', + ApiError = 'ApiError', + ComplianceAlert = 'ComplianceAlert', + OrderStatus = 'OrderStatus', } +export enum NotificationCategoryPreferences { + General = 'General', // release updates + Transfers = 'Transfers', // transfers + Trading = 'Trading', // order status, positions / liquidations, trading rewards + MustSee = 'MustSee', // cannot be hidden: compliance, api errors +} + +export const NotificationTypeCategory: { + [key in NotificationType]: NotificationCategoryPreferences; +} = { + [NotificationType.ReleaseUpdates]: NotificationCategoryPreferences.General, + [NotificationType.SquidTransfer]: NotificationCategoryPreferences.Transfers, + [NotificationType.AbacusGenerated]: NotificationCategoryPreferences.Trading, + [NotificationType.TriggerOrder]: NotificationCategoryPreferences.Trading, + [NotificationType.OrderStatus]: NotificationCategoryPreferences.Trading, + [NotificationType.ApiError]: NotificationCategoryPreferences.MustSee, + [NotificationType.ComplianceAlert]: NotificationCategoryPreferences.MustSee, +}; + +export const SingleSessionNotificationTypes = [ + NotificationType.AbacusGenerated, + NotificationType.ApiError, + NotificationType.ComplianceAlert, + NotificationType.OrderStatus, +]; + export enum NotificationComponentType {} export type NotificationId = string | number; @@ -100,6 +130,14 @@ export type NotificationDisplayData = { actionDescription?: string; + renderActionSlot?: ({ + isToast, + notification, + }: { + isToast?: boolean; + notification: Notification; + }) => React.ReactNode; // Custom Notification + /** Screen reader: instructions for performing toast action after its timer expires */ actionAltText?: string; @@ -122,6 +160,8 @@ export type NotificationDisplayData = { Push notification: requires interaction. */ toastDuration?: number; + + withClose?: boolean; // Show close button for Notification }; export enum TransferNotificationTypes { @@ -129,7 +169,6 @@ export enum TransferNotificationTypes { Deposit = 'deposit', } -// Notification types export type TransferNotifcation = { txHash: string; type?: TransferNotificationTypes; @@ -141,18 +180,20 @@ export type TransferNotifcation = { errorCount?: number; status?: StatusResponse; isExchange?: boolean; + requestId?: string; }; export enum ReleaseUpdateNotificationIds { - RewardsAndFullTradingLive = 'rewards-and-full-trading-live', - IncentivesS3 = 'incentives-s3', + RevampedConditionalOrders = 'revamped-conditional-orders', + IncentivesS4 = 'incentives-s4', + IncentivesDistributedS3 = 'incentives-distributed-s3', } /** - * @description Struct to store whether a NotificationType should be triggered + * @description Struct to store whether a NotificationType belonging to each NotificationCategoryType should be triggered */ export type NotificationPreferences = { - [key in NotificationType]: boolean; + [key in NotificationCategoryPreferences]: boolean; } & { version: string }; export const DEFAULT_TOAST_AUTO_CLOSE_MS = 5000; diff --git a/src/constants/numbers.ts b/src/constants/numbers.ts index dc6914dab..b040661d3 100644 --- a/src/constants/numbers.ts +++ b/src/constants/numbers.ts @@ -10,7 +10,7 @@ export const LEVERAGE_DECIMALS = 2; export const TOKEN_DECIMALS = 4; export const LARGE_TOKEN_DECIMALS = 2; export const FEE_DECIMALS = 3; -export const FUNDING_DECIMALS = 6; +export const FUNDING_DECIMALS = 4; export const QUANTUM_MULTIPLIER = 1_000_000; diff --git a/src/constants/objects.ts b/src/constants/objects.ts new file mode 100644 index 000000000..019d63405 --- /dev/null +++ b/src/constants/objects.ts @@ -0,0 +1,2 @@ +export const EMPTY_OBJ = Object.freeze({}) as {}; +export const EMPTY_ARR = Object.freeze([]) as []; diff --git a/src/constants/orderbook.ts b/src/constants/orderbook.ts index 3e8b490ef..169843ee1 100644 --- a/src/constants/orderbook.ts +++ b/src/constants/orderbook.ts @@ -2,7 +2,7 @@ * @description Orderbook display constants */ export const ORDERBOOK_MAX_ROWS_PER_SIDE = 30; -export const ORDERBOOK_ANIMATION_DURATION = 400; +export const ORDERBOOK_ANIMATION_DURATION = 100; /** * @description Orderbook pixel constants diff --git a/src/constants/potentialMarkets.ts b/src/constants/potentialMarkets.ts index e2292c3e4..e10d03866 100644 --- a/src/constants/potentialMarkets.ts +++ b/src/constants/potentialMarkets.ts @@ -4,22 +4,36 @@ export type ExchangeConfigItem = { adjustByMarket?: string; }; -export type PotentialMarketItem = { - baseAsset: string; - referencePrice: string; - numOracles: number; +export type NewMarketParams = { + id: number; + ticker: string; + marketType?: 'PERPETUAL_MARKET_TYPE_ISOLATED' | 'PERPETUAL_MARKET_TYPE_CROSS'; + priceExponent: number; + minExchanges: number; + minPriceChange: number; + exchangeConfigJson: ExchangeConfigItem[]; liquidityTier: number; - assetName: string; - p: number; atomicResolution: number; - minExchanges: number; - minPriceChangePpm: number; - priceExponent: number; - stepBaseQuantum: number; - ticksizeExponent: number; - subticksPerTick: number; - minOrderSize: number; quantumConversionExponent: number; + defaultFundingPpm: number; + stepBaseQuantums: number; + subticksPerTick: number; + delayBlocks: number; +}; + +export type NewMarketProposal = { + title: string; + summary: string; + params: NewMarketParams; + meta: { + assetName: string; + referencePrice: number; + }; + initial_deposit: { + denom: string; + amount: string; + }; + baseAsset: string; }; -export const NUM_ORACLES_TO_QUALIFY_AS_SAFE = 6; +export const NUM_ORACLES_TO_QUALIFY_AS_SAFE = 5; diff --git a/src/constants/styles/colors.ts b/src/constants/styles/colors.ts index 7075c1748..54bddf84d 100644 --- a/src/constants/styles/colors.ts +++ b/src/constants/styles/colors.ts @@ -27,9 +27,12 @@ export type ThemeColorBase = BaseColors & Filters; type BaseColors = { + black: string; white: string; green: string; red: string; + + whiteFaded: string; }; type LayerColors = { diff --git a/src/constants/tooltips/deposit.ts b/src/constants/tooltips/deposit.ts index de30ab297..bdb5db7f1 100644 --- a/src/constants/tooltips/deposit.ts +++ b/src/constants/tooltips/deposit.ts @@ -1,4 +1,6 @@ -import { type TooltipStrings, TOOLTIP_STRING_KEYS } from '@/constants/localization'; +import { TOOLTIP_STRING_KEYS, type TooltipStrings } from '@/constants/localization'; + +import { CCTP_MAINNET_CHAIN_NAMES_CAPITALIZED } from '../cctp'; export const depositTooltips: TooltipStrings = { 'minimum-deposit-amount': ({ stringGetter }) => ({ @@ -9,4 +11,21 @@ export const depositTooltips: TooltipStrings = { title: stringGetter({ key: TOOLTIP_STRING_KEYS.SWAP_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.SWAP_BODY }), }), + 'lowest-fees-deposit': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.LOWEST_FEE_DEPOSITS_TITLE }), + body: stringGetter({ + key: TOOLTIP_STRING_KEYS.LOWEST_FEE_DEPOSITS_BODY, + params: { + LOWEST_FEE_TOKEN_NAMES: CCTP_MAINNET_CHAIN_NAMES_CAPITALIZED.join(', '), + }, + }), + }), + 'gas-fees-deposit': ({ stringGetter, stringParams }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.GAS_FEES_DEPOSIT_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.GAS_FEES_DEPOSIT_BODY, params: stringParams }), + }), + 'bridge-fees-deposit': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.BRIDGE_FEES_DEPOSIT_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.BRIDGE_FEES_DEPOSIT_BODY }), + }), } as const; diff --git a/src/constants/tooltips/general.ts b/src/constants/tooltips/general.ts index 9da6b4558..84068127b 100644 --- a/src/constants/tooltips/general.ts +++ b/src/constants/tooltips/general.ts @@ -1,4 +1,4 @@ -import { type TooltipStrings, TOOLTIP_STRING_KEYS } from '@/constants/localization'; +import { TOOLTIP_STRING_KEYS, type TooltipStrings } from '@/constants/localization'; export const generalTooltips: TooltipStrings = { 'legacy-signing': ({ stringGetter }) => ({ diff --git a/src/constants/tooltips/index.ts b/src/constants/tooltips/index.ts index d36075c7e..5b228cefd 100644 --- a/src/constants/tooltips/index.ts +++ b/src/constants/tooltips/index.ts @@ -4,6 +4,7 @@ import { depositTooltips } from './deposit'; import { generalTooltips } from './general'; import { portfolioTooltips } from './portfolio'; import { tradeTooltips } from './trade'; +import { triggersTooltips } from './triggers'; import { withdrawTooltips } from './withdraw'; export const tooltipStrings: TooltipStrings = { @@ -11,5 +12,6 @@ export const tooltipStrings: TooltipStrings = { ...generalTooltips, ...portfolioTooltips, ...tradeTooltips, + ...triggersTooltips, ...withdrawTooltips, } as const; diff --git a/src/constants/tooltips/portfolio.ts b/src/constants/tooltips/portfolio.ts index b995f40ae..827a54285 100644 --- a/src/constants/tooltips/portfolio.ts +++ b/src/constants/tooltips/portfolio.ts @@ -1,4 +1,4 @@ -import { type TooltipStrings, TOOLTIP_STRING_KEYS } from '@/constants/localization'; +import { TOOLTIP_STRING_KEYS, type TooltipStrings } from '@/constants/localization'; export const portfolioTooltips: TooltipStrings = { 'holding-hedgies': ({ stringGetter, stringParams }) => ({ diff --git a/src/constants/tooltips/trade.ts b/src/constants/tooltips/trade.ts index 4382ce87a..5a76d1eaf 100644 --- a/src/constants/tooltips/trade.ts +++ b/src/constants/tooltips/trade.ts @@ -1,4 +1,4 @@ -import { type TooltipStrings, TOOLTIP_STRING_KEYS } from '@/constants/localization'; +import { TOOLTIP_STRING_KEYS, type TooltipStrings } from '@/constants/localization'; export const tradeTooltips: TooltipStrings = { 'account-leverage': ({ stringGetter }) => ({ @@ -57,9 +57,10 @@ export const tradeTooltips: TooltipStrings = { title: stringGetter({ key: TOOLTIP_STRING_KEYS.INDEX_PRICE_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.INDEX_PRICE_BODY }), }), - 'initial-margin-fraction': ({ stringGetter }) => ({ + 'initial-margin-fraction': ({ stringGetter, urlConfigs }) => ({ title: stringGetter({ key: TOOLTIP_STRING_KEYS.INITIAL_MARGIN_FRACTION_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.INITIAL_MARGIN_FRACTION_BODY }), + learnMoreLink: urlConfigs?.initialMarginFractionLearnMore, }), 'initial-stop': ({ stringGetter }) => ({ title: stringGetter({ key: TOOLTIP_STRING_KEYS.INITIAL_STOP_TITLE }), @@ -85,6 +86,14 @@ export const tradeTooltips: TooltipStrings = { title: stringGetter({ key: TOOLTIP_STRING_KEYS.LIQUIDATION_PRICE_GENERAL_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.LIQUIDATION_PRICE_GENERAL_BODY }), }), + 'liquidation-warning-long': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.STOP_LOSS_BELOW_LIQUIDATION_PRICE_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.STOP_LOSS_BELOW_LIQUIDATION_PRICE_BODY }), + }), + 'liquidation-warning-short': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.STOP_LOSS_ABOVE_LIQUIDATION_PRICE_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.STOP_LOSS_ABOVE_LIQUIDATION_PRICE_BODY }), + }), liquidity: ({ stringGetter }) => ({ title: stringGetter({ key: TOOLTIP_STRING_KEYS.LIQUIDITY_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.LIQUIDITY_BODY }), @@ -133,6 +142,14 @@ export const tradeTooltips: TooltipStrings = { title: stringGetter({ key: TOOLTIP_STRING_KEYS.ORDER_AMOUNT_USD_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.ORDER_AMOUNT_USD_BODY, params: stringParams }), }), + 'partial-close-stop-loss': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.PARTIAL_CLOSE_STOP_LOSS_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.PARTIAL_CLOSE_STOP_LOSS_BODY }), + }), + 'partial-close-take-profit': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.PARTIAL_CLOSE_TAKE_PROFIT_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.PARTIAL_CLOSE_TAKE_PROFIT_BODY }), + }), 'post-only': ({ stringGetter }) => ({ title: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_BODY }), diff --git a/src/constants/tooltips/triggers.ts b/src/constants/tooltips/triggers.ts new file mode 100644 index 000000000..8039bbc4d --- /dev/null +++ b/src/constants/tooltips/triggers.ts @@ -0,0 +1,24 @@ +import { TOOLTIP_STRING_KEYS, type TooltipStrings } from '@/constants/localization'; + +export const triggersTooltips: TooltipStrings = { + 'custom-amount': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.CUSTOM_AMOUNT_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.CUSTOM_AMOUNT_BODY }), + }), + 'limit-price': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.LIMIT_PRICE_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.LIMIT_PRICE_BODY }), + }), + 'stop-loss': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.STOP_LOSS_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.STOP_LOSS_BODY }), + }), + 'take-profit': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.TAKE_PROFIT_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.TAKE_PROFIT_BODY }), + }), + 'unequal-order-sizes': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.UNEQUAL_ORDER_SIZES_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.UNEQUAL_ORDER_SIZES_BODY }), + }), +} as const; diff --git a/src/constants/tooltips/withdraw.ts b/src/constants/tooltips/withdraw.ts index e5c0f418d..db077d078 100644 --- a/src/constants/tooltips/withdraw.ts +++ b/src/constants/tooltips/withdraw.ts @@ -1,4 +1,6 @@ -import { type TooltipStrings, TOOLTIP_STRING_KEYS } from '@/constants/localization'; +import { TOOLTIP_STRING_KEYS, type TooltipStrings } from '@/constants/localization'; + +import { CCTP_MAINNET_CHAIN_NAMES_CAPITALIZED } from '../cctp'; export const withdrawTooltips: TooltipStrings = { 'fast-withdraw-fee': ({ stringGetter }) => ({ @@ -13,4 +15,21 @@ export const withdrawTooltips: TooltipStrings = { title: stringGetter({ key: TOOLTIP_STRING_KEYS.WITHDRAW_TYPES_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.WITHDRAW_TYPES_BODY }), }), + 'gas-fees': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.GAS_FEES_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.GAS_FEES_BODY }), + }), + 'bridge-fees': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.BRIDGE_FEES_TITLE }), + body: stringGetter({ key: TOOLTIP_STRING_KEYS.BRIDGE_FEES_BODY }), + }), + 'lowest-fees': ({ stringGetter }) => ({ + title: stringGetter({ key: TOOLTIP_STRING_KEYS.LOWEST_FEE_WITHDRAWALS_TITLE }), + body: stringGetter({ + key: TOOLTIP_STRING_KEYS.LOWEST_FEE_WITHDRAWALS_BODY, + params: { + LOWEST_FEE_TOKEN_NAMES: CCTP_MAINNET_CHAIN_NAMES_CAPITALIZED.join(', '), + }, + }), + }), } as const; diff --git a/src/constants/trade.ts b/src/constants/trade.ts index 658134deb..ac9a3b0db 100644 --- a/src/constants/trade.ts +++ b/src/constants/trade.ts @@ -78,12 +78,12 @@ export const ORDER_TYPE_STRINGS: Record< descriptionKey: STRING_KEYS.STOP_MARKET_DESCRIPTION, }, [TradeTypes.TAKE_PROFIT]: { - orderTypeKeyShort: STRING_KEYS.TAKE_PROFIT_LIMIT, + orderTypeKeyShort: STRING_KEYS.TAKE_PROFIT_LIMIT_SHORT, orderTypeKey: STRING_KEYS.TAKE_PROFIT_LIMIT, descriptionKey: STRING_KEYS.TAKE_PROFIT_LIMIT_DESCRIPTION, }, [TradeTypes.TAKE_PROFIT_MARKET]: { - orderTypeKeyShort: STRING_KEYS.TAKE_PROFIT_MARKET, + orderTypeKeyShort: STRING_KEYS.TAKE_PROFIT_MARKET_SHORT, orderTypeKey: STRING_KEYS.TAKE_PROFIT_MARKET, descriptionKey: STRING_KEYS.TAKE_PROFIT_MARKET_DESCRIPTION, }, @@ -149,6 +149,7 @@ export enum MobilePlaceOrderSteps { PreviewOrder = 'PreviewOrder', PlacingOrder = 'PlacingOrder', Confirmation = 'Confirmation', + PlaceOrderFailed = 'PlaceOrderFailed', } export const CLEARED_TRADE_INPUTS = { @@ -162,3 +163,29 @@ export const CLEARED_SIZE_INPUTS = { usdAmountInput: '', leverageInput: '', }; + +export enum PlaceOrderStatuses { + Submitted, + Placed, + Filled, +} + +export enum CancelOrderStatuses { + Submitted, + Canceled, +} + +export type LocalPlaceOrderData = { + marketId: string; + clientId: number; + orderId?: string; + orderType: TradeTypes; + submissionStatus: PlaceOrderStatuses; + errorStringKey?: string; +}; + +export type LocalCancelOrderData = { + orderId: string; + submissionStatus: CancelOrderStatuses; + errorStringKey?: string; +}; diff --git a/src/constants/tvchart.ts b/src/constants/tvchart.ts index 62503accc..05671c9e6 100644 --- a/src/constants/tvchart.ts +++ b/src/constants/tvchart.ts @@ -1,5 +1,4 @@ import { OrderSide } from '@dydxprotocol/v4-client-js'; - import type { IChartingLibraryWidget, IOrderLineAdapter, @@ -8,7 +7,8 @@ import type { export type TvWidget = IChartingLibraryWidget & { _id?: string; _ready?: boolean }; -export type ChartLineType = OrderSide | 'position'; +export type PositionLineType = 'entry' | 'liquidation'; +export type ChartLineType = OrderSide | PositionLineType; export type ChartLine = { line: IOrderLineAdapter | IPositionLineAdapter; diff --git a/src/constants/wallets.ts b/src/constants/wallets.ts index 83411cc15..a092057c2 100644 --- a/src/constants/wallets.ts +++ b/src/constants/wallets.ts @@ -1,5 +1,5 @@ -import type { ExternalProvider } from '@ethersproject/providers'; import { type onboarding } from '@dydxprotocol/v4-client-js'; +import type { ExternalProvider } from '@ethersproject/providers'; import type { suggestChain } from 'graz'; import { STRING_KEYS } from '@/constants/localization'; @@ -8,8 +8,9 @@ import { BitkeepIcon, BitpieIcon, CloverWalletIcon, - CoinbaseIcon, Coin98Icon, + CoinbaseIcon, + EmailIcon, GenericWalletIcon, HuobiIcon, ImTokenIcon, @@ -32,6 +33,7 @@ import { DydxChainId, WALLETS_CONFIG_MAP } from './networks'; export enum WalletConnectionType { CoinbaseWalletSdk = 'coinbaseWalletSdk', CosmosSigner = 'CosmosSigner', + Privy = 'Privy', InjectedEip1193 = 'injectedEip1193', WalletConnect2 = 'walletConnect2', TestWallet = 'TestWallet', @@ -74,6 +76,9 @@ export const walletConnectionTypes: Record = { icon: GenericWalletIcon, connectionTypes: [WalletConnectionType.TestWallet], }, + [WalletType.Privy]: { + type: WalletType.Privy, + stringKey: STRING_KEYS.EMAIL_OR_SOCIAL, + icon: EmailIcon, + connectionTypes: [WalletConnectionType.Privy], + }, }; // Injected EIP-1193 Providers diff --git a/src/constants/websocket.ts b/src/constants/websocket.ts deleted file mode 100644 index e6c9c4c57..000000000 --- a/src/constants/websocket.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const PING_INTERVAL_MS = 2000; -export const PONG_TIMEOUT_MS = 5000; - -export const PONG_MESSAGE_TYPE = 'pong'; - -export const OUTGOING_PING_MESSAGE = JSON.stringify({ type: 'ping' }); diff --git a/src/hooks/Orderbook/useDrawOrderbook.ts b/src/hooks/Orderbook/useDrawOrderbook.ts index 757779639..958b06a70 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -1,9 +1,9 @@ import { useEffect, useMemo, useRef } from 'react'; + import { shallowEqual, useSelector } from 'react-redux'; import type { PerpetualMarketOrderbookLevel } from '@/constants/abacus'; import { SMALL_USD_DECIMALS, TOKEN_DECIMALS } from '@/constants/numbers'; - import { ORDERBOOK_ANIMATION_DURATION, ORDERBOOK_HEIGHT, @@ -17,7 +17,6 @@ import { useAppThemeAndColorModeContext } from '@/hooks/useAppThemeAndColorMode' import { getCurrentMarketConfig, getCurrentMarketOrderbookMap } from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; - import { getHistogramXValues, getRektFromIdx, @@ -41,6 +40,8 @@ enum OrderbookRowAnimationType { NONE, } +export type Rekt = { x1: number; x2: number; y1: number; y2: number }; + export const useDrawOrderbook = ({ data, histogramRange, @@ -51,7 +52,7 @@ export const useDrawOrderbook = ({ const canvas = canvasRef.current; const currentOrderbookMap = useSelector(getCurrentMarketOrderbookMap, shallowEqual); const { stepSizeDecimals = TOKEN_DECIMALS, tickSizeDecimals = SMALL_USD_DECIMALS } = - useSelector(getCurrentMarketConfig, shallowEqual) || {}; + useSelector(getCurrentMarketConfig, shallowEqual) ?? {}; const prevData = useRef(data); const theme = useAppThemeAndColorModeContext(); @@ -59,7 +60,7 @@ export const useDrawOrderbook = ({ * Scale canvas using device pixel ratio to unblur drawn text * @url https://stackoverflow.com/questions/15661339/how-do-i-fix-blurry-text-in-my-html5-canvas/65124939#65124939 * @returns adjusted canvas width/height/rowHeight used in coordinates for drawing - **/ + * */ const { canvasWidth, canvasHeight, rowHeight } = useMemo(() => { const ratio = window.devicePixelRatio || 1; @@ -96,8 +97,8 @@ export const useDrawOrderbook = ({ depthOrSizeValue, gradientMultiplier, histogramAccentColor, - histogramSide, - idx, + histogramSide: inHistogramSide, + rekt, }: { barType: 'depth' | 'size'; ctx: CanvasRenderingContext2D; @@ -105,15 +106,9 @@ export const useDrawOrderbook = ({ gradientMultiplier: number; histogramAccentColor: string; histogramSide: 'left' | 'right'; - idx: number; + rekt: Rekt; }) => { - const { x1, x2, y1, y2 } = getRektFromIdx({ - idx, - rowHeight, - canvasWidth, - canvasHeight, - side, - }); + const { x1, x2, y1, y2 } = rekt; // X values const maxHistogramBarWidth = x2 - x1 - (barType === 'size' ? 8 : 2); @@ -125,7 +120,7 @@ export const useDrawOrderbook = ({ barWidth, canvasWidth, gradientMultiplier, - histogramSide, + histogramSide: inHistogramSide, }); // Gradient @@ -151,7 +146,7 @@ export const useDrawOrderbook = ({ y, bar.x2, rowHeight - 2, - histogramSide === 'right' ? [2, 0, 0, 2] : [0, 2, 2, 0] + inHistogramSide === 'right' ? [2, 0, 0, 2] : [0, 2, 2, 0] ); } else { ctx.rect(bar.x1, y, bar.x2, rowHeight - 2); @@ -163,25 +158,19 @@ export const useDrawOrderbook = ({ const drawText = ({ animationType = OrderbookRowAnimationType.NONE, ctx, - idx, mine, price, size, + rekt, }: { animationType?: OrderbookRowAnimationType; ctx: CanvasRenderingContext2D; - idx: number; mine?: number; price?: number; size?: number; + rekt: Rekt; }) => { - const { y1 } = getRektFromIdx({ - idx, - rowHeight, - canvasWidth, - canvasHeight, - side, - }); + const { y1 } = rekt; const { text: y } = getYForElements({ y: y1, rowHeight }); @@ -190,7 +179,7 @@ export const useDrawOrderbook = ({ switch (animationType) { case OrderbookRowAnimationType.REMOVE: { - textColor = theme.textSecondary; + textColor = theme.textTertiary; break; } @@ -251,6 +240,13 @@ export const useDrawOrderbook = ({ if (!rowToRender) return; const { depth, mine, price, size } = rowToRender; const histogramAccentColor = side === 'bid' ? theme.positiveFaded : theme.negativeFaded; + const rekt = getRektFromIdx({ + idx, + rowHeight, + canvasWidth, + canvasHeight, + side, + }); // Depth Bar if (depth) { @@ -261,7 +257,7 @@ export const useDrawOrderbook = ({ gradientMultiplier: 1.3, histogramAccentColor, histogramSide, - idx, + rekt, }); } @@ -273,17 +269,17 @@ export const useDrawOrderbook = ({ gradientMultiplier: 5, histogramAccentColor, histogramSide, - idx, + rekt, }); // Size, Price, Mine drawText({ animationType, ctx, - idx, mine, price, size, + rekt, }); }; @@ -299,59 +295,24 @@ export const useDrawOrderbook = ({ // Animate row removal and update const mapOfOrderbookPriceLevels = side && currentOrderbookMap?.[side === 'ask' ? 'asks' : 'bids']; - const empty: number[] = []; - const removed: number[] = []; - const updated: number[] = []; prevData.current.forEach((row, idx) => { - if (!row) { - empty.push(idx); - return; - } + if (!row) return; + + let animationType = OrderbookRowAnimationType.NEW; if (mapOfOrderbookPriceLevels?.[row.price] === 0) { - removed.push(idx); - drawOrderbookRow({ - ctx, - idx, - rowToRender: row, - animationType: OrderbookRowAnimationType.REMOVE, - }); - } else if (mapOfOrderbookPriceLevels?.[row.price] === row?.size) { - drawOrderbookRow({ - ctx, - idx, - rowToRender: data[idx], - animationType: OrderbookRowAnimationType.NONE, - }); - } else { - updated.push(idx); - drawOrderbookRow({ - ctx, - idx, - rowToRender: row, - animationType: OrderbookRowAnimationType.NEW, - }); + animationType = OrderbookRowAnimationType.REMOVE; + } else if (mapOfOrderbookPriceLevels?.[row.price] === row.size) { + animationType = OrderbookRowAnimationType.NONE; } + + drawOrderbookRow({ ctx, idx, rowToRender: row, animationType }); }); setTimeout(() => { - [...empty, ...removed, ...updated].forEach((idx) => { - const { x1, y1, x2, y2 } = getRektFromIdx({ - idx, - rowHeight, - canvasWidth, - canvasHeight, - side, - }); - - ctx.clearRect(x1, y1, x2 - x1, y2 - y1); - drawOrderbookRow({ - ctx, - idx, - rowToRender: data[idx], - }); - }); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + data.forEach((row, idx) => drawOrderbookRow({ ctx, idx, rowToRender: row })); }, ORDERBOOK_ANIMATION_DURATION); prevData.current = data; diff --git a/src/hooks/Orderbook/useOrderbookValues.ts b/src/hooks/Orderbook/useOrderbookValues.ts index d4418ed80..97d09cd91 100644 --- a/src/hooks/Orderbook/useOrderbookValues.ts +++ b/src/hooks/Orderbook/useOrderbookValues.ts @@ -1,31 +1,32 @@ import { useMemo } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; + import { OrderSide } from '@dydxprotocol/v4-client-js'; +import { shallowEqual, useSelector } from 'react-redux'; -import { type PerpetualMarketOrderbookLevel } from '@/constants/abacus'; -import { DepthChartSeries, DepthChartDatum } from '@/constants/charts'; +import { OrderbookLine, type PerpetualMarketOrderbookLevel } from '@/constants/abacus'; +import { DepthChartDatum, DepthChartSeries } from '@/constants/charts'; +import { getSubaccountOrderSizeBySideAndPrice } from '@/state/accountSelectors'; import { getCurrentMarketOrderbook } from '@/state/perpetualsSelectors'; -import { getSubaccountOpenOrdersBySideAndPrice } from '@/state/accountSelectors'; import { MustBigNumber } from '@/lib/numbers'; export const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => { const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual); - const openOrdersBySideAndPrice = - useSelector(getSubaccountOpenOrdersBySideAndPrice, shallowEqual) || {}; + const subaccountOrderSizeBySideAndPrice = + useSelector(getSubaccountOrderSizeBySideAndPrice, shallowEqual) || {}; return useMemo(() => { const asks: Array = ( orderbook?.asks?.toArray() ?? [] ) .map( - (row, idx: number) => + (row: OrderbookLine, idx: number) => ({ key: `ask-${idx}`, side: 'ask', - mine: openOrdersBySideAndPrice[OrderSide.SELL]?.[row.price]?.size, + mine: subaccountOrderSizeBySideAndPrice[OrderSide.SELL]?.[row.price], ...row, } as PerpetualMarketOrderbookLevel) ) @@ -35,11 +36,11 @@ export const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: orderbook?.bids?.toArray() ?? [] ) .map( - (row, idx: number) => + (row: OrderbookLine, idx: number) => ({ key: `bid-${idx}`, side: 'bid', - mine: openOrdersBySideAndPrice[OrderSide.BUY]?.[row.price]?.size, + mine: subaccountOrderSizeBySideAndPrice[OrderSide.BUY]?.[row.price], ...row, } as PerpetualMarketOrderbookLevel) ) @@ -74,8 +75,8 @@ export const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: const spreadPercent = orderbook?.spreadPercent; const histogramRange = Math.max( - isNaN(Number(bids[bids.length - 1]?.depth)) ? 0 : Number(bids[bids.length - 1]?.depth), - isNaN(Number(asks[asks.length - 1]?.depth)) ? 0 : Number(asks[asks.length - 1]?.depth) + Number.isNaN(Number(bids[bids.length - 1]?.depth)) ? 0 : Number(bids[bids.length - 1]?.depth), + Number.isNaN(Number(asks[asks.length - 1]?.depth)) ? 0 : Number(asks[asks.length - 1]?.depth) ); return { @@ -86,7 +87,7 @@ export const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: histogramRange, hasOrderbook: !!orderbook, }; - }, [orderbook, openOrdersBySideAndPrice]); + }, [orderbook, subaccountOrderSizeBySideAndPrice]); }; export const useOrderbookValuesForDepthChart = () => { diff --git a/src/hooks/Orderbook/useSpreadRowScrollListener.ts b/src/hooks/Orderbook/useSpreadRowScrollListener.ts index e82c0b709..cdbfd8aff 100644 --- a/src/hooks/Orderbook/useSpreadRowScrollListener.ts +++ b/src/hooks/Orderbook/useSpreadRowScrollListener.ts @@ -1,4 +1,4 @@ -import { type RefObject, useEffect, useState } from 'react'; +import { useEffect, useState, type RefObject } from 'react'; export const useSpreadRowScrollListener = ({ orderbookRef, diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index fc985a8e8..000000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useApiState } from './useApiState'; -import { useBreakpoints } from './useBreakpoints'; -import { useTokenConfigs } from './useTokenConfigs'; -import { useCommandMenu } from './useCommandMenu'; -import { useCurrentMarketId } from './useCurrentMarketId'; -import { useDebounce } from './useDebounce'; -import { useInterval } from './useInterval'; -import { useDocumentTitle } from './useDocumentTitle'; -import { useDydxClient } from './useDydxClient'; -import { useGovernanceVariables } from './useGovernanceVariables'; -import { useAccountBalance } from './useAccountBalance'; -import { useAccounts } from './useAccounts'; -import { useAnalytics } from './useAnalytics'; -import { useInitializePage } from './useInitializePage'; -import { useIsFirstRender } from './useIsFirstRender'; -import { useLocaleSeparators } from './useLocaleSeparators'; -import { useLocalStorage } from './useLocalStorage'; -import { useNextClobPairId } from './useNextClobPairId'; -import { useNow } from './useNow'; -import { useOnClickOutside } from './useOnClickOutside'; -import { usePageTitlePriceUpdates } from './usePageTitlePriceUpdates'; -import { useRestrictions } from './useRestrictions'; -import { useShouldShowFooter } from './useShouldShowFooter'; -import { useSelectedNetwork } from './useSelectedNetwork'; -import { useStringGetter } from './useStringGetter'; -import { useSubaccount } from './useSubaccount'; -import { useTradeFormInputs } from './useTradeFormInputs'; -import { useURLConfigs } from './useURLConfigs'; - -export { - useApiState, - useBreakpoints, - useTokenConfigs, - useCommandMenu, - useCurrentMarketId, - useDebounce, - useDocumentTitle, - useDydxClient, - useGovernanceVariables, - useAccountBalance, - useAccounts, - useAnalytics, - useInitializePage, - useInterval, - useIsFirstRender, - useLocaleSeparators, - useLocalStorage, - useNextClobPairId, - useNow, - useOnClickOutside, - usePageTitlePriceUpdates, - useRestrictions, - useShouldShowFooter, - useSelectedNetwork, - useStringGetter, - useSubaccount, - useTradeFormInputs, - useURLConfigs, -}; diff --git a/src/hooks/tradingView/useChartLines.ts b/src/hooks/tradingView/useChartLines.ts index e4f6bd0eb..987770642 100644 --- a/src/hooks/tradingView/useChartLines.ts +++ b/src/hooks/tradingView/useChartLines.ts @@ -4,21 +4,22 @@ import { shallowEqual, useSelector } from 'react-redux'; import { AbacusOrderStatus, ORDER_SIDES, SubaccountOrder } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; -import { type OrderType, ORDER_TYPE_STRINGS } from '@/constants/trade'; -import type { ChartLine, TvWidget } from '@/constants/tvchart'; - -import { useStringGetter } from '@/hooks'; +import { ORDER_TYPE_STRINGS, type OrderType } from '@/constants/trade'; +import type { ChartLine, PositionLineType, TvWidget } from '@/constants/tvchart'; import { getCurrentMarketOrders, getCurrentMarketPositionData, getIsAccountConnected, } from '@/state/accountSelectors'; -import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; +import { getAppColorMode, getAppTheme } from '@/state/configsSelectors'; +import { getCurrentMarketId } from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; import { getChartLineColors } from '@/lib/tradingView/utils'; +import { useStringGetter } from '../useStringGetter'; + /** * @description Hook to handle drawing chart lines */ @@ -43,9 +44,14 @@ export const useChartLines = ({ const isAccountConnected = useSelector(getIsAccountConnected); + const currentMarketId = useSelector(getCurrentMarketId); const currentMarketPositionData = useSelector(getCurrentMarketPositionData, shallowEqual); const currentMarketOrders: SubaccountOrder[] = useSelector(getCurrentMarketOrders, shallowEqual); + useEffect(() => { + return () => deleteChartLines(); + }, [currentMarketId]); + useEffect(() => { if (isChartReady && displayButton) { displayButton.onclick = () => setShowOrderLines(!showOrderLines); @@ -66,7 +72,7 @@ export const useChartLines = ({ if (showOrderLines) { displayButton?.classList?.add('order-lines-active'); drawOrderLines(); - drawPositionLine(); + drawPositionLines(); } else { displayButton?.classList?.remove('order-lines-active'); deleteChartLines(); @@ -77,47 +83,79 @@ export const useChartLines = ({ } }, [isChartReady, showOrderLines, currentMarketPositionData, currentMarketOrders]); - const drawPositionLine = () => { + const drawPositionLines = () => { if (!currentMarketPositionData) return; const entryPrice = currentMarketPositionData.entryPrice?.current; + const liquidationPrice = currentMarketPositionData.liquidationPrice?.current; const size = currentMarketPositionData.size?.current; - const key = currentMarketPositionData.id; - const price = MustBigNumber(entryPrice).toNumber(); + const entryLineKey = `entry-${currentMarketPositionData.id}`; + const liquidationLineKey = `liquidation-${currentMarketPositionData.id}`; + + maybeDrawPositionLine({ + key: entryLineKey, + label: stringGetter({ key: STRING_KEYS.ENTRY_PRICE_SHORT }), + chartLineType: 'entry', + price: entryPrice, + size, + }); + maybeDrawPositionLine({ + key: liquidationLineKey, + label: stringGetter({ key: STRING_KEYS.LIQUIDATION }), + chartLineType: 'liquidation', + price: liquidationPrice, + size, + }); + }; + + const maybeDrawPositionLine = ({ + key, + label, + chartLineType, + price, + size, + }: { + key: string; + label: string; + chartLineType: PositionLineType; + price?: number | null; + size?: number | null; + }) => { + const shouldShow = !!(size && price); const maybePositionLine = chartLinesRef.current[key]?.line; - const shouldShow = size && size !== 0; if (!shouldShow) { if (maybePositionLine) { maybePositionLine.remove(); delete chartLinesRef.current[key]; - return; } - } else { - const quantity = size.toString(); + return; + } - if (maybePositionLine) { - if (maybePositionLine.getQuantity() !== quantity) { - maybePositionLine.setQuantity(quantity); - } - if (maybePositionLine.getPrice() !== price) { - maybePositionLine.setPrice(price); - } - } else { - const positionLine = tvWidget - ?.chart() - .createPositionLine({ disableUndo: false }) - .setText(stringGetter({ key: STRING_KEYS.ENTRY_PRICE_SHORT })) - .setPrice(price) - .setQuantity(quantity); - - if (positionLine) { - const chartLine: ChartLine = { line: positionLine, chartLineType: 'position' }; - setLineColors({ chartLine: chartLine }); - chartLinesRef.current[key] = chartLine; - } + const formattedPrice = MustBigNumber(price).toNumber(); + const quantity = Math.abs(size).toString(); + + if (maybePositionLine) { + if (maybePositionLine.getPrice() !== formattedPrice) { + maybePositionLine.setPrice(formattedPrice); + } + if (maybePositionLine.getQuantity() !== quantity) { + maybePositionLine.setQuantity(quantity); + } + } else { + const positionLine = tvWidget + ?.chart() + .createPositionLine({ disableUndo: false }) + .setPrice(formattedPrice) + .setQuantity(quantity) + .setText(label); + + if (positionLine) { + const chartLine: ChartLine = { line: positionLine, chartLineType }; + setLineColors({ chartLine }); + chartLinesRef.current[key] = chartLine; } } }; @@ -152,15 +190,18 @@ export const useChartLines = ({ (status === AbacusOrderStatus.open || status === AbacusOrderStatus.untriggered); const maybeOrderLine = chartLinesRef.current[key]?.line; + const formattedPrice = MustBigNumber(triggerPrice ?? price).toNumber(); if (!shouldShow) { if (maybeOrderLine) { maybeOrderLine.remove(); delete chartLinesRef.current[key]; - return; } } else { if (maybeOrderLine) { + if (maybeOrderLine.getPrice() !== formattedPrice) { + maybeOrderLine.setPrice(formattedPrice); + } if (maybeOrderLine.getQuantity() !== quantity) { maybeOrderLine.setQuantity(quantity); } @@ -168,7 +209,7 @@ export const useChartLines = ({ const orderLine = tvWidget ?.chart() .createOrderLine({ disableUndo: false }) - .setPrice(MustBigNumber(triggerPrice ?? price).toNumber()) + .setPrice(formattedPrice) .setQuantity(quantity) .setText(orderString); @@ -177,7 +218,7 @@ export const useChartLines = ({ line: orderLine, chartLineType: ORDER_SIDES[side.name], }; - setLineColors({ chartLine: chartLine }); + setLineColors({ chartLine }); chartLinesRef.current[key] = chartLine; } } @@ -209,8 +250,9 @@ export const useChartLines = ({ .setBodyTextColor(textColor) .setQuantityTextColor(textButtonColor); - maybeQuantityColor && + if (maybeQuantityColor != null) { line.setLineColor(maybeQuantityColor).setQuantityBackgroundColor(maybeQuantityColor); + } }; return { chartLines: chartLinesRef.current }; diff --git a/src/hooks/tradingView/useChartMarketAndResolution.ts b/src/hooks/tradingView/useChartMarketAndResolution.ts index 9b4f71d21..6f1831911 100644 --- a/src/hooks/tradingView/useChartMarketAndResolution.ts +++ b/src/hooks/tradingView/useChartMarketAndResolution.ts @@ -1,8 +1,7 @@ import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - import type { ResolutionString } from 'public/tradingview/charting_library'; +import { useDispatch, useSelector } from 'react-redux'; import { DEFAULT_RESOLUTION, RESOLUTION_CHART_CONFIGS } from '@/constants/candles'; import { DEFAULT_MARKETID } from '@/constants/markets'; @@ -26,10 +25,10 @@ export const useChartMarketAndResolution = ({ }) => { const dispatch = useDispatch(); - const currentMarketId: string = useSelector(getCurrentMarketId) || DEFAULT_MARKETID; + const currentMarketId: string = useSelector(getCurrentMarketId) ?? DEFAULT_MARKETID; const selectedResolution: string = - useSelector(getSelectedResolutionForMarket(currentMarketId)) || DEFAULT_RESOLUTION; + useSelector(getSelectedResolutionForMarket(currentMarketId)) ?? DEFAULT_RESOLUTION; const chart = isWidgetReady ? tvWidget?.chart() : undefined; const chartResolution = chart?.resolution?.(); @@ -39,7 +38,7 @@ export const useChartMarketAndResolution = ({ */ useEffect(() => { if (isWidgetReady && currentMarketId !== tvWidget?.activeChart().symbol()) { - const resolution = savedResolution || selectedResolution; + const resolution = savedResolution ?? selectedResolution; tvWidget?.setSymbol(currentMarketId, resolution as ResolutionString, () => {}); } }, [currentMarketId, isWidgetReady]); diff --git a/src/hooks/tradingView/useTradingView.ts b/src/hooks/tradingView/useTradingView.ts index 0de4d1893..734ded617 100644 --- a/src/hooks/tradingView/useTradingView.ts +++ b/src/hooks/tradingView/useTradingView.ts @@ -1,27 +1,32 @@ -import React, { useEffect } from 'react'; - -import { shallowEqual, useSelector } from 'react-redux'; +import React, { useEffect, useState } from 'react'; +import BigNumber from 'bignumber.js'; import isEmpty from 'lodash/isEmpty'; - -import { LanguageCode, ResolutionString, widget } from 'public/tradingview/charting_library'; +import { + LanguageCode, + ResolutionString, + widget as Widget, +} from 'public/tradingview/charting_library'; +import { shallowEqual, useSelector } from 'react-redux'; import { DEFAULT_RESOLUTION } from '@/constants/candles'; -import { SUPPORTED_LOCALE_BASE_TAGS, STRING_KEYS } from '@/constants/localization'; import { LocalStorageKey } from '@/constants/localStorage'; +import { STRING_KEYS, SUPPORTED_LOCALE_BASE_TAGS } from '@/constants/localization'; import type { TvWidget } from '@/constants/tvchart'; -import { useDydxClient, useLocalStorage, useStringGetter } from '@/hooks'; - import { store } from '@/state/_store'; import { getSelectedNetwork } from '@/state/appSelectors'; -import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; +import { getAppColorMode, getAppTheme } from '@/state/configsSelectors'; import { getSelectedLocale } from '@/state/localizationSelectors'; import { getCurrentMarketId, getMarketIds } from '@/state/perpetualsSelectors'; import { getDydxDatafeed } from '@/lib/tradingView/dydxfeed'; import { getSavedResolution, getWidgetOptions, getWidgetOverrides } from '@/lib/tradingView/utils'; +import { useDydxClient } from '../useDydxClient'; +import { useLocalStorage } from '../useLocalStorage'; +import { useStringGetter } from '../useStringGetter'; + /** * @description Hook to initialize TradingView Chart */ @@ -43,7 +48,8 @@ export const useTradingView = ({ const marketIds = useSelector(getMarketIds, shallowEqual); const selectedLocale = useSelector(getSelectedLocale); const selectedNetwork = useSelector(getSelectedNetwork); - const { getCandlesForDatafeed, isConnected: isClientConnected } = useDydxClient(); + + const { getCandlesForDatafeed, getMarketTickSize } = useDydxClient(); const [savedTvChartConfig, setTvChartConfig] = useLocalStorage({ key: LocalStorageKey.TradingViewChartConfig, @@ -51,23 +57,41 @@ export const useTradingView = ({ }); const savedResolution = getSavedResolution({ savedConfig: savedTvChartConfig }); + + const [initialPriceScale, setInitialPriceScale] = useState(null); + const hasMarkets = marketIds.length > 0; + const hasPriceScaleInfo = initialPriceScale !== null || hasMarkets; + + useEffect(() => { + // we only need tick size from current market for the price scale settings + // if markets haven't been loaded via abacus, get the current market info from indexer + (async () => { + if (marketId && !hasPriceScaleInfo) { + const marketTickSize = await getMarketTickSize(marketId); + const priceScale = BigNumber(10).exponentiatedBy( + BigNumber(marketTickSize).decimalPlaces() ?? 2 + ); + setInitialPriceScale(priceScale.toNumber()); + } + })(); + }, [marketId, hasPriceScaleInfo]); useEffect(() => { - if (hasMarkets && isClientConnected && marketId) { + if (marketId && hasPriceScaleInfo) { const widgetOptions = getWidgetOptions(); const widgetOverrides = getWidgetOverrides({ appTheme, appColorMode }); const options = { ...widgetOptions, ...widgetOverrides, - datafeed: getDydxDatafeed(store, getCandlesForDatafeed), - interval: (savedResolution || DEFAULT_RESOLUTION) as ResolutionString, + datafeed: getDydxDatafeed(store, getCandlesForDatafeed, initialPriceScale), + interval: (savedResolution ?? DEFAULT_RESOLUTION) as ResolutionString, locale: SUPPORTED_LOCALE_BASE_TAGS[selectedLocale] as LanguageCode, symbol: marketId, saved_data: !isEmpty(savedTvChartConfig) ? savedTvChartConfig : undefined, }; - const tvChartWidget = new widget(options); + const tvChartWidget = new Widget(options); tvWidgetRef.current = tvChartWidget; tvWidgetRef.current.onChartReady(() => { @@ -99,14 +123,7 @@ export const useTradingView = ({ tvWidgetRef.current = null; setIsChartReady(false); }; - }, [ - getCandlesForDatafeed, - isClientConnected, - hasMarkets, - selectedLocale, - selectedNetwork, - !!marketId, - ]); + }, [selectedLocale, selectedNetwork, !!marketId, hasPriceScaleInfo]); return { savedResolution }; }; diff --git a/src/hooks/tradingView/useTradingViewTheme.ts b/src/hooks/tradingView/useTradingViewTheme.ts index 8930cbf64..448fd1746 100644 --- a/src/hooks/tradingView/useTradingViewTheme.ts +++ b/src/hooks/tradingView/useTradingViewTheme.ts @@ -6,9 +6,10 @@ import { THEME_NAMES } from '@/constants/styles/colors'; import type { ChartLine, TvWidget } from '@/constants/tvchart'; import { AppColorMode, AppTheme } from '@/state/configs'; -import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; +import { getAppColorMode, getAppTheme } from '@/state/configsSelectors'; -import { getWidgetOverrides, getChartLineColors } from '@/lib/tradingView/utils'; +import { assertNever } from '@/lib/assertNever'; +import { getChartLineColors, getWidgetOverrides } from '@/lib/tradingView/utils'; /** * @description Method to define a type guard and check that an element is an IFRAME @@ -56,10 +57,15 @@ export const useTradingViewTheme = ({ case AppTheme.Light: innerHtml?.classList.remove('theme-dark'); innerHtml?.classList.add('theme-light'); + break; + default: + assertNever(appTheme); + break; } } } + // eslint-disable-next-line @typescript-eslint/naming-convention const { overrides, studies_overrides } = getWidgetOverrides({ appTheme, appColorMode }); tvWidget?.applyOverrides(overrides); tvWidget?.applyStudiesOverrides(studies_overrides); @@ -81,7 +87,7 @@ export const useTradingViewTheme = ({ // Necessary to update existing chart lines Object.values(chartLines).forEach(({ chartLineType, line }) => { const { maybeQuantityColor, borderColor, backgroundColor, textColor, textButtonColor } = - getChartLineColors({ chartLineType: chartLineType, appTheme, appColorMode }); + getChartLineColors({ chartLineType, appTheme, appColorMode }); if (maybeQuantityColor) { line.setLineColor(maybeQuantityColor).setQuantityBackgroundColor(maybeQuantityColor); diff --git a/src/hooks/useAccountBalance.ts b/src/hooks/useAccountBalance.ts index 89cdf557c..b31671798 100644 --- a/src/hooks/useAccountBalance.ts +++ b/src/hooks/useAccountBalance.ts @@ -1,19 +1,20 @@ import { useCallback } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; -import { useBalance } from 'wagmi'; + import { StargateClient } from '@cosmjs/stargate'; import { useQuery } from 'react-query'; +import { shallowEqual, useSelector } from 'react-redux'; import { formatUnits } from 'viem'; +import { useBalance } from 'wagmi'; import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; import { EvmAddress } from '@/constants/wallets'; -import { convertBech32Address } from '@/lib/addressUtils'; -import { MustBigNumber } from '@/lib/numbers'; - import { getBalances, getStakingBalances } from '@/state/accountSelectors'; import { getSelectedNetwork } from '@/state/appSelectors'; +import { convertBech32Address } from '@/lib/addressUtils'; +import { MustBigNumber } from '@/lib/numbers'; + import { useAccounts } from './useAccounts'; import { useTokenConfigs } from './useTokenConfigs'; @@ -74,6 +75,7 @@ export const useAccountBalance = ({ return formatUnits(BigInt(balanceAsCoin.amount), decimals); } + return undefined; }, [addressOrDenom, chainId, rpc]); const cosmosQuery = useQuery({ @@ -87,7 +89,7 @@ export const useAccountBalance = ({ staleTime: 10_000, }); - const { formatted: evmBalance } = evmQuery.data || {}; + const { formatted: evmBalance } = evmQuery.data ?? {}; const balance = isCosmosChain ? cosmosQuery.data : evmBalance; const nativeTokenCoinBalance = balances?.[chainTokenDenom]; diff --git a/src/hooks/useAccounts.tsx b/src/hooks/useAccounts.tsx index 66a966e66..88cb0a77b 100644 --- a/src/hooks/useAccounts.tsx +++ b/src/hooks/useAccounts.tsx @@ -1,23 +1,23 @@ -import { useCallback, useContext, createContext, useEffect, useState, useMemo } from 'react'; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { OfflineSigner } from '@cosmjs/proto-signing'; +import { LocalWallet, NOBLE_BECH32_PREFIX, type Subaccount } from '@dydxprotocol/v4-client-js'; +import { usePrivy } from '@privy-io/react-auth'; import { AES, enc } from 'crypto-js'; -import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js'; +import { useDispatch } from 'react-redux'; import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account'; -import { DialogTypes } from '@/constants/dialogs'; -import { STRING_KEYS } from '@/constants/localization'; -import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage'; +import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage'; import { DydxAddress, EvmAddress, PrivateInformation, TEST_WALLET_EVM_ADDRESS, + WalletConnectionType, WalletType, } from '@/constants/wallets'; -import { setOnboardingState, setOnboardingGuard } from '@/state/account'; -import { forceOpenDialog } from '@/state/dialogs'; +import { setOnboardingGuard, setOnboardingState } from '@/state/account'; import abacusStateManager from '@/lib/abacus'; import { log } from '@/lib/telemetry'; @@ -25,7 +25,7 @@ import { testFlags } from '@/lib/testFlags'; import { useDydxClient } from './useDydxClient'; import { useLocalStorage } from './useLocalStorage'; -import { useRestrictions } from './useRestrictions'; +import useSignForWalletDerivation from './useSignForWalletDerivation'; import { useWalletConnection } from './useWalletConnection'; const AccountsContext = createContext | undefined>(undefined); @@ -75,6 +75,8 @@ const useAccountsContext = () => { setPreviousEvmAddress(evmAddress); }, [evmAddress]); + const { ready, authenticated } = usePrivy(); + // EVM → dYdX account derivation const [evmDerivedAddresses, saveEvmDerivedAddresses] = useLocalStorage({ @@ -89,17 +91,17 @@ const useAccountsContext = () => { }, []); const saveEvmDerivedAccount = ({ - evmAddress, + evmAddressInner, dydxAddress, }: { - evmAddress: EvmAddress; + evmAddressInner: EvmAddress; dydxAddress?: DydxAddress; }) => { saveEvmDerivedAddresses({ ...evmDerivedAddresses, version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.EvmDerivedAddresses], - [evmAddress]: { - ...evmDerivedAddresses[evmAddress], + [evmAddressInner]: { + ...evmDerivedAddresses[evmAddressInner], dydxAddress, }, }); @@ -135,29 +137,24 @@ const useAccountsContext = () => { }; // dYdXClient Onboarding & Account Helpers - const { compositeClient, getWalletFromEvmSignature } = useDydxClient(); + const { indexerClient, getWalletFromEvmSignature } = useDydxClient(); // dYdX subaccounts const [dydxSubaccounts, setDydxSubaccounts] = useState(); - const { getSubaccounts } = useMemo( - () => ({ - getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => { - try { - const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress); - setDydxSubaccounts(response?.subaccounts); - return response?.subaccounts ?? []; - } catch (error) { - // 404 is expected if the user has no subaccounts - if (error.status === 404) { - return []; - } else { - throw error; - } - } - }, - }), - [compositeClient] - ); + const getSubaccounts = async ({ dydxAddress }: { dydxAddress: DydxAddress }) => { + try { + const response = await indexerClient.account.getSubaccounts(dydxAddress); + setDydxSubaccounts(response?.subaccounts); + return response?.subaccounts ?? []; + } catch (error) { + // 404 is expected if the user has no subaccounts + // 403 is expected if the user account is blocked + if (error.status === 404 || error.status === 403) { + return []; + } + throw error; + } + }; // dYdX wallet / onboarding state const [localDydxWallet, setLocalDydxWallet] = useState(); @@ -171,10 +168,7 @@ const useAccountsContext = () => { [localDydxWallet] ); - const nobleAddress = useMemo( - () => localNobleWallet?.address, - [localNobleWallet] - ); + const nobleAddress = useMemo(() => localNobleWallet?.address, [localNobleWallet]); const setWalletFromEvmSignature = async (signature: string) => { const { wallet, mnemonic, privateKey, publicKey } = await getWalletFromEvmSignature({ @@ -186,24 +180,27 @@ const useAccountsContext = () => { useEffect(() => { if (evmAddress) { - saveEvmDerivedAccount({ evmAddress, dydxAddress }); + saveEvmDerivedAccount({ evmAddressInner: evmAddress, dydxAddress }); } }, [evmAddress, dydxAddress]); + const signTypedDataAsync = useSignForWalletDerivation(); + useEffect(() => { (async () => { if (walletType === WalletType.TestWallet) { // Get override values. Use the testFlags value if it exists, otherwise use the previously // saved value where possible. If neither exist, use a default garbage value. - const addressOverride: DydxAddress = testFlags.addressOverride as DydxAddress || - evmDerivedAddresses?.[TEST_WALLET_EVM_ADDRESS]?.dydxAddress as DydxAddress || + const addressOverride: DydxAddress = + (testFlags.addressOverride as DydxAddress) || + (evmDerivedAddresses?.[TEST_WALLET_EVM_ADDRESS]?.dydxAddress as DydxAddress) || 'dydx1'; dispatch(setOnboardingState(OnboardingState.WalletConnected)); // Set variables. saveEvmDerivedAccount({ - evmAddress: TEST_WALLET_EVM_ADDRESS, + evmAddressInner: TEST_WALLET_EVM_ADDRESS, dydxAddress: addressOverride, }); const wallet = new LocalWallet(); @@ -214,7 +211,7 @@ const useAccountsContext = () => { } else if (connectedDydxAddress && signerGraz) { dispatch(setOnboardingState(OnboardingState.WalletConnected)); try { - setLocalDydxWallet(await LocalWallet.fromOfflineSigner(signerGraz)); + setLocalDydxWallet(await LocalWallet.fromOfflineSigner(signerGraz as OfflineSigner)); dispatch(setOnboardingState(OnboardingState.AccountConnected)); } catch (error) { log('useAccounts/setLocalDydxWallet', error); @@ -225,7 +222,17 @@ const useAccountsContext = () => { const evmDerivedAccount = evmDerivedAddresses[evmAddress]; - if (evmDerivedAccount?.encryptedSignature) { + if (walletConnectionType === WalletConnectionType.Privy && authenticated && ready) { + try { + const signature = await signTypedDataAsync(); + + await setWalletFromEvmSignature(signature); + dispatch(setOnboardingState(OnboardingState.AccountConnected)); + } catch (error) { + log('useAccounts/decryptSignature', error); + forgetEvmSignature(); + } + } else if (evmDerivedAccount?.encryptedSignature) { try { const signature = decryptSignature(evmDerivedAccount.encryptedSignature); @@ -248,9 +255,9 @@ const useAccountsContext = () => { // abacus useEffect(() => { - if (dydxAddress) abacusStateManager.setAccount(localDydxWallet); + if (dydxAddress) abacusStateManager.setAccount(localDydxWallet, hdKey); else abacusStateManager.attemptDisconnectAccount(); - }, [localDydxWallet]); + }, [localDydxWallet, hdKey]); useEffect(() => { const setNobleWallet = async () => { @@ -285,7 +292,7 @@ const useAccountsContext = () => { value: hasAcknowledgedTerms, }) ); - }, [hasAcknowledgedTerms]); + }, [dispatch, hasAcknowledgedTerms]); useEffect(() => { const hasPreviousTransactions = Boolean(dydxSubaccounts?.length); @@ -296,22 +303,7 @@ const useAccountsContext = () => { value: hasPreviousTransactions, }) ); - }, [dydxSubaccounts]); - - // Restrictions - const { isBadActor, sanctionedAddresses } = useRestrictions(); - - useEffect(() => { - if ( - dydxAddress && - (isBadActor || - sanctionedAddresses.has(dydxAddress) || - (evmAddress && sanctionedAddresses.has(evmAddress))) - ) { - dispatch(forceOpenDialog({ type: DialogTypes.RestrictedWallet })); - disconnect(); - } - }, [isBadActor, evmAddress, dydxAddress, sanctionedAddresses]); + }, [dispatch, dydxSubaccounts]); // Disconnect wallet / accounts const disconnectLocalDydxWallet = () => { diff --git a/src/hooks/useAnalytics.ts b/src/hooks/useAnalytics.ts index 3415ba958..8a7af8dfd 100644 --- a/src/hooks/useAnalytics.ts +++ b/src/hooks/useAnalytics.ts @@ -1,48 +1,52 @@ import { useEffect, useState } from 'react'; -import { useSelector, shallowEqual } from 'react-redux'; -import { useLocation } from 'react-router-dom'; - -import { AnalyticsEvent, AnalyticsUserProperty } from '@/constants/analytics'; -import { track, identify } from '@/lib/analytics'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; -import { useApiState } from './useApiState'; -import { useBreakpoints } from './useBreakpoints'; -import { useSelectedNetwork } from './useSelectedNetwork'; -import { useAccounts } from './useAccounts'; -import { useDydxClient } from './useDydxClient'; +import { + AnalyticsEvent, + AnalyticsUserProperty, + lastSuccessfulWebsocketRequestByOrigin, +} from '@/constants/analytics'; +import type { DialogTypes } from '@/constants/dialogs'; -import { getSelectedLocale } from '@/state/localizationSelectors'; -import { getOnboardingState, getSubaccountId } from '@/state/accountSelectors'; import { calculateOnboardingStep } from '@/state/accountCalculators'; +import { getOnboardingState, getSubaccountId } from '@/state/accountSelectors'; import { getActiveDialog } from '@/state/dialogsSelectors'; -import type { DialogTypes } from '@/constants/dialogs'; +import { getInputTradeData } from '@/state/inputsSelectors'; +import { getSelectedLocale } from '@/state/localizationSelectors'; +import { identify, track } from '@/lib/analytics'; import { getSelectedTradeType } from '@/lib/tradeData'; -import { getInputTradeData } from '@/state/inputsSelectors'; + +import { useAccounts } from './useAccounts'; +import { useApiState } from './useApiState'; +import { useBreakpoints } from './useBreakpoints'; +import { useDydxClient } from './useDydxClient'; +import { useSelectedNetwork } from './useSelectedNetwork'; export const useAnalytics = () => { - const { walletType, walletConnectionType, evmAddress, dydxAddress, selectedWalletType } = useAccounts(); - const { compositeClient } = useDydxClient(); + const latestTag = import.meta.env.VITE_LAST_TAG; + const { walletType, walletConnectionType, evmAddress, dydxAddress, selectedWalletType } = + useAccounts(); + const { indexerClient } = useDydxClient(); /** User properties */ // AnalyticsUserProperty.Breakpoint const breakpointMatches = useBreakpoints(); - const breakpoint = - breakpointMatches.isMobile ? - 'MOBILE' - : breakpointMatches.isTablet ? - 'TABLET' - : breakpointMatches.isDesktopSmall ? - 'DESKTOP_SMALL' - : breakpointMatches.isDesktopMedium ? - 'DESKTOP_MEDIUM' - : breakpointMatches.isDesktopLarge ? - 'DESKTOP_LARGE' - : - 'UNSUPPORTED'; + const breakpoint = breakpointMatches.isMobile + ? 'MOBILE' + : breakpointMatches.isTablet + ? 'TABLET' + : breakpointMatches.isDesktopSmall + ? 'DESKTOP_SMALL' + : breakpointMatches.isDesktopMedium + ? 'DESKTOP_MEDIUM' + : breakpointMatches.isDesktopLarge + ? 'DESKTOP_LARGE' + : 'UNSUPPORTED'; useEffect(() => { identify(AnalyticsUserProperty.Breakpoint, breakpoint); @@ -55,6 +59,13 @@ export const useAnalytics = () => { identify(AnalyticsUserProperty.Locale, selectedLocale); }, [selectedLocale]); + // AnalyticsUserProperty.Version + useEffect(() => { + if (latestTag !== undefined) { + identify(AnalyticsUserProperty.Version, latestTag.split(`release/v`).at(-1)); + } + }, [latestTag]); + // AnalyticsUserProperty.Network const { selectedNetwork } = useSelectedNetwork(); @@ -74,7 +85,7 @@ export const useAnalytics = () => { // AnalyticsUserProperty.WalletAddress useEffect(() => { - identify(AnalyticsUserProperty.WalletAddress, evmAddress || dydxAddress); + identify(AnalyticsUserProperty.WalletAddress, evmAddress ?? dydxAddress); }, [evmAddress, dydxAddress]); // AnalyticsUserProperty.DydxAddress @@ -88,7 +99,6 @@ export const useAnalytics = () => { identify(AnalyticsUserProperty.SubaccountNumber, subaccountNumber); }, [subaccountNumber]); - /** Events */ // AnalyticsEvent.AppStart @@ -97,11 +107,11 @@ export const useAnalytics = () => { }, []); // AnalyticsEvent.NetworkStatus - const { height, indexerHeight, status, trailingBlocks} = useApiState(); + const { height, indexerHeight, status, trailingBlocks } = useApiState(); useEffect(() => { if (status) { - const websocketEndpoint = compositeClient?.indexerClient?.config.websocketEndpoint; + const websocketEndpoint = indexerClient.config.websocketEndpoint; const lastSuccessfulIndexerRpcQuery = (websocketEndpoint && @@ -114,7 +124,7 @@ export const useAnalytics = () => { elapsedTime: lastSuccessfulIndexerRpcQuery && Date.now() - lastSuccessfulIndexerRpcQuery, blockHeight: height ?? undefined, indexerBlockHeight: indexerHeight ?? undefined, - trailingBlocks: trailingBlocks ?? undefined + trailingBlocks: trailingBlocks ?? undefined, }); } }, [status]); @@ -152,7 +162,11 @@ export const useAnalytics = () => { const onClick = (e: MouseEvent) => { const anchorElement = (e.target as Element).closest('a'); - if (anchorElement instanceof HTMLAnchorElement && anchorElement.href && anchorElement.hostname !== globalThis.location.hostname) + if ( + anchorElement instanceof HTMLAnchorElement && + anchorElement.href && + anchorElement.hostname !== globalThis.location.hostname + ) track(AnalyticsEvent.NavigateExternal, { link: anchorElement.href }); }; globalThis.addEventListener('click', onClick); @@ -176,8 +190,8 @@ export const useAnalytics = () => { } }, [onboardingState, currentOnboardingStep]); - // AnalyticsEvent.OnboardingConnectWallet - // AnalyticsEvent.OnboardingDisconnectWallet + // AnalyticsEvent.ConnectWallet + // AnalyticsEvent.DisconnectWallet const [previousSelectedWalletType, setPreviousSelectedWalletType] = useState(); @@ -209,6 +223,3 @@ export const useAnalytics = () => { } }, [selectedOrderType]); }; - -export const lastSuccessfulRestRequestByOrigin: Record = {}; -export const lastSuccessfulWebsocketRequestByOrigin: Record = {}; diff --git a/src/hooks/useAnimationFrame.ts b/src/hooks/useAnimationFrame.ts index 324e2b41c..50d3b6796 100644 --- a/src/hooks/useAnimationFrame.ts +++ b/src/hooks/useAnimationFrame.ts @@ -1,20 +1,18 @@ import { useCallback, useEffect, useRef } from 'react'; -export const useAnimationFrame = ( - callback: (_: number) => void, - deps: React.DependencyList -) => { +export const useAnimationFrame = (callback: (_: number) => void, deps: React.DependencyList) => { const requestRef = useRef(); const previousTimeRef = useRef(); const animate = useCallback(async (time: number) => { - if (previousTimeRef.current != undefined) { + if (previousTimeRef.current != null) { const deltaTime = time - previousTimeRef.current; callback(deltaTime); } previousTimeRef.current = time; requestRef.current = requestAnimationFrame(animate); + // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); useEffect(() => { @@ -23,5 +21,6 @@ export const useAnimationFrame = ( return () => { if (requestRef.current) cancelAnimationFrame(requestRef.current); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); }; diff --git a/src/hooks/useApiState.ts b/src/hooks/useApiState.ts index a35893d4f..212f08180 100644 --- a/src/hooks/useApiState.ts +++ b/src/hooks/useApiState.ts @@ -4,56 +4,75 @@ import type { AbacusApiState, Nullable } from '@/constants/abacus'; import { AbacusApiStatus } from '@/constants/abacus'; import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; -import { getApiState } from '@/state/appSelectors'; +import { getApiState, getInitializationError } from '@/state/appSelectors'; import { useStringGetter } from './useStringGetter'; -const getStatusErrorMessage = ({ +export enum ConnectionErrorType { + CHAIN_DISRUPTION = 'CHAIN_DISRUPTION', + INDEXER_TRAILING = 'INDEXER_TRAILING', +} + +const ErrorMessageMap = { + [ConnectionErrorType.CHAIN_DISRUPTION]: { + title: STRING_KEYS.CHAIN_DISRUPTION_DETECTED, + body: STRING_KEYS.CHAIN_DISRUPTION_DETECTED_BODY, + }, + [ConnectionErrorType.INDEXER_TRAILING]: { + title: STRING_KEYS.ORDERBOOK_LAGGING, + body: STRING_KEYS.ORDERBOOK_LAGGING_BODY, + }, +}; + +const getConnectionError = ({ apiState, - stringGetter, + initializationError, }: { apiState: Nullable; - stringGetter: StringGetterFunction; + initializationError?: string; }) => { - const { haltedBlock, trailingBlocks, status } = apiState || {}; + const { status } = apiState ?? {}; + + if (initializationError) { + return ConnectionErrorType.CHAIN_DISRUPTION; + } switch (status) { - case AbacusApiStatus.INDEXER_DOWN: { - return stringGetter({ key: STRING_KEYS.INDEXER_DOWN }); - } - case AbacusApiStatus.INDEXER_HALTED: { - return stringGetter({ - key: STRING_KEYS.INDEXER_HALTED, - params: { HALTED_BLOCK: haltedBlock }, - }); - } case AbacusApiStatus.INDEXER_TRAILING: { - return stringGetter({ - key: STRING_KEYS.INDEXER_TRAILING, - params: { TRAILING_BLOCKS: trailingBlocks }, - }); - } - case AbacusApiStatus.VALIDATOR_DOWN: { - return stringGetter({ key: STRING_KEYS.VALIDATOR_DOWN }); - } - case AbacusApiStatus.VALIDATOR_HALTED: { - return stringGetter({ - key: STRING_KEYS.VALIDATOR_HALTED, - params: { HALTED_BLOCK: haltedBlock }, - }); + return ConnectionErrorType.INDEXER_TRAILING; } + case AbacusApiStatus.INDEXER_DOWN: + case AbacusApiStatus.INDEXER_HALTED: + case AbacusApiStatus.VALIDATOR_DOWN: + case AbacusApiStatus.VALIDATOR_HALTED: case AbacusApiStatus.UNKNOWN: { - return stringGetter({ key: STRING_KEYS.UNKNOWN_API_ERROR }); + return ConnectionErrorType.CHAIN_DISRUPTION; } case AbacusApiStatus.NORMAL: default: { - return null; + return undefined; } } }; +const getStatusErrorMessage = ({ + connectionError, + stringGetter, +}: { + connectionError?: ConnectionErrorType; + stringGetter: StringGetterFunction; +}) => { + if (connectionError && ErrorMessageMap[connectionError]) { + return { + title: stringGetter({ key: ErrorMessageMap[connectionError].title }), + body: stringGetter({ key: ErrorMessageMap[connectionError].body }), + }; + } + return null; +}; + export const getIndexerHeight = (apiState: Nullable) => { - const { haltedBlock, trailingBlocks, status, height } = apiState || {}; + const { haltedBlock, trailingBlocks, status, height } = apiState ?? {}; switch (status) { case AbacusApiStatus.INDEXER_HALTED: { @@ -71,8 +90,13 @@ export const getIndexerHeight = (apiState: Nullable) => { export const useApiState = () => { const stringGetter = useStringGetter(); const apiState = useSelector(getApiState, shallowEqual); - const { haltedBlock, height, status, trailingBlocks} = apiState ?? {}; - const statusErrorMessage = getStatusErrorMessage({ apiState, stringGetter }); + const initializationError = useSelector(getInitializationError); + const { haltedBlock, height, status, trailingBlocks } = apiState ?? {}; + const connectionError = getConnectionError({ + apiState, + initializationError, + }); + const statusErrorMessage = getStatusErrorMessage({ connectionError, stringGetter }); const indexerHeight = getIndexerHeight(apiState); return { @@ -80,6 +104,7 @@ export const useApiState = () => { height, indexerHeight, status, + connectionError, statusErrorMessage, trailingBlocks, }; diff --git a/src/hooks/useAppThemeAndColorMode.tsx b/src/hooks/useAppThemeAndColorMode.tsx index 1cbc3decf..88f301e87 100644 --- a/src/hooks/useAppThemeAndColorMode.tsx +++ b/src/hooks/useAppThemeAndColorMode.tsx @@ -1,12 +1,15 @@ import { useEffect, useState } from 'react'; + import { useSelector } from 'react-redux'; import { ThemeProvider } from 'styled-components'; -import { AppTheme, AppThemeSetting, AppColorMode, AppThemeSystemSetting } from '@/state/configs'; -import { getAppThemeSetting, getAppColorMode } from '@/state/configsSelectors'; - import { Themes } from '@/styles/themes'; +import { AppColorMode, AppTheme, AppThemeSetting, AppThemeSystemSetting } from '@/state/configs'; +import { getAppColorMode, getAppThemeSetting } from '@/state/configsSelectors'; + +import { assertNever } from '@/lib/assertNever'; + export const AppThemeAndColorModeProvider = ({ ...props }) => { return ; }; @@ -22,7 +25,7 @@ export const useAppThemeAndColorModeContext = () => { ); useEffect(() => { - const handler = (e) => { + const handler = (e: MediaQueryListEvent) => { if (e.matches) { setSystemPreference(AppTheme.Dark); } else { @@ -41,6 +44,9 @@ export const useAppThemeAndColorModeContext = () => { case AppTheme.Dark: case AppTheme.Light: return themeSetting; + default: + assertNever(themeSetting, true); + return systemPreference; } }; diff --git a/src/hooks/useBreakpoints.ts b/src/hooks/useBreakpoints.ts index c9d3b3b7b..b81849a5d 100644 --- a/src/hooks/useBreakpoints.ts +++ b/src/hooks/useBreakpoints.ts @@ -20,7 +20,7 @@ export const mediaQueryLists = { [MediaQueryKeys.isDesktopSmall]: globalThis.matchMedia(breakpoints.desktopSmall), [MediaQueryKeys.isDesktopMedium]: globalThis.matchMedia(breakpoints.desktopMedium), [MediaQueryKeys.isDesktopLarge]: globalThis.matchMedia(breakpoints.desktopLarge), -}; +} as const; export const uniqueMediaQueryLists = { ...mediaQueryLists }; @@ -29,6 +29,8 @@ export const useBreakpoints = () => { const state = Object.fromEntries( Object.entries(mediaQueryLists).map(([key, mediaQueryList]) => [ key, + // this is technically okay since the loop is fully deterministic and the object won't change + // eslint-disable-next-line react-hooks/rules-of-hooks useState(mediaQueryList.matches), ]) ); diff --git a/src/hooks/useCommandMenu.ts b/src/hooks/useCommandMenu.ts index 0918f2ad1..50daf62ad 100644 --- a/src/hooks/useCommandMenu.ts +++ b/src/hooks/useCommandMenu.ts @@ -5,9 +5,9 @@ export const useCommandMenu = () => { useEffect(() => { document.addEventListener('keydown', (event: KeyboardEvent) => { - if(event.key === 'k' && (event.ctrlKey || event.metaKey)) - setIsCommandMenuOpen(!isCommandMenuOpen) - }) + if (event.key === 'k' && (event.ctrlKey || event.metaKey)) + setIsCommandMenuOpen(!isCommandMenuOpen); + }); }, []); return { diff --git a/src/hooks/useComplianceState.tsx b/src/hooks/useComplianceState.tsx new file mode 100644 index 000000000..2bb70e627 --- /dev/null +++ b/src/hooks/useComplianceState.tsx @@ -0,0 +1,86 @@ +import { shallowEqual, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; + +import { ComplianceStatus } from '@/constants/abacus'; +import { CLOSE_ONLY_GRACE_PERIOD, ComplianceStates } from '@/constants/compliance'; +import { STRING_KEYS } from '@/constants/localization'; +import { isMainnet } from '@/constants/networks'; + +import { LinkOutIcon } from '@/icons'; + +import { getComplianceStatus, getComplianceUpdatedAt, getGeo } from '@/state/accountSelectors'; +import { getSelectedLocale } from '@/state/localizationSelectors'; + +import { isBlockedGeo } from '@/lib/compliance'; + +import { useStringGetter } from './useStringGetter'; +import { useURLConfigs } from './useURLConfigs'; + +export const useComplianceState = () => { + const stringGetter = useStringGetter(); + const complianceStatus = useSelector(getComplianceStatus, shallowEqual); + const complianceUpdatedAt = useSelector(getComplianceUpdatedAt); + const geo = useSelector(getGeo); + const selectedLocale = useSelector(getSelectedLocale); + + const updatedAtDate = complianceUpdatedAt ? new Date(complianceUpdatedAt) : undefined; + updatedAtDate?.setDate(updatedAtDate.getDate() + CLOSE_ONLY_GRACE_PERIOD); + + const { complianceSupportEmail } = useURLConfigs(); + + let complianceState = ComplianceStates.FULL_ACCESS; + + let complianceMessage; + + if ( + complianceStatus === ComplianceStatus.FIRST_STRIKE_CLOSE_ONLY || + complianceStatus === ComplianceStatus.CLOSE_ONLY + ) { + complianceState = ComplianceStates.CLOSE_ONLY; + } else if ( + complianceStatus === ComplianceStatus.BLOCKED || + (geo && isBlockedGeo(geo) && isMainnet) + ) { + complianceState = ComplianceStates.READ_ONLY; + } + + if (complianceStatus === ComplianceStatus.FIRST_STRIKE_CLOSE_ONLY) { + complianceMessage = `${stringGetter({ key: STRING_KEYS.CLICK_TO_VIEW })} →`; + } else if (complianceStatus === ComplianceStatus.CLOSE_ONLY) { + complianceMessage = stringGetter({ + key: STRING_KEYS.CLOSE_ONLY_MESSAGE, + params: { + DATE: updatedAtDate + ? updatedAtDate.toLocaleString(selectedLocale, { + dateStyle: 'medium', + timeStyle: 'short', + }) + : undefined, + EMAIL: complianceSupportEmail, + }, + }) as string; + } else if (complianceStatus === ComplianceStatus.BLOCKED) { + complianceMessage = stringGetter({ + key: STRING_KEYS.PERMANENTLY_BLOCKED_MESSAGE, + params: { EMAIL: complianceSupportEmail }, + }) as string; + } else if (geo && isBlockedGeo(geo)) { + complianceMessage = stringGetter({ + key: STRING_KEYS.BLOCKED_MESSAGE, + params: { + LINK: ( + + + + ), + }, + }) as string; + } + + return { + geo, + complianceStatus, + complianceState, + complianceMessage, + }; +}; diff --git a/src/hooks/useCurrentMarketId.ts b/src/hooks/useCurrentMarketId.ts index c9a916620..d4dad9bde 100644 --- a/src/hooks/useCurrentMarketId.ts +++ b/src/hooks/useCurrentMarketId.ts @@ -1,14 +1,20 @@ import { useEffect, useMemo } from 'react'; + import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { useMatch, useNavigate } from 'react-router-dom'; +import { SubaccountPosition } from '@/constants/abacus'; +import { TradeBoxDialogTypes } from '@/constants/dialogs'; import { LocalStorageKey } from '@/constants/localStorage'; import { DEFAULT_MARKETID } from '@/constants/markets'; import { AppRoute } from '@/constants/routes'; + import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { getOpenPositions } from '@/state/accountSelectors'; import { getSelectedNetwork } from '@/state/appSelectors'; import { closeDialogInTradeBox } from '@/state/dialogs'; +import { getActiveTradeBoxDialog } from '@/state/dialogsSelectors'; import { setCurrentMarketId } from '@/state/perpetuals'; import { getMarketIds } from '@/state/perpetualsSelectors'; @@ -20,8 +26,10 @@ export const useCurrentMarketId = () => { const { marketId } = match?.params ?? {}; const dispatch = useDispatch(); const selectedNetwork = useSelector(getSelectedNetwork); + const openPositions = useSelector(getOpenPositions, shallowEqual); const marketIds = useSelector(getMarketIds, shallowEqual); const hasMarketIds = marketIds.length > 0; + const activeTradeBoxDialog = useSelector(getActiveTradeBoxDialog); const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({ key: LocalStorageKey.LastViewedMarket, @@ -57,6 +65,15 @@ export const useCurrentMarketId = () => { // If marketId is valid, set currentMarketId setLastViewedMarket(marketId); dispatch(setCurrentMarketId(marketId)); + + if ( + activeTradeBoxDialog?.type === TradeBoxDialogTypes.ClosePosition && + openPositions?.find((position: SubaccountPosition) => position.id === marketId) + ) { + // Keep the close positions dialog open between market changes as long as there exists an open position + return; + } + dispatch(closeDialogInTradeBox()); } } diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts index 5b38cc4bb..398975caa 100644 --- a/src/hooks/useDebounce.ts +++ b/src/hooks/useDebounce.ts @@ -10,7 +10,7 @@ export function useDebounce(value: T, delay?: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { - const timer = setTimeout(() => setDebouncedValue(value), delay || 500); + const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500); return () => { clearTimeout(timer); diff --git a/src/hooks/useDialogArea.tsx b/src/hooks/useDialogArea.tsx index 05231654c..c78d06df2 100644 --- a/src/hooks/useDialogArea.tsx +++ b/src/hooks/useDialogArea.tsx @@ -1,6 +1,8 @@ -import { useContext, createContext, useState } from 'react'; +import { createContext, useContext, useRef } from 'react'; -const DialogAreaContext = createContext | undefined>(undefined); +const DialogAreaContext = createContext | undefined>( + undefined +); DialogAreaContext.displayName = 'DialogArea'; @@ -8,13 +10,11 @@ export const DialogAreaProvider = ({ ...props }) => ( ); -export const useDialogArea = () => useContext(DialogAreaContext)!; +export const useDialogArea = () => useContext(DialogAreaContext); const useDialogAreaContext = () => { - const [dialogArea, setDialogArea] = useState(); - + const dialogAreaRef = useRef(null); return { - dialogArea, - setDialogArea + dialogAreaRef, }; }; diff --git a/src/hooks/useDisplayedWallets.ts b/src/hooks/useDisplayedWallets.ts index 876b5d7c3..4c9a41d76 100644 --- a/src/hooks/useDisplayedWallets.ts +++ b/src/hooks/useDisplayedWallets.ts @@ -4,7 +4,7 @@ import { WalletType } from '@/constants/wallets'; import { isTruthy } from '@/lib/isTruthy'; export const useDisplayedWallets = () => { - return [ + const displayedWallets = [ WalletType.MetaMask, isDev && WalletType.Keplr, @@ -22,6 +22,10 @@ export const useDisplayedWallets = () => { // WalletType.BitKeep, // WalletType.Coin98, + Boolean(import.meta.env.VITE_PRIVY_APP_ID) && WalletType.Privy, + WalletType.OtherWallet, ].filter(isTruthy); + + return displayedWallets; }; diff --git a/src/hooks/useDydxClient.tsx b/src/hooks/useDydxClient.tsx index 494f3156d..d0a2057ae 100644 --- a/src/hooks/useDydxClient.tsx +++ b/src/hooks/useDydxClient.tsx @@ -1,28 +1,33 @@ -import { createContext, useCallback, useContext, useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { BECH32_PREFIX, CompositeClient, FaucetClient, + IndexerClient, IndexerConfig, LocalWallet, - onboarding, Network, + SelectedGasDenom, ValidatorConfig, + onboarding, type ProposalStatus, } from '@dydxprotocol/v4-client-js'; - import type { ResolutionString } from 'public/tradingview/charting_library'; +import { useSelector } from 'react-redux'; import type { ConnectNetworkEvent, NetworkConfig } from '@/constants/abacus'; import { DEFAULT_TRANSACTION_MEMO } from '@/constants/analytics'; -import { type Candle, RESOLUTION_MAP } from '@/constants/candles'; +import { RESOLUTION_MAP, type Candle } from '@/constants/candles'; +import { LocalStorageKey } from '@/constants/localStorage'; import { getSelectedNetwork } from '@/state/appSelectors'; +import abacusStateManager from '@/lib/abacus'; import { log } from '@/lib/telemetry'; +import { useEndpointsConfig } from './useEndpointsConfig'; +import { useLocalStorage } from './useLocalStorage'; import { useRestrictions } from './useRestrictions'; import { useTokenConfigs } from './useTokenConfigs'; @@ -58,6 +63,12 @@ const useDydxClientContext = () => { const [compositeClient, setCompositeClient] = useState(); const [faucetClient, setFaucetClient] = useState(); + const { indexer: indexerEndpoints } = useEndpointsConfig(); + const indexerClient = useMemo(() => { + const config = new IndexerConfig(indexerEndpoints.api, indexerEndpoints.socket); + return new IndexerClient(config); + }, [indexerEndpoints]); + useEffect(() => { (async () => { if ( @@ -105,6 +116,31 @@ const useDydxClientContext = () => { })(); }, [networkConfig]); + // ------ Gas Denom ------ // + + const [gasDenom, setGasDenom] = useLocalStorage({ + key: LocalStorageKey.SelectedGasDenom, + defaultValue: SelectedGasDenom.USDC, + }); + + const setSelectedGasDenom = useCallback( + (selectedGasDenom: SelectedGasDenom) => { + if (compositeClient) { + compositeClient.validatorClient.setSelectedGasDenom(selectedGasDenom); + abacusStateManager.setSelectedGasDenom(selectedGasDenom); + setGasDenom(selectedGasDenom); + } + }, + [compositeClient, setGasDenom] + ); + + useEffect(() => { + if (compositeClient) { + setSelectedGasDenom(gasDenom); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [compositeClient, setSelectedGasDenom]); + // ------ Wallet Methods ------ // const getWalletFromEvmSignature = async ({ signature }: { signature: string }) => { const { mnemonic, privateKey, publicKey } = @@ -119,16 +155,25 @@ const useDydxClientContext = () => { }; // ------ Public Methods ------ // - const requestAllPerpetualMarkets = useCallback(async () => { + const requestAllPerpetualMarkets = async () => { try { - const { markets } = - (await compositeClient?.indexerClient.markets.getPerpetualMarkets()) || {}; + const { markets } = (await indexerClient.markets.getPerpetualMarkets()) ?? {}; return markets || []; } catch (error) { log('useDydxClient/getPerpetualMarkets', error); return []; } - }, [compositeClient]); + }; + + const getMarketTickSize = async (marketId: string) => { + try { + const { markets } = (await indexerClient.markets.getPerpetualMarkets(marketId)) ?? {}; + return markets?.[marketId]?.tickSize; + } catch (error) { + log('useDydxClient/getMarketTickSize', error); + return undefined; + } + }; /** * @param proposalStatus - Optional filter for proposal status. If not provided, all proposals in ProposalStatus.VotingPeriod will be returned. @@ -149,104 +194,110 @@ const useDydxClientContext = () => { [compositeClient] ); - const requestCandles = useCallback( - async ({ - marketId, - marketType = 'perpetualMarkets', - resolution, - fromIso, - toIso, - }: { - marketId: string; - marketType?: string; - resolution: ResolutionString; - fromIso?: string; - toIso?: string; - }): Promise => { - try { - const { candles } = - (await compositeClient?.indexerClient.markets.getPerpetualMarketCandles( - marketId, - RESOLUTION_MAP[resolution], - fromIso, - toIso - )) || {}; - return candles || []; - } catch (error) { - log('useDydxClient/getPerpetualMarketCandles', error); - return []; - } - }, - [compositeClient] - ); - - const getCandlesForDatafeed = useCallback( - async ({ - marketId, - resolution, - fromMs, - toMs, - }: { - marketId: string; - resolution: ResolutionString; - fromMs: number; - toMs: number; - }) => { - const fromIso = new Date(fromMs).toISOString(); - let toIso = new Date(toMs).toISOString(); - const candlesInRange: Candle[] = []; - - while (true) { - const candles = await requestCandles({ + const requestCandles = async ({ + marketId, + resolution, + fromIso, + toIso, + limit, + }: { + marketId: string; + resolution: ResolutionString; + fromIso?: string; + toIso?: string; + limit?: number; + }): Promise => { + try { + const { candles } = + (await indexerClient.markets.getPerpetualMarketCandles( marketId, - resolution, + RESOLUTION_MAP[resolution], fromIso, toIso, - }); + limit + )) || {}; + return candles || []; + } catch (error) { + log('useDydxClient/getPerpetualMarketCandles', error); + return []; + } + }; - if (!candles || candles.length === 0) { - break; - } + const getCandlesForDatafeed = async ({ + marketId, + resolution, + fromMs, + toMs, + }: { + marketId: string; + resolution: ResolutionString; + fromMs: number; + toMs: number; + }) => { + const fromIso = new Date(fromMs).toISOString(); + let toIso = new Date(toMs).toISOString(); + const candlesInRange: Candle[] = []; + + while (true) { + // eslint-disable-next-line no-await-in-loop + const candles = await requestCandles({ + marketId, + resolution, + fromIso, + toIso, + }); + + if (!candles || candles.length === 0) { + break; + } - candlesInRange.push(...candles); - const length = candlesInRange.length; + candlesInRange.push(...candles); + const length = candlesInRange.length; - if (length) { - const oldestTime = new Date(candlesInRange[length - 1].startedAt).getTime(); + if (length) { + const oldestTime = new Date(candlesInRange[length - 1].startedAt).getTime(); - if (oldestTime > fromMs) { - toIso = candlesInRange[length - 1].startedAt; - } else { - break; - } + if (oldestTime > fromMs) { + toIso = candlesInRange[length - 1].startedAt; } else { break; } + } else { + break; } + } - return candlesInRange; - }, - [requestCandles] - ); + return candlesInRange; + }; const { updateSanctionedAddresses } = useRestrictions(); - const screenAddresses = useCallback( - async ({ addresses }: { addresses: string[] }) => { - if (compositeClient) { - const promises = addresses.map((address) => - compositeClient.indexerClient.utility.screen(address) - ); + const screenAddresses = async ({ addresses }: { addresses: string[] }) => { + const promises = addresses.map((address) => indexerClient.utility.screen(address)); - const results = await Promise.all(promises); + const results = await Promise.all(promises); - const screenedAddresses = Object.fromEntries( - addresses.map((address, index) => [address, results[index]?.restricted]) - ); + const screenedAddresses = Object.fromEntries( + addresses.map((address, index) => [address, results[index]?.restricted]) + ); - updateSanctionedAddresses(screenedAddresses); - return screenedAddresses; - } + updateSanctionedAddresses(screenedAddresses); + return screenedAddresses; + }; + + const getPerpetualMarketSparklines = async ({ + period = 'SEVEN_DAYS', + }: { + period?: 'ONE_DAY' | 'SEVEN_DAYS'; + }) => indexerClient.markets.getPerpetualMarketSparklines(period); + + const getWithdrawalAndTransferGatingStatus = useCallback(async () => { + return compositeClient?.validatorClient.get.getWithdrawalAndTransferGatingStatus(); + }, [compositeClient]); + + const getWithdrawalCapacityByDenom = useCallback( + async ({ denom }: { denom: string }) => { + return compositeClient?.validatorClient.get.getWithdrawalCapacityByDenom(denom); }, [compositeClient] ); @@ -257,7 +308,12 @@ const useDydxClientContext = () => { networkConfig, compositeClient, faucetClient, - isConnected: !!compositeClient, + indexerClient, + isCompositeClientConnected: !!compositeClient, + + // Gas Denom + setSelectedGasDenom, + selectedGasDenom: gasDenom, // Wallet Methods getWalletFromEvmSignature, @@ -266,6 +322,11 @@ const useDydxClientContext = () => { requestAllPerpetualMarkets, requestAllGovernanceProposals, getCandlesForDatafeed, + getCandles: requestCandles, + getMarketTickSize, + getPerpetualMarketSparklines, screenAddresses, + getWithdrawalAndTransferGatingStatus, + getWithdrawalCapacityByDenom, }; }; diff --git a/src/hooks/useEndpointsConfig.ts b/src/hooks/useEndpointsConfig.ts new file mode 100644 index 000000000..09fecdc84 --- /dev/null +++ b/src/hooks/useEndpointsConfig.ts @@ -0,0 +1,29 @@ +import { useSelector } from 'react-redux'; + +import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; + +import { getSelectedNetwork } from '@/state/appSelectors'; + +interface EndpointsConfig { + indexers: { + api: string; + socket: string; + }[]; + validators: string[]; + '0xsquid': string; + nobleValidator: string; + faucet?: string; +} + +export const useEndpointsConfig = () => { + const selectedNetwork = useSelector(getSelectedNetwork); + const endpointsConfig = ENVIRONMENT_CONFIG_MAP[selectedNetwork].endpoints as EndpointsConfig; + + return { + indexer: endpointsConfig.indexers[0], // assume there's only one option for indexer endpoints + validators: endpointsConfig.validators, + '0xsquid': endpointsConfig['0xsquid'], + nobleValidator: endpointsConfig.nobleValidator, + faucet: endpointsConfig.faucet, + }; +}; diff --git a/src/hooks/useEnvFeatures.ts b/src/hooks/useEnvFeatures.ts new file mode 100644 index 000000000..0d308e266 --- /dev/null +++ b/src/hooks/useEnvFeatures.ts @@ -0,0 +1,19 @@ +import { useSelector } from 'react-redux'; + +import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; + +import { getSelectedNetwork } from '@/state/appSelectors'; + +export interface EnvironmentFeatures { + reduceOnlySupported: boolean; + withdrawalSafetyEnabled: boolean; + CCTPWithdrawalOnly: boolean; + CCTPDepositOnly: boolean; + isSlTpEnabled: boolean; + isSlTpLimitOrdersEnabled: boolean; +} + +export const useEnvFeatures = (): EnvironmentFeatures => { + const selectedNetwork = useSelector(getSelectedNetwork); + return ENVIRONMENT_CONFIG_MAP[selectedNetwork].featureFlags; +}; diff --git a/src/hooks/useInitializePage.ts b/src/hooks/useInitializePage.ts index 9a61b9703..a225dd2f7 100644 --- a/src/hooks/useInitializePage.ts +++ b/src/hooks/useInitializePage.ts @@ -1,17 +1,17 @@ import { useEffect } from 'react'; + import { useDispatch } from 'react-redux'; import { LocalStorageKey } from '@/constants/localStorage'; - import { DEFAULT_APP_ENVIRONMENT, type DydxNetwork } from '@/constants/networks'; -import { useLocalStorage } from '@/hooks'; - import { initializeLocalization } from '@/state/app'; import abacusStateManager from '@/lib/abacus'; import { validateAgainstAvailableEnvironments } from '@/lib/network'; +import { useLocalStorage } from './useLocalStorage'; + export const useInitializePage = () => { const dispatch = useDispatch(); diff --git a/src/hooks/useInterval.tsx b/src/hooks/useInterval.tsx index e6fb01bba..b37758d94 100644 --- a/src/hooks/useInterval.tsx +++ b/src/hooks/useInterval.tsx @@ -8,7 +8,7 @@ type ElementProps = { export const useInterval = ({ callback, periodInMs = 1000 }: ElementProps) => { useEffect(() => { callback?.(); - + const interval = setInterval(() => { callback?.(); }, periodInMs); diff --git a/src/hooks/useIsFirstRender.ts b/src/hooks/useIsFirstRender.ts index 15e7e35fa..1a90cb51b 100644 --- a/src/hooks/useIsFirstRender.ts +++ b/src/hooks/useIsFirstRender.ts @@ -1,4 +1,4 @@ -import { useRef, useEffect } from 'react'; +import { useEffect, useRef } from 'react'; /** * indicate whether the current render is the first render (when the component was mounted) diff --git a/src/hooks/useLocalNotifications.tsx b/src/hooks/useLocalNotifications.tsx index 450e6d5f0..27eca44d8 100644 --- a/src/hooks/useLocalNotifications.tsx +++ b/src/hooks/useLocalNotifications.tsx @@ -1,10 +1,14 @@ -import { createContext, useContext, useCallback, useEffect, useMemo } from 'react'; +import { createContext, useCallback, useContext, useEffect } from 'react'; + import { useQuery } from 'react-query'; +import { AnalyticsEvent } from '@/constants/analytics'; import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage'; -import { type TransferNotifcation } from '@/constants/notifications'; +import type { TransferNotifcation } from '@/constants/notifications'; + import { useAccounts } from '@/hooks/useAccounts'; +import { track } from '@/lib/analytics'; import { fetchSquidStatus, STATUS_ERROR_GRACE_PERIOD } from '@/lib/squid'; import { useLocalStorage } from './useLocalStorage'; @@ -25,7 +29,6 @@ const TRANSFER_STATUS_FETCH_INTERVAL = 10_000; const ERROR_COUNT_THRESHOLD = 3; const useLocalNotificationsContext = () => { - // transfer notifications const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{ [key: `dydx${string}`]: TransferNotifcation[]; version: string; @@ -55,25 +58,45 @@ const useLocalNotificationsContext = () => { const setTransferNotifications = useCallback( (notifications: TransferNotifcation[]) => { if (!dydxAddress) return; - const updatedNotifications = { ...allTransferNotifications }; - updatedNotifications[dydxAddress] = notifications; - setAllTransferNotifications(updatedNotifications); + setAllTransferNotifications((currentAllNotifications) => { + const updatedNotifications = { ...currentAllNotifications }; + + updatedNotifications[dydxAddress] = [ + ...notifications, + ...(updatedNotifications[dydxAddress] || []).slice(notifications.length), + ]; + + return updatedNotifications; + }); }, - [setAllTransferNotifications, dydxAddress] + [setAllTransferNotifications, dydxAddress, allTransferNotifications] ); const addTransferNotification = useCallback( - (notification: TransferNotifcation) => - setTransferNotifications([...transferNotifications, notification]), + (notification: TransferNotifcation) => { + const { txHash, triggeredAt, toAmount, type } = notification; + setTransferNotifications([...transferNotifications, notification]); + // track initialized new transfer notification + track(AnalyticsEvent.TransferNotification, { + triggeredAt, + timeSpent: triggeredAt ? Date.now() - triggeredAt : undefined, + txHash, + toAmount, + type, + status: 'new', + }); + }, [transferNotifications] ); useQuery({ queryKey: 'getTransactionStatus', queryFn: async () => { - const processTransferNotifications = async (transferNotifications: TransferNotifcation[]) => { + const processTransferNotifications = async ( + transferNotificationsInner: TransferNotifcation[] + ) => { const newTransferNotifications = await Promise.all( - transferNotifications.map(async (transferNotification) => { + transferNotificationsInner.map(async (transferNotification) => { const { txHash, toChainId, @@ -83,6 +106,7 @@ const useLocalNotificationsContext = () => { errorCount, status: currentStatus, isExchange, + requestId, } = transferNotification; const hasErrors = @@ -104,16 +128,35 @@ const useLocalNotificationsContext = () => { toChainId, fromChainId, }, - isCctp + isCctp, + undefined, + requestId ); - if (status) { transferNotification.status = status; + if (status.squidTransactionStatus === 'success') { + track(AnalyticsEvent.TransferNotification, { + triggeredAt, + timeSpent: triggeredAt ? Date.now() - triggeredAt : undefined, + toAmount: transferNotification.toAmount, + status: 'success', + type: transferNotification.type, + txHash, + }); + } } } catch (error) { if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) { if (errorCount && errorCount > ERROR_COUNT_THRESHOLD) { transferNotification.status = error; + track(AnalyticsEvent.TransferNotification, { + triggeredAt, + timeSpent: triggeredAt ? Date.now() - triggeredAt : undefined, + toAmount: transferNotification.toAmount, + status: 'error', + type: transferNotification.type, + txHash, + }); } else { transferNotification.errorCount = errorCount ? errorCount + 1 : 1; } @@ -132,8 +175,8 @@ const useLocalNotificationsContext = () => { }, refetchInterval: TRANSFER_STATUS_FETCH_INTERVAL, }); - return { + // Transfer notifications transferNotifications, addTransferNotification, }; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index fe2b40065..1075fe49a 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -28,6 +28,7 @@ export const useLocalStorage = ({ return false; } } + return undefined; }; globalThis.window.addEventListener('storage', onStorage); diff --git a/src/hooks/useLocaleSeparators.tsx b/src/hooks/useLocaleSeparators.tsx index 870f7478d..dc27647a9 100644 --- a/src/hooks/useLocaleSeparators.tsx +++ b/src/hooks/useLocaleSeparators.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; + import { useSelector } from 'react-redux'; import { SUPPORTED_BASE_TAGS_LOCALE_MAPPING } from '@/constants/localization'; diff --git a/src/hooks/useMarketsData.ts b/src/hooks/useMarketsData.ts index b5c60114f..65abe0dd8 100644 --- a/src/hooks/useMarketsData.ts +++ b/src/hooks/useMarketsData.ts @@ -1,12 +1,19 @@ import { useMemo } from 'react'; -import { useSelector, shallowEqual } from 'react-redux'; -import { MarketFilters, MARKET_FILTER_LABELS, type MarketData } from '@/constants/markets'; +import { shallowEqual, useSelector } from 'react-redux'; + +import { MARKET_FILTER_LABELS, MarketFilters, type MarketData } from '@/constants/markets'; + +import { + SEVEN_DAY_SPARKLINE_ENTRIES, + usePerpetualMarketSparklines, +} from '@/hooks/usePerpetualMarketSparklines'; import { getAssets } from '@/state/assetsSelectors'; import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; import { isTruthy } from '@/lib/isTruthy'; +import { orEmptyObj } from '@/lib/typeUtils'; const filterFunctions = { [MarketFilters.ALL]: () => true, @@ -16,6 +23,24 @@ const filterFunctions = { [MarketFilters.DEFI]: (market: MarketData) => { return market.asset.tags?.toArray().includes('Defi'); }, + [MarketFilters.LAYER_2]: (market: MarketData) => { + return market.asset.tags?.toArray().includes('Layer 2'); + }, + [MarketFilters.NFT]: (market: MarketData) => { + return market.asset.tags?.toArray().includes('NFT'); + }, + [MarketFilters.GAMING]: (market: MarketData) => { + return market.asset.tags?.toArray().includes('Gaming'); + }, + [MarketFilters.AI]: (market: MarketData) => { + return market.asset.tags?.toArray().includes('AI'); + }, + [MarketFilters.MEME]: (market: MarketData) => { + return market.asset.tags?.toArray().includes('Meme'); + }, + [MarketFilters.NEW]: (market: MarketData) => { + return market.isNew; + }, }; export const useMarketsData = ( @@ -26,20 +51,44 @@ export const useMarketsData = ( filteredMarkets: MarketData[]; marketFilters: string[]; } => { - const allPerpetualMarkets = useSelector(getPerpetualMarkets, shallowEqual) || {}; - const allAssets = useSelector(getAssets, shallowEqual) || {}; + const allPerpetualMarkets = orEmptyObj(useSelector(getPerpetualMarkets, shallowEqual)); + const allAssets = orEmptyObj(useSelector(getAssets, shallowEqual)); + const sevenDaysSparklineData = usePerpetualMarketSparklines(); const markets = useMemo(() => { return Object.values(allPerpetualMarkets) .filter(isTruthy) - .map((marketData) => ({ - asset: allAssets[marketData.assetId] ?? {}, - tickSizeDecimals: marketData.configs?.tickSizeDecimals, - ...marketData, - ...marketData.perpetual, - ...marketData.configs, - })) as MarketData[]; - }, [allPerpetualMarkets, allAssets]); + .map((marketData) => { + const sevenDaySparklineEntries = sevenDaysSparklineData?.[marketData.id]?.length ?? 0; + const isNew = Boolean( + sevenDaysSparklineData && sevenDaySparklineEntries < SEVEN_DAY_SPARKLINE_ENTRIES + ); + + /** + * There is no date in the services to determine when it was listed, but we can calculate it approximately. + * Keeping in mind that the `/sparklines` service using the period `SEVEN_DAYS` as a parameter, + * returns a maximum of 6 entries for each day with a timeframe of 4 hours. + * For this it is possible to estimate the listing date as follows: + * `Hours elapsed since listing = (Total sparklines entries * 6)` + */ + let listingDate: Date | undefined; + + if (isNew) { + listingDate = new Date(); + listingDate.setHours(listingDate.getHours() - sevenDaySparklineEntries * 4); + } + + return { + asset: allAssets[marketData.assetId] ?? {}, + tickSizeDecimals: marketData.configs?.tickSizeDecimals, + isNew, + listingDate, + ...marketData, + ...marketData.perpetual, + ...marketData.configs, + }; + }) as MarketData[]; + }, [allPerpetualMarkets, allAssets, sevenDaysSparklineData]); const filteredMarkets = useMemo(() => { const filtered = markets.filter(filterFunctions[filter]); @@ -47,9 +96,9 @@ export const useMarketsData = ( if (searchFilter) { return filtered.filter( ({ asset, id }) => - asset?.name?.toLocaleLowerCase().includes(searchFilter.toLowerCase()) || - asset?.id?.toLocaleLowerCase().includes(searchFilter.toLowerCase()) || - id?.toLocaleLowerCase().includes(searchFilter.toLowerCase()) + !!asset?.name?.toLocaleLowerCase().includes(searchFilter.toLowerCase()) || + !!asset?.id?.toLocaleLowerCase().includes(searchFilter.toLowerCase()) || + !!id?.toLocaleLowerCase().includes(searchFilter.toLowerCase()) ); } return filtered; @@ -58,6 +107,7 @@ export const useMarketsData = ( const marketFilters = useMemo( () => [ MarketFilters.ALL, + MarketFilters.NEW, ...Object.keys(MARKET_FILTER_LABELS).filter((marketFilter) => markets.some((market) => market.asset?.tags?.toArray().some((tag) => tag === marketFilter)) ), diff --git a/src/hooks/useMatchingEvmNetwork.ts b/src/hooks/useMatchingEvmNetwork.ts index b793837f3..99150e92a 100644 --- a/src/hooks/useMatchingEvmNetwork.ts +++ b/src/hooks/useMatchingEvmNetwork.ts @@ -1,6 +1,12 @@ import { useCallback, useEffect, useMemo } from 'react'; + +import { useSwitchNetwork as useSwitchNetworkPrivy } from '@privy-io/wagmi-connector'; import { useNetwork, useSwitchNetwork } from 'wagmi'; +import { WalletConnectionType } from '@/constants/wallets'; + +import { useWalletConnection } from './useWalletConnection'; + export const useMatchingEvmNetwork = ({ chainId, switchAutomatically = false, @@ -11,7 +17,10 @@ export const useMatchingEvmNetwork = ({ onError?: (error: Error) => void; }) => { const { chain } = useNetwork(); + const { walletConnectionType } = useWalletConnection(); const { isLoading, switchNetworkAsync } = useSwitchNetwork({ onError }); + const { isLoading: isLoadingPrivy, switchNetworkAsync: switchNetworkAsyncPrivy } = + useSwitchNetworkPrivy({ onError }); // If chainId is not a number, we can assume it is a non EVM compatible chain const isMatchingNetwork = useMemo( @@ -21,7 +30,11 @@ export const useMatchingEvmNetwork = ({ const matchNetwork = useCallback(async () => { if (!isMatchingNetwork) { - await switchNetworkAsync?.(Number(chainId)); + if (walletConnectionType === WalletConnectionType.Privy) { + await switchNetworkAsyncPrivy?.(Number(chainId)); + } else { + await switchNetworkAsync?.(Number(chainId)); + } } }, [chainId, chain]); @@ -34,6 +47,6 @@ export const useMatchingEvmNetwork = ({ return { isMatchingNetwork, matchNetwork, - isSwitchingNetwork: isLoading, + isSwitchingNetwork: isLoading || isLoadingPrivy, }; }; diff --git a/src/hooks/useNextClobPairId.ts b/src/hooks/useNextClobPairId.ts index f2c74e6ae..fcee63efc 100644 --- a/src/hooks/useNextClobPairId.ts +++ b/src/hooks/useNextClobPairId.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { useQuery } from 'react-query'; import { MsgCreateClobPair, @@ -13,16 +12,65 @@ import { TYPE_URL_MSG_DELAY_MESSAGE, TYPE_URL_MSG_UPDATE_CLOB_PAIR, } from '@dydxprotocol/v4-client-js'; +import { useQuery } from 'react-query'; import type { PerpetualMarketResponse } from '@/constants/indexer'; + import { useDydxClient } from '@/hooks/useDydxClient'; +/** + * + * @param message from proposal. Each message is wrapped in a type any (on purpose). + * @param callback method used to compile all clobPairIds, perpetualIds, marketIds, etc. + */ +const decodeMsgForClobPairId = ( + message: any, + addIdFromProposal: (id?: number) => void, + addTickerFromProposal: (ticker?: string) => void +): any => { + const { typeUrl, value } = message; + + switch (typeUrl) { + case TYPE_URL_MSG_CREATE_ORACLE_MARKET: { + const decodedValue = MsgCreateOracleMarket.decode(value); + addIdFromProposal(decodedValue.params?.id); + addTickerFromProposal(decodedValue.params?.pair); + break; + } + case TYPE_URL_MSG_CREATE_PERPETUAL: { + const decodedValue = MsgCreatePerpetual.decode(value); + addIdFromProposal(decodedValue.params?.id); + addIdFromProposal(decodedValue.params?.marketId); + break; + } + case TYPE_URL_MSG_CREATE_CLOB_PAIR: { + const decodedValue = MsgCreateClobPair.decode(value); + addIdFromProposal(decodedValue.clobPair?.id); + addIdFromProposal(decodedValue.clobPair?.perpetualClobMetadata?.perpetualId); + break; + } + case TYPE_URL_MSG_UPDATE_CLOB_PAIR: { + const decodedValue = MsgUpdateClobPair.decode(value); + addIdFromProposal(decodedValue.clobPair?.id); + addIdFromProposal(decodedValue.clobPair?.perpetualClobMetadata?.perpetualId); + break; + } + case TYPE_URL_MSG_DELAY_MESSAGE: { + const decodedValue = MsgDelayMessage.decode(value); + decodeMsgForClobPairId(decodedValue.msg, addIdFromProposal, addTickerFromProposal); + break; + } + default: { + break; + } + } +}; + export const useNextClobPairId = () => { - const { isConnected, requestAllPerpetualMarkets, requestAllGovernanceProposals } = + const { isCompositeClientConnected, requestAllPerpetualMarkets, requestAllGovernanceProposals } = useDydxClient(); const { data: perpetualMarkets, status: perpetualMarketsStatus } = useQuery({ - enabled: isConnected, queryKey: 'requestAllPerpetualMarkets', queryFn: requestAllPerpetualMarkets, refetchInterval: 60_000, @@ -30,69 +78,35 @@ export const useNextClobPairId = () => { }); const { data: allGovProposals, status: allGovProposalsStatus } = useQuery({ - enabled: isConnected, + enabled: isCompositeClientConnected, queryKey: 'requestAllActiveGovernanceProposals', queryFn: () => requestAllGovernanceProposals(), refetchInterval: 10_000, staleTime: 10_000, }); - /** - * - * @param message from proposal. Each message is wrapped in a type any (on purpose). - * @param callback method used to compile all clobPairIds, perpetualIds, marketIds, etc. - */ - const decodeMsgForClobPairId = (message: any, callback: (id?: number) => void): any => { - const { typeUrl, value } = message; - - switch (typeUrl) { - case TYPE_URL_MSG_CREATE_ORACLE_MARKET: { - const decodedValue = MsgCreateOracleMarket.decode(value); - callback(decodedValue.params?.id); - break; - } - case TYPE_URL_MSG_CREATE_PERPETUAL: { - const decodedValue = MsgCreatePerpetual.decode(value); - callback(decodedValue.params?.id); - callback(decodedValue.params?.marketId); - break; - } - case TYPE_URL_MSG_CREATE_CLOB_PAIR: { - const decodedValue = MsgCreateClobPair.decode(value); - callback(decodedValue.clobPair?.id); - callback(decodedValue.clobPair?.perpetualClobMetadata?.perpetualId); - break; - } - case TYPE_URL_MSG_UPDATE_CLOB_PAIR: { - const decodedValue = MsgUpdateClobPair.decode(value); - callback(decodedValue.clobPair?.id); - callback(decodedValue.clobPair?.perpetualClobMetadata?.perpetualId); - break; - } - case TYPE_URL_MSG_DELAY_MESSAGE: { - const decodedValue = MsgDelayMessage.decode(value); - decodeMsgForClobPairId(decodedValue.msg, callback); - break; - } - default: { - break; + const { nextAvailableClobPairId, tickersFromProposals } = useMemo(() => { + const idsFromProposals: number[] = []; + const newTickersFromProposals: Set = new Set(); + + const addIdFromProposal = (id?: number) => { + if (id) { + idsFromProposals.push(id); } - } - }; + }; - const nextAvailableClobPairId = useMemo(() => { - const idsFromProposals: number[] = []; + const addTickerFromProposal = (ticker?: string) => { + if (ticker) { + newTickersFromProposals.add(ticker); + } + }; if (allGovProposals && Object.values(allGovProposals.proposals).length > 0) { const proposals = allGovProposals.proposals; proposals.forEach((proposal) => { if (proposal.messages) { - proposal.messages.map((message) => { - decodeMsgForClobPairId(message, (id?: number) => { - if (id) { - idsFromProposals.push(id); - } - }); + proposal.messages.forEach((message) => { + decodeMsgForClobPairId(message, addIdFromProposal, addTickerFromProposal); }); } }); @@ -103,16 +117,23 @@ export const useNextClobPairId = () => { Number((perpetualMarket as PerpetualMarketResponse).clobPairId) ); - const nextAvailableClobPairId = Math.max(...[...clobPairIds, ...idsFromProposals]) + 1; - return nextAvailableClobPairId; + const newNextAvailableClobPairId = Math.max(...[...clobPairIds, ...idsFromProposals]) + 1; + return { + nextAvailableClobPairId: newNextAvailableClobPairId, + tickersFromProposals: newTickersFromProposals, + }; } - return undefined; + return { + nextAvailableClobPairId: undefined, + tickersFromProposals: newTickersFromProposals, + }; }, [perpetualMarkets, allGovProposals]); return { allGovProposalsStatus, perpetualMarketsStatus, nextAvailableClobPairId, + tickersFromProposals, }; }; diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx index a6c0bb412..fce60f6d2 100644 --- a/src/hooks/useNotificationTypes.tsx +++ b/src/hooks/useNotificationTypes.tsx @@ -1,57 +1,76 @@ -import { type ReactNode, useEffect } from 'react'; -import styled from 'styled-components'; +import { useEffect } from 'react'; + +import { groupBy, isEqual } from 'lodash'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import { isEqual, groupBy } from 'lodash'; import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import { ComplianceStatus } from '@/constants/abacus'; +import { ComplianceStates } from '@/constants/compliance'; import { DialogTypes } from '@/constants/dialogs'; -import { AppRoute, TokenRoute } from '@/constants/routes'; -import { DydxChainAsset } from '@/constants/wallets'; - import { STRING_KEYS, STRING_KEY_VALUES, type StringGetterFunction, type StringKey, } from '@/constants/localization'; - import { - type NotificationTypeConfig, - NotificationType, DEFAULT_TOAST_AUTO_CLOSE_MS, - TransferNotificationTypes, + NotificationDisplayData, + NotificationType, ReleaseUpdateNotificationIds, + TransferNotificationTypes, + type NotificationTypeConfig, } from '@/constants/notifications'; +import { AppRoute, TokenRoute } from '@/constants/routes'; +import { DydxChainAsset } from '@/constants/wallets'; -import { useStringGetter, useTokenConfigs } from '@/hooks'; import { useLocalNotifications } from '@/hooks/useLocalNotifications'; import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; +import { Link } from '@/components/Link'; +// eslint-disable-next-line import/no-cycle import { BlockRewardNotification } from '@/views/notifications/BlockRewardNotification'; +import { IncentiveSeasonDistributionNotification } from '@/views/notifications/IncentiveSeasonDistributionNotification'; +import { OrderCancelNotification } from '@/views/notifications/OrderCancelNotification'; +import { OrderStatusNotification } from '@/views/notifications/OrderStatusNotification'; import { TradeNotification } from '@/views/notifications/TradeNotification'; import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification'; -import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors'; +import { + getLocalCancelOrders, + getLocalPlaceOrders, + getSubaccountFills, + getSubaccountOrders, +} from '@/state/accountSelectors'; +import { getSelectedDydxChainId } from '@/state/appSelectors'; import { openDialog } from '@/state/dialogs'; import { getAbacusNotifications } from '@/state/notificationsSelectors'; import { getMarketIds } from '@/state/perpetualsSelectors'; -import { getSelectedDydxChainId } from '@/state/appSelectors'; import { formatSeconds } from '@/lib/timeUtils'; +import { useAccounts } from './useAccounts'; +import { useApiState } from './useApiState'; +import { useComplianceState } from './useComplianceState'; +import { useQueryChaosLabsIncentives } from './useQueryChaosLabsIncentives'; +import { useStringGetter } from './useStringGetter'; +import { useTokenConfigs } from './useTokenConfigs'; +import { useURLConfigs } from './useURLConfigs'; + const parseStringParamsForNotification = ({ stringGetter, value, }: { stringGetter: StringGetterFunction; value: unknown; -}): ReactNode => { +}) => { if (STRING_KEY_VALUES[value as StringKey]) { - return stringGetter({ key: value as StringKey }); + return stringGetter({ key: value as string }); } - return value as ReactNode; + return value as string; }; export const notificationTypes: NotificationTypeConfig[] = [ @@ -60,8 +79,12 @@ export const notificationTypes: NotificationTypeConfig[] = [ useTrigger: ({ trigger }) => { const stringGetter = useStringGetter(); const abacusNotifications = useSelector(getAbacusNotifications, isEqual); + const orders = useSelector(getSubaccountOrders, shallowEqual) ?? []; + const ordersById = groupBy(orders, 'id'); + const localPlaceOrders = useSelector(getLocalPlaceOrders, shallowEqual); useEffect(() => { + // eslint-disable-next-line no-restricted-syntax for (const abacusNotif of abacusNotifications) { const [abacusNotificationType = '', id = ''] = abacusNotif.id.split(':'); const parsedData = abacusNotif.data ? JSON.parse(abacusNotif.data) : {}; @@ -74,6 +97,13 @@ export const notificationTypes: NotificationTypeConfig[] = [ switch (abacusNotificationType) { case 'order': { + const order = ordersById[id]?.[0]; + const clientId: number | undefined = order?.clientId ?? undefined; + const localOrderExists = + clientId && localPlaceOrders.some((ordr) => ordr.clientId === clientId); + + if (localOrderExists) return; // already handled by OrderStatusNotification + trigger( abacusNotif.id, { @@ -139,15 +169,16 @@ export const notificationTypes: NotificationTypeConfig[] = [ }, useNotificationAction: () => { const dispatch = useDispatch(); - const orders = useSelector(getSubaccountOrders, shallowEqual) || []; + const orders = useSelector(getSubaccountOrders, shallowEqual) ?? []; const ordersById = groupBy(orders, 'id'); - const fills = useSelector(getSubaccountFills, shallowEqual) || []; + const fills = useSelector(getSubaccountFills, shallowEqual) ?? []; const fillsById = groupBy(fills, 'id'); const marketIds = useSelector(getMarketIds, shallowEqual); const navigate = useNavigate(); return (notificationId: string) => { - const [abacusNotificationType = '', id = ''] = notificationId.split(':'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [abacusNotificationType, id = ''] = notificationId.split(':'); if (ordersById[id]) { dispatch( @@ -179,6 +210,7 @@ export const notificationTypes: NotificationTypeConfig[] = [ const selectedDydxChainId = useSelector(getSelectedDydxChainId); useEffect(() => { + // eslint-disable-next-line no-restricted-syntax for (const transfer of transferNotifications) { const { fromChainId, status, txHash, toAmount, type, isExchange } = transfer; const isFinished = @@ -186,9 +218,10 @@ export const notificationTypes: NotificationTypeConfig[] = [ const icon = ; const transferType = - type ?? fromChainId === selectedDydxChainId + type ?? + (fromChainId === selectedDydxChainId ? TransferNotificationTypes.Withdrawal - : TransferNotificationTypes.Deposit; + : TransferNotificationTypes.Deposit); const title = stringGetter({ key: { @@ -197,7 +230,7 @@ export const notificationTypes: NotificationTypeConfig[] = [ }[transferType], }); - const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0; + const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration ?? 0; const estimatedDuration = formatSeconds(Math.max(toChainEta, 0)); const body = stringGetter({ key: STRING_KEYS.DEPOSIT_STATUS, @@ -241,72 +274,259 @@ export const notificationTypes: NotificationTypeConfig[] = [ useTrigger: ({ trigger }) => { const { chainTokenLabel } = useTokenConfigs(); const stringGetter = useStringGetter(); - const expirationDate = new Date('2024-03-08T23:59:59'); + + const incentivesExpirationDate = new Date('2024-05-09T23:59:59'); + const conditionalOrdersExpirationDate = new Date('2024-06-01T23:59:59'); + const currentDate = new Date(); useEffect(() => { - trigger( - ReleaseUpdateNotificationIds.RewardsAndFullTradingLive, - { - icon: , - title: stringGetter({ key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.TITLE' }), - body: stringGetter({ - key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.BODY', - params: { - DOS_BLOGPOST: ( - <$Link - href="https://www.dydxopsdao.com/blog/deep-dive-full-trading" - target="_blank" - rel="noopener noreferrer" - > - {stringGetter({ key: STRING_KEYS.HERE })} - - ), - TRADING_BLOGPOST: ( - <$Link - href="https://dydx.exchange/blog/v4-full-trading" - target="_blank" - rel="noopener noreferrer" - > - {stringGetter({ key: STRING_KEYS.HERE })} - - ), - }, - }), - toastSensitivity: 'foreground', - groupKey: ReleaseUpdateNotificationIds.RewardsAndFullTradingLive, - }, - [] - ); - if (currentDate <= expirationDate) { + if (currentDate <= incentivesExpirationDate) { trigger( - ReleaseUpdateNotificationIds.IncentivesS3, + ReleaseUpdateNotificationIds.IncentivesS4, { icon: , - title: stringGetter({ key: 'NOTIFICATIONS.INCENTIVES_SEASON_BEGUN.TITLE' }), + title: stringGetter({ + key: 'NOTIFICATIONS.INCENTIVES_SEASON_BEGUN.TITLE', + params: { SEASON_NUMBER: '4' }, + }), body: stringGetter({ key: 'NOTIFICATIONS.INCENTIVES_SEASON_BEGUN.BODY', params: { - SEASON_NUMBER: '3', - PREV_SEASON_NUMBER: '1', - DYDX_AMOUNT: '34', - USDC_AMOUNT: '100', + PREV_SEASON_NUMBER: '2', + DYDX_AMOUNT: '16', + USDC_AMOUNT: '50', }, }), toastSensitivity: 'foreground', - groupKey: ReleaseUpdateNotificationIds.IncentivesS3, + groupKey: ReleaseUpdateNotificationIds.IncentivesS4, + }, + [] + ); + } + + if (currentDate <= conditionalOrdersExpirationDate) { + trigger( + ReleaseUpdateNotificationIds.RevampedConditionalOrders, + { + icon: , + title: stringGetter({ + key: 'NOTIFICATIONS.CONDITIONAL_ORDERS_REVAMP.TITLE', + }), + body: stringGetter({ + key: 'NOTIFICATIONS.CONDITIONAL_ORDERS_REVAMP.BODY', + params: { + TWITTER_LINK: ( + <$Link href="https://twitter.com/dYdX/status/1785339109268935042"> + {stringGetter({ key: STRING_KEYS.HERE })} + + ), + }, + }), + toastSensitivity: 'foreground', + groupKey: ReleaseUpdateNotificationIds.RevampedConditionalOrders, }, [] ); } }, [stringGetter]); + + const { dydxAddress } = useAccounts(); + const { data, status } = useQueryChaosLabsIncentives({ + dydxAddress, + season: 3, + }); + + const { dydxRewards } = data ?? {}; + + useEffect(() => { + if (dydxAddress && status === 'success') { + trigger( + ReleaseUpdateNotificationIds.IncentivesDistributedS3, + { + icon: , + title: 'Season 3 launch rewards have been distributed!', + body: `Season 3 rewards: +${dydxRewards ?? 0} ${chainTokenLabel}`, + renderCustomBody({ isToast, notification }) { + return ( + + ); + }, + toastSensitivity: 'foreground', + groupKey: ReleaseUpdateNotificationIds.IncentivesDistributedS3, + }, + [] + ); + } + }, [dydxAddress, status, dydxRewards]); }, useNotificationAction: () => { const { chainTokenLabel } = useTokenConfigs(); const navigate = useNavigate(); + return (notificationId: string) => { - if (notificationId === ReleaseUpdateNotificationIds.IncentivesS3) { + if (notificationId === ReleaseUpdateNotificationIds.IncentivesS4) { navigate(`${chainTokenLabel}/${TokenRoute.TradingRewards}`); + } else if (notificationId === ReleaseUpdateNotificationIds.IncentivesDistributedS3) { + navigate(`${chainTokenLabel}/${TokenRoute.StakingRewards}`); + } + }; + }, + }, + { + type: NotificationType.ApiError, + useTrigger: ({ trigger }) => { + const stringGetter = useStringGetter(); + const { statusErrorMessage } = useApiState(); + const { statusPage } = useURLConfigs(); + + useEffect(() => { + if (statusErrorMessage) { + trigger( + NotificationType.ApiError, + { + icon: <$WarningIcon iconName={IconName.Warning} />, + title: statusErrorMessage.title, + body: statusErrorMessage.body, + toastSensitivity: 'foreground', + groupKey: NotificationType.ApiError, + withClose: false, + actionAltText: stringGetter({ key: STRING_KEYS.STATUS_PAGE }), + renderActionSlot: () => ( + {stringGetter({ key: STRING_KEYS.STATUS_PAGE })} → + ), + }, + [] + ); + } + }, [stringGetter, statusErrorMessage?.body, statusErrorMessage?.title]); + }, + useNotificationAction: () => { + return () => {}; + }, + }, + { + type: NotificationType.ComplianceAlert, + useTrigger: ({ trigger }) => { + const stringGetter = useStringGetter(); + const { complianceMessage, complianceState, complianceStatus } = useComplianceState(); + + useEffect(() => { + if (complianceState !== ComplianceStates.FULL_ACCESS) { + const displayData: NotificationDisplayData = { + icon: <$WarningIcon iconName={IconName.Warning} />, + title: stringGetter({ key: STRING_KEYS.COMPLIANCE_WARNING }), + body: complianceMessage, + toastSensitivity: 'foreground', + groupKey: NotificationType.ComplianceAlert, + withClose: false, + }; + + trigger(`${NotificationType.ComplianceAlert}-${complianceStatus}`, displayData, []); + } + }, [stringGetter, complianceMessage, complianceState, complianceStatus]); + }, + useNotificationAction: () => { + const dispatch = useDispatch(); + const { complianceStatus } = useComplianceState(); + + return () => { + if (complianceStatus === ComplianceStatus.FIRST_STRIKE_CLOSE_ONLY) { + dispatch( + openDialog({ + type: DialogTypes.GeoCompliance, + }) + ); + } + }; + }, + }, + { + type: NotificationType.OrderStatus, + useTrigger: ({ trigger }) => { + const localPlaceOrders = useSelector(getLocalPlaceOrders, shallowEqual); + const localCancelOrders = useSelector(getLocalCancelOrders, shallowEqual); + const allOrders = useSelector(getSubaccountOrders, shallowEqual); + const stringGetter = useStringGetter(); + + useEffect(() => { + // eslint-disable-next-line no-restricted-syntax + for (const localPlace of localPlaceOrders) { + const key = localPlace.clientId.toString(); + trigger( + key, + { + icon: null, + title: stringGetter({ key: STRING_KEYS.ORDER_STATUS }), + toastSensitivity: 'background', + groupKey: key, // do not collapse + toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS, + renderCustomBody: ({ isToast, notification }) => ( + + ), + }, + [localPlace.submissionStatus], + true + ); + } + }, [localPlaceOrders]); + + useEffect(() => { + // eslint-disable-next-line no-restricted-syntax + for (const localCancel of localCancelOrders) { + // ensure order exists + const existingOrder = allOrders?.find((order) => order.id === localCancel.orderId); + if (!existingOrder) return; + + // share same notification with existing local order if exists + const key = (existingOrder.clientId ?? localCancel.orderId).toString(); + + trigger( + key, + { + icon: null, + title: stringGetter({ key: STRING_KEYS.ORDER_STATUS }), + toastSensitivity: 'background', + groupKey: key, + toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS, + renderCustomBody: ({ isToast, notification }) => ( + + ), + }, + [localCancel.submissionStatus], + true + ); + } + }, [localCancelOrders]); + }, + useNotificationAction: () => { + const dispatch = useDispatch(); + const orders = useSelector(getSubaccountOrders, shallowEqual) ?? []; + + return (orderClientId: string) => { + const order = orders.find((o) => o.clientId?.toString() === orderClientId); + if (order) { + dispatch( + openDialog({ + type: DialogTypes.OrderDetails, + dialogProps: { orderId: order.id }, + }) + ); } }; }, @@ -318,6 +538,11 @@ const $Icon = styled.img` width: 1.5rem; `; -const $Link = styled.a` - --link-color: var(--color-text-2); +const $WarningIcon = styled(Icon)` + color: var(--color-warning); +`; + +const $Link = styled(Link)` + --link-color: var(--color-accent); + display: inline-block; `; diff --git a/src/hooks/useNotifications.tsx b/src/hooks/useNotifications.tsx index ba44b52e4..3de4004e1 100644 --- a/src/hooks/useNotifications.tsx +++ b/src/hooks/useNotifications.tsx @@ -1,22 +1,34 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { + ReactElement, + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { AnalyticsEvent } from '@/constants/analytics'; import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage'; import { + NotificationCategoryPreferences, + NotificationStatus, + NotificationType, + NotificationTypeCategory, + SingleSessionNotificationTypes, type Notification, type NotificationDisplayData, type NotificationPreferences, type Notifications, - NotificationStatus, - NotificationType, } from '@/constants/notifications'; -import { useLocalStorage } from './useLocalStorage'; -import { notificationTypes } from './useNotificationTypes'; - import { track } from '@/lib/analytics'; import { renderSvgToDataUrl } from '@/lib/renderSvgToDataUrl'; +import { useLocalStorage } from './useLocalStorage'; +// eslint-disable-next-line import/no-cycle +import { notificationTypes } from './useNotificationTypes'; + type NotificationsContextType = ReturnType; const NotificationsContext = createContext( @@ -33,11 +45,11 @@ export const useNotifications = () => useContext(NotificationsContext)!; const useNotificationsContext = () => { // Local storage - // const [notifications, setNotifications] = useState({}); - const [notifications, setNotifications] = useLocalStorage({ + const [localStorageNotifications, setLocalStorageNotifications] = useLocalStorage({ key: LocalStorageKey.Notifications, defaultValue: {}, }); + const [notifications, setNotifications] = useState(localStorageNotifications); const [notificationsLastUpdated, setNotificationsLastUpdated] = useLocalStorage({ key: LocalStorageKey.NotificationsLastUpdated, @@ -48,16 +60,28 @@ const useNotificationsContext = () => { useLocalStorage({ key: LocalStorageKey.NotificationPreferences, defaultValue: { - [NotificationType.AbacusGenerated]: true, - [NotificationType.SquidTransfer]: true, - [NotificationType.ReleaseUpdates]: true, + [NotificationCategoryPreferences.General]: true, + [NotificationCategoryPreferences.Transfers]: true, + [NotificationCategoryPreferences.Trading]: true, + [NotificationCategoryPreferences.MustSee]: true, version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences], }, }); useEffect(() => { setNotificationsLastUpdated(Date.now()); - }, [notifications]); + }, [notifications, setNotificationsLastUpdated]); + + useEffect(() => { + // save notifications to localstorage, but filter out single session notifications + const originalEntries = Object.entries(notifications); + const filteredEntries = originalEntries.filter( + ([, value]) => !SingleSessionNotificationTypes.includes(value.type) + ); + + const newNotifications = Object.fromEntries(filteredEntries); + setLocalStorageNotifications(newNotifications); + }, [notifications, setLocalStorageNotifications]); const getKey = useCallback( (notification: Pick, 'type' | 'id'>) => @@ -72,7 +96,7 @@ const useNotificationsContext = () => { const getDisplayData = useCallback( (notification: Notification) => notificationsDisplayData[getKey(notification)], - [notificationsDisplayData] + [getKey, notificationsDisplayData] ); // Check for version changes @@ -82,9 +106,10 @@ const useNotificationsContext = () => { LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences] ) { setNotificationPreferences({ - [NotificationType.AbacusGenerated]: true, - [NotificationType.SquidTransfer]: true, - [NotificationType.ReleaseUpdates]: true, + [NotificationCategoryPreferences.General]: true, + [NotificationCategoryPreferences.Transfers]: true, + [NotificationCategoryPreferences.Trading]: true, + [NotificationCategoryPreferences.MustSee]: true, version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences], }); } @@ -95,9 +120,12 @@ const useNotificationsContext = () => { (notification: Notification, status: NotificationStatus) => { notification.status = status; notification.timestamps[notification.status] = Date.now(); - setNotifications({ ...notifications, [getKey(notification)]: notification }); + setNotifications((ns) => ({ + ...ns, + [getKey(notification)]: notification, + })); }, - [notifications, getKey] + [getKey] ); const { markUnseen, markSeen, markCleared } = useMemo( @@ -122,14 +150,16 @@ const useNotificationsContext = () => { ); const markAllCleared = useCallback(() => { - for (const notification of Object.values(notifications)) { - markCleared(notification); - } + Object.values(notifications).forEach((n) => markCleared(n)); }, [notifications, markCleared]); // Trigger - for (const { type, useTrigger } of notificationTypes) + // eslint-disable-next-line no-restricted-syntax + for (const { type, useTrigger } of notificationTypes) { + const notificationCategory = NotificationTypeCategory[type]; + // eslint-disable-next-line react-hooks/rules-of-hooks useTrigger({ + // eslint-disable-next-line react-hooks/rules-of-hooks trigger: useCallback( (id, displayData, updateKey, isNew = true) => { const key = getKey({ type, id }); @@ -137,26 +167,26 @@ const useNotificationsContext = () => { const notification = notifications[key]; // Filter out notifications that are not enabled - if (notificationPreferences[type] !== false) { + if (notificationPreferences[notificationCategory] !== false) { // New unique key - create new notification if (!notification) { - const notification = (notifications[key] = { + const thisNotification = (notifications[key] = { id, type, timestamps: {}, updateKey, } as Notification); updateStatus( - notification, + thisNotification, isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared ); } else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) { // updateKey changed - update existing notification - const notification = notifications[key]; + const thisNotification = notifications[key]; - notification.updateKey = updateKey; - updateStatus(notification, NotificationStatus.Updated); + thisNotification.updateKey = updateKey; + updateStatus(thisNotification, NotificationStatus.Updated); } } else { // Notification is disabled - remove it @@ -166,22 +196,24 @@ const useNotificationsContext = () => { notificationsDisplayData[key] = displayData; setNotificationsDisplayData({ ...notificationsDisplayData }); }, - [notifications, updateStatus, notificationPreferences[type]] + [notifications, updateStatus, notificationPreferences[notificationCategory]] ), lastUpdated: notificationsLastUpdated, }); + } // Actions const actions = Object.fromEntries( notificationTypes.map( + // eslint-disable-next-line react-hooks/rules-of-hooks ({ type, useNotificationAction }) => [type, useNotificationAction?.()] as const ) ); - const onNotificationAction = async (notification: Notification) => { + const onNotificationAction = (notification: Notification) => { track(AnalyticsEvent.NotificationAction, { type: notification.type, id: notification.id }); - return await actions[notification.type]?.(notification.id); + return actions[notification.type]?.(notification.id); }; // Push notifications @@ -214,6 +246,7 @@ const useNotificationsContext = () => { (async () => { if (!hasEnabledPush) return; + // eslint-disable-next-line no-restricted-syntax for (const notification of Object.values(notifications)) if ( notification.status < NotificationStatus.Seen && @@ -223,25 +256,23 @@ const useNotificationsContext = () => { const displayData = getDisplayData(notification); const iconUrl = - displayData.icon && (await renderSvgToDataUrl(displayData.icon).catch(() => undefined)); + displayData.icon && + // eslint-disable-next-line no-await-in-loop + (await renderSvgToDataUrl(displayData.icon as ReactElement).catch( + () => undefined + )); const pushNotification = new globalThis.Notification(displayData.title, { renotify: true, tag: getKey(notification), data: notification, - description: displayData.body, - icon: iconUrl ?? '/favicon.svg', - badge: iconUrl ?? '/favicon.svg', - image: iconUrl ?? '/favicon.svg', - vibrate: displayData.toastSensitivity === 'foreground', + body: displayData.body, + icon: iconUrl?.toString() ?? '/favicon.svg', + badge: iconUrl?.toString() ?? '/favicon.svg', + image: iconUrl?.toString() ?? '/favicon.svg', + vibrate: displayData.toastSensitivity === 'foreground' ? 200 : undefined, requireInteraction: displayData.toastDuration === Infinity, - // actions: [ - // { - // action: displayData.actionDescription, - // title: displayData.actionDescription, - // } - // ].slice(0, globalThis.Notification.maxActions), - }); + } as any); pushNotification.addEventListener('click', () => { onNotificationAction(notification); @@ -287,7 +318,7 @@ const useNotificationsContext = () => { notificationPreferences, setNotificationPreferences, getNotificationPreferenceForType: useCallback( - (type: NotificationType) => notificationPreferences[type], + (type: NotificationType) => notificationPreferences[NotificationTypeCategory[type]], [notificationPreferences] ), }; diff --git a/src/hooks/useOnClickOutside.ts b/src/hooks/useOnClickOutside.ts index 2467f60ea..8bd48b57c 100644 --- a/src/hooks/useOnClickOutside.ts +++ b/src/hooks/useOnClickOutside.ts @@ -1,4 +1,5 @@ import React, { useEffect } from 'react'; + import useLatest from 'use-latest'; export type Handler = (event: MouseEvent) => void; @@ -13,7 +14,7 @@ export const useOnClickOutside = ({ const onClickOutsideRef = useLatest(onClickOutside); useEffect(() => { - if (!onClickOutside) return; + if (!onClickOutside) return undefined; const handleClickOutside: (e: MouseEvent) => void = (e) => { if (ref.current && onClickOutsideRef.current && !ref.current.contains(e.target as Node)) { @@ -29,5 +30,5 @@ export const useOnClickOutside = ({ clearTimeout(timeoutId); globalThis.removeEventListener('click', handleClickOutside); }; - }, [!onClickOutside]); + }, [onClickOutside]); }; diff --git a/src/hooks/useOnLastOrderIndexed.ts b/src/hooks/useOnLastOrderIndexed.ts index bdaece8ba..d6e362b97 100644 --- a/src/hooks/useOnLastOrderIndexed.ts +++ b/src/hooks/useOnLastOrderIndexed.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; + import { useSelector } from 'react-redux'; import { getLatestOrderClientId } from '@/state/accountSelectors'; diff --git a/src/hooks/usePageTitlePriceUpdates.ts b/src/hooks/usePageTitlePriceUpdates.ts index 98013d85a..4040b06a7 100644 --- a/src/hooks/usePageTitlePriceUpdates.ts +++ b/src/hooks/usePageTitlePriceUpdates.ts @@ -1,4 +1,5 @@ import { useEffect } from 'react'; + import { useSelector } from 'react-redux'; import { DEFAULT_DOCUMENT_TITLE } from '@/constants/routes'; diff --git a/src/hooks/usePerpetualMarketSparklines.ts b/src/hooks/usePerpetualMarketSparklines.ts new file mode 100644 index 000000000..6f3cd880e --- /dev/null +++ b/src/hooks/usePerpetualMarketSparklines.ts @@ -0,0 +1,41 @@ +import { useQuery } from 'react-query'; + +import type { PerpetualMarketSparklineResponse } from '@/constants/indexer'; +import { timeUnits } from '@/constants/time'; + +import { log } from '@/lib/telemetry'; + +import { useDydxClient } from './useDydxClient'; + +const POLLING_MS = timeUnits.hour; +export const SEVEN_DAY_SPARKLINE_ENTRIES = 42; +export const ONE_DAY_SPARKLINE_ENTRIES = 24; + +type UsePerpetualMarketSparklinesProps = { + period?: 'ONE_DAY' | 'SEVEN_DAYS'; + refetchInterval?: number; +}; + +export const usePerpetualMarketSparklines = (props: UsePerpetualMarketSparklinesProps = {}) => { + const { period = 'SEVEN_DAYS', refetchInterval = POLLING_MS } = props; + const { getPerpetualMarketSparklines, compositeClient } = useDydxClient(); + + const { data } = useQuery({ + enabled: Boolean(compositeClient), + queryKey: ['perpetualMarketSparklines', period], + queryFn: () => { + try { + return getPerpetualMarketSparklines({ period }); + } catch (error) { + log('usePerpetualMarketSparklines', error); + return undefined; + } + }, + refetchInterval, + refetchOnWindowFocus: false, + cacheTime: 1_000 * 60 * 5, // 5 minutes + staleTime: 1_000 * 60 * 10, // 10 minutes + }); + + return data; +}; diff --git a/src/hooks/usePerpetualMarketsStats.ts b/src/hooks/usePerpetualMarketsStats.ts new file mode 100644 index 000000000..4efeabec1 --- /dev/null +++ b/src/hooks/usePerpetualMarketsStats.ts @@ -0,0 +1,79 @@ +import { useMemo } from 'react'; + +import { getChainRevenue } from '@/services'; +import { useQuery } from 'react-query'; +import { shallowEqual, useSelector } from 'react-redux'; + +import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; + +import { log } from '@/lib/telemetry'; +import { isPresent, orEmptyObj } from '@/lib/typeUtils'; + +const endDate = new Date(); +const startDate = new Date(); +startDate.setDate(startDate.getDate() - 1); + +export const usePerpetualMarketsStats = () => { + const perpetualMarkets = orEmptyObj(useSelector(getPerpetualMarkets, shallowEqual)); + + const markets = useMemo( + () => Object.values(perpetualMarkets).filter(isPresent), + [perpetualMarkets] + ); + + const { data } = useQuery({ + queryKey: ['chain-revenue', startDate.toISOString(), endDate.toISOString()], + queryFn: () => { + try { + return getChainRevenue({ + startDate, + endDate, + }); + } catch (error) { + log('usePerpetualMarketsStats getChainRevenue', error); + return undefined; + } + }, + refetchOnWindowFocus: false, + cacheTime: 1_000 * 60 * 5, // 5 minutes + staleTime: 1_000 * 60 * 10, // 10 minutes + }); + + const feesEarned = useMemo(() => { + if (!data) return null; + + return data.reduce((acc, { total }) => acc + total, 0); + }, [data]); + + const stats = useMemo(() => { + let volume24HUSDC = 0; + let openInterestUSDC = 0; + + // eslint-disable-next-line no-restricted-syntax + for (const { oraclePrice, perpetual } of markets) { + const { volume24H, openInterest = 0 } = perpetual ?? {}; + volume24HUSDC += volume24H ?? 0; + if (oraclePrice) openInterestUSDC += openInterest * oraclePrice; + } + + return { + volume24HUSDC, + openInterestUSDC, + feesEarned, + }; + }, [markets, feesEarned]); + + const feesEarnedChart = useMemo( + () => + data?.map((point, x) => ({ + x: x + 1, + y: point.total, + })) ?? [], + [data] + ); + + return { + stats, + feesEarnedChart, + }; +}; diff --git a/src/hooks/usePotentialMarkets.tsx b/src/hooks/usePotentialMarkets.tsx index 2cad7efa5..a7e3c6226 100644 --- a/src/hooks/usePotentialMarkets.tsx +++ b/src/hooks/usePotentialMarkets.tsx @@ -1,7 +1,7 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { STRING_KEYS } from '@/constants/localization'; -import type { ExchangeConfigItem, PotentialMarketItem } from '@/constants/potentialMarkets'; +import type { NewMarketProposal } from '@/constants/potentialMarkets'; import { log } from '@/lib/telemetry'; @@ -9,7 +9,6 @@ import { useStringGetter } from './useStringGetter'; const PotentialMarketsContext = createContext>({ potentialMarkets: undefined, - exchangeConfigs: undefined, hasPotentialMarketsData: false, liquidityTiers: { 0: { @@ -47,37 +46,27 @@ export const PotentialMarketsProvider = ({ ...props }) => ( export const usePotentialMarkets = () => useContext(PotentialMarketsContext); - -const EXCHANGE_CONFIG_FILE_PATH = '/configs/otherMarketExchangeConfig.json'; -const POTENTIAL_MARKETS_FILE_PATH = '/configs/otherMarketParameters.json'; +const POTENTIAL_MARKETS_FILE_PATH = '/configs/otherMarketData.json'; export const usePotentialMarketsContext = () => { const stringGetter = useStringGetter(); - const [potentialMarkets, setPotentialMarkets] = useState(); - const [exchangeConfigs, setExchangeConfigs] = useState>(); + const [potentialMarkets, setPotentialMarkets] = useState(); useEffect(() => { try { fetch(POTENTIAL_MARKETS_FILE_PATH) .then((response) => response.json()) - .then((data) => { - setPotentialMarkets(data as PotentialMarketItem[]); + .then((data: Record>) => { + const newPotentialMarkets = Object.entries(data).map(([key, value]) => ({ + ...value, + baseAsset: key, + })); + setPotentialMarkets(newPotentialMarkets); }); } catch (error) { log('usePotentialMarkets/potentialMarkets', error); setPotentialMarkets(undefined); } - - try { - fetch(EXCHANGE_CONFIG_FILE_PATH) - .then((response) => response.json()) - .then((data) => { - setExchangeConfigs(data as Record); - }); - } catch (error) { - log('usePotentialMarkets/exchangeConfigs', error); - setExchangeConfigs(undefined); - } }, []); const liquidityTiers = useMemo( @@ -112,8 +101,7 @@ export const usePotentialMarketsContext = () => { return { potentialMarkets, - exchangeConfigs, - hasPotentialMarketsData: Boolean(potentialMarkets && exchangeConfigs), + hasPotentialMarketsData: Boolean(potentialMarkets), liquidityTiers, }; }; diff --git a/src/hooks/useQueryChaosLabsIncentives.ts b/src/hooks/useQueryChaosLabsIncentives.ts new file mode 100644 index 000000000..22d066150 --- /dev/null +++ b/src/hooks/useQueryChaosLabsIncentives.ts @@ -0,0 +1,32 @@ +import { useQuery } from 'react-query'; + +import type { DydxAddress } from '@/constants/wallets'; + +import { log } from '@/lib/telemetry'; + +type ChaosLabsIncentivesResponse = { + dydxRewards: number; + incentivePoints: number; + marketMakingIncentivePoints: number; +}; + +export const useQueryChaosLabsIncentives = ({ + dydxAddress, + season, +}: { + dydxAddress?: DydxAddress; + season?: number; +}) => { + return useQuery({ + enabled: !!dydxAddress, + queryKey: ['launch_incentives_rewards', dydxAddress, season], + queryFn: async () => { + if (!dydxAddress) return undefined; + const resp = await fetch( + `https://cloud.chaoslabs.co/query/api/dydx/points/${dydxAddress}?n=${season}` + ); + return resp.json(); + }, + onError: (error: Error) => log('LaunchIncentives/fetchPoints', error), + }); +}; diff --git a/src/hooks/useRestrictions.tsx b/src/hooks/useRestrictions.tsx index 184a523f4..9694b89ce 100644 --- a/src/hooks/useRestrictions.tsx +++ b/src/hooks/useRestrictions.tsx @@ -1,26 +1,15 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { createContext, useCallback, useContext, useMemo, useState } from 'react'; + +import { shallowEqual, useSelector } from 'react-redux'; import { RestrictionType } from '@/constants/abacus'; -import { DialogTypes } from '@/constants/dialogs'; import { getRestrictionType } from '@/state/accountSelectors'; -import { forceOpenDialog } from '@/state/dialogs'; -import { isTruthy } from '@/lib/isTruthy'; const useRestrictionContext = () => { - const dispatch = useDispatch(); const restrictionType = useSelector(getRestrictionType, shallowEqual); const [sanctionedAddresses, setSanctionedAddresses] = useState([]); - useEffect(() => { - if (restrictionType === RestrictionType.GEO_RESTRICTED) { - dispatch( - forceOpenDialog({ type: DialogTypes.RestrictedGeo, dialogProps: { preventClose: true } }) - ); - } - }, [restrictionType, dispatch]); - const updateSanctionedAddresses = useCallback( (screenedAddresses: { [address: string]: boolean }) => { const toAdd = Object.entries(screenedAddresses) diff --git a/src/hooks/useSelectedNetwork.ts b/src/hooks/useSelectedNetwork.ts index b7086b786..cc76b40a6 100644 --- a/src/hooks/useSelectedNetwork.ts +++ b/src/hooks/useSelectedNetwork.ts @@ -1,16 +1,19 @@ import { useCallback } from 'react'; + +import { useWallets } from '@privy-io/react-auth'; import { useDispatch, useSelector } from 'react-redux'; import { LocalStorageKey } from '@/constants/localStorage'; -import { DEFAULT_APP_ENVIRONMENT, DydxNetwork } from '@/constants/networks'; - -import { useAccounts, useLocalStorage } from '@/hooks'; +import { DEFAULT_APP_ENVIRONMENT, DydxNetwork, ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; import { setSelectedNetwork } from '@/state/app'; import { getSelectedNetwork } from '@/state/appSelectors'; import { validateAgainstAvailableEnvironments } from '@/lib/network'; +import { useAccounts } from './useAccounts'; +import { useLocalStorage } from './useLocalStorage'; + export const useSelectedNetwork = (): { switchNetwork: (network: DydxNetwork) => void; selectedNetwork: DydxNetwork; @@ -19,6 +22,9 @@ export const useSelectedNetwork = (): { const { disconnect } = useAccounts(); const selectedNetwork = useSelector(getSelectedNetwork); + const { wallets } = useWallets(); + const privyWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); + const [, setLocalStorageNetwork] = useLocalStorage({ key: LocalStorageKey.SelectedNetwork, defaultValue: DEFAULT_APP_ENVIRONMENT, @@ -31,6 +37,8 @@ export const useSelectedNetwork = (): { setLocalStorageNetwork(network); dispatch(setSelectedNetwork(network)); + const chainId = Number(ENVIRONMENT_CONFIG_MAP[selectedNetwork].ethereumChainId); + privyWallet?.switchChain(chainId); }, [dispatch, disconnect, setLocalStorageNetwork] ); diff --git a/src/hooks/useShouldShowFooter.ts b/src/hooks/useShouldShowFooter.ts index 07d037347..bc2aeeca0 100644 --- a/src/hooks/useShouldShowFooter.ts +++ b/src/hooks/useShouldShowFooter.ts @@ -1,5 +1,5 @@ -import { matchPath, useLocation } from 'react-router-dom'; import { useSelector } from 'react-redux'; +import { matchPath, useLocation } from 'react-router-dom'; import { TRADE_ROUTE } from '@/constants/routes'; diff --git a/src/hooks/useSignForWalletDerivation.tsx b/src/hooks/useSignForWalletDerivation.tsx new file mode 100644 index 000000000..b69fe0b2c --- /dev/null +++ b/src/hooks/useSignForWalletDerivation.tsx @@ -0,0 +1,23 @@ +import { useSelector } from 'react-redux'; +import { useSignTypedData } from 'wagmi'; + +import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; +import { getSignTypedData } from '@/constants/wallets'; + +import { getSelectedDydxChainId, getSelectedNetwork } from '@/state/appSelectors'; + +export default function useSignForWalletDerivation() { + const selectedDydxChainId = useSelector(getSelectedDydxChainId); + const selectedNetwork = useSelector(getSelectedNetwork); + const chainId = Number(ENVIRONMENT_CONFIG_MAP[selectedNetwork].ethereumChainId); + + const signTypedData = getSignTypedData(selectedDydxChainId); + const { signTypedDataAsync } = useSignTypedData({ + ...signTypedData, + domain: { + ...signTypedData.domain, + chainId, + }, + }); + return signTypedDataAsync; +} diff --git a/src/hooks/useStringGetter.ts b/src/hooks/useStringGetter.ts index 43ebae8fa..de8cbc93d 100644 --- a/src/hooks/useStringGetter.ts +++ b/src/hooks/useStringGetter.ts @@ -1,11 +1,10 @@ -import { useSelector, shallowEqual } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; import type { StringGetterFunction } from '@/constants/localization'; -import { getIsLocaleLoaded, getLocaleStringGetter } from '@/state/localizationSelectors'; +import { getLocaleStringGetter } from '@/state/localizationSelectors'; export const useStringGetter = (): StringGetterFunction => { - const isLocaleLoaded = useSelector(getIsLocaleLoaded); const stringGetterFunction = useSelector(getLocaleStringGetter, shallowEqual); - return isLocaleLoaded ? stringGetterFunction : () => ''; + return stringGetterFunction; }; diff --git a/src/hooks/useSubaccount.tsx b/src/hooks/useSubaccount.tsx index c975f76cf..9b5f0aecd 100644 --- a/src/hooks/useSubaccount.tsx +++ b/src/hooks/useSubaccount.tsx @@ -1,40 +1,51 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { shallowEqual, useSelector, useDispatch } from 'react-redux'; -import type { Nullable } from '@dydxprotocol/v4-abacus'; -import Long from 'long'; -import { type IndexedTx } from '@cosmjs/stargate'; + import type { EncodeObject } from '@cosmjs/proto-signing'; +import { type IndexedTx } from '@cosmjs/stargate'; import { Method } from '@cosmjs/tendermint-rpc'; - +import type { Nullable } from '@dydxprotocol/v4-abacus'; import { - type LocalWallet, SubaccountClient, - type GovAddNewMarketParams, utils, + type GovAddNewMarketParams, + type LocalWallet, } from '@dydxprotocol/v4-client-js'; +import Long from 'long'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import type { AccountBalance, + HumanReadableCancelOrderPayload, HumanReadablePlaceOrderPayload, + HumanReadableTriggerOrdersPayload, ParsingError, SubAccountHistoricalPNLs, } from '@/constants/abacus'; - import { AMOUNT_RESERVED_FOR_GAS_USDC } from '@/constants/account'; +import { STRING_KEYS } from '@/constants/localization'; import { QUANTUM_MULTIPLIER } from '@/constants/numbers'; +import { TradeTypes } from '@/constants/trade'; import { DydxAddress } from '@/constants/wallets'; -import { setSubaccount, setHistoricalPnl, removeUncommittedOrderClientId } from '@/state/account'; +import { + cancelOrderConfirmed, + cancelOrderFailed, + cancelOrderSubmitted, + placeOrderFailed, + placeOrderSubmitted, + setHistoricalPnl, + setSubaccount, +} from '@/state/account'; import { getBalances } from '@/state/accountSelectors'; import abacusStateManager from '@/lib/abacus'; -import { hashFromTx } from '@/lib/txUtils'; import { log } from '@/lib/telemetry'; +import { hashFromTx } from '@/lib/txUtils'; import { useAccounts } from './useAccounts'; -import { useTokenConfigs } from './useTokenConfigs'; import { useDydxClient } from './useDydxClient'; import { useGovernanceVariables } from './useGovernanceVariables'; +import { useTokenConfigs } from './useTokenConfigs'; type SubaccountContextType = ReturnType; const SubaccountContext = createContext({} as SubaccountContextType); @@ -55,7 +66,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo const { usdcDenom, usdcDecimals } = useTokenConfigs(); const { compositeClient, faucetClient } = useDydxClient(); - const { getFaucetFunds } = useMemo( + const { getFaucetFunds, getNativeTokens } = useMemo( () => ({ getFaucetFunds: async ({ dydxAddress, @@ -63,7 +74,10 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo }: { dydxAddress: DydxAddress; subaccountNumber: number; - }) => await faucetClient?.fill(dydxAddress, subaccountNumber, 100), + }) => faucetClient?.fill(dydxAddress, subaccountNumber, 100), + + getNativeTokens: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => + faucetClient?.fillNative(dydxAddress), }), [faucetClient] ); @@ -224,7 +238,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo [compositeClient] ); - const [subaccountNumber, setSubaccountNumber] = useState(0); + const [subaccountNumber] = useState(0); useEffect(() => { abacusStateManager.setSubaccountNumber(subaccountNumber); @@ -250,8 +264,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo const amount = parseFloat(balance.amount) - AMOUNT_RESERVED_FOR_GAS_USDC; if (amount > 0) { - const subaccountClient = new SubaccountClient(localDydxWallet, 0); - await depositToSubaccount({ amount, subaccountClient }); + const newSubaccountClient = new SubaccountClient(localDydxWallet, 0); + await depositToSubaccount({ amount, subaccountClient: newSubaccountClient }); } }, [localDydxWallet, depositToSubaccount] @@ -269,10 +283,10 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo const deposit = useCallback( async (amount: number) => { if (!subaccountClient) { - return; + return undefined; } - return await depositToSubaccount({ subaccountClient, amount }); + return depositToSubaccount({ subaccountClient, amount }); }, [subaccountClient, depositToSubaccount] ); @@ -280,10 +294,10 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo const withdraw = useCallback( async (amount: number) => { if (!subaccountClient) { - return; + return undefined; } - return await withdrawFromSubaccount({ subaccountClient, amount }); + return withdrawFromSubaccount({ subaccountClient, amount }); }, [subaccountClient, withdrawFromSubaccount] ); @@ -293,7 +307,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo const transfer = useCallback( async (amount: number, recipient: string, coinDenom: string) => { if (!subaccountClient) { - return; + return undefined; } return (await (coinDenom === usdcDenom ? transferFromSubaccountToAddress @@ -305,23 +319,24 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo const sendSquidWithdraw = useCallback( async (amount: number, payload: string, isCctp?: boolean) => { const cctpWithdraw = () => { - return new Promise((resolve, reject) => + return new Promise((resolve, reject) => { abacusStateManager.cctpWithdraw((success, error, data) => { const parsedData = JSON.parse(data); + // eslint-disable-next-line eqeqeq if (success && parsedData?.code == 0) { resolve(parsedData?.transactionHash); } else { reject(error); } - }) - ); + }); + }); }; if (isCctp) { - return await cctpWithdraw(); + return cctpWithdraw(); } if (!subaccountClient) { - return; + return undefined; } const tx = await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload }); return hashFromTx(tx?.hash); @@ -331,19 +346,22 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo // ------ Faucet Methods ------ // const requestFaucetFunds = useCallback(async () => { - if (!dydxAddress) return; - try { - await getFaucetFunds({ dydxAddress, subaccountNumber }); + if (!dydxAddress) throw new Error('dydxAddress is not connected'); + + await Promise.all([ + getFaucetFunds({ dydxAddress, subaccountNumber }), + getNativeTokens({ dydxAddress }), + ]); } catch (error) { log('useSubaccount/getFaucetFunds', error); throw error; } - }, [dydxAddress, getFaucetFunds, subaccountNumber]); + }, [dydxAddress, getFaucetFunds, getNativeTokens, subaccountNumber]); // ------ Trading Methods ------ // const placeOrder = useCallback( - async ({ + ({ isClosePosition = false, onError, onSuccess, @@ -363,7 +381,12 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo onError?.({ errorStringKey: parsingError?.stringKey }); if (data?.clientId !== undefined) { - dispatch(removeUncommittedOrderClientId(data.clientId)); + dispatch( + placeOrderFailed({ + clientId: data.clientId, + errorStringKey: parsingError?.stringKey ?? STRING_KEYS.SOMETHING_WENT_WRONG, + }) + ); } } }; @@ -376,24 +399,34 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo placeOrderParams = abacusStateManager.placeOrder(callback); } + if (placeOrderParams?.clientId) { + dispatch( + placeOrderSubmitted({ + marketId: placeOrderParams.marketId, + clientId: placeOrderParams.clientId, + orderType: placeOrderParams.type as TradeTypes, + }) + ); + } + return placeOrderParams; }, [subaccountClient] ); const closePosition = useCallback( - async ({ + ({ onError, onSuccess, }: { onError: (onErrorParams?: { errorStringKey?: Nullable }) => void; onSuccess?: (placeOrderPayload: Nullable) => void; - }) => await placeOrder({ isClosePosition: true, onError, onSuccess }), + }) => placeOrder({ isClosePosition: true, onError, onSuccess }), [placeOrder] ); const cancelOrder = useCallback( - async ({ + ({ orderId, onError, onSuccess, @@ -404,17 +437,96 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo }) => { const callback = (success: boolean, parsingError?: Nullable) => { if (success) { + dispatch(cancelOrderConfirmed(orderId)); onSuccess?.(); } else { + dispatch( + cancelOrderFailed({ + orderId, + errorStringKey: parsingError?.stringKey ?? STRING_KEYS.SOMETHING_WENT_WRONG, + }) + ); onError?.({ errorStringKey: parsingError?.stringKey }); } }; + dispatch(cancelOrderSubmitted(orderId)); abacusStateManager.cancelOrder(orderId, callback); }, [subaccountClient] ); + // ------ Trigger Orders Methods ------ // + const placeTriggerOrders = useCallback( + async ({ + onError, + onSuccess, + }: { + onError?: (onErrorParams?: { errorStringKey?: Nullable }) => void; + onSuccess?: () => void; + } = {}) => { + const callback = ( + success: boolean, + parsingError?: Nullable, + data?: Nullable + ) => { + const placeOrderPayloads = data?.placeOrderPayloads.toArray() ?? []; + const cancelOrderPayloads = data?.cancelOrderPayloads.toArray() ?? []; + + if (success) { + onSuccess?.(); + + cancelOrderPayloads.forEach((payload: HumanReadableCancelOrderPayload) => { + dispatch(cancelOrderConfirmed(payload.orderId)); + }); + } else { + onError?.({ errorStringKey: parsingError?.stringKey }); + + placeOrderPayloads.forEach((payload: HumanReadablePlaceOrderPayload) => { + dispatch( + placeOrderFailed({ + clientId: payload.clientId, + errorStringKey: parsingError?.stringKey ?? STRING_KEYS.SOMETHING_WENT_WRONG, + }) + ); + }); + + cancelOrderPayloads.forEach((payload: HumanReadableCancelOrderPayload) => { + dispatch( + cancelOrderFailed({ + orderId: payload.orderId, + errorStringKey: parsingError?.stringKey ?? STRING_KEYS.SOMETHING_WENT_WRONG, + }) + ); + }); + } + }; + + const triggerOrderParams = abacusStateManager.triggerOrders(callback); + + triggerOrderParams?.placeOrderPayloads + .toArray() + .forEach((payload: HumanReadablePlaceOrderPayload) => { + dispatch( + placeOrderSubmitted({ + marketId: payload.marketId, + clientId: payload.clientId, + orderType: payload.type as TradeTypes, + }) + ); + }); + + triggerOrderParams?.cancelOrderPayloads + .toArray() + .forEach((payload: HumanReadableCancelOrderPayload) => { + dispatch(cancelOrderSubmitted(payload.orderId)); + }); + + return triggerOrderParams; + }, + [subaccountClient] + ); + const { newMarketProposal } = useGovernanceVariables(); // ------ Governance Methods ------ // @@ -455,6 +567,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo placeOrder, closePosition, cancelOrder, + placeTriggerOrders, // Governance Methods submitNewMarketProposal, diff --git a/src/hooks/useTablePagination.ts b/src/hooks/useTablePagination.ts new file mode 100644 index 000000000..eaf253af5 --- /dev/null +++ b/src/hooks/useTablePagination.ts @@ -0,0 +1,78 @@ +/** + * @description Hook to handle pagination on table views + */ +import { useEffect, useState } from 'react'; + +import { MenuItem } from '@/constants/menus'; + +import { PageSize } from '@/components/Table/TablePaginationRow'; + +const MAX_NUM_PAGE_BUTTONS = 7; +const PAGE_TOGGLE_PLACEHOLDER = '...'; + +export const useTablePagination = ({ + initialPageSize, + totalRows, +}: { + initialPageSize: PageSize; + totalRows: number; +}) => { + const [pageSize, setPageSize] = useState(initialPageSize); + const [currentPage, setCurrentPage] = useState(0); + const [pages, setPages] = useState[]>([]); + + const pageNumberToDisplay = (pageNumber: number) => String(pageNumber + 1); + const pageNumberToMenuItem = (pageNumber: number) => ({ + value: pageNumberToDisplay(pageNumber), + label: pageNumberToDisplay(pageNumber), + }); + const placeholderPageItem = (key: string) => { + return { + value: key, + label: PAGE_TOGGLE_PLACEHOLDER, + disabled: true, + }; + }; + + useEffect(() => { + const lastPage = Math.max(1, Math.ceil(totalRows / pageSize)) - 1; + if (currentPage > lastPage) { + setCurrentPage(lastPage); + } + }, [pageSize]); + + useEffect(() => { + const totalPages = Math.max(1, Math.ceil(totalRows / pageSize)); + const lastPage = totalPages - 1; + + if (totalPages <= MAX_NUM_PAGE_BUTTONS) { + setPages( + [...Array(Math.min(MAX_NUM_PAGE_BUTTONS, totalPages)).keys()].map((i) => + pageNumberToMenuItem(i) + ) + ); + } else if (currentPage < 2 || lastPage - currentPage < 2) { + setPages([ + pageNumberToMenuItem(0), + pageNumberToMenuItem(1), + pageNumberToMenuItem(2), + placeholderPageItem('placeholder'), + pageNumberToMenuItem(lastPage - 2), + pageNumberToMenuItem(lastPage - 1), + pageNumberToMenuItem(lastPage), + ]); + } else { + setPages([ + pageNumberToMenuItem(0), + placeholderPageItem('placeholder1'), + pageNumberToMenuItem(currentPage - 1), + pageNumberToMenuItem(currentPage), + pageNumberToMenuItem(currentPage + 1), + placeholderPageItem('placeholder2'), + pageNumberToMenuItem(lastPage), + ]); + } + }, [pageSize, currentPage, totalRows]); + + return { currentPage, pageSize, pages, setPageSize, setCurrentPage }; +}; diff --git a/src/hooks/useTradeFormInputs.ts b/src/hooks/useTradeFormInputs.ts index fa36827c8..dd9ff6d0d 100644 --- a/src/hooks/useTradeFormInputs.ts +++ b/src/hooks/useTradeFormInputs.ts @@ -1,9 +1,11 @@ -import { getTradeFormInputs } from '@/state/inputsSelectors'; import { useEffect } from 'react'; + import { shallowEqual, useSelector } from 'react-redux'; import { TradeInputField } from '@/constants/abacus'; +import { getTradeFormInputs } from '@/state/inputsSelectors'; + import abacusStateManager from '@/lib/abacus'; export const useTradeFormInputs = () => { diff --git a/src/hooks/useTriggerOrdersFormInputs.ts b/src/hooks/useTriggerOrdersFormInputs.ts new file mode 100644 index 000000000..a0442a5be --- /dev/null +++ b/src/hooks/useTriggerOrdersFormInputs.ts @@ -0,0 +1,147 @@ +import { useEffect, useState } from 'react'; + +import { shallowEqual, useSelector } from 'react-redux'; + +import { AbacusOrderType, SubaccountOrder, TriggerOrdersInputField } from '@/constants/abacus'; + +import { getTriggerOrdersInputErrors } from '@/state/inputsSelectors'; + +import abacusStateManager from '@/lib/abacus'; +import { isTruthy } from '@/lib/isTruthy'; +import { MustBigNumber } from '@/lib/numbers'; +import { isLimitOrderType } from '@/lib/orders'; + +export const useTriggerOrdersFormInputs = ({ + marketId, + positionSize, + stopLossOrder, + takeProfitOrder, +}: { + marketId: string; + positionSize: number | null; + stopLossOrder?: SubaccountOrder; + takeProfitOrder?: SubaccountOrder; +}) => { + const inputErrors = useSelector(getTriggerOrdersInputErrors, shallowEqual); + + const [differingOrderSizes, setDifferingOrderSizes] = useState(false); + const [inputSize, setInputSize] = useState(null); + + const setSize = (size: number | null) => { + const absSize = size ? Math.abs(size) : null; + abacusStateManager.setTriggerOrdersValue({ + field: TriggerOrdersInputField.size, + value: absSize != null ? MustBigNumber(absSize).toString() : null, + }); + setInputSize(absSize); + }; + + useEffect(() => { + // Initialize trigger order data on mount + if (stopLossOrder) { + [ + { + field: TriggerOrdersInputField.stopLossOrderId, + value: stopLossOrder.id, + }, + { + field: TriggerOrdersInputField.stopLossOrderSize, + value: stopLossOrder.size, + }, + { + field: TriggerOrdersInputField.stopLossPrice, + value: MustBigNumber(stopLossOrder.triggerPrice).toString(), + }, + isLimitOrderType(stopLossOrder.type) && { + field: TriggerOrdersInputField.stopLossLimitPrice, + value: MustBigNumber(stopLossOrder.price).toString(), + }, + { + field: TriggerOrdersInputField.stopLossOrderType, + value: stopLossOrder.type.rawValue, + }, + ] + .filter(isTruthy) + .map(({ field, value }) => abacusStateManager.setTriggerOrdersValue({ field, value })); + } else { + abacusStateManager.setTriggerOrdersValue({ + field: TriggerOrdersInputField.stopLossOrderType, + value: AbacusOrderType.stopMarket.rawValue, + }); + } + + if (takeProfitOrder) { + [ + { + field: TriggerOrdersInputField.takeProfitOrderId, + value: takeProfitOrder.id, + }, + { + field: TriggerOrdersInputField.takeProfitOrderSize, + value: takeProfitOrder.size, + }, + { + field: TriggerOrdersInputField.takeProfitPrice, + value: MustBigNumber(takeProfitOrder.triggerPrice).toString(), + }, + isLimitOrderType(takeProfitOrder.type) && { + field: TriggerOrdersInputField.takeProfitLimitPrice, + value: MustBigNumber(takeProfitOrder.price).toString(), + }, + { + field: TriggerOrdersInputField.takeProfitOrderType, + value: takeProfitOrder.type.rawValue, + }, + ] + .filter(isTruthy) + .map(({ field, value }) => abacusStateManager.setTriggerOrdersValue({ field, value })); + } else { + abacusStateManager.setTriggerOrdersValue({ + field: TriggerOrdersInputField.takeProfitOrderType, + value: AbacusOrderType.takeProfitMarket.rawValue, + }); + } + + if (stopLossOrder?.size && takeProfitOrder?.size) { + if (stopLossOrder?.size === takeProfitOrder?.size) { + setSize(stopLossOrder?.size); + } else { + setSize(null); + setDifferingOrderSizes(true); + } + } else if (stopLossOrder?.size) { + setSize(stopLossOrder?.size); + } else if (takeProfitOrder?.size) { + setSize(takeProfitOrder?.size); + } else { + // Default to full position size for initial order creation + setSize(positionSize); + } + + return () => { + abacusStateManager.resetInputState(); + }; + }, []); + + useEffect(() => { + abacusStateManager.setTriggerOrdersValue({ + field: TriggerOrdersInputField.marketId, + value: marketId, + }); + }, [marketId]); + + return { + inputErrors, + existingStopLossOrder: stopLossOrder, + existingTakeProfitOrder: takeProfitOrder, + // True if an SL + TP order exist, and if they are set on different order sizes + differingOrderSizes, + // Default input size to be shown on custom amount slider, null if different order sizes + inputSize, + // Boolean to signify whether the limit box should be checked on initial render of the triggers order form + existsLimitOrder: !!( + (stopLossOrder && isLimitOrderType(stopLossOrder.type)) || + (takeProfitOrder && isLimitOrderType(takeProfitOrder.type)) + ), + }; +}; diff --git a/src/hooks/useURLConfigs.ts b/src/hooks/useURLConfigs.ts index 882097322..112162df8 100644 --- a/src/hooks/useURLConfigs.ts +++ b/src/hooks/useURLConfigs.ts @@ -7,26 +7,31 @@ import { getSelectedDydxChainId } from '@/state/appSelectors'; const FALLBACK_URL = 'https://help.dydx.exchange/'; export interface LinksConfigs { - tos: string; - privacy: string; - statusPage: string; - mintscan: string; - mintscanBase: string; - feedback?: string; - help?: string; + accountExportLearnMore?: string; blogs?: string; - foundation?: string; - initialMarginFractionLearnMore?: string; - reduceOnlyLearnMore?: string; - documentation?: string; community?: string; + documentation?: string; + feedback?: string; + foundation?: string; governanceLearnMore?: string; + help?: string; + initialMarginFractionLearnMore?: string; + keplrDashboard?: string; + launchIncentive?: string; + mintscan: string; + mintscanBase: string; newMarketProposalLearnMore: string; + privacy: string; + reduceOnlyLearnMore?: string; + statusPage: string; stakingLearnMore?: string; - keplrDashboard?: string; strideZoneApp?: string; - accountExportLearnMore?: string; + tos: string; + tradingRewardsLearnMore?: string; walletLearnMore?: string; + withdrawalGateLearnMore?: string; + exchangeStats?: string; + complianceSupportEmail?: string; } export const useURLConfigs = (): LinksConfigs => { @@ -34,25 +39,30 @@ export const useURLConfigs = (): LinksConfigs => { const linksConfigs = LINKS_CONFIG_MAP[selectedDydxChainId] as LinksConfigs; return { - tos: linksConfigs.tos, - privacy: linksConfigs.privacy, - statusPage: linksConfigs.statusPage, + accountExportLearnMore: linksConfigs.accountExportLearnMore ?? FALLBACK_URL, + blogs: linksConfigs.blogs ?? FALLBACK_URL, + community: linksConfigs.community ?? FALLBACK_URL, + documentation: linksConfigs.documentation ?? FALLBACK_URL, + feedback: linksConfigs.feedback ?? FALLBACK_URL, + foundation: linksConfigs.foundation ?? FALLBACK_URL, + governanceLearnMore: linksConfigs.governanceLearnMore ?? FALLBACK_URL, + help: linksConfigs.help ?? FALLBACK_URL, + initialMarginFractionLearnMore: linksConfigs.initialMarginFractionLearnMore ?? FALLBACK_URL, + keplrDashboard: linksConfigs.keplrDashboard ?? FALLBACK_URL, + launchIncentive: linksConfigs.launchIncentive ?? FALLBACK_URL, mintscan: linksConfigs.mintscan, mintscanBase: linksConfigs.mintscanBase, - feedback: linksConfigs.feedback || FALLBACK_URL, - help: linksConfigs.help || FALLBACK_URL, - blogs: linksConfigs.blogs || FALLBACK_URL, - foundation: linksConfigs.foundation || FALLBACK_URL, - initialMarginFractionLearnMore: linksConfigs.initialMarginFractionLearnMore || FALLBACK_URL, - reduceOnlyLearnMore: linksConfigs.reduceOnlyLearnMore || FALLBACK_URL, - documentation: linksConfigs.documentation || FALLBACK_URL, - community: linksConfigs.community || FALLBACK_URL, - governanceLearnMore: linksConfigs.governanceLearnMore || FALLBACK_URL, - newMarketProposalLearnMore: linksConfigs.newMarketProposalLearnMore || FALLBACK_URL, - stakingLearnMore: linksConfigs.stakingLearnMore || FALLBACK_URL, - keplrDashboard: linksConfigs.keplrDashboard || FALLBACK_URL, - strideZoneApp: linksConfigs.strideZoneApp || FALLBACK_URL, - accountExportLearnMore: linksConfigs.accountExportLearnMore || FALLBACK_URL, - walletLearnMore: linksConfigs.walletLearnMore || FALLBACK_URL, + newMarketProposalLearnMore: linksConfigs.newMarketProposalLearnMore ?? FALLBACK_URL, + privacy: linksConfigs.privacy, + reduceOnlyLearnMore: linksConfigs.reduceOnlyLearnMore ?? FALLBACK_URL, + statusPage: linksConfigs.statusPage, + stakingLearnMore: linksConfigs.stakingLearnMore ?? FALLBACK_URL, + strideZoneApp: linksConfigs.strideZoneApp ?? FALLBACK_URL, + tos: linksConfigs.tos, + tradingRewardsLearnMore: linksConfigs.tradingRewardsLearnMore ?? FALLBACK_URL, + walletLearnMore: linksConfigs.walletLearnMore ?? FALLBACK_URL, + withdrawalGateLearnMore: linksConfigs.withdrawalGateLearnMore ?? FALLBACK_URL, + exchangeStats: linksConfigs.exchangeStats ?? FALLBACK_URL, + complianceSupportEmail: linksConfigs.complianceSupportEmail ?? FALLBACK_URL, }; }; diff --git a/src/hooks/useWalletConnection.ts b/src/hooks/useWalletConnection.ts index 39b88879e..5c7d989f0 100644 --- a/src/hooks/useWalletConnection.ts +++ b/src/hooks/useWalletConnection.ts @@ -1,45 +1,45 @@ -import { useCallback, useEffect, useState, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useLogin, useLogout, usePrivy } from '@privy-io/react-auth'; +import { + WalletType as CosmosWalletType, + useAccount as useAccountGraz, + useSuggestChainAndConnect as useConnectGraz, + useDisconnect as useDisconnectGraz, + useOfflineSigners as useOfflineSignersGraz, +} from 'graz'; import { useSelector } from 'react-redux'; +import { + useAccount as useAccountWagmi, + useConnect as useConnectWagmi, + useDisconnect as useDisconnectWagmi, + usePublicClient as usePublicClientWagmi, + useWalletClient as useWalletClientWagmi, +} from 'wagmi'; import { EvmDerivedAddresses } from '@/constants/account'; -import { STRING_KEYS } from '@/constants/localization'; import { LocalStorageKey } from '@/constants/localStorage'; -import { ENVIRONMENT_CONFIG_MAP, WALLETS_CONFIG_MAP } from '@/constants/networks'; - +import { STRING_KEYS } from '@/constants/localization'; +import { WALLETS_CONFIG_MAP } from '@/constants/networks'; import { - type DydxAddress, - type EvmAddress, + DYDX_CHAIN_INFO, WalletConnectionType, WalletType, wallets, - DYDX_CHAIN_INFO, + type DydxAddress, + type EvmAddress, } from '@/constants/wallets'; import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { - useConnect as useConnectWagmi, - useAccount as useAccountWagmi, - useDisconnect as useDisconnectWagmi, - usePublicClient as usePublicClientWagmi, - useWalletClient as useWalletClientWagmi, -} from 'wagmi'; -import { - useSuggestChainAndConnect as useConnectGraz, - useAccount as useAccountGraz, - useDisconnect as useDisconnectGraz, - useOfflineSigners as useOfflineSignersGraz, - WalletType as CosmosWalletType, -} from 'graz'; - import { getSelectedDydxChainId } from '@/state/appSelectors'; +import { log } from '@/lib/telemetry'; +import { testFlags } from '@/lib/testFlags'; import { resolveWagmiConnector } from '@/lib/wagmi'; import { getWalletConnection, parseWalletError } from '@/lib/wallet'; -import { log } from '@/lib/telemetry'; import { useStringGetter } from './useStringGetter'; -import { testFlags } from '@/lib/testFlags'; export const useWalletConnection = () => { const stringGetter = useStringGetter(); @@ -113,10 +113,20 @@ export const useWalletConnection = () => { key: LocalStorageKey.EvmDerivedAddresses, defaultValue: {} as EvmDerivedAddresses, }); + const { ready, authenticated } = usePrivy(); + const { login } = useLogin({ + onError: (error) => { + if (error !== 'exited_auth_flow') { + log('useWalletConnection/privy/useLogin', new Error(`Privy: ${error}`)); + setSelectedWalletError('Privy login failed'); + } + }, + }); + const { logout } = useLogout(); const connectWallet = useCallback( async ({ - walletType, + walletType: wType, forceConnect, isAccountConnected, }: { @@ -124,22 +134,24 @@ export const useWalletConnection = () => { forceConnect?: boolean; isAccountConnected?: boolean; }) => { - if (!walletType) return { walletType, walletConnectionType }; + if (!wType) return { walletType: wType, walletConnectionType }; - const walletConnection = getWalletConnection({ walletType }); + const walletConnection = getWalletConnection({ walletType: wType }); try { if (!walletConnection) { throw new Error('Onboarding: No wallet connection found.'); + } else if (walletConnection.type === WalletConnectionType.Privy) { + if (!isConnectedWagmi && ready && !authenticated) { + login(); + } } else if (walletConnection.type === WalletConnectionType.CosmosSigner) { const cosmosWalletType = { [WalletType.Keplr as string]: CosmosWalletType.KEPLR, - }[walletType]; + }[wType]; if (!cosmosWalletType) { - throw new Error( - `${stringGetter({ key: wallets[walletType].stringKey })} was not found.` - ); + throw new Error(`${stringGetter({ key: wallets[wType].stringKey })} was not found.`); } if (!isConnectedGraz) { @@ -155,7 +167,7 @@ export const useWalletConnection = () => { if (!isConnectedWagmi && (forceConnect || !isAccountConnected)) { await connectWagmi({ connector: resolveWagmiConnector({ - walletType, + walletType: wType, walletConnection, walletConnectConfig, }), @@ -175,11 +187,11 @@ export const useWalletConnection = () => { } return { - walletType, + walletType: wType, walletConnectionType: walletConnection?.type, }; }, - [isConnectedGraz, signerGraz, isConnectedWagmi, signerWagmi] + [isConnectedGraz, signerGraz, isConnectedWagmi, signerWagmi, ready, authenticated, login] ); const disconnectWallet = useCallback(async () => { @@ -188,28 +200,37 @@ export const useWalletConnection = () => { if (isConnectedWagmi) await disconnectWagmi(); if (isConnectedGraz) await disconnectGraz(); - }, [isConnectedGraz, isConnectedWagmi]); + if (authenticated) await logout(); + }, [isConnectedGraz, isConnectedWagmi, authenticated, logout]); // Wallet selection const [selectedWalletType, setSelectedWalletType] = useState(walletType); const [selectedWalletError, setSelectedWalletError] = useState(); + async function disconnectSelectedWallet() { + setSelectedWalletType(undefined); + setWalletType(undefined); + setWalletConnectionType(undefined); + + await disconnectWallet(); + } + useEffect(() => { (async () => { setSelectedWalletError(undefined); if (selectedWalletType) { try { - const { walletType, walletConnectionType } = await connectWallet({ + const { walletType: wType, walletConnectionType: wConnectionType } = await connectWallet({ walletType: selectedWalletType, isAccountConnected: Boolean( evmAddress && evmDerivedAddresses[evmAddress]?.encryptedSignature ), }); - setWalletType(walletType); - setWalletConnectionType(walletConnectionType); + setWalletType(wType); + setWalletConnectionType(wConnectionType); } catch (error) { const { walletErrorType, message } = parseWalletError({ error, @@ -222,21 +243,18 @@ export const useWalletConnection = () => { } } } else { - setWalletType(undefined); - setWalletConnectionType(undefined); - - await disconnectWallet(); + await disconnectSelectedWallet(); } })(); }, [selectedWalletType, signerWagmi, signerGraz, evmDerivedAddresses, evmAddress]); - const selectWalletType = async (walletType: WalletType | undefined) => { + const selectWalletType = async (wType: WalletType | undefined) => { if (selectedWalletType) { setSelectedWalletType(undefined); await new Promise(requestAnimationFrame); } - setSelectedWalletType(walletType); + setSelectedWalletType(wType); }; // On page load, if testFlag.address is set, connect to the test wallet. diff --git a/src/hooks/useWithdrawalInfo.ts b/src/hooks/useWithdrawalInfo.ts new file mode 100644 index 000000000..b09f95828 --- /dev/null +++ b/src/hooks/useWithdrawalInfo.ts @@ -0,0 +1,134 @@ +import { useEffect, useMemo } from 'react'; + +import { encodeJson } from '@dydxprotocol/v4-client-js'; +import { ByteArrayEncoding } from '@dydxprotocol/v4-client-js/build/src/lib/helpers'; +import BigNumber from 'bignumber.js'; +import { useQuery } from 'react-query'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; + +import { DialogTypes } from '@/constants/dialogs'; +import { isMainnet } from '@/constants/networks'; + +import { getApiState } from '@/state/appSelectors'; +import { closeDialog, openDialog } from '@/state/dialogs'; +import { getSelectedLocale } from '@/state/localizationSelectors'; + +import { formatRelativeTime } from '@/lib/dateTime'; +import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers'; +import { log } from '@/lib/telemetry'; + +import { useDydxClient } from './useDydxClient'; +import { useEnvFeatures } from './useEnvFeatures'; +import { useTokenConfigs } from './useTokenConfigs'; + +const BLOCK_TIME = isMainnet ? 1_000 : 1_500; + +export const useWithdrawalInfo = ({ + transferType, +}: { + transferType: 'withdrawal' | 'transfer'; +}) => { + const { getWithdrawalAndTransferGatingStatus, getWithdrawalCapacityByDenom } = useDydxClient(); + const { usdcDenom, usdcDecimals } = useTokenConfigs(); + const apiState = useSelector(getApiState, shallowEqual); + const { height } = apiState ?? {}; + const selectedLocale = useSelector(getSelectedLocale); + const dispatch = useDispatch(); + const { withdrawalSafetyEnabled } = useEnvFeatures(); + + const { data: usdcWithdrawalCapacity } = useQuery({ + enabled: withdrawalSafetyEnabled, + queryKey: 'usdcWithdrawalCapacity', + queryFn: async () => { + try { + const response = await getWithdrawalCapacityByDenom({ denom: usdcDenom }); + return JSON.parse(encodeJson(response, ByteArrayEncoding.BIGINT)); + } catch (error) { + log('useWithdrawalInfo/getWithdrawalCapacityByDenom', error); + return undefined; + } + }, + refetchInterval: 60_000, + staleTime: 60_000, + }); + + const { data: withdrawalAndTransferGatingStatus } = useQuery({ + enabled: withdrawalSafetyEnabled, + queryKey: 'withdrawalTransferGateStatus', + queryFn: async () => { + try { + return await getWithdrawalAndTransferGatingStatus(); + } catch (error) { + log('useWithdrawalInfo/getWithdrawalAndTransferGatingStatus', error); + return undefined; + } + }, + refetchInterval: 60_000, + staleTime: 60_000, + }); + + const capacity = useMemo(() => { + const capacityList = usdcWithdrawalCapacity?.limiterCapacityList; + if (!capacityList || capacityList.length < 2) { + if (!withdrawalSafetyEnabled) { + return BigNumber(Infinity); + } + + return BIG_NUMBERS.ZERO; + } + + const [{ capacity: daily }, { capacity: weekly }] = capacityList; + const dailyBN = MustBigNumber(daily); + const weeklyBN = MustBigNumber(weekly); + return BigNumber.minimum(dailyBN, weeklyBN).div(10 ** usdcDecimals); + }, [usdcDecimals, usdcWithdrawalCapacity]); + + const withdrawalAndTransferGatingStatusValue = useMemo(() => { + const { withdrawalsAndTransfersUnblockedAtBlock } = withdrawalAndTransferGatingStatus ?? {}; + if ( + height && + withdrawalsAndTransfersUnblockedAtBlock && + height < withdrawalsAndTransfersUnblockedAtBlock && + withdrawalSafetyEnabled + ) { + return { + estimatedUnblockTime: formatRelativeTime( + Date.now() + (withdrawalsAndTransfersUnblockedAtBlock - height) * BLOCK_TIME, + { + locale: selectedLocale, + largestUnit: 'day', + } + ), + isGated: true, + }; + } + return { + estimatedUnblockTime: null, + isGated: false, + }; + }, [height, withdrawalAndTransferGatingStatus, withdrawalSafetyEnabled]); + + useEffect(() => { + if ( + withdrawalAndTransferGatingStatusValue.isGated && + withdrawalAndTransferGatingStatusValue.estimatedUnblockTime && + withdrawalSafetyEnabled + ) { + dispatch(closeDialog()); + dispatch( + openDialog({ + type: DialogTypes.WithdrawalGated, + dialogProps: { + transferType, + estimatedUnblockTime: withdrawalAndTransferGatingStatusValue.estimatedUnblockTime, + }, + }) + ); + } + }, [transferType, withdrawalAndTransferGatingStatusValue.isGated, withdrawalSafetyEnabled]); + + return { + usdcWithdrawalCapacity: capacity, + withdrawalAndTransferGatingStatus, + }; +}; diff --git a/src/icons/arrow.svg b/src/icons/arrow.svg index 2400364a7..34393884c 100644 --- a/src/icons/arrow.svg +++ b/src/icons/arrow.svg @@ -1,3 +1,3 @@ - - - \ No newline at end of file + + + diff --git a/src/icons/download.svg b/src/icons/download.svg new file mode 100644 index 000000000..94c1363be --- /dev/null +++ b/src/icons/download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/index.ts b/src/icons/index.ts index af109cb9d..5cbe5862d 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -68,6 +68,7 @@ export { default as TriangleIcon } from './triangle.svg'; export { default as TryAgainIcon } from './try-again.svg'; export { default as WarningIcon } from './warning.svg'; export { default as WithdrawIcon } from './withdraw.svg'; +export { default as DownloadIcon } from './download.svg'; // Wallets export { default as BitkeepIcon } from './wallets/bitkeep.svg'; @@ -91,6 +92,11 @@ export { default as TrustWalletIcon } from './wallets/trust-wallet.svg'; export { default as WalletConnectIcon } from './wallets/walletconnect.svg'; export { default as WebsiteIcon } from './website.svg'; export { default as WhitepaperIcon } from './whitepaper.svg'; +export { default as Discord2Icon } from './wallets/discord.svg'; +export { default as TwitterIcon } from './wallets/twitter.svg'; +export { default as GoogleIcon } from './wallets/google.svg'; +export { default as AppleIcon } from './wallets/apple.svg'; +export { default as EmailIcon } from './wallets/email.svg'; // Logos export { default as ChaosLabsIcon } from './chaos-labs'; @@ -104,3 +110,5 @@ export { default as OrderOpenIcon } from './trade/order-open.svg'; export { default as OrderPartiallyFilledIcon } from './trade/order-partially-filled.svg'; export { default as OrderPendingIcon } from './trade/order-pending.svg'; export { default as OrderUntriggeredIcon } from './trade/order-untriggered.svg'; + +export { default as PositionPartialIcon } from './trade/position-partial.svg'; diff --git a/src/icons/trade/position-partial.svg b/src/icons/trade/position-partial.svg new file mode 100644 index 000000000..48e64e115 --- /dev/null +++ b/src/icons/trade/position-partial.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/wallets/apple.svg b/src/icons/wallets/apple.svg new file mode 100644 index 000000000..14e474dfd --- /dev/null +++ b/src/icons/wallets/apple.svg @@ -0,0 +1 @@ + diff --git a/src/icons/wallets/discord.svg b/src/icons/wallets/discord.svg new file mode 100644 index 000000000..c03e8e127 --- /dev/null +++ b/src/icons/wallets/discord.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/icons/wallets/email.svg b/src/icons/wallets/email.svg new file mode 100644 index 000000000..f47945d4a --- /dev/null +++ b/src/icons/wallets/email.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/wallets/google.svg b/src/icons/wallets/google.svg new file mode 100644 index 000000000..088288fa3 --- /dev/null +++ b/src/icons/wallets/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/wallets/twitter.svg b/src/icons/wallets/twitter.svg new file mode 100644 index 000000000..1aa490261 --- /dev/null +++ b/src/icons/wallets/twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/layout/DialogManager.tsx b/src/layout/DialogManager.tsx index cef213ec2..c856a14a6 100644 --- a/src/layout/DialogManager.tsx +++ b/src/layout/DialogManager.tsx @@ -2,36 +2,41 @@ import { useDispatch, useSelector } from 'react-redux'; import { DialogTypes } from '@/constants/dialogs'; -import { closeDialog, openDialog } from '@/state/dialogs'; - -import { getActiveDialog } from '@/state/dialogsSelectors'; - +import { AdjustIsolatedMarginDialog } from '@/views/dialogs/AdjustIsolatedMarginDialog'; +import { AdjustTargetLeverageDialog } from '@/views/dialogs/AdjustTargetLeverageDialog'; import { ClosePositionDialog } from '@/views/dialogs/ClosePositionDialog'; +import { ComplianceConfigDialog } from '@/views/dialogs/ComplianceConfigDialog'; import { DepositDialog } from '@/views/dialogs/DepositDialog'; +import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog'; +import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog'; import { DisconnectDialog } from '@/views/dialogs/DisconnectDialog'; import { DisplaySettingsDialog } from '@/views/dialogs/DisplaySettingsDialog'; import { ExchangeOfflineDialog } from '@/views/dialogs/ExchangeOfflineDialog'; -import { HelpDialog } from '@/views/dialogs/HelpDialog'; import { ExternalLinkDialog } from '@/views/dialogs/ExternalLinkDialog'; import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog'; +import { ExternalNavStrideDialog } from '@/views/dialogs/ExternalNavStrideDialog'; +import { GeoComplianceDialog } from '@/views/dialogs/GeoComplianceDialog'; +import { HelpDialog } from '@/views/dialogs/HelpDialog'; +import { ManageFundsDialog } from '@/views/dialogs/ManageFundsDialog'; import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog'; +import { MobileDownloadDialog } from '@/views/dialogs/MobileDownloadDialog'; import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog'; +import { NewMarketAgreementDialog } from '@/views/dialogs/NewMarketAgreementDialog'; +import { NewMarketMessageDetailsDialog } from '@/views/dialogs/NewMarketMessageDetailsDialog'; import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog'; import { PreferencesDialog } from '@/views/dialogs/PreferencesDialog'; import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog'; import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog'; +import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog'; +import { SelectMarginModeDialog } from '@/views/dialogs/SelectMarginModeDialog'; import { TradeDialog } from '@/views/dialogs/TradeDialog'; import { TransferDialog } from '@/views/dialogs/TransferDialog'; -import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog'; +import { TriggersDialog } from '@/views/dialogs/TriggersDialog'; import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog'; -import { ManageFundsDialog } from '@/views/dialogs/ManageFundsDialog'; +import { WithdrawalGateDialog } from '@/views/dialogs/WithdrawalGateDialog'; -import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog'; -import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog'; -import { NewMarketMessageDetailsDialog } from '@/views/dialogs/NewMarketMessageDetailsDialog'; -import { NewMarketAgreementDialog } from '@/views/dialogs/NewMarketAgreementDialog'; -import { ExternalNavStrideDialog } from '@/views/dialogs/ExternalNavStrideDialog'; -import { MobileDownloadDialog } from '@/views/dialogs/MobileDownloadDialog'; +import { closeDialog, openDialog } from '@/state/dialogs'; +import { getActiveDialog } from '@/state/dialogsSelectors'; export const DialogManager = () => { const dispatch = useDispatch(); @@ -52,30 +57,37 @@ export const DialogManager = () => { }; return { + [DialogTypes.AdjustIsolatedMargin]: , + [DialogTypes.AdjustTargetLeverage]: , [DialogTypes.ClosePosition]: , + [DialogTypes.ComplianceConfig]: , [DialogTypes.Deposit]: , [DialogTypes.DisplaySettings]: , [DialogTypes.DisconnectWallet]: , [DialogTypes.ExchangeOffline]: , - [DialogTypes.FillDetails]: , - [DialogTypes.Help]: , - [DialogTypes.ExternalNavKeplr]: , [DialogTypes.ExternalLink]: , + [DialogTypes.ExternalNavKeplr]: , [DialogTypes.ExternalNavStride]: , + [DialogTypes.FillDetails]: , + [DialogTypes.GeoCompliance]: , + [DialogTypes.Help]: , + [DialogTypes.ManageFunds]: , [DialogTypes.MnemonicExport]: , - [DialogTypes.MobileSignIn]: , [DialogTypes.MobileDownload]: , + [DialogTypes.MobileSignIn]: , + [DialogTypes.NewMarketAgreement]: , + [DialogTypes.NewMarketMessageDetails]: , [DialogTypes.Onboarding]: , [DialogTypes.OrderDetails]: , [DialogTypes.Preferences]: , [DialogTypes.RateLimit]: , [DialogTypes.RestrictedGeo]: , [DialogTypes.RestrictedWallet]: , + [DialogTypes.SelectMarginMode]: , [DialogTypes.Trade]: , [DialogTypes.Transfer]: , + [DialogTypes.Triggers]: , [DialogTypes.Withdraw]: , - [DialogTypes.ManageFunds]: , - [DialogTypes.NewMarketMessageDetails]: , - [DialogTypes.NewMarketAgreement]: , + [DialogTypes.WithdrawalGated]: , }[type]; }; diff --git a/src/layout/Footer/FooterDesktop.tsx b/src/layout/Footer/FooterDesktop.tsx index 9a6c5249e..8da2dc7c0 100644 --- a/src/layout/Footer/FooterDesktop.tsx +++ b/src/layout/Footer/FooterDesktop.tsx @@ -1,13 +1,15 @@ -import styled, { type AnyStyledComponent, css } from 'styled-components'; +import styled, { css } from 'styled-components'; import { AbacusApiStatus } from '@/constants/abacus'; import { ButtonSize, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { isDev } from '@/constants/networks'; -import { useApiState, useStringGetter, useURLConfigs } from '@/hooks'; -import { ChatIcon, LinkOutIcon } from '@/icons'; +import { useApiState } from '@/hooks/useApiState'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; +import { ChatIcon, LinkOutIcon } from '@/icons'; import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; @@ -15,6 +17,8 @@ import { Details } from '@/components/Details'; import { Output, OutputType } from '@/components/Output'; import { WithTooltip } from '@/components/WithTooltip'; +import { isPresent } from '@/lib/typeUtils'; + enum FooterItems { ChainHeight, IndexerHeight, @@ -30,85 +34,90 @@ export const FooterDesktop = () => { const { height, indexerHeight, status, statusErrorMessage } = useApiState(); const { statusPage } = useURLConfigs(); - const { exchangeStatus, label } = - !status || status === AbacusApiStatus.NORMAL - ? { - exchangeStatus: ExchangeStatus.Operational, - label: stringGetter({ key: STRING_KEYS.OPERATIONAL }), - } - : { - exchangeStatus: ExchangeStatus.Degraded, - label: stringGetter({ key: STRING_KEYS.DEGRADED }), - }; + const isStatusLoading = !status && !statusErrorMessage; + + const { exchangeStatus, label } = isStatusLoading + ? { + exchangeStatus: undefined, + label: stringGetter({ key: STRING_KEYS.CONNECTING }), + } + : status === AbacusApiStatus.NORMAL + ? { + exchangeStatus: ExchangeStatus.Operational, + label: stringGetter({ key: STRING_KEYS.OPERATIONAL }), + } + : { + exchangeStatus: ExchangeStatus.Degraded, + label: stringGetter({ key: STRING_KEYS.DEGRADED }), + }; return ( - - + <$Footer> + <$Row> -
    {statusErrorMessage}
    +
    {statusErrorMessage.body}
    ) } > - } + slotLeft={<$StatusDot exchangeStatus={exchangeStatus} />} slotRight={statusPage && } size={ButtonSize.XSmall} state={{ isDisabled: !statusPage }} href={statusPage} > {label} - +
    {globalThis?.Intercom && ( - } size={ButtonSize.XSmall} onClick={() => globalThis.Intercom('show')} > {stringGetter({ key: STRING_KEYS.HELP_AND_SUPPORT })} - + )} -
    + {isDev && ( - , }, - height !== indexerHeight && { - key: FooterItems.IndexerHeight, - label: 'Indexer Block Height', - value: ( - - ), - }, - ].filter(Boolean)} + height !== indexerHeight + ? { + key: FooterItems.IndexerHeight.toString(), + label: 'Indexer Block Height', + value: ( + <$WarningOutput useGrouping type={OutputType.Number} value={indexerHeight} /> + ), + } + : undefined, + ].filter(isPresent)} layout="row" /> )} -
    + ); }; - -const Styled: Record = {}; - -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${layoutMixins.stickyFooter} ${layoutMixins.spacedRow} grid-area: Footer; `; -Styled.Row = styled.div` +const $Row = styled.div` ${layoutMixins.row} ${layoutMixins.spacedRow} width: var(--sidebar-width); @@ -117,20 +126,22 @@ Styled.Row = styled.div` border-right: 1px solid var(--color-border); `; -Styled.StatusDot = styled.div<{ exchangeStatus: ExchangeStatus }>` +const $StatusDot = styled.div<{ exchangeStatus?: ExchangeStatus }>` width: 0.5rem; height: 0.5rem; border-radius: 50%; margin-right: 0.25rem; + background-color: var(--color-text-0); background-color: ${({ exchangeStatus }) => - ({ + exchangeStatus && + { [ExchangeStatus.Degraded]: css`var(--color-warning)`, [ExchangeStatus.Operational]: css`var(--color-success)`, - }[exchangeStatus])}; + }[exchangeStatus]}; `; -Styled.FooterButton = styled(Button)` +const $FooterButton = styled(Button)` --button-height: 1.5rem; --button-radius: 0.25rem; --button-backgroundColor: transparent; @@ -147,11 +158,11 @@ Styled.FooterButton = styled(Button)` } `; -Styled.WarningOutput = styled(Output)` +const $WarningOutput = styled(Output)` color: var(--color-warning); `; -Styled.Details = styled(Details)` +const $Details = styled(Details)` ${layoutMixins.scrollArea} font: var(--font-tiny-book); `; diff --git a/src/layout/Footer/FooterMobile.tsx b/src/layout/Footer/FooterMobile.tsx index 9ce2a24a9..3fc807541 100644 --- a/src/layout/Footer/FooterMobile.tsx +++ b/src/layout/Footer/FooterMobile.tsx @@ -1,18 +1,19 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { DEFAULT_MARKETID } from '@/constants/markets'; import { AppRoute } from '@/constants/routes'; -import { useShouldShowFooter, useStringGetter } from '@/hooks'; +import { useShouldShowFooter } from '@/hooks/useShouldShowFooter'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { BellIcon, MarketsIcon, PortfolioIcon, ProfileIcon } from '@/icons'; import { layoutMixins } from '@/styles/layoutMixins'; -import { NavigationMenu } from '@/components/NavigationMenu'; import { Icon, IconName } from '@/components/Icon'; -import { BellIcon, MarketsIcon, PortfolioIcon, ProfileIcon } from '@/icons'; -import { IconButton } from '@/components/IconButton'; +import { NavigationMenu } from '@/components/NavigationMenu'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; import { openDialog } from '@/state/dialogs'; @@ -30,8 +31,8 @@ export const FooterMobile = () => { if (!useShouldShowFooter()) return null; return ( - - + <$NavigationMenu items={[ { group: 'navigation', @@ -41,9 +42,9 @@ export const FooterMobile = () => { value: 'trade', label: stringGetter({ key: STRING_KEYS.TRADE }), slotBefore: ( - + <$StartIcon> - + ), href: `${AppRoute.Trade}/${marketId ?? DEFAULT_MARKETID}`, } @@ -51,34 +52,34 @@ export const FooterMobile = () => { value: 'onboarding', label: stringGetter({ key: STRING_KEYS.ONBOARDING }), slotBefore: ( - + <$StartIcon> - + ), onClick: () => dispatch(openDialog({ type: DialogTypes.Onboarding })), }, { value: 'portfolio', label: stringGetter({ key: STRING_KEYS.PORTFOLIO }), - slotBefore: , + slotBefore: <$Icon iconComponent={PortfolioIcon as any} />, href: AppRoute.Portfolio, }, { value: 'markets', label: stringGetter({ key: STRING_KEYS.MARKETS }), - slotBefore: , + slotBefore: <$Icon iconComponent={MarketsIcon as any} />, href: AppRoute.Markets, }, { value: 'alerts', label: stringGetter({ key: STRING_KEYS.ALERTS }), - slotBefore: , + slotBefore: <$Icon iconComponent={BellIcon as any} />, href: AppRoute.Alerts, }, { value: 'profile', label: stringGetter({ key: STRING_KEYS.PROFILE }), - slotBefore: , + slotBefore: <$Icon iconComponent={ProfileIcon as any} />, href: AppRoute.Profile, }, ], @@ -87,19 +88,16 @@ export const FooterMobile = () => { orientation="horizontal" itemOrientation="vertical" /> - + ); }; - -const Styled: Record = {}; - -Styled.MobileNav = styled.footer` +const $MobileNav = styled.footer` grid-area: Footer; ${layoutMixins.stickyFooter} `; -Styled.NavigationMenu = styled(NavigationMenu)` +const $NavigationMenu = styled(NavigationMenu)` --navigationMenu-height: var(--page-currentFooterHeight); --navigationMenu-item-height: var(--page-currentFooterHeight); --navigationMenu-item-radius: 0; @@ -156,15 +154,11 @@ Styled.NavigationMenu = styled(NavigationMenu)` } `; -Styled.IconButton = styled(IconButton)` - margin-top: -0.25rem; -`; - -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` font-size: 1.5rem; `; -Styled.StartIcon = styled.div` +const $StartIcon = styled.div` display: inline-flex; flex-direction: row; justify-content: center; diff --git a/src/layout/Header/HeaderDesktop.tsx b/src/layout/Header/HeaderDesktop.tsx index 97e5fd707..2c9da899a 100644 --- a/src/layout/Header/HeaderDesktop.tsx +++ b/src/layout/Header/HeaderDesktop.tsx @@ -1,37 +1,42 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; -import { Link } from 'react-router-dom'; +import { isTruthy } from '@dydxprotocol/v4-client-js/build/src/network_optimizer'; import { useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; import { ButtonShape } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { AppRoute } from '@/constants/routes'; -import { LogoShortIcon, BellStrokeIcon } from '@/icons'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; + +import { BellStrokeIcon, LogoShortIcon } from '@/icons'; +import breakpoints from '@/styles/breakpoints'; import { headerMixins } from '@/styles/headerMixins'; import { layoutMixins } from '@/styles/layoutMixins'; -import breakpoints from '@/styles/breakpoints'; - -import { useTokenConfigs, useStringGetter, useURLConfigs } from '@/hooks'; import { Icon, IconName } from '@/components/Icon'; import { IconButton } from '@/components/IconButton'; import { NavigationMenu } from '@/components/NavigationMenu'; import { VerticalSeparator } from '@/components/Separator'; - import { AccountMenu } from '@/views/menus/AccountMenu'; +import { LanguageSelector } from '@/views/menus/LanguageSelector'; import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu'; import { NotificationsMenu } from '@/views/menus/NotificationsMenu'; -import { LanguageSelector } from '@/views/menus/LanguageSelector'; -import { openDialog } from '@/state/dialogs'; import { getHasSeenLaunchIncentives } from '@/state/configsSelectors'; +import { openDialog } from '@/state/dialogs'; export const HeaderDesktop = () => { const stringGetter = useStringGetter(); - const { documentation, community, mintscanBase } = useURLConfigs(); + const { documentation, community, mintscanBase, exchangeStats } = useURLConfigs(); const dispatch = useDispatch(); const { chainTokenLabel } = useTokenConfigs(); + const { complianceState } = useComplianceState(); const hasSeenLaunchIncentives = useSelector(getHasSeenLaunchIncentives); @@ -54,11 +59,11 @@ export const HeaderDesktop = () => { label: stringGetter({ key: STRING_KEYS.MARKETS }), href: AppRoute.Markets, }, - { + complianceState === ComplianceStates.FULL_ACCESS && { value: chainTokenLabel, label: chainTokenLabel, href: `/${chainTokenLabel}`, - slotAfter: !hasSeenLaunchIncentives && , + slotAfter: !hasSeenLaunchIncentives && <$UnreadIndicator />, }, { value: 'MORE', @@ -98,39 +103,44 @@ export const HeaderDesktop = () => { value: 'HELP', slotBefore: , label: stringGetter({ key: STRING_KEYS.HELP }), - onClick: (e: MouseEvent) => { - e.preventDefault(); + onClick: () => { dispatch(openDialog({ type: DialogTypes.Help })); }, }, + { + value: 'STATS', + slotBefore: , + label: stringGetter({ key: STRING_KEYS.STATISTICS }), + href: exchangeStats, + }, ], }, - ], + ].filter(isTruthy), }, ]; return ( - - + <$Header> + <$LogoLink to="/"> - + - + <$NavBefore> - + - + <$NavigationMenu items={navItems} orientation="horizontal" />
    - - + <$IconButton shape={ButtonShape.Rectangle} iconName={IconName.HelpCircle} onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))} @@ -140,21 +150,21 @@ export const HeaderDesktop = () => { + <$IconButton + shape={ButtonShape.Rectangle} + iconComponent={BellStrokeIcon as React.ElementType} + /> } /> - - + + ); }; - -const Styled: Record = {}; - -Styled.Header = styled.header` +const $Header = styled.header` --header-horizontal-padding-mobile: 0.5rem; --trigger-height: 2.25rem; --logo-width: 3.5rem; @@ -193,7 +203,7 @@ Styled.Header = styled.header` } `; -Styled.NavigationMenu = styled(NavigationMenu)` +const $NavigationMenu = styled(NavigationMenu)` & { --navigationMenu-height: var(--stickyArea-topHeight); --navigationMenu-item-height: var(--trigger-height); @@ -202,9 +212,9 @@ Styled.NavigationMenu = styled(NavigationMenu)` ${layoutMixins.scrollArea} padding: 0 0.5rem; scroll-padding: 0 0.5rem; -`; +` as typeof NavigationMenu; -Styled.NavBefore = styled.div` +const $NavBefore = styled.div` ${layoutMixins.flexEqualColumns} > * { @@ -213,7 +223,7 @@ Styled.NavBefore = styled.div` } `; -Styled.LogoLink = styled(Link)` +const $LogoLink = styled(Link)` display: flex; align-self: stretch; @@ -224,7 +234,7 @@ Styled.LogoLink = styled(Link)` } `; -Styled.NavAfter = styled.div` +const $NavAfter = styled.div` ${layoutMixins.row} justify-self: end; padding-right: 0.75rem; @@ -236,14 +246,14 @@ Styled.NavAfter = styled.div` } `; -Styled.IconButton = styled(IconButton)<{ size?: string }>` +const $IconButton = styled(IconButton)<{ size?: string }>` ${headerMixins.button} --button-border: none; --button-icon-size: 1rem; --button-padding: 0 0.5em; `; -Styled.UnreadIndicator = styled.div` +const $UnreadIndicator = styled.div` width: 0.4375rem; height: 0.4375rem; border-radius: 50%; diff --git a/src/layout/NotificationsToastArea/NotifcationStack.tsx b/src/layout/NotificationsToastArea/NotifcationStack.tsx index a2f16ddbb..f65c71c35 100644 --- a/src/layout/NotificationsToastArea/NotifcationStack.tsx +++ b/src/layout/NotificationsToastArea/NotifcationStack.tsx @@ -1,14 +1,17 @@ import { useState } from 'react'; + import styled, { css } from 'styled-components'; +import { ButtonShape, ButtonSize } from '@/constants/buttons'; import { + NotificationStatus, type Notification, type NotificationDisplayData, - NotificationStatus, } from '@/constants/notifications'; -import { ButtonShape, ButtonSize } from '@/constants/buttons'; -import { useNotifications } from '@/hooks/useNotifications'; + import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useNotifications } from '@/hooks/useNotifications'; + import { ChevronLeftIcon } from '@/icons'; import { breakpoints } from '@/styles'; @@ -68,9 +71,13 @@ export const NotificationStack = ({ notifications, className }: ElementProps & S slotCustomContent={displayData.renderCustomBody?.({ isToast: true, notification })} onClick={() => onNotificationAction(notification)} slotAction={ - + displayData.renderActionSlot ? ( + displayData.renderActionSlot({ isToast: true, notification }) + ) : displayData.actionDescription ? ( + + ) : undefined } actionDescription={displayData.actionDescription} actionAltText={displayData.actionAltText} diff --git a/src/layout/NotificationsToastArea/index.tsx b/src/layout/NotificationsToastArea/index.tsx index fea600490..ee61f2150 100644 --- a/src/layout/NotificationsToastArea/index.tsx +++ b/src/layout/NotificationsToastArea/index.tsx @@ -1,13 +1,15 @@ import { useMemo } from 'react'; + import { groupBy } from 'lodash'; import styled from 'styled-components'; -import { breakpoints } from '@/styles'; -import { layoutMixins } from '@/styles/layoutMixins'; - import { NotificationStatus } from '@/constants/notifications'; + import { useNotifications } from '@/hooks/useNotifications'; +import { breakpoints } from '@/styles'; +import { layoutMixins } from '@/styles/layoutMixins'; + import { NotificationStack } from './NotifcationStack'; type StyleProps = { diff --git a/src/lib/__test__/formatZeroNumbers.spec.ts b/src/lib/__test__/formatZeroNumbers.spec.ts new file mode 100644 index 000000000..efd84803a --- /dev/null +++ b/src/lib/__test__/formatZeroNumbers.spec.ts @@ -0,0 +1,166 @@ +import { describe, expect, it } from 'vitest'; + +import { formatZeroNumbers } from '../formatZeroNumbers'; + +describe('formatZeroNumbers function', () => { + it('should not compress zeros with and handle the absence of currency symbol', () => { + expect(formatZeroNumbers('123.00')).toEqual({ + currencySign: undefined, + significantDigits: '123', + punctuationSymbol: '.', + zeros: 0, + decimalDigits: '00', + }); + expect(formatZeroNumbers('123,00')).toEqual({ + currencySign: undefined, + significantDigits: '123', + punctuationSymbol: ',', + zeros: 0, + decimalDigits: '00', + }); + }); + + it('should not compress zeros even if there is a currency symbol', () => { + expect(formatZeroNumbers('$0.00')).toEqual({ + currencySign: '$', + significantDigits: '0', + punctuationSymbol: '.', + zeros: 0, + decimalDigits: '00', + }); + expect(formatZeroNumbers('$0,00')).toEqual({ + currencySign: '$', + significantDigits: '0', + punctuationSymbol: ',', + zeros: 0, + decimalDigits: '00', + }); + }); + + it('should correctly handle significant digits with leading zeros', () => { + expect(formatZeroNumbers('$001.2300')).toEqual({ + currencySign: '$', + significantDigits: '001', + punctuationSymbol: '.', + zeros: 0, + decimalDigits: '2300', + }); + expect(formatZeroNumbers('$001,2300')).toEqual({ + currencySign: '$', + significantDigits: '001', + punctuationSymbol: ',', + zeros: 0, + decimalDigits: '2300', + }); + }); + + it('should return original value if there are no zeros to compress', () => { + expect(formatZeroNumbers('$123.45')).toEqual({ + currencySign: '$', + significantDigits: '123', + punctuationSymbol: '.', + zeros: 0, + decimalDigits: '45', + }); + expect(formatZeroNumbers('$123,45')).toEqual({ + currencySign: '$', + significantDigits: '123', + punctuationSymbol: ',', + zeros: 0, + decimalDigits: '45', + }); + }); + + it('should correctly handle cases with only leading zeros less than the default threshold', () => { + expect(formatZeroNumbers('$00.005')).toEqual({ + currencySign: '$', + significantDigits: '00', + punctuationSymbol: '.', + zeros: 0, + decimalDigits: '005', + }); + expect(formatZeroNumbers('$00,005')).toEqual({ + currencySign: '$', + significantDigits: '00', + punctuationSymbol: ',', + zeros: 0, + decimalDigits: '005', + }); + }); + + it('should handle cases with no decimal part', () => { + expect(formatZeroNumbers('$123')).toEqual({ + currencySign: '$', + significantDigits: '123', + }); + expect(formatZeroNumbers('$123')).toEqual({ + currencySign: '$', + significantDigits: '123', + }); + }); + + it('should handle cases with no significant digits', () => { + expect(formatZeroNumbers('$0.00')).toEqual({ + currencySign: '$', + significantDigits: '0', + punctuationSymbol: '.', + zeros: 0, + decimalDigits: '00', + }); + expect(formatZeroNumbers('$0,00')).toEqual({ + currencySign: '$', + significantDigits: '0', + punctuationSymbol: ',', + zeros: 0, + decimalDigits: '00', + }); + }); + it('should compress zeros with the default threshold', () => { + expect(formatZeroNumbers('$0.00000029183')).toEqual({ + currencySign: '$', + significantDigits: '0', + punctuationSymbol: '.', + zeros: 6, + decimalDigits: '29183', + }); + expect(formatZeroNumbers('$0,00000029183')).toEqual({ + currencySign: '$', + significantDigits: '0', + punctuationSymbol: ',', + zeros: 6, + decimalDigits: '29183', + }); + }); + it('should not compress zeros with a different threshold', () => { + expect(formatZeroNumbers('$1.000000323', 8)).toEqual({ + currencySign: '$', + significantDigits: '1', + punctuationSymbol: '.', + zeros: 0, + decimalDigits: '000000323', + }); + expect(formatZeroNumbers('$1,000000323', 8)).toEqual({ + currencySign: '$', + significantDigits: '1', + punctuationSymbol: ',', + zeros: 0, + decimalDigits: '000000323', + }); + }); + it('should compress zeros with a different threshold', () => { + expect(formatZeroNumbers('$1.00000000323', 5)).toEqual({ + currencySign: '$', + significantDigits: '1', + punctuationSymbol: '.', + zeros: 8, + decimalDigits: '323', + }); + expect(formatZeroNumbers('$1,00000000323', 5)).toEqual({ + currencySign: '$', + significantDigits: '1', + punctuationSymbol: ',', + zeros: 8, + decimalDigits: '323', + }); + }); +}); diff --git a/src/lib/__test__/numbers.spec.ts b/src/lib/__test__/numbers.spec.ts index 633cb2d2a..9ba7a6ddf 100644 --- a/src/lib/__test__/numbers.spec.ts +++ b/src/lib/__test__/numbers.spec.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest'; import BigNumber from 'bignumber.js'; +import { describe, expect, it } from 'vitest'; import { getFractionDigits, isNumber, roundToNearestFactor } from '../numbers'; diff --git a/src/lib/__test__/timeUtils.ts b/src/lib/__test__/timeUtils.ts index 7e61de7a1..a9f0421ab 100644 --- a/src/lib/__test__/timeUtils.ts +++ b/src/lib/__test__/timeUtils.ts @@ -1,14 +1,13 @@ -import { describe, expect, it } from 'vitest'; import { DateTime, Duration } from 'luxon'; +import { describe, expect, it } from 'vitest'; import { MustBigNumber } from '@/lib/numbers'; - import { - getTimestamp, getStringsForDateTimeDiff, getStringsForTimeInterval, - getTimeTillNextUnit, getTimeString, + getTimeTillNextUnit, + getTimestamp, } from '@/lib/timeUtils'; describe('getTimestamp', () => { diff --git a/src/lib/__test__/tradeData.spec.ts b/src/lib/__test__/tradeData.spec.ts index a66650536..c76e4b245 100644 --- a/src/lib/__test__/tradeData.spec.ts +++ b/src/lib/__test__/tradeData.spec.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest'; import { PositionSide } from '@/constants/trade'; -import { hasPositionSideChanged } from '../tradeData'; + +import { BIG_NUMBERS, MustBigNumber } from '../numbers'; +import { calculatePositionMargin, hasPositionSideChanged } from '../tradeData'; describe('hasPositionSideChanged', () => { describe('Should return false when the position side has not changed', () => { @@ -76,3 +78,39 @@ describe('hasPositionSideChanged', () => { }); }); }); + +describe('calculatePositionMargin', () => { + it('should calculate the position margin', () => { + expect(calculatePositionMargin({ notionalTotal: 100, adjustedMmf: 0.1 })).toEqual( + MustBigNumber(10) + ); + }); + + it('should calculate the position margin with a notionalTotal of 0', () => { + expect(calculatePositionMargin({ notionalTotal: 0, adjustedMmf: 0.1 })).toEqual( + BIG_NUMBERS.ZERO + ); + }); + + it('should calculate the position margin with a adjustedMmf of 0', () => { + expect(calculatePositionMargin({ notionalTotal: 100, adjustedMmf: 0 })).toEqual( + BIG_NUMBERS.ZERO + ); + }); + + it('should calculate the position margin with a notionalTotal of 0 and a adjustedMmf of 0', () => { + expect(calculatePositionMargin({ notionalTotal: 0, adjustedMmf: 0 })).toEqual(BIG_NUMBERS.ZERO); + }); + + it('should calculate the position margin with a negative notionalTotal', () => { + expect(calculatePositionMargin({ notionalTotal: -100, adjustedMmf: 0.1 })).toEqual( + MustBigNumber(-10) + ); + }); + + it('should handle undefined notionalTotal', () => { + expect(calculatePositionMargin({ notionalTotal: undefined, adjustedMmf: 0.1 })).toEqual( + BIG_NUMBERS.ZERO + ); + }); +}); diff --git a/src/lib/abacus/conversions.ts b/src/lib/abacus/conversions.ts index ee6822239..9a79f6d86 100644 --- a/src/lib/abacus/conversions.ts +++ b/src/lib/abacus/conversions.ts @@ -2,12 +2,11 @@ import { OrderSide } from '@dydxprotocol/v4-client-js'; import { AbacusOrderSide, - type Nullable, - type AbacusOrderSides, AbacusPositionSide, AbacusPositionSides, + type AbacusOrderSides, + type Nullable, } from '@/constants/abacus'; - import { PositionSide } from '@/constants/trade'; /** @deprecated use ORDER_SIDES from constants/abacus */ diff --git a/src/lib/abacus/dydxChainTransactions.ts b/src/lib/abacus/dydxChainTransactions.ts index 79bc0d028..8d83ef301 100644 --- a/src/lib/abacus/dydxChainTransactions.ts +++ b/src/lib/abacus/dydxChainTransactions.ts @@ -1,59 +1,68 @@ -import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus'; -import Long from 'long'; -import type { IndexedTx } from '@cosmjs/stargate'; -import { GAS_MULTIPLIER, encodeJson } from '@dydxprotocol/v4-client-js'; import { EncodeObject } from '@cosmjs/proto-signing'; - +import type { IndexedTx } from '@cosmjs/stargate'; +import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus'; import { CompositeClient, + GAS_MULTIPLIER, IndexerConfig, - type LocalWallet, Network, NetworkOptimizer, NobleClient, - SubaccountClient, - ValidatorConfig, - OrderType, + OrderExecution, OrderSide, OrderTimeInForce, - OrderExecution, + OrderType, + SubaccountClient, + ValidatorConfig, + encodeJson, + type LocalWallet, + type SelectedGasDenom, } from '@dydxprotocol/v4-client-js'; +import Long from 'long'; import { - type AbacusDYDXChainTransactionsProtocol, QueryType, - type QueryTypes, TransactionType, - type TransactionTypes, - type HumanReadablePlaceOrderPayload, + type AbacusDYDXChainTransactionsProtocol, type HumanReadableCancelOrderPayload, - type HumanReadableWithdrawPayload, + type HumanReadablePlaceOrderPayload, type HumanReadableTransferPayload, + type HumanReadableWithdrawPayload, + type QueryTypes, + type TransactionTypes, } from '@/constants/abacus'; - +import { Hdkey } from '@/constants/account'; import { DEFAULT_TRANSACTION_MEMO } from '@/constants/analytics'; -import { DialogTypes } from '@/constants/dialogs'; -import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade'; import { DydxChainId, isTestnet } from '@/constants/networks'; +import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade'; +// TODO Fix cycle +// eslint-disable-next-line import/no-cycle import { RootStore } from '@/state/_store'; -import { addUncommittedOrderClientId, removeUncommittedOrderClientId } from '@/state/account'; -import { openDialog } from '@/state/dialogs'; +import { placeOrderTimeout } from '@/state/account'; +import { setInitializationError } from '@/state/app'; +import { signComplianceSignature } from '../compliance'; import { StatefulOrderError } from '../errors'; import { bytesToBigInt } from '../numbers'; import { log } from '../telemetry'; -import { hashFromTx, getMintscanTxLink } from '../txUtils'; +import { getMintscanTxLink, hashFromTx } from '../txUtils'; -(BigInt.prototype as any).toJSON = function () { +(BigInt.prototype as any).toJSON = function toJSON() { return this.toString(); }; class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { private compositeClient: CompositeClient | undefined; + private nobleClient: NobleClient | undefined; + private store: RootStore | undefined; + + private hdkey: Hdkey | undefined; + private localWallet: LocalWallet | undefined; + private nobleWallet: LocalWallet | undefined; constructor() { @@ -69,6 +78,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { this.store = store; } + setHdkey(hdkey: Hdkey) { + this.hdkey = hdkey; + } + setLocalWallet(localWallet: LocalWallet) { this.localWallet = localWallet; } @@ -145,13 +158,15 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { globalThis.dispatchEvent(customEvent); callback(JSON.stringify({ success: true })); } catch (error) { - this.store?.dispatch( - openDialog({ type: DialogTypes.ExchangeOffline, dialogProps: { preventClose: true } }) - ); + this.store?.dispatch(setInitializationError(error?.message ?? 'Unknown error')); log('DydxChainTransactions/connectNetwork', error); } } + setSelectedGasDenom(denom: SelectedGasDenom) { + this.compositeClient?.setSelectedGasDenom(denom); + } + parseToPrimitives(x: T): T { if (typeof x === 'number' || typeof x === 'string' || typeof x === 'boolean' || x === null) { return x; @@ -171,6 +186,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { if (typeof x === 'object') { const parsedObj: { [key: string]: any } = {}; + // eslint-disable-next-line no-restricted-syntax for (const key in x) { if (Object.prototype.hasOwnProperty.call(x, key)) { parsedObj[key] = this.parseToPrimitives((x as any)[key]); @@ -201,17 +217,15 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { clientId, timeInForce, goodTilTimeInSeconds, + goodTilBlock, execution, postOnly, reduceOnly, triggerPrice, } = params || {}; - // Observe uncommitted order - this.store?.dispatch(addUncommittedOrderClientId(clientId)); - setTimeout(() => { - this.store?.dispatch(removeUncommittedOrderClientId(clientId)); + this.store?.dispatch(placeOrderTimeout(clientId)); }, UNCOMMITTED_ORDER_TIMEOUT_MS); // Place order @@ -228,7 +242,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { execution as OrderExecution, postOnly ?? undefined, reduceOnly ?? undefined, - triggerPrice ?? undefined + triggerPrice ?? undefined, + undefined, + undefined, + goodTilBlock ?? undefined ); // Handle stateful orders @@ -241,9 +258,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { const hash = parsedTx.hash.toUpperCase(); if (isTestnet) { + // eslint-disable-next-line no-console console.log( getMintscanTxLink(this.compositeClient.network.getString() as DydxChainId, hash) ); + // eslint-disable-next-line no-console } else console.log(`txHash: ${hash}`); return encodedTx; @@ -272,14 +291,15 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { clientId, orderFlags, clobPairId, - goodTilBlock || undefined, - goodTilBlockTime || undefined + goodTilBlock === 0 ? undefined : goodTilBlock ?? undefined, + goodTilBlockTime === 0 ? undefined : goodTilBlockTime ?? undefined ); const encodedTx = encodeJson(tx); if (import.meta.env.MODE === 'development') { const parsedTx = JSON.parse(encodedTx); + // eslint-disable-next-line no-console console.log(parsedTx, parsedTx.hash.toUpperCase()); } @@ -332,9 +352,8 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { throw new Error('Missing compositeClient or localWallet'); } - const { subaccountNumber, amount, recipient } = params ?? {}; + const { amount, recipient } = params ?? {}; const compositeClient = this.compositeClient; - const subaccountClient = new SubaccountClient(this.localWallet, subaccountNumber); try { const tx = await compositeClient.simulate( @@ -495,6 +514,37 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { } } + async signCompliancePayload(params: { + message: string; + action: string; + status: string; + }): Promise { + if (!this.hdkey?.privateKey || !this.hdkey?.publicKey) { + throw new Error('Missing hdkey'); + } + + try { + const { signedMessage, timestamp } = await signComplianceSignature( + params.message, + params.action, + params.status, + this.hdkey + ); + + return JSON.stringify({ + signedMessage, + publicKey: Buffer.from(this.hdkey.publicKey).toString('base64'), + timestamp, + }); + } catch (error) { + log('DydxChainTransactions/signComplianceMessage', error); + + return JSON.stringify({ + error, + }); + } + } + async transaction( type: TransactionTypes, paramsInJson: Abacus.Nullable, @@ -539,6 +589,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { callback(result); break; } + case TransactionType.SignCompliancePayload: { + const result = await this.signCompliancePayload(params); + callback(result); + break; + } default: { break; } @@ -564,11 +619,12 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { const params = paramsInJson ? JSON.parse(paramsInJson) : undefined; switch (type) { - case QueryType.Height: + case QueryType.Height: { const block = await this.compositeClient?.validatorClient.get.latestBlock(); callback(JSON.stringify(block)); break; - case QueryType.OptimalNode: + } + case QueryType.OptimalNode: { const networkOptimizer = new NetworkOptimizer(); const optimalNode = await networkOptimizer.findOptimalNode( params.endpointUrls, @@ -576,32 +632,37 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { ); callback(JSON.stringify({ url: optimalNode })); break; - case QueryType.EquityTiers: + } + case QueryType.EquityTiers: { const equityTiers = await this.compositeClient?.validatorClient.get.getEquityTierLimitConfiguration(); const parsedEquityTiers = this.parseToPrimitives(equityTiers); callback(JSON.stringify(parsedEquityTiers)); break; - case QueryType.FeeTiers: + } + case QueryType.FeeTiers: { const feeTiers = await this.compositeClient?.validatorClient.get.getFeeTiers(); const parsedFeeTiers = this.parseToPrimitives(feeTiers); callback(JSON.stringify(parsedFeeTiers)); break; - case QueryType.UserFeeTier: + } + case QueryType.UserFeeTier: { const userFeeTier = await this.compositeClient?.validatorClient.get.getUserFeeTier( params.address ); const parsedUserFeeTier = this.parseToPrimitives(userFeeTier); callback(JSON.stringify(parsedUserFeeTier)); break; - case QueryType.UserStats: + } + case QueryType.UserStats: { const userStats = await this.compositeClient?.validatorClient.get.getUserStats( params.address ); const parsedUserStats = this.parseToPrimitives(userStats); callback(JSON.stringify(parsedUserStats)); break; - case QueryType.GetAccountBalances: + } + case QueryType.GetAccountBalances: { if (!this.localWallet?.address) throw new Error('Missing localWallet'); const accountBalances = await this.compositeClient?.validatorClient.get.getAccountBalances( @@ -610,31 +671,37 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { const parsedAccountBalances = this.parseToPrimitives(accountBalances); callback(JSON.stringify(parsedAccountBalances)); break; - case QueryType.RewardsParams: + } + case QueryType.RewardsParams: { const rewardsParams = await this.compositeClient?.validatorClient.get.getRewardsParams(); const parsedRewardsParams = this.parseToPrimitives(rewardsParams); callback(JSON.stringify(parsedRewardsParams)); break; - case QueryType.GetMarketPrice: + } + case QueryType.GetMarketPrice: { const price = await this.compositeClient?.validatorClient.get.getPrice(params.marketId); const parsedPrice = this.parseToPrimitives(price); callback(JSON.stringify(parsedPrice)); break; - case QueryType.GetDelegations: + } + case QueryType.GetDelegations: { const delegations = await this.compositeClient?.validatorClient.get.getDelegatorDelegations(params.address); const parseDelegations = this.parseToPrimitives(delegations); callback(JSON.stringify(parseDelegations)); break; - case QueryType.GetNobleBalance: + } + case QueryType.GetNobleBalance: { if (this.nobleClient?.isConnected) { const nobleBalance = await this.nobleClient.getAccountBalance('uusdc'); const parsedNobleBalance = this.parseToPrimitives(nobleBalance); callback(JSON.stringify(parsedNobleBalance)); } break; - default: + } + default: { break; + } } } catch (error) { log('DydxChainTransactions/get', error); diff --git a/src/lib/abacus/filesystem.ts b/src/lib/abacus/filesystem.ts index db7408669..d02b13721 100644 --- a/src/lib/abacus/filesystem.ts +++ b/src/lib/abacus/filesystem.ts @@ -1,11 +1,11 @@ -import type { AbacusFileSystemProtocol, FileLocation, Nullable } from '@/constants/abacus'; +import type { AbacusFileSystemProtocol, Nullable } from '@/constants/abacus'; class AbacusFileSystem implements AbacusFileSystemProtocol { - readTextFile(location: FileLocation, path: string): Nullable { + readTextFile(): Nullable { return null; } - writeTextFile(path: string, text: string): boolean { + writeTextFile(): boolean { return true; } } diff --git a/src/lib/abacus/formatter.ts b/src/lib/abacus/formatter.ts index bebfe0d24..725c9b208 100644 --- a/src/lib/abacus/formatter.ts +++ b/src/lib/abacus/formatter.ts @@ -1,6 +1,6 @@ import type { AbacusFormatterProtocol } from '@/constants/abacus'; -import { type LocaleSeparators, MustBigNumber, getFractionDigits } from '../numbers'; +import { MustBigNumber, getFractionDigits, type LocaleSeparators } from '../numbers'; class AbacusFormatter implements AbacusFormatterProtocol { localeSeparators: LocaleSeparators; diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index c3be440a0..bf9267685 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -1,61 +1,72 @@ -import type { LocalWallet } from '@dydxprotocol/v4-client-js'; +import type { LocalWallet, SelectedGasDenom } from '@dydxprotocol/v4-client-js'; import type { ClosePositionInputFields, - Nullable, + HistoricalPnlPeriods, HistoricalTradingRewardsPeriod, HistoricalTradingRewardsPeriods, - HumanReadablePlaceOrderPayload, HumanReadableCancelOrderPayload, + HumanReadablePlaceOrderPayload, + HumanReadableTriggerOrdersPayload, + Nullable, + ParsingError, TradeInputFields, TransferInputFields, - HistoricalPnlPeriods, - ParsingError, + TriggerOrdersInputFields, } from '@/constants/abacus'; - import { - AsyncAbacusStateManager, + AbacusAppConfig, AbacusHelper, + ApiData, + AsyncAbacusStateManager, ClosePositionInputField, + ComplianceAction, + CoroutineTimer, HistoricalPnlPeriod, + IOImplementations, TradeInputField, TransferInputField, - IOImplementations, - UIImplementations, - CoroutineTimer, TransferType, - AbacusAppConfig, - ApiData, + TriggerOrdersInputField, + UIImplementations, } from '@/constants/abacus'; - +import { Hdkey } from '@/constants/account'; import { DEFAULT_MARKETID } from '@/constants/markets'; -import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork, isMainnet } from '@/constants/networks'; +import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks'; import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade'; import type { RootStore } from '@/state/_store'; import { setTradeFormInputs } from '@/state/inputs'; import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors'; -import AbacusRest from './rest'; +import { LocaleSeparators } from '../numbers'; import AbacusAnalytics from './analytics'; -import AbacusWebsocket from './websocket'; +// eslint-disable-next-line import/no-cycle import AbacusChainTransaction from './dydxChainTransactions'; -import AbacusStateNotifier from './stateNotification'; -import AbacusLocalizer from './localizer'; +import AbacusFileSystem from './filesystem'; import AbacusFormatter from './formatter'; +import AbacusLocalizer from './localizer'; +import AbacusLogger from './logger'; +import AbacusRest from './rest'; +import AbacusStateNotifier from './stateNotification'; import AbacusThreading from './threading'; -import AbacusFileSystem from './filesystem'; -import { LocaleSeparators } from '../numbers'; +import AbacusWebsocket from './websocket'; class AbacusStateManager { private store: RootStore | undefined; + private currentMarket: string | undefined; stateManager: InstanceType; + websocket: AbacusWebsocket; + stateNotifier: AbacusStateNotifier; + analytics: AbacusAnalytics; + abacusFormatter: AbacusFormatter; + chainTransactions: AbacusChainTransaction; constructor() { @@ -75,7 +86,8 @@ class AbacusStateManager { this.analytics, new AbacusThreading(), new CoroutineTimer(), - new AbacusFileSystem() + new AbacusFileSystem(), + new AbacusLogger() ); const uiImplementations = new UIImplementations( @@ -84,7 +96,11 @@ class AbacusStateManager { this.abacusFormatter ); - const appConfigs = AbacusAppConfig.Companion.forWeb; + const appConfigs = new AbacusAppConfig( + false, // subscribeToCandles + true, // loadRemote + import.meta.env.MODE === 'development' && import.meta.env.VITE_ENABLE_ABACUS_LOGGING // enableLogger + ); appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2; this.stateManager = new AsyncAbacusStateManager( @@ -172,13 +188,38 @@ class AbacusStateManager { this.setTransferValue({ value: null, field: TransferInputField.usdcSize }); }; + clearTriggerOrdersInputValues = () => { + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.size }); + + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.stopLossOrderId }); + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.stopLossPrice }); + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.stopLossLimitPrice }); + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.stopLossPercentDiff }); + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.stopLossUsdcDiff }); + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.stopLossOrderType }); + + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.takeProfitOrderId }); + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.takeProfitPrice }); + this.setTriggerOrdersValue({ + value: null, + field: TriggerOrdersInputField.takeProfitLimitPrice, + }); + this.setTriggerOrdersValue({ + value: null, + field: TriggerOrdersInputField.takeProfitPercentDiff, + }); + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.takeProfitUsdcDiff }); + this.setTriggerOrdersValue({ value: null, field: TriggerOrdersInputField.takeProfitOrderType }); + }; + resetInputState = () => { this.clearTransferInputValues(); this.setTransferValue({ field: TransferInputField.type, value: null, }); - this.clearTradeInputValues(); + this.clearTriggerOrdersInputValues(); + this.clearTradeInputValues({ shouldResetSize: true }); }; // ------ Set Data ------ // @@ -188,10 +229,11 @@ class AbacusStateManager { this.chainTransactions.setStore(store); }; - setAccount = (localWallet?: LocalWallet) => { + setAccount = (localWallet?: LocalWallet, hdkey?: Hdkey) => { if (localWallet) { this.stateManager.accountAddress = localWallet.address; this.chainTransactions.setLocalWallet(localWallet); + if (hdkey) this.chainTransactions.setHdkey(hdkey); } }; @@ -214,6 +256,10 @@ class AbacusStateManager { this.stateManager.market = marketId; }; + setSelectedGasDenom = (denom: SelectedGasDenom) => { + this.chainTransactions.setSelectedGasDenom(denom); + }; + setTradeValue = ({ value, field }: { value: any; field: TradeInputFields }) => { this.stateManager.trade(value, field); }; @@ -222,6 +268,10 @@ class AbacusStateManager { this.stateManager.transfer(value, field); }; + setTriggerOrdersValue = ({ value, field }: { value: any; field: TriggerOrdersInputFields }) => { + this.stateManager.triggerOrders(value, field); + }; + setHistoricalPnlPeriod = ( period: (typeof HistoricalPnlPeriod)[keyof typeof HistoricalPnlPeriod] ) => { @@ -280,10 +330,23 @@ class AbacusStateManager { ) => void ) => this.stateManager.cancelOrder(orderId, callback); + triggerOrders = ( + callback: ( + success: boolean, + parsingError: Nullable, + data: Nullable + ) => void + ): Nullable => this.stateManager.commitTriggerOrders(callback); + cctpWithdraw = ( callback: (success: boolean, parsingError: Nullable, data: string) => void ): void => this.stateManager.commitCCTPWithdraw(callback); + triggerCompliance = ( + action: typeof ComplianceAction.VALID_SURVEY | typeof ComplianceAction.INVALID_SURVEY, + callback: (success: boolean, parsingError: Nullable, data: string) => void + ): void => this.stateManager.triggerCompliance(action, callback); + // ------ Utils ------ // getHistoricalPnlPeriod = (): Nullable => this.stateManager.historicalPnlPeriod; diff --git a/src/lib/abacus/localizer.ts b/src/lib/abacus/localizer.ts index 3c5c03bfb..a51bdc0f1 100644 --- a/src/lib/abacus/localizer.ts +++ b/src/lib/abacus/localizer.ts @@ -1,7 +1,7 @@ import type { AbacusLocalizerProtocol } from '@/constants/abacus'; class AbacusLocalizer implements Omit { - localize(path: string, paramsAsJson: string): string { + localize(path: string): string { return path; } } diff --git a/src/lib/abacus/logger.ts b/src/lib/abacus/logger.ts new file mode 100644 index 000000000..ee4053c75 --- /dev/null +++ b/src/lib/abacus/logger.ts @@ -0,0 +1,18 @@ +/* eslint-disable no-console */ +import type { AbacusLoggingProtocol } from '@/constants/abacus'; + +class AbacusLogger implements Omit { + d(tag: string, message: string) { + if (import.meta.env.VITE_ENABLE_ABACUS_LOGGING) { + console.log(`${tag}: ${message}`); + } + } + + e(tag: string, message: string) { + if (import.meta.env.VITE_ENABLE_ABACUS_LOGGING) { + console.error(`${tag}: ${message}`); + } + } +} + +export default AbacusLogger; diff --git a/src/lib/abacus/rest.ts b/src/lib/abacus/rest.ts index 12245f578..443c9bb06 100644 --- a/src/lib/abacus/rest.ts +++ b/src/lib/abacus/rest.ts @@ -1,11 +1,10 @@ import type { Nullable, kollections } from '@dydxprotocol/v4-abacus'; import type { AbacusRestProtocol } from '@/constants/abacus'; - -import { lastSuccessfulRestRequestByOrigin } from '@/hooks/useAnalytics'; +import { lastSuccessfulRestRequestByOrigin } from '@/constants/analytics'; type Headers = Nullable>; -type FetchResponseCallback = (p0: Nullable, p1: number) => void; +type FetchResponseCallback = (p0: Nullable, p1: number, p2: Nullable) => void; class AbacusRest implements AbacusRestProtocol { get(url: string, headers: Headers, callback: FetchResponseCallback): void { @@ -48,21 +47,29 @@ class AbacusRest implements AbacusRestProtocol { }; if (!url) { - callback(null, 0); + callback(null, 0, null); return; } fetch(url, options) .then(async (response) => { const data = await response.text(); + const headersObj: Record = {}; + response.headers.forEach((value, key) => { + headersObj[key] = value; + }); + // Stringify the headers object + const headersJson = JSON.stringify(headersObj); + + callback(data, response.status, headersJson); - callback(data, response.status); - try { lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now(); - } catch {} + } catch (error) { + // expected error when bad url + } }) - .catch(() => callback(null, 0)); // Network error or request couldn't be made + .catch(() => callback(null, 0, null)); // Network error or request couldn't be made } private mapToHeaders(map: Headers): HeadersInit { diff --git a/src/lib/abacus/stateNotification.ts b/src/lib/abacus/stateNotification.ts index b8b328cc0..2bf3230c7 100644 --- a/src/lib/abacus/stateNotification.ts +++ b/src/lib/abacus/stateNotification.ts @@ -1,10 +1,11 @@ import { kollections } from '@dydxprotocol/v4-abacus'; +import { fromPairs } from 'lodash'; import type { - AccountBalance, AbacusApiState, AbacusNotification, AbacusStateNotificationProtocol, + AccountBalance, Asset, Nullable, ParsingErrors, @@ -13,32 +14,32 @@ import type { PerpetualStateChanges, SubaccountOrder, } from '@/constants/abacus'; - import { Changes } from '@/constants/abacus'; import type { RootStore } from '@/state/_store'; - import { setBalances, - setStakingBalances, + setCompliance, setFills, setFundingPayments, setHistoricalPnl, setLatestOrder, setRestrictionType, + setStakingBalances, setSubaccount, + setTradingRewards, setTransfers, setWallet, - setTradingRewards, } from '@/state/account'; - import { setApiState } from '@/state/app'; import { setAssets } from '@/state/assets'; import { setConfigs } from '@/state/configs'; import { setInputs } from '@/state/inputs'; import { updateNotifications } from '@/state/notifications'; import { setHistoricalFundings, setLiveTrades, setMarkets, setOrderbook } from '@/state/perpetuals'; + import { isTruthy } from '../isTruthy'; +import { testFlags } from '../testFlags'; class AbacusStateNotifier implements AbacusStateNotificationProtocol { private store: RootStore | undefined; @@ -47,13 +48,10 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { this.store = undefined; } - environmentsChanged(): void { - return; - } + environmentsChanged(): void {} notificationsChanged(notifications: kollections.List): void { this.store?.dispatch(updateNotifications(notifications.toArray())); - return; } stateChanged( @@ -62,7 +60,7 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { ): void { if (!this.store) return; const { dispatch } = this.store; - const changes = new Set(incomingChanges?.changes.toArray() || []); + const changes = new Set(incomingChanges?.changes.toArray() ?? []); const marketIds = incomingChanges?.markets?.toArray(); const subaccountNumbers = incomingChanges?.subaccountNumbers?.toArray(); @@ -71,7 +69,7 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { dispatch( setAssets( Object.fromEntries( - (updatedState?.assetIds()?.toArray() || []).map((assetId: string) => { + (updatedState?.assetIds()?.toArray() ?? []).map((assetId: string) => { const assetData = updatedState?.asset(assetId); return [assetId, assetData]; }) @@ -82,17 +80,15 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { if (changes.has(Changes.accountBalances)) { if (updatedState.account?.balances) { - const balances: Record = {}; - for (const { k, v } of updatedState.account.balances.toArray()) { - balances[k] = v; - } + const balances: Record = fromPairs( + updatedState.account.balances.toArray().map(({ k, v }) => [k, v]) + ); dispatch(setBalances(balances)); } if (updatedState.account?.stakingBalances) { - const stakingBalances: Record = {}; - for (const { k, v } of updatedState.account.stakingBalances.toArray()) { - stakingBalances[k] = v; - } + const stakingBalances: Record = fromPairs( + updatedState.account.stakingBalances.toArray().map(({ k, v }) => [k, v]) + ); dispatch(setStakingBalances(stakingBalances)); } } @@ -119,7 +115,7 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { dispatch( setMarkets({ markets: Object.fromEntries( - (marketIds || updatedState.marketIds()?.toArray() || []) + (marketIds ?? updatedState.marketIds()?.toArray() ?? []) .map((marketId: string) => { const marketData = updatedState.market(marketId); return [marketId, marketData]; @@ -135,6 +131,14 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { dispatch(setRestrictionType(updatedState.restriction)); } + if ( + changes.has(Changes.compliance) && + updatedState.compliance && + testFlags.enableComplianceApi + ) { + dispatch(setCompliance(updatedState.compliance)); + } + subaccountNumbers?.forEach((subaccountId: number) => { if (subaccountId !== null) { if (changes.has(Changes.subaccount)) { @@ -142,24 +146,24 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { } if (changes.has(Changes.fills)) { - const fills = updatedState.subaccountFills(subaccountId)?.toArray() || []; + const fills = updatedState.subaccountFills(subaccountId)?.toArray() ?? []; dispatch(setFills(fills)); } if (changes.has(Changes.fundingPayments)) { const fundingPayments = - updatedState.subaccountFundingPayments(subaccountId)?.toArray() || []; + updatedState.subaccountFundingPayments(subaccountId)?.toArray() ?? []; dispatch(setFundingPayments(fundingPayments)); } if (changes.has(Changes.transfers)) { - const transfers = updatedState.subaccountTransfers(subaccountId)?.toArray() || []; + const transfers = updatedState.subaccountTransfers(subaccountId)?.toArray() ?? []; dispatch(setTransfers(transfers)); } if (changes.has(Changes.historicalPnl)) { const historicalPnl = - updatedState.subaccountHistoricalPnl(subaccountId)?.toArray() || []; + updatedState.subaccountHistoricalPnl(subaccountId)?.toArray() ?? []; dispatch(setHistoricalPnl(historicalPnl)); } } @@ -175,12 +179,12 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { } if (changes.has(Changes.trades)) { - const trades = updatedState.marketTrades(market)?.toArray() || []; + const trades = updatedState.marketTrades(market)?.toArray() ?? []; dispatch(setLiveTrades({ trades, marketId: market })); } if (changes.has(Changes.historicalFundings)) { - const historicalFundings = updatedState.historicalFunding(market)?.toArray() || []; + const historicalFundings = updatedState.historicalFunding(market)?.toArray() ?? []; dispatch( setHistoricalFundings({ @@ -198,6 +202,7 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { } errorsEmitted(errors: ParsingErrors) { + // eslint-disable-next-line no-console console.error('parse errors', errors.toArray()); } diff --git a/src/lib/abacus/websocket.ts b/src/lib/abacus/websocket.ts index 3712f14bb..f646b52b9 100644 --- a/src/lib/abacus/websocket.ts +++ b/src/lib/abacus/websocket.ts @@ -1,17 +1,9 @@ import type { AbacusWebsocketProtocol } from '@/constants/abacus'; +import { lastSuccessfulWebsocketRequestByOrigin } from '@/constants/analytics'; import type { TradingViewBar } from '@/constants/candles'; import { isDev } from '@/constants/networks'; -import { - PING_INTERVAL_MS, - PONG_TIMEOUT_MS, - OUTGOING_PING_MESSAGE, - PONG_MESSAGE_TYPE, -} from '@/constants/websocket'; - -import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics'; import { testFlags } from '@/lib/testFlags'; - import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache'; import { mapCandle } from '@/lib/tradingView/utils'; @@ -21,14 +13,21 @@ const RECONNECT_INTERVAL_MS = 10_000; class AbacusWebsocket implements Omit { private socket: WebSocket | null = null; + private url: string | null = null; + private connectedCallback: ((p0: boolean) => void) | null = null; + private receivedCallback: ((p0: string) => void) | null = null; - private pingPongTimer?: NodeJS.Timer; private disconnectTimer?: NodeJS.Timer; + + private reconnectTimer?: NodeJS.Timer; + private currentCandleId: string | undefined; + private isConnecting: boolean = false; + connect(url: string, connected: (p0: boolean) => void, received: (p0: string) => void): void { this.url = url; this.connectedCallback = connected; @@ -85,17 +84,17 @@ class AbacusWebsocket implements Omit { if (!this.url || !this.connectedCallback || !this.receivedCallback) return; + if ((this.socket && this.socket.readyState === WebSocket.OPEN) || this.isConnecting) { + return; + } + + this.isConnecting = true; + this.socket = new WebSocket(this.url); this.socket.onopen = () => { + this.isConnecting = false; if (this.socket?.readyState === WebSocket.OPEN) { - this.pingPongTimer = setInterval(() => { - if (this.socket && this.socket.readyState === WebSocket.OPEN) { - this.socket.send(OUTGOING_PING_MESSAGE); - } - }, PING_INTERVAL_MS); - this._setDisconnectTimeout(); - this._setReconnectInterval(); if (this.currentCandleId) { @@ -113,65 +112,60 @@ class AbacusWebsocket implements Omit + handler.callback(bar) + ); + } } - case 'v4_candles': { - shouldProcess = false; - const { id, contents } = parsedMessage; - - if (id && contents) { - const subscriptionItem = subscriptionsByChannelId.get(id); - const updatedCandle = contents[0]; - if (updatedCandle && subscriptionItem) { - const bar: TradingViewBar = mapCandle(updatedCandle); - subscriptionItem.lastBar = bar; + break; + } + case 'v4_markets': { + if (testFlags.displayInitializingMarkets) { + shouldProcess = false; + const { contents } = parsedMessage; - // send data to every subscriber of that symbol - Object.values(subscriptionItem.handlers).forEach((handler: any) => - handler.callback(bar) - ); + Object.keys(contents.markets ?? {}).forEach((market: any) => { + const status = contents.markets[market].status; + if (status === 'INITIALIZING') { + contents.markets[market].status = 'ONLINE'; } - } + }); - break; + this.receivedCallback?.(JSON.stringify(parsedMessage)); } - case 'v4_markets': { - if (testFlags.displayInitializingMarkets) { - shouldProcess = false; - const { contents } = parsedMessage; - - Object.keys(contents.markets ?? {}).forEach((market: any) => { - const status = contents.markets[market].status; - if (status === 'INITIALIZING') { - contents.markets[market].status = 'ONLINE'; - } - }); - - this.receivedCallback?.(JSON.stringify(parsedMessage)); - } - break; - } - default: { - break; - } + break; } - - if (shouldProcess && this.receivedCallback) { - this.receivedCallback(m.data); + default: { + break; } } + if (shouldProcess && this.receivedCallback) { + this.receivedCallback(m.data); + } + lastSuccessfulWebsocketRequestByOrigin[new URL(this.url!).origin] = Date.now(); } catch (error) { log('AbacusWebsocketProtocol/onmessage', error); @@ -179,14 +173,18 @@ class AbacusWebsocket implements Omit { + this.isConnecting = false; this.connectedCallback?.(false); if (!isDev) return; + // eslint-disable-next-line no-console console.warn('AbacusStateManager > WS > close > ', e); }; this.socket.onerror = (e) => { + this.isConnecting = false; this.connectedCallback?.(false); if (!isDev) return; + // eslint-disable-next-line no-console console.error('AbacusStateManager > WS > error > ', e); }; }; @@ -196,9 +194,6 @@ class AbacusWebsocket implements Omit { - setInterval(() => { + if (this.reconnectTimer !== null) clearInterval(this.reconnectTimer); + + this.reconnectTimer = setInterval(() => { if ( !this.socket || this.socket.readyState === WebSocket.CLOSED || @@ -217,12 +214,6 @@ class AbacusWebsocket implements Omit { - this.disconnectTimer = setTimeout(() => { - this._clearSocket(); - }, PONG_TIMEOUT_MS); - }; } export default AbacusWebsocket; diff --git a/src/lib/addressUtils.ts b/src/lib/addressUtils.ts index 1e2f954bc..e6c366c0b 100644 --- a/src/lib/addressUtils.ts +++ b/src/lib/addressUtils.ts @@ -1,4 +1,4 @@ -import { fromBech32, toBech32, fromHex, toHex } from '@cosmjs/encoding'; +import { fromBech32, fromHex, toBech32, toHex } from '@cosmjs/encoding'; // ============ Byte Helpers ============ export const stripHexPrefix = (input: string): string => { diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 7904c786a..0bc39fecf 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -1,10 +1,12 @@ import type { - AnalyticsUserProperty, - AnalyticsUserPropertyValue, AnalyticsEvent, AnalyticsEventData, + AnalyticsUserProperty, + AnalyticsUserPropertyValue, } from '@/constants/analytics'; +import { testFlags } from './testFlags'; + const DEBUG_ANALYTICS = false; export const identify = ( @@ -12,6 +14,7 @@ export const identify = ( propertyValue: AnalyticsUserPropertyValue ) => { if (DEBUG_ANALYTICS) { + // eslint-disable-next-line no-console console.log(`[Analytics:Identify] ${property}`, propertyValue); } const customEvent = new CustomEvent('dydx:identify', { @@ -25,11 +28,13 @@ export const track = ( eventType: T, eventData?: AnalyticsEventData ) => { + const eventDataWithReferrer = { ...(eventData ?? {}), referrer: testFlags.referrer }; if (DEBUG_ANALYTICS) { - console.log(`[Analytics] ${eventType}`, eventData); + // eslint-disable-next-line no-console + console.log(`[Analytics] ${eventType}`, eventDataWithReferrer); } const customEvent = new CustomEvent('dydx:track', { - detail: { eventType, eventData }, + detail: { eventType, eventData: eventDataWithReferrer }, }); globalThis.dispatchEvent(customEvent); diff --git a/src/lib/assertNever.ts b/src/lib/assertNever.ts new file mode 100644 index 000000000..552bd3b0d --- /dev/null +++ b/src/lib/assertNever.ts @@ -0,0 +1,7 @@ +export function assertNever(value: never, noThrow?: boolean): never { + if (noThrow) { + return value; + } + + throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`); +} diff --git a/src/lib/compliance.ts b/src/lib/compliance.ts new file mode 100644 index 000000000..618ce0e4f --- /dev/null +++ b/src/lib/compliance.ts @@ -0,0 +1,30 @@ +import { Secp256k1, sha256 } from '@cosmjs/crypto'; + +import { Hdkey } from '@/constants/account'; +import { BLOCKED_COUNTRIES, CountryCodes, OFAC_SANCTIONED_COUNTRIES } from '@/constants/geo'; + +export const signComplianceSignature = async ( + message: string, + action: string, + status: string, + hdkey: Hdkey +): Promise<{ signedMessage: string; timestamp: number }> => { + if (!hdkey?.privateKey || !hdkey?.publicKey) { + throw new Error('Missing hdkey'); + } + + const timestampInSeconds = Math.floor(Date.now() / 1000); + const messageToSign: string = `${message}:${action}"${status || ''}:${timestampInSeconds}`; + const messageHash = sha256(Buffer.from(messageToSign)); + + const signed = await Secp256k1.createSignature(messageHash, hdkey.privateKey); + const signedMessage = signed.toFixedLength(); + return { + signedMessage: Buffer.from(signedMessage).toString('base64'), + timestamp: timestampInSeconds, + }; +}; + +export const isBlockedGeo = (geo: string): boolean => { + return [...BLOCKED_COUNTRIES, ...OFAC_SANCTIONED_COUNTRIES].includes(geo as CountryCodes); +}; diff --git a/src/lib/csv.ts b/src/lib/csv.ts new file mode 100644 index 000000000..d7f88d53c --- /dev/null +++ b/src/lib/csv.ts @@ -0,0 +1,15 @@ +import { ConfigOptions, download, generateCsv, mkConfig } from 'export-to-csv'; + +export const exportCSV = (data: T[], options: ConfigOptions = {}) => { + const { filename = 'generated', showColumnHeaders = true, ...rest } = options; + const config = mkConfig({ showColumnHeaders, filename, ...rest }); + + const csv = generateCsv(config)( + data as { + [k: string]: unknown; + [k: number]: unknown; + }[] + ); + + download(config)(csv); +}; diff --git a/src/lib/dateTime.ts b/src/lib/dateTime.ts index 063aa1288..5c8005b09 100644 --- a/src/lib/dateTime.ts +++ b/src/lib/dateTime.ts @@ -1,4 +1,4 @@ -import { timeUnits, allTimeUnits } from '@/constants/time'; +import { allTimeUnits, timeUnits } from '@/constants/time'; // Given a literal from Intl.RelativeTimeFormat formatToParts, // strip out words/symbols unrelated to time unit @@ -44,7 +44,7 @@ export const formatRelativeTime = ( locale: string; relativeToTimestamp?: number; format?: 'long' | 'short' | 'narrow' | 'singleCharacter'; - largestUnit: keyof typeof timeUnits; + largestUnit?: keyof typeof timeUnits; resolution?: number; stripRelativeWords?: boolean; } @@ -54,8 +54,9 @@ export const formatRelativeTime = ( const unitParts = []; + // eslint-disable-next-line no-restricted-syntax for (const [unit, amount] of Object.entries(timeUnits).slice( - Object.keys(timeUnits).findIndex((unit) => unit === largestUnit) + Object.keys(timeUnits).findIndex((u) => u === largestUnit) )) if (Math.abs(elapsed) >= amount) { unitParts.push( @@ -72,6 +73,7 @@ export const formatRelativeTime = ( }).formatToParts(sign * Math.floor(elapsed / amount), unit as keyof typeof timeUnits) ); + // eslint-disable-next-line no-plusplus, no-param-reassign if (--resolution === 0) break; elapsed %= amount; @@ -112,17 +114,43 @@ export const formatAbsoluteTime = ( ) => new Intl.DateTimeFormat( locale, - ({ - millisecond: { hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 3, hour12: false }, - centisecond: { hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 3, hour12: false }, - decisecond: { hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 2, hour12: false }, - second: { hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 1, hour12: false }, - minute: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false }, - hour: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false }, - day: { hour: 'numeric', minute: 'numeric' }, - threeDays: { weekday: 'short', hour: 'numeric' }, - week: { weekday: 'short', hour: 'numeric' }, - month: { month: 'numeric', day: 'numeric', hour: 'numeric' }, - year: { year: 'numeric', month: 'numeric', day: 'numeric' }, - } as const)[resolutionUnit] + ( + { + millisecond: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + fractionalSecondDigits: 3, + hour12: false, + }, + centisecond: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + fractionalSecondDigits: 3, + hour12: false, + }, + decisecond: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + fractionalSecondDigits: 2, + hour12: false, + }, + second: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + fractionalSecondDigits: 1, + hour12: false, + }, + minute: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false }, + hour: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false }, + day: { hour: 'numeric', minute: 'numeric' }, + threeDays: { weekday: 'short', hour: 'numeric' }, + week: { weekday: 'short', hour: 'numeric' }, + month: { month: 'numeric', day: 'numeric', hour: 'numeric' }, + year: { year: 'numeric', month: 'numeric', day: 'numeric' }, + } as const + )[resolutionUnit] ).format(timestamp); diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 72bd68e5d..d991d8c28 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -3,6 +3,7 @@ */ export class StatefulOrderError extends Error { response: any; + code: number; constructor(message: any, response: any) { diff --git a/src/lib/formatString.ts b/src/lib/formatString.ts index a2ea72b8a..8eb86a04a 100644 --- a/src/lib/formatString.ts +++ b/src/lib/formatString.ts @@ -1,21 +1,27 @@ // implemntation based on https://github.com/stefalda/react-localization/blob/master/src/LocalizedStrings.js import React from 'react'; -const placeholderRegex = /(\{[\d|\w]+\})/; -const formatString = (str: string, params: { [key: string]: string | React.ReactNode } = {}): string | Array => { +const PLACEHOLDER_REGEX = /(\{[\d|\w]+\})/; + +export type StringGetterParams = Record; + +const formatString = ( + str: string, + params?: T +): string | Array => { let hasObject = false; const res = (str || '') - .split(placeholderRegex) + .split(PLACEHOLDER_REGEX) .filter((textPart) => !!textPart) .map((textPart, index) => { - if (textPart.match(placeholderRegex)) { + if (textPart.match(PLACEHOLDER_REGEX)) { const matchedKey = textPart.slice(1, -1); - let valueForPlaceholder = params[matchedKey]; + const valueForPlaceholder = params?.[matchedKey]; if (React.isValidElement(valueForPlaceholder)) { hasObject = true; return React.Children.toArray(valueForPlaceholder).map((component) => ({ - ...component, + ...(component as React.ReactElement), key: index.toString(), })); } diff --git a/src/lib/formatZeroNumbers.ts b/src/lib/formatZeroNumbers.ts new file mode 100644 index 000000000..a3df03247 --- /dev/null +++ b/src/lib/formatZeroNumbers.ts @@ -0,0 +1,60 @@ +const ZERO = '0'; + +const countZeros = (decimalDigits: string): number => { + let zeroCount = 0; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < decimalDigits.length && decimalDigits[i] === ZERO; i++) { + // eslint-disable-next-line no-plusplus + zeroCount++; + } + return zeroCount; +}; + +export const formatZeroNumbers = (formattedValue: string, zerosThreshold: number = 4) => { + const punctuationSymbol = formattedValue.match(/[.,]/g)?.pop(); + const hasCurrencySymbol = formattedValue.match(/^[^\d]/)?.pop() !== undefined; + const significantDigitsSubStart = hasCurrencySymbol ? 1 : 0; + const currencySign = hasCurrencySymbol ? formattedValue[0] : undefined; + + if (!punctuationSymbol) { + return { + currencySign, + significantDigits: formattedValue.substring(significantDigitsSubStart), + }; + } + + const punctIdx = formattedValue.lastIndexOf(punctuationSymbol); + if (!formattedValue.includes(ZERO, punctIdx + 1) || punctIdx === formattedValue.length - 1) { + return { + currencySign, + significantDigits: formattedValue.substring(significantDigitsSubStart, punctIdx), + punctuationSymbol, + zeros: 0, + decimalDigits: formattedValue.substring(punctIdx + 1), + }; + } + + const charsAfterPunct = formattedValue.slice(punctIdx + 1); + const zerosCount = countZeros(charsAfterPunct); + + if (zerosCount < zerosThreshold) { + return { + currencySign, + significantDigits: formattedValue.substring(significantDigitsSubStart, punctIdx), + punctuationSymbol, + zeros: 0, + decimalDigits: charsAfterPunct, + }; + } + + const otherDigits = charsAfterPunct.substring(zerosCount); + const canDisplayZeros = zerosCount !== 0 || otherDigits.length !== 0; + + return { + currencySign, + significantDigits: formattedValue.substring(significantDigitsSubStart, punctIdx), + zeros: canDisplayZeros ? zerosCount : 0, + decimalDigits: otherDigits, + punctuationSymbol, + }; +}; diff --git a/src/lib/genericFunctionalComponentUtils.ts b/src/lib/genericFunctionalComponentUtils.ts new file mode 100644 index 000000000..184ca0daf --- /dev/null +++ b/src/lib/genericFunctionalComponentUtils.ts @@ -0,0 +1,27 @@ +import { ForwardedRef, PropsWithoutRef, ReactNode, RefAttributes, forwardRef } from 'react'; + +// a functional-only version of forwardRef. This only accepts and returns a functional component essentially and thus +// Typescript can fully apply higher order function type inference +// this is only necessary when render function props has generic type arguments +export const forwardRefFn: ( + render: (props: P, ref: ForwardedRef) => ReactNode +) => (props: PropsWithoutRef

    & RefAttributes) => ReactNode = forwardRef; + +// if your input and output components are fully functional, use this to type the styled result +// only necessary when render function props has generic type arguments +/* +Usage: + +type NavItemStyleProps = { orientation: 'horizontal' | 'vertical' }; +const navItemTypeTemp = getSimpleStyledOutputType(NavItem, {} as NavItemStyleProps); + +const $NavItem = styled(NavItem)` + ...styles here +` as typeof navItemTypeTemp; +*/ + +// Note the output is a total lie, never use it at runtime, just for type inference +export const getSimpleStyledOutputType: ( + render: (props: P) => React.ReactNode, + style?: C +) => (props: P & C) => React.ReactNode = () => undefined as any; diff --git a/src/lib/isExternalLink.tsx b/src/lib/isExternalLink.tsx index 152cef90a..060006281 100644 --- a/src/lib/isExternalLink.tsx +++ b/src/lib/isExternalLink.tsx @@ -1,7 +1,10 @@ export const isExternalLink = (href: string | undefined) => { - if (href) + if (href) { try { return new URL(href).hostname !== globalThis.location.hostname; - } catch { } + } catch (error) { + return false; + } + } return false; }; diff --git a/src/lib/isTruthy.ts b/src/lib/isTruthy.ts index 3c622de6d..7eb742ffe 100644 --- a/src/lib/isTruthy.ts +++ b/src/lib/isTruthy.ts @@ -1,3 +1,2 @@ /** Boolean() with type narrowing */ -export const isTruthy = (n?: T | false | null | undefined | 0): n is T => - Boolean(n); +export const isTruthy = (n?: T | false | null | undefined | 0): n is T => Boolean(n); diff --git a/src/lib/localStorage.ts b/src/lib/localStorage.ts index e430d9022..e1aeebd35 100644 --- a/src/lib/localStorage.ts +++ b/src/lib/localStorage.ts @@ -3,7 +3,10 @@ import { LocalStorageKey } from '@/constants/localStorage'; import { log } from './telemetry'; export const setLocalStorage = ({ key, value }: { key: LocalStorageKey; value: Value }) => { - if (value === undefined) return removeLocalStorage({ key }); + if (value === undefined) { + removeLocalStorage({ key }); + return; + } const serializedValue = JSON.stringify(value); diff --git a/src/lib/math.ts b/src/lib/math.ts index 6a0a111b8..f94653041 100644 --- a/src/lib/math.ts +++ b/src/lib/math.ts @@ -2,4 +2,5 @@ export const clamp = (n: number, min: number, max: number) => Math.max(min, Math export const lerp = (percent: number, from: number, to: number) => from + percent * (to - from); -export const map = (n: number, start1: number, stop1: number, start2: number, stop2: number) => lerp((n - start1) / (stop1 - start1), start2, stop2); +export const map = (n: number, start1: number, stop1: number, start2: number, stop2: number) => + lerp((n - start1) / (stop1 - start1), start2, stop2); diff --git a/src/lib/network.ts b/src/lib/network.ts index 75585ea9a..91a10c698 100644 --- a/src/lib/network.ts +++ b/src/lib/network.ts @@ -1,4 +1,4 @@ -import { type DydxNetwork, ENVIRONMENT_CONFIG_MAP, type DydxChainId } from '@/constants/networks'; +import { ENVIRONMENT_CONFIG_MAP, type DydxNetwork } from '@/constants/networks'; export const validateAgainstAvailableEnvironments = (value: DydxNetwork) => - Object.keys(ENVIRONMENT_CONFIG_MAP).includes(value); \ No newline at end of file + Object.keys(ENVIRONMENT_CONFIG_MAP).includes(value); diff --git a/src/lib/numbers.ts b/src/lib/numbers.ts index 986a4c1f1..ec5cb8503 100644 --- a/src/lib/numbers.ts +++ b/src/lib/numbers.ts @@ -1,5 +1,7 @@ import { BigNumber } from 'bignumber.js'; +import { NumberSign } from '@/constants/numbers'; + export type BigNumberish = BigNumber | string | number; export type LocaleSeparators = { group?: string; decimal?: string }; @@ -8,7 +10,9 @@ export const BIG_NUMBERS = { ONE: new BigNumber(1), }; -export const MustBigNumber = (amount?: BigNumberish | null) => new BigNumber(amount || 0); +export const MustBigNumber = (amount?: BigNumberish | null): BigNumber => + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + new BigNumber(amount || 0); /** * @description Rounds the input to the nearest multiple of `factor`, which must be non-zero. @@ -75,8 +79,16 @@ export function bytesToBigInt(u: Uint8Array): bigint { if (u.length <= 1) { return BigInt(0); } + // eslint-disable-next-line no-bitwise const negated: boolean = (u[0] & 1) === 1; const hex: string = Buffer.from(u.slice(1)).toString('hex'); const abs: bigint = BigInt(`0x${hex}`); return negated ? -abs : abs; } + +export const getNumberSign = (n: any): NumberSign => + MustBigNumber(n).gt(0) + ? NumberSign.Positive + : MustBigNumber(n).lt(0) + ? NumberSign.Negative + : NumberSign.Neutral; diff --git a/src/lib/objectEntries.ts b/src/lib/objectEntries.ts index 3fd4d4387..b0e862fb1 100644 --- a/src/lib/objectEntries.ts +++ b/src/lib/objectEntries.ts @@ -1,2 +1,3 @@ /** Object.entries() with key types preserved */ -export const objectEntries = (t: T) => Object.entries(t) as { [K in keyof T]: [K, T[K]] }[keyof T][]; +export const objectEntries = (t: T) => + Object.entries(t) as { [K in keyof T]: [K, T[K]] }[keyof T][]; diff --git a/src/lib/orderbookHelpers.ts b/src/lib/orderbookHelpers.ts index 45b49de5c..a590b76f5 100644 --- a/src/lib/orderbookHelpers.ts +++ b/src/lib/orderbookHelpers.ts @@ -1,5 +1,4 @@ // ------ Canvas helper methods ------ // - import type { MarketOrderbook, Nullable, PerpetualMarketOrderbookLevel } from '@/constants/abacus'; /** diff --git a/src/lib/orders.ts b/src/lib/orders.ts index 55c0e467c..3ef6c5f03 100644 --- a/src/lib/orders.ts +++ b/src/lib/orders.ts @@ -1,70 +1,61 @@ import { DateTime } from 'luxon'; -import { STRING_KEYS } from '@/constants/localization'; - import { AbacusOrderStatus, AbacusOrderType, AbacusOrderTypes, + KotlinIrEnumValues, + TRADE_TYPES, type Asset, + type OrderStatus, + type PerpetualMarket, type SubaccountFill, type SubaccountFundingPayment, type SubaccountOrder, - type Nullable, - type OrderStatus, - type PerpetualMarket, } from '@/constants/abacus'; import { IconName } from '@/components/Icon'; import { convertAbacusOrderSide } from '@/lib/abacus/conversions'; -import { MustBigNumber } from '@/lib/numbers'; -export const getStatusIconInfo = ({ - status, - totalFilled, -}: { - status: OrderStatus; - totalFilled: Nullable; -}) => { +export const getOrderStatusInfo = ({ status }: { status: string }) => { switch (status) { - case AbacusOrderStatus.open: { - return MustBigNumber(totalFilled).gt(0) - ? { - statusIcon: IconName.OrderPartiallyFilled, - statusIconColor: `var(--color-warning)`, - statusStringKey: STRING_KEYS.PARTIALLY_FILLED, - } - : { - statusIcon: IconName.OrderOpen, - statusIconColor: `var(--color-text-2)`, - }; + case AbacusOrderStatus.open.rawValue: { + return { + statusIcon: IconName.OrderOpen, + statusIconColor: `var(--color-text-2)`, + }; } - case AbacusOrderStatus.filled: { + case AbacusOrderStatus.partiallyFilled.rawValue: + return { + statusIcon: IconName.OrderPartiallyFilled, + statusIconColor: `var(--color-warning)`, + }; + case AbacusOrderStatus.filled.rawValue: { return { statusIcon: IconName.OrderFilled, statusIconColor: `var(--color-success)`, }; } - case AbacusOrderStatus.cancelled: { + case AbacusOrderStatus.cancelled.rawValue: { return { statusIcon: IconName.OrderCanceled, statusIconColor: `var(--color-error)`, }; } - case AbacusOrderStatus.canceling: { + case AbacusOrderStatus.canceling.rawValue: { return { statusIcon: IconName.OrderPending, statusIconColor: `var(--color-error)`, }; } - case AbacusOrderStatus.untriggered: { + case AbacusOrderStatus.untriggered.rawValue: { return { statusIcon: IconName.OrderUntriggered, statusIconColor: `var(--color-text-2)`, }; } - case AbacusOrderStatus.pending: + case AbacusOrderStatus.pending.rawValue: default: { return { statusIcon: IconName.OrderPending, @@ -88,6 +79,26 @@ export const isMarketOrderType = (type?: AbacusOrderTypes) => AbacusOrderType.trailingStop, ].some(({ ordinal }) => ordinal === type.ordinal); +export const isLimitOrderType = (type?: AbacusOrderTypes) => + type && + [AbacusOrderType.limit, AbacusOrderType.stopLimit, AbacusOrderType.takeProfitLimit].some( + ({ ordinal }) => ordinal === type.ordinal + ); + +export const isStopLossOrder = (order: SubaccountOrder, isSlTpLimitOrdersEnabled: boolean) => { + const validOrderTypes = isSlTpLimitOrdersEnabled + ? [AbacusOrderType.stopLimit, AbacusOrderType.stopMarket] + : [AbacusOrderType.stopMarket]; + return validOrderTypes.some(({ ordinal }) => ordinal === order.type.ordinal) && order.reduceOnly; +}; + +export const isTakeProfitOrder = (order: SubaccountOrder, isSlTpLimitOrdersEnabled: boolean) => { + const validOrderTypes = isSlTpLimitOrdersEnabled + ? [AbacusOrderType.takeProfitLimit, AbacusOrderType.takeProfitMarket] + : [AbacusOrderType.takeProfitMarket]; + return validOrderTypes.some(({ ordinal }) => ordinal === order.type.ordinal) && order.reduceOnly; +}; + export const relativeTimeString = ({ timeInMs, selectedLocale, @@ -112,3 +123,6 @@ export const getHydratedTradingData = ({ tickSizeDecimals: perpetualMarkets?.[data.marketId]?.configs?.tickSizeDecimals, ...('side' in data && { orderSide: convertAbacusOrderSide(data.side) }), }); + +export const getTradeType = (orderType: string) => + TRADE_TYPES[orderType as KotlinIrEnumValues]; diff --git a/src/lib/positions.ts b/src/lib/positions.ts new file mode 100644 index 000000000..c877ea8ef --- /dev/null +++ b/src/lib/positions.ts @@ -0,0 +1,27 @@ +import { Nullable, SubaccountPosition, type Asset, type PerpetualMarket } from '@/constants/abacus'; +import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; + +type HydratedPositionData = SubaccountPosition & { + asset?: Asset; + stepSizeDecimals: number; + tickSizeDecimals: number; + oraclePrice: Nullable; +}; + +export const getHydratedPositionData = ({ + data, + assets, + perpetualMarkets, +}: { + data: SubaccountPosition; + assets?: Record; + perpetualMarkets?: Record; +}) => { + return { + ...data, + asset: assets?.[data.assetId], + stepSizeDecimals: perpetualMarkets?.[data.id]?.configs?.stepSizeDecimals ?? TOKEN_DECIMALS, + tickSizeDecimals: perpetualMarkets?.[data.id]?.configs?.tickSizeDecimals ?? USD_DECIMALS, + oraclePrice: perpetualMarkets?.[data.id]?.oraclePrice, + } as HydratedPositionData; +}; diff --git a/src/lib/renderSvgToDataUrl.ts b/src/lib/renderSvgToDataUrl.ts index 13ee32908..548d17267 100644 --- a/src/lib/renderSvgToDataUrl.ts +++ b/src/lib/renderSvgToDataUrl.ts @@ -12,21 +12,22 @@ const applyComputedStyles = (html: string) => { computedStyle.getPropertyPriority(key) ) ); - const html = node.outerHTML; + const newHtml = node.outerHTML; document.body.removeChild(node); - return html; + return newHtml; } + return undefined; }; const toDataUrl = (bytes: string, type = 'image/svg+xml') => new Promise((resolve, reject) => { Object.assign(new FileReader(), { - onload: (e) => resolve(e.target.result), - onerror: (e) => reject(e.target.error), + onload: (e: ProgressEvent) => resolve(e.target?.result ?? null), + onerror: (e: ProgressEvent) => reject(e.target?.error), }).readAsDataURL(new File([bytes], '', { type })); }); export const renderSvgToDataUrl = async (node: React.ReactElement) => { const { renderToString } = await import('react-dom/server'); - return await toDataUrl(applyComputedStyles(renderToString(node))!); + return toDataUrl(applyComputedStyles(renderToString(node))!); }; diff --git a/src/lib/squid.ts b/src/lib/squid.ts index 362d92254..87123b9f9 100644 --- a/src/lib/squid.ts +++ b/src/lib/squid.ts @@ -1,6 +1,7 @@ -import { isMainnet } from '@/constants/networks'; import { GetStatus, StatusResponse } from '@0xsquid/sdk'; +import { isMainnet } from '@/constants/networks'; + export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; export const STATUS_ERROR_GRACE_PERIOD = 300_000; @@ -19,7 +20,8 @@ const getSquidStatusUrl = (isV2: boolean) => { export const fetchSquidStatus = async ( params: GetStatus, isV2?: boolean, - integratorId?: string + integratorId?: string, + requestId?: string ): Promise => { const parsedParams: { [key: string]: string } = { transactionId: params.transactionId, @@ -31,7 +33,8 @@ export const fetchSquidStatus = async ( const response = await fetch(url, { headers: { - 'x-integrator-id': integratorId || 'dYdX-api', + 'x-integrator-id': integratorId ?? 'dYdX-api', + 'x-request-id': requestId ?? '', }, }); diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts index 339834b41..a7f810768 100644 --- a/src/lib/telemetry.ts +++ b/src/lib/telemetry.ts @@ -1,7 +1,8 @@ -import { isDev } from "@/constants/networks"; +import { isDev } from '@/constants/networks'; export const log = (location: string, error: Error, metadata?: any) => { if (isDev) { + // eslint-disable-next-line no-console console.warn('telemetry/log:', { location, error, metadata }); } diff --git a/src/lib/testFlags.ts b/src/lib/testFlags.ts index c9cd939f6..cc70e14fb 100644 --- a/src/lib/testFlags.ts +++ b/src/lib/testFlags.ts @@ -3,15 +3,24 @@ class TestFlags { constructor() { this.queryParams = {}; - const hash = window.location.hash; - const queryIndex = hash.indexOf('?'); - if (queryIndex === -1) return; - const queryParamsString = hash.substring(queryIndex + 1); - const params = new URLSearchParams(queryParamsString); + if (import.meta.env.VITE_ROUTER_TYPE === 'hash') { + const hash = window.location.hash; + const queryIndex = hash.indexOf('?'); + if (queryIndex === -1) return; - for (const [key, value] of params) { - this.queryParams[key.toLowerCase()] = value; + const queryParamsString = hash.substring(queryIndex + 1); + const params = new URLSearchParams(queryParamsString); + + params.forEach((value, key) => { + this.queryParams[key.toLowerCase()] = value; + }); + } else { + const params = new URLSearchParams(window.location.search); + + params.forEach((value, key) => { + this.queryParams[key.toLowerCase()] = value; + }); } } @@ -23,8 +32,20 @@ class TestFlags { return this.queryParams.address; } - get showTradingRewards() { - return !!this.queryParams.tradingrewards; + get isolatedMargin() { + return !!this.queryParams.isolatedmargin; + } + + get withNewMarketType() { + return !!this.queryParams.withnewmarkettype; + } + + get enableComplianceApi() { + return !!this.queryParams.complianceapi; + } + + get referrer() { + return this.queryParams.utm_source; } } diff --git a/src/lib/timeUtils.ts b/src/lib/timeUtils.ts index f676ed1bc..08088262f 100644 --- a/src/lib/timeUtils.ts +++ b/src/lib/timeUtils.ts @@ -60,14 +60,20 @@ export const getStringsForTimeInterval = (timeInterval: Duration) => { export const getTimeTillNextUnit = (unit: 'minute' | 'hour' | 'day') => { const now = new Date(); switch (unit) { - case 'minute': + case 'minute': { return 60 - now.getSeconds(); - case 'hour': + } + case 'hour': { return 3600 - (now.getMinutes() * 60 + now.getSeconds()); - case 'day': + } + case 'day': { const secondsTillNextHour = 3600 - (now.getMinutes() * 60 + now.getSeconds()); const hoursTillNextDay = 24 - (now.getHours() + 1); return secondsTillNextHour + hoursTillNextDay * 3600; + } + default: { + return undefined; + } } }; @@ -77,4 +83,4 @@ export const formatSeconds = (seconds: number) => { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${getTimeString(minutes)}:${getTimeString(remainingSeconds)}`; -} +}; diff --git a/src/lib/tradeData.ts b/src/lib/tradeData.ts index f8ba9704b..a733a5c1e 100644 --- a/src/lib/tradeData.ts +++ b/src/lib/tradeData.ts @@ -1,18 +1,16 @@ -import { type Location, matchPath } from 'react-router-dom'; import { OrderSide } from '@dydxprotocol/v4-client-js'; - -import { StringGetterFunction } from '@/constants/localization'; +import { matchPath, type Location } from 'react-router-dom'; import { - type Nullable, AbacusOrderSide, - type AbacusOrderSides, AbacusOrderTypes, - ValidationError, ErrorType, + ValidationError, + type AbacusOrderSides, + type Nullable, } from '@/constants/abacus'; - import { AlertType } from '@/constants/alerts'; +import type { StringGetterFunction } from '@/constants/localization'; import { PERCENT_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; import { TRADE_ROUTE } from '@/constants/routes'; import { PositionSide, TradeTypes } from '@/constants/trade'; @@ -87,7 +85,7 @@ const formatErrorParam = ({ return `$${dollarBN.toFixed(tickSizeDecimals ?? USD_DECIMALS)}`; } default: { - return value || ''; + return value ?? ''; } } }; @@ -106,10 +104,10 @@ export const getTradeInputAlert = ({ stepSizeDecimals: Nullable; tickSizeDecimals: Nullable; }) => { - const inputAlerts = abacusInputErrors.map(({ action: errorAction, resources, type }) => { + const inputAlerts = abacusInputErrors.map(({ action: errorAction, resources, type, code }) => { const { action, text } = resources || {}; - const { stringKey: actionStringKey } = action || {}; - const { stringKey: alertStringKey, params: stringParams } = text || {}; + const { stringKey: actionStringKey } = action ?? {}; + const { stringKey: alertStringKey, params: stringParams } = text ?? {}; const params = stringParams?.toArray() && @@ -128,8 +126,21 @@ export const getTradeInputAlert = ({ alertStringKey, alertString: alertStringKey && stringGetter({ key: alertStringKey, params }), type: type === ErrorType.warning ? AlertType.Warning : AlertType.Error, + code, }; }); return inputAlerts?.[0]; }; + +export const calculatePositionMargin = ({ + notionalTotal, + adjustedMmf, +}: { + notionalTotal?: Nullable; + adjustedMmf?: Nullable; +}) => { + const notionalTotalBN = MustBigNumber(notionalTotal); + const adjustedMmfBN = MustBigNumber(adjustedMmf); + return notionalTotalBN.times(adjustedMmfBN); +}; diff --git a/src/lib/tradingView/dydxfeed/index.ts b/src/lib/tradingView/dydxfeed/index.ts index 9e529a67d..d4a4cb981 100644 --- a/src/lib/tradingView/dydxfeed/index.ts +++ b/src/lib/tradingView/dydxfeed/index.ts @@ -1,5 +1,4 @@ import { DateTime } from 'luxon'; - import type { DatafeedConfiguration, ErrorCallback, @@ -13,21 +12,18 @@ import type { } from 'public/tradingview/charting_library'; import { Candle, RESOLUTION_MAP } from '@/constants/candles'; -import { useDydxClient } from '@/hooks'; +import { DEFAULT_MARKETID } from '@/constants/markets'; + +import { useDydxClient } from '@/hooks/useDydxClient'; import { RootStore } from '@/state/_store'; import { setCandles } from '@/state/perpetuals'; +import { getMarketConfig, getPerpetualBarsForPriceChart } from '@/state/perpetualsSelectors'; -import { - getMarketConfig, - getMarketIds, - getPerpetualBarsForPriceChart, -} from '@/state/perpetualsSelectors'; - +import { log } from '../../telemetry'; +import { getHistorySlice, getSymbol, mapCandle } from '../utils'; import { lastBarsCache } from './cache'; import { subscribeOnStream, unsubscribeFromStream } from './streaming'; -import { getAllSymbols, getHistorySlice, mapCandle } from '../utils'; -import { log } from '../../telemetry'; const timezone = DateTime.local().get('zoneName') as unknown as Timezone; @@ -50,10 +46,11 @@ const configurationData: DatafeedConfiguration = { export const getDydxDatafeed = ( store: RootStore, - getCandlesForDatafeed: ReturnType['getCandlesForDatafeed'] + getCandlesForDatafeed: ReturnType['getCandlesForDatafeed'], + initialPriceScale: number | null ) => ({ onReady: (callback: OnReadyCallback) => { - setTimeout(() => callback(configurationData)); + setTimeout(() => callback(configurationData), 0); }, searchSymbols: ( @@ -65,23 +62,11 @@ export const getDydxDatafeed = ( onResultReadyCallback([]); }, - resolveSymbol: async ( - symbolName: string, - onSymbolResolvedCallback: ResolveCallback, - onResolveErrorCallback: ErrorCallback - ) => { - const marketIds = getMarketIds(store.getState()); - const symbols = getAllSymbols(marketIds); - const symbolItem = symbols.find(({ symbol }: any) => symbol === symbolName); - - if (!symbolItem) { - onResolveErrorCallback('cannot resolve symbol'); - return; - } - - const { tickSizeDecimals } = getMarketConfig(symbolItem.symbol)(store.getState()) || {}; + resolveSymbol: async (symbolName: string, onSymbolResolvedCallback: ResolveCallback) => { + const symbolItem = getSymbol(symbolName || DEFAULT_MARKETID); + const { tickSizeDecimals } = getMarketConfig(symbolItem.symbol)(store.getState()) ?? {}; - const pricescale = tickSizeDecimals ? 10 ** tickSizeDecimals : 100; + const pricescale = tickSizeDecimals ? 10 ** tickSizeDecimals : initialPriceScale ?? 100; const symbolInfo: LibrarySymbolInfo = { ticker: symbolItem.full_name, @@ -163,7 +148,7 @@ export const getDydxDatafeed = ( ); } - const bars = [...cachedBars, ...(fetchedCandles?.map(mapCandle) || [])].reverse(); + const bars = [...cachedBars, ...(fetchedCandles?.map(mapCandle) ?? [])].reverse(); if (bars.length === 0) { onHistoryCallback([], { diff --git a/src/lib/tradingView/dydxfeed/streaming.ts b/src/lib/tradingView/dydxfeed/streaming.ts index e4a54d60c..45d08d935 100644 --- a/src/lib/tradingView/dydxfeed/streaming.ts +++ b/src/lib/tradingView/dydxfeed/streaming.ts @@ -6,6 +6,7 @@ import type { } from 'public/tradingview/charting_library'; import { RESOLUTION_MAP } from '@/constants/candles'; + import abacusStateManager from '@/lib/abacus'; import { subscriptionsByChannelId } from './cache'; @@ -15,7 +16,6 @@ export const subscribeOnStream = ({ resolution, onRealtimeCallback, subscribeUID, - onResetCacheNeededCallback, lastBar, }: { symbolInfo: LibrarySymbolInfo; @@ -57,14 +57,14 @@ export const subscribeOnStream = ({ export const unsubscribeFromStream = (subscriberUID: string) => { // find a subscription with id === subscriberUID + // eslint-disable-next-line no-restricted-syntax for (const channelId of subscriptionsByChannelId.keys()) { const subscriptionItem = subscriptionsByChannelId.get(channelId); - const { handlers } = subscriptionItem || {}; + const { handlers } = subscriptionItem ?? {}; if (subscriptionItem && handlers?.[subscriberUID]) { // remove from handlers delete subscriptionItem.handlers[subscriberUID]; - const id = channelId; if (Object.keys(subscriptionItem.handlers).length === 0) { // unsubscribe from the channel, if it was the last handler diff --git a/src/lib/tradingView/utils.ts b/src/lib/tradingView/utils.ts index 4359731c7..be34c3e4c 100644 --- a/src/lib/tradingView/utils.ts +++ b/src/lib/tradingView/utils.ts @@ -4,10 +4,10 @@ import { Candle, TradingViewBar, TradingViewSymbol } from '@/constants/candles'; import { THEME_NAMES } from '@/constants/styles/colors'; import type { ChartLineType } from '@/constants/tvchart'; -import { type AppColorMode, AppTheme } from '@/state/configs'; - import { Themes } from '@/styles/themes'; +import { AppTheme, type AppColorMode } from '@/state/configs'; + export const mapCandle = ({ startedAt, open, @@ -24,14 +24,13 @@ export const mapCandle = ({ volume: Math.ceil(Number(baseTokenVolume)), }); -export const getAllSymbols = (marketIds: string[]): TradingViewSymbol[] => - marketIds.map((marketId) => ({ - description: marketId, - exchange: 'dYdX', - full_name: marketId, - symbol: marketId, - type: 'crypto', - })); +export const getSymbol = (marketId: string): TradingViewSymbol => ({ + description: marketId, + exchange: 'dYdX', + full_name: marketId, + symbol: marketId, + type: 'crypto', +}); export const getHistorySlice = ({ bars, @@ -64,7 +63,8 @@ export const getChartLineColors = ({ const orderColors = { [OrderSide.BUY]: theme.positive, [OrderSide.SELL]: theme.negative, - ['position']: null, + entry: null, + liquidation: theme.warning, }; return { diff --git a/src/lib/typeUtils.ts b/src/lib/typeUtils.ts new file mode 100644 index 000000000..79ae49647 --- /dev/null +++ b/src/lib/typeUtils.ts @@ -0,0 +1,12 @@ +import { EMPTY_OBJ } from '@/constants/objects'; + +// preserves reference and empty object never churns +export function orEmptyObj( + obj: T | null | undefined +): T extends Record ? Record : Partial { + return obj ?? (EMPTY_OBJ as any); +} + +export function isPresent(value: T | undefined | null): value is T { + return value != null; +} diff --git a/src/lib/wagmi.ts b/src/lib/wagmi.ts index 70ad3b1ed..e8139c098 100644 --- a/src/lib/wagmi.ts +++ b/src/lib/wagmi.ts @@ -1,55 +1,59 @@ -import { createConfig, configureChains, mainnet, Chain } from 'wagmi'; -import { goerli } from 'wagmi/chains'; - +// Custom connectors +import type { ExternalProvider } from '@ethersproject/providers'; +import type { PrivyClientConfig } from '@privy-io/react-auth'; import { arbitrum, arbitrumGoerli, avalanche, avalancheFuji, - bsc, - bscTestnet, - optimism, - optimismGoerli, base, baseGoerli, - polygon, - polygonMumbai, + bsc, + bscTestnet, + celo, + celoAlfajores, + fantom, + fantomTestnet, + filecoin, + filecoinHyperspace, + kava, linea, lineaTestnet, mantle, mantleTestnet, - moonbeam, moonbaseAlpha, - filecoin, - filecoinHyperspace, - fantom, - fantomTestnet, - celo, - celoAlfajores, + moonbeam, + optimism, + optimismGoerli, + polygon, + polygonMumbai, scroll, - kava, sepolia, } from 'viem/chains'; - -import { alchemyProvider } from 'wagmi/providers/alchemy'; -import { jsonRpcProvider } from 'wagmi/providers/jsonRpc'; -import { publicProvider } from 'wagmi/providers/public'; - +import { Chain, configureChains, createConfig, mainnet } from 'wagmi'; +import { goerli } from 'wagmi/chains'; import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet'; import { InjectedConnector } from 'wagmi/connectors/injected'; import { MetaMaskConnector } from 'wagmi/connectors/metaMask'; import { WalletConnectConnector } from 'wagmi/connectors/walletConnect'; +import { alchemyProvider } from 'wagmi/providers/alchemy'; +import { jsonRpcProvider } from 'wagmi/providers/jsonRpc'; +import { publicProvider } from 'wagmi/providers/public'; +import { LocalStorageKey } from '@/constants/localStorage'; +import { DEFAULT_APP_ENVIRONMENT, ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; import { - type WalletConnection, + WALLET_CONNECT_EXPLORER_RECOMMENDED_IDS, WalletConnectionType, - type WalletType, walletConnectionTypes, wallets, - WALLET_CONNECT_EXPLORER_RECOMMENDED_IDS, + type WalletConnection, + type WalletType, } from '@/constants/wallets'; import { isTruthy } from './isTruthy'; +import { getLocalStorage } from './localStorage'; +import { validateAgainstAvailableEnvironments } from './network'; // Config @@ -85,7 +89,26 @@ export const WAGMI_SUPPORTED_CHAINS: Chain[] = [ kava, ]; -const { chains, publicClient, webSocketPublicClient } = configureChains( +const defaultSelectedNetwork = getLocalStorage({ + key: LocalStorageKey.SelectedNetwork, + defaultValue: DEFAULT_APP_ENVIRONMENT, + validateFn: validateAgainstAvailableEnvironments, +}); +const defaultChainId = Number(ENVIRONMENT_CONFIG_MAP[defaultSelectedNetwork].ethereumChainId); + +export const privyConfig: PrivyClientConfig = { + embeddedWallets: { + createOnLogin: 'users-without-wallets', + requireUserPasswordOnCreate: false, + noPromptOnSignature: true, + }, + appearance: { + theme: '#28283c', + }, + defaultChain: defaultChainId === mainnet.id ? mainnet : sepolia, +}; + +export const configureChainsConfig = configureChains( WAGMI_SUPPORTED_CHAINS, [ import.meta.env.VITE_ALCHEMY_API_KEY && @@ -96,6 +119,7 @@ const { chains, publicClient, webSocketPublicClient } = configureChains( publicProvider(), ].filter(isTruthy) ); +const { chains, publicClient, webSocketPublicClient } = configureChainsConfig; const injectedConnectorOptions = { chains, @@ -133,6 +157,8 @@ const getWalletconnect2ConnectorOptions = ( qrModalOptions: { themeMode: 'dark' as const, themeVariables: { + // TODO: figure out why --wcm-accent-color isn't considered a known property + // @ts-ignore '--wcm-accent-color': '#5973fe', '--wcm-font-family': 'var(--fontFamily-base)', }, @@ -166,10 +192,6 @@ export const config = createConfig({ webSocketPublicClient, }); -// Custom connectors - -import type { ExternalProvider } from '@ethersproject/providers'; - // Create a custom wagmi InjectedConnector using a specific injected EIP-1193 provider (instead of wagmi's default detection logic) const createInjectedConnectorWithProvider = (provider: ExternalProvider) => new (class extends InjectedConnector { diff --git a/src/lib/wallet/index.ts b/src/lib/wallet/index.ts index 7ce29c9ac..100ae2126 100644 --- a/src/lib/wallet/index.ts +++ b/src/lib/wallet/index.ts @@ -1,11 +1,10 @@ import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; - import { - type WalletConnection, - wallets, WalletConnectionType, WalletErrorType, WalletType, + wallets, + type WalletConnection, } from '@/constants/wallets'; import { detectInjectedEip1193Providers } from './providers'; @@ -27,9 +26,11 @@ export const getWalletConnection = ({ }): WalletConnection | undefined => { const walletConfig = wallets[walletType]; + // eslint-disable-next-line no-restricted-syntax for (const connectionType of walletConfig.connectionTypes) { switch (connectionType) { case WalletConnectionType.InjectedEip1193: { + // eslint-disable-next-line no-restricted-syntax for (const provider of detectInjectedEip1193Providers()) { if (walletConfig.matchesInjectedEip1193?.(provider)) { /* @ts-ignore */ @@ -41,35 +42,39 @@ export const getWalletConnection = ({ }; } } - break; } - case WalletConnectionType.WalletConnect2: { return { type: WalletConnectionType.WalletConnect2, }; } - case WalletConnectionType.CoinbaseWalletSdk: { return { type: WalletConnectionType.CoinbaseWalletSdk, }; } - case WalletConnectionType.CosmosSigner: { return { type: WalletConnectionType.CosmosSigner, }; } - case WalletConnectionType.TestWallet: { return { type: WalletConnectionType.TestWallet, }; } + case WalletConnectionType.Privy: { + return { + type: WalletConnectionType.Privy, + }; + } + default: { + continue; + } } } + return undefined; }; export const getWalletErrorType = ({ error }: { error: Error }) => { diff --git a/src/lib/wallet/providers.ts b/src/lib/wallet/providers.ts index df9748f82..d059e3912 100644 --- a/src/lib/wallet/providers.ts +++ b/src/lib/wallet/providers.ts @@ -1,11 +1,11 @@ import type { ExternalProvider } from '@ethersproject/providers'; import { - type InjectedEthereumProvider, type InjectedCoinbaseWalletExtensionProvider, + type InjectedEthereumProvider, type WithInjectedEthereumProvider, - type WithInjectedWeb3Provider, type WithInjectedOkxWalletProvider, + type WithInjectedWeb3Provider, } from '@/constants/wallets'; import { isTruthy } from '../isTruthy'; diff --git a/src/main.tsx b/src/main.tsx index c73a8ac16..96238f214 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,24 +1,26 @@ import './polyfills'; + import { StrictMode } from 'react'; + import ReactDOM from 'react-dom/client'; -import { BrowserRouter, HashRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; +import { BrowserRouter, HashRouter } from 'react-router-dom'; import { store } from '@/state/_store'; +import App from './App'; import { ErrorBoundary } from './components/ErrorBoundary'; - import './index.css'; -import App from './App'; - const Router = import.meta.env.VITE_ROUTER_TYPE === 'hash' ? HashRouter : BrowserRouter; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - } /> + + + diff --git a/src/pages/PrivacyPolicyPage.tsx b/src/pages/PrivacyPolicyPage.tsx index 226ddb587..7816c4229 100644 --- a/src/pages/PrivacyPolicyPage.tsx +++ b/src/pages/PrivacyPolicyPage.tsx @@ -1,19 +1,16 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { articleMixins } from '@/styles/articleMixins'; const PrivacyPolicyPage = () => ( - + <$Article>

    Privacy Policy

    - + ); export default PrivacyPolicyPage; - -const Styled: Record = {}; - -Styled.Article = styled.article` +const $Article = styled.article` ${articleMixins.article} `; diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 0d5253ce2..9896f2ccc 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,26 +1,31 @@ -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { Link } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { Link, useNavigate } from 'react-router-dom'; +import styled, { css } from 'styled-components'; import { useEnsName } from 'wagmi'; -import { useNavigate } from 'react-router-dom'; import { TransferType } from '@/constants/abacus'; import { OnboardingState } from '@/constants/account'; import { ButtonSize } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; -import { AppRoute, PortfolioRoute, HistoryRoute } from '@/constants/routes'; +import { AppRoute, HistoryRoute, PortfolioRoute } from '@/constants/routes'; import { wallets } from '@/constants/wallets'; -import { useAccounts, useStringGetter, useTokenConfigs } from '@/hooks'; + +import { useAccounts } from '@/hooks/useAccounts'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; + import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; +import { AssetIcon } from '@/components/AssetIcon'; import { Details } from '@/components/Details'; -import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; import { Icon, IconName } from '@/components/Icon'; import { IconButton, type IconButtonProps } from '@/components/IconButton'; +import { Output, OutputType } from '@/components/Output'; import { Panel } from '@/components/Panel'; import { Toolbar } from '@/components/Toolbar'; +import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; import { getHistoricalTradingRewardsForCurrentWeek, @@ -32,14 +37,22 @@ import { isTruthy } from '@/lib/isTruthy'; import { truncateAddress } from '@/lib/wallet'; import { DYDXBalancePanel } from './token/rewards/DYDXBalancePanel'; -import { MigratePanel } from './token/rewards/MigratePanel'; import { GovernancePanel } from './token/rewards/GovernancePanel'; +import { MigratePanel } from './token/rewards/MigratePanel'; +import { NewMarketsPanel } from './token/rewards/NewMarketsPanel'; import { StakingPanel } from './token/staking/StakingPanel'; import { StrideStakingPanel } from './token/staking/StrideStakingPanel'; -import { NewMarketsPanel } from './token/rewards/NewMarketsPanel'; const ENS_CHAIN_ID = 1; // Ethereum +type Action = { + key: string; + label: string; + icon: IconButtonProps; + href?: string; + onClick?: () => void; +}; + const Profile = () => { const stringGetter = useStringGetter(); const dispatch = useDispatch(); @@ -58,7 +71,7 @@ const Profile = () => { const currentWeekTradingReward = useSelector(getHistoricalTradingRewardsForCurrentWeek); - const actions = [ + const actions: Action[] = [ { key: 'deposit', label: stringGetter({ key: STRING_KEYS.DEPOSIT }), @@ -121,38 +134,30 @@ const Profile = () => { dispatch(openDialog({ type: DialogTypes.Onboarding })); }, }, - ].filter(isTruthy) as { - key: string; - label: string; - icon: IconButtonProps; - href?: string; - onClick?: () => void; - }[]; + ].filter(isTruthy); return ( - - - + <$MobileProfileLayout> + <$Header> + <$ProfileIcon />
    - - {isConnected ? ensName || truncateAddress(dydxAddress) : '-'} - + <$Address>{isConnected ? ensName ?? truncateAddress(dydxAddress) : '-'} {isConnected && walletType ? ( - - + <$SubHeader> + <$ConnectedIcon /> {stringGetter({ key: STRING_KEYS.CONNECTED_TO })} {stringGetter({ key: wallets[walletType].stringKey })} - + ) : ( - )}
    -
    - + + <$Actions withSeparators={false}> {actions.map(({ key, label, href, icon, onClick }) => { const action = ( <> - + <$ActionButton {...icon} size={ButtonSize.Large} onClick={onClick} /> {label} ); @@ -161,56 +166,63 @@ const Profile = () => { {action} ) : ( + // eslint-disable-next-line jsx-a11y/label-has-associated-control ); })} - + - + <$InlineRow> {stringGetter({ key: STRING_KEYS.SETTINGS })} - + } onClick={() => navigate(AppRoute.Settings)} /> - + <$InlineRow> {stringGetter({ key: STRING_KEYS.HELP })} - + } onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))} /> - + <$MigratePanel /> - + <$DYDXBalancePanel /> - - } + type={OutputType.Asset} + value={currentWeekTradingReward?.amount} + /> + ), }, ]} layout="grid" /> - - + <$FeesPanel slotHeaderContent={stringGetter({ key: STRING_KEYS.FEES })} href={`${AppRoute.Portfolio}/${PortfolioRoute.Fees}`} hasSeparator > - { ]} layout="grid" /> - + - { ]} withInnerBorders={false} /> - + - - - - -
    + <$GovernancePanel /> + <$NewMarketsPanel /> + <$StakingPanel /> + <$StrideStakingPanel /> + ); }; export default Profile; - -const Styled: Record = {}; - -Styled.MobileProfileLayout = styled.div` +const $MobileProfileLayout = styled.div` ${layoutMixins.contentContainerPage} display: grid; @@ -285,13 +294,13 @@ Styled.MobileProfileLayout = styled.div` } `; -Styled.Header = styled.header` +const $Header = styled.header` grid-area: header; ${layoutMixins.row} padding: 0 1rem; `; -Styled.ProfileIcon = styled.div` +const $ProfileIcon = styled.div` width: 4rem; height: 4rem; margin-right: 1rem; @@ -304,7 +313,7 @@ Styled.ProfileIcon = styled.div` ); `; -Styled.SubHeader = styled.div` +const $SubHeader = styled.div` ${layoutMixins.row} gap: 0.25rem; @@ -317,7 +326,7 @@ Styled.SubHeader = styled.div` } `; -Styled.ConnectedIcon = styled.div` +const $ConnectedIcon = styled.div` height: 0.5rem; width: 0.5rem; margin-right: 0.25rem; @@ -327,11 +336,11 @@ Styled.ConnectedIcon = styled.div` box-shadow: 0 0 0 0.2rem var(--color-gradient-success); `; -Styled.Address = styled.h1` +const $Address = styled.h1` font: var(--font-extra-medium); `; -Styled.Actions = styled(Toolbar)` +const $Actions = styled(Toolbar)` ${layoutMixins.spacedRow} --stickyArea-topHeight: 5rem; grid-area: actions; @@ -348,7 +357,7 @@ Styled.Actions = styled(Toolbar)` } `; -Styled.ActionButton = styled(IconButton)<{ iconName?: IconName }>` +const $ActionButton = styled(IconButton)<{ iconName?: IconName }>` margin-bottom: 0.5rem; ${({ iconName }) => @@ -365,12 +374,12 @@ Styled.ActionButton = styled(IconButton)<{ iconName?: IconName }>` `} `; -Styled.Details = styled(Details)` +const $Details = styled(Details)` font: var(--font-small-book); --details-value-font: var(--font-medium-book); `; -Styled.RewardsPanel = styled(Panel)` +const $RewardsPanel = styled(Panel)` grid-area: rewards; align-self: flex-start; height: 100%; @@ -383,11 +392,11 @@ Styled.RewardsPanel = styled(Panel)` } `; -Styled.FeesPanel = styled(Panel)` +const $FeesPanel = styled(Panel)` grid-area: fees; `; -Styled.HistoryPanel = styled(Panel)` +const $HistoryPanel = styled(Panel)` grid-area: history; --panel-content-paddingY: 0; --panel-content-paddingX: 0; @@ -418,45 +427,49 @@ Styled.HistoryPanel = styled(Panel)` } `; -Styled.InlineRow = styled.div` +const $InlineRow = styled.div` ${layoutMixins.inlineRow} padding: 1rem; gap: 0.5rem; `; -Styled.PanelButton = styled(Panel)` +const $PanelButton = styled(Panel)` --panel-paddingY: 0 --panel-paddingX:0; `; -Styled.SettingsButton = styled(Styled.PanelButton)` +const $SettingsButton = styled($PanelButton)` grid-area: settings; `; -Styled.HelpButton = styled(Styled.PanelButton)` +const $HelpButton = styled($PanelButton)` grid-area: help; `; -Styled.MigratePanel = styled(MigratePanel)` +const $MigratePanel = styled(MigratePanel)` grid-area: migrate; `; -Styled.DYDXBalancePanel = styled(DYDXBalancePanel)` +const $DYDXBalancePanel = styled(DYDXBalancePanel)` grid-area: balance; `; -Styled.GovernancePanel = styled(GovernancePanel)` +const $GovernancePanel = styled(GovernancePanel)` grid-area: governance; `; -Styled.StakingPanel = styled(StakingPanel)` +const $StakingPanel = styled(StakingPanel)` grid-area: keplr; `; -Styled.NewMarketsPanel = styled(NewMarketsPanel)` +const $NewMarketsPanel = styled(NewMarketsPanel)` grid-area: newMarkets; `; -Styled.StrideStakingPanel = styled(StrideStakingPanel)` +const $StrideStakingPanel = styled(StrideStakingPanel)` grid-area: stride; `; + +const $AssetIcon = styled(AssetIcon)` + margin-left: 0.5ch; +`; diff --git a/src/pages/TermsOfUsePage.tsx b/src/pages/TermsOfUsePage.tsx index 63f431cb9..d3c0f92b9 100644 --- a/src/pages/TermsOfUsePage.tsx +++ b/src/pages/TermsOfUsePage.tsx @@ -1,12 +1,13 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +/* eslint-disable no-irregular-whitespace */ +import styled from 'styled-components'; import { articleMixins } from '@/styles/articleMixins'; const TermsOfUsePage = () => ( - + <$Article>

    Sample Terms of Use

    - +

    @@ -1073,13 +1074,10 @@ const TermsOfUsePage = () => ( 19. CONTACT INFORMATION

    -
    + ); export default TermsOfUsePage; - -const Styled: Record = {}; - -Styled.Article = styled.article` +const $Article = styled.article` ${articleMixins.article} `; diff --git a/src/pages/markets/Markets.tsx b/src/pages/markets/Markets.tsx index 7644f8b12..386205569 100644 --- a/src/pages/markets/Markets.tsx +++ b/src/pages/markets/Markets.tsx @@ -1,88 +1,120 @@ -import styled, { AnyStyledComponent } from 'styled-components'; -import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; -import { breakpoints } from '@/styles'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import { ButtonAction } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { AppRoute, MarketsRoute } from '@/constants/routes'; -import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks'; + +import { useDocumentTitle } from '@/hooks/useDocumentTitle'; import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; import { ContentSectionHeader } from '@/components/ContentSectionHeader'; -import { ExchangeBillboards } from '@/views/ExchangeBillboards'; +import { Switch } from '@/components/Switch'; +import { MarketsStats } from '@/views/MarketsStats'; import { MarketsTable } from '@/views/tables/MarketsTable'; const Markets = () => { const stringGetter = useStringGetter(); - const { isNotTablet } = useBreakpoints(); const navigate = useNavigate(); + const [showHighlights, setShowHighlights] = useState(true); const { hasPotentialMarketsData } = usePotentialMarkets(); useDocumentTitle(stringGetter({ key: STRING_KEYS.MARKETS })); return ( - - - + <$HeaderSection> + <$ContentSectionHeader title={stringGetter({ key: STRING_KEYS.MARKETS })} - subtitle={isNotTablet && stringGetter({ key: STRING_KEYS.DISCOVER_NEW_ASSETS })} slotRight={ hasPotentialMarketsData && ( - ) } /> - - + <$Highlights htmlFor="highlights"> + {stringGetter({ key: STRING_KEYS.HIDE })} + + + - - + <$MarketsStats showHighlights={showHighlights} /> + + + <$MarketsTable /> + ); }; -const Styled: Record = {}; - -Styled.Page = styled.div` +const $Page = styled.div` ${layoutMixins.contentContainerPage} - gap: 1.5rem; +`; +const $ContentSectionHeader = styled(ContentSectionHeader)` + margin-top: 1rem; + margin-bottom: 0.25rem; - @media ${breakpoints.tablet} { - gap: 0.75rem; + h3 { + font: var(--font-extra-medium); } -`; -Styled.ContentSectionHeader = styled(ContentSectionHeader)` @media ${breakpoints.tablet} { - padding: 1.25rem 1.875rem 0; + margin-top: 0; + padding: 1.25rem 1.5rem 0; h3 { font: var(--font-extra-medium); } } `; - -Styled.HeaderSection = styled.section` +const $HeaderSection = styled.section` ${layoutMixins.contentSectionDetached} + margin-bottom: 2rem; + @media ${breakpoints.tablet} { ${layoutMixins.flexColumn} gap: 1rem; - margin-bottom: 0.5rem; + margin-bottom: 1rem; } `; - -Styled.ExchangeBillboards = styled(ExchangeBillboards)` - ${layoutMixins.contentSectionDetachedScrollable} -`; - -Styled.MarketsTable = styled(MarketsTable)` +const $MarketsTable = styled(MarketsTable)` ${layoutMixins.contentSectionAttached} `; +const $MarketsStats = styled(MarketsStats)<{ + showHighlights?: boolean; +}>` + ${({ showHighlights }) => !showHighlights && 'display: none;'} +`; +const $Highlights = styled.label` + align-items: center; + gap: 1rem; + margin-bottom: 1.25rem; + display: none; + cursor: pointer; + + @media ${breakpoints.desktopSmall} { + padding-left: 1rem; + padding-right: 1rem; + } + @media ${breakpoints.tablet} { + padding-left: 1.5rem; + padding-right: 1.5rem; + margin-bottom: 0; + display: flex; + } +`; export default Markets; diff --git a/src/pages/markets/NewMarket.tsx b/src/pages/markets/NewMarket.tsx index 0d722c96a..45ca76ddb 100644 --- a/src/pages/markets/NewMarket.tsx +++ b/src/pages/markets/NewMarket.tsx @@ -1,39 +1,46 @@ import { useMemo, useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { isMainnet } from '@/constants/networks'; import { AppRoute } from '@/constants/routes'; -import { - useBreakpoints, - useDocumentTitle, - useGovernanceVariables, - useStringGetter, - useTokenConfigs, -} from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useGovernanceVariables } from '@/hooks/useGovernanceVariables'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; import { ContentSectionHeader } from '@/components/ContentSectionHeader'; -import { IconButton } from '@/components/IconButton'; import { Icon, IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; import { Link } from '@/components/Link'; import { NewMarketForm } from '@/views/forms/NewMarketForm'; import { MustBigNumber } from '@/lib/numbers'; -const StepItem = ({ step, subtitle, title }: { step: number; subtitle: string; title: string }) => ( - - {step} - - {title} - {subtitle} - - +const StepItem = ({ + step, + subtitle, + title, +}: { + step: number; + subtitle: React.ReactNode; + title: string; +}) => ( + <$StepItem> + <$StepNumber>{step} + <$Column> + <$Title>{title} + <$Subtitle>{subtitle} + + ); const NewMarket = () => { @@ -55,9 +62,9 @@ const NewMarket = () => { key: STRING_KEYS.ADD_MARKET_STEP_1_DESCRIPTION, params: { HERE: ( - + <$Link href={newMarketProposal.newMarketsMethodology}> {stringGetter({ key: STRING_KEYS.HERE })} - + ), }, }), @@ -81,23 +88,23 @@ const NewMarket = () => { }), }, ]; - }, [stringGetter, newMarketProposal, chainTokenLabel]); + }, [stringGetter, newMarketProposal, chainTokenLabel, chainTokenDecimals]); return ( - - - + <$HeaderSection> + <$ContentSectionHeader title={stringGetter({ key: STRING_KEYS.SUGGEST_NEW_MARKET })} slotRight={ navigate(AppRoute.Markets)} /> } subtitle={isNotTablet && stringGetter({ key: STRING_KEYS.ADD_DETAILS_TO_LAUNCH_MARKET })} /> - - + + <$Content>
    {displaySteps && ( <> - - {stringGetter({ key: STRING_KEYS.STEPS_TO_CREATE })} - + <$StepsTitle>{stringGetter({ key: STRING_KEYS.STEPS_TO_CREATE })} {steps.map((item) => ( { )}
    - + <$FormContainer> - -
    -
    + + + ); }; - -const Styled: Record = {}; - -Styled.Page = styled.div` +const $Page = styled.div` ${layoutMixins.contentContainerPage} gap: 1.5rem; @@ -150,9 +152,9 @@ Styled.Page = styled.div` } `; -Styled.ContentSectionHeader = styled(ContentSectionHeader)` +const $ContentSectionHeader = styled(ContentSectionHeader)` @media ${breakpoints.notTablet} { - padding: 1rem 0; + padding: 1rem; } @media ${breakpoints.tablet} { @@ -164,7 +166,7 @@ Styled.ContentSectionHeader = styled(ContentSectionHeader)` } `; -Styled.HeaderSection = styled.section` +const $HeaderSection = styled.section` ${layoutMixins.contentSectionDetached} @media ${breakpoints.tablet} { @@ -175,7 +177,7 @@ Styled.HeaderSection = styled.section` } `; -Styled.Content = styled.div` +const $Content = styled.div` display: flex; flex-direction: row; gap: 2rem; @@ -189,7 +191,7 @@ Styled.Content = styled.div` } `; -Styled.StepsTitle = styled.h2` +const $StepsTitle = styled.h2` font: var(--font-large-medium); color: var(--color-text-2); margin: 1rem; @@ -199,11 +201,11 @@ Styled.StepsTitle = styled.h2` } `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` margin-right: 0.5ch; `; -Styled.StepItem = styled.div` +const $StepItem = styled.div` display: flex; flex-direction: row; gap: 1rem; @@ -211,7 +213,7 @@ Styled.StepItem = styled.div` margin-bottom: 1rem; `; -Styled.StepNumber = styled.div` +const $StepNumber = styled.div` width: 2.5rem; height: 2.5rem; min-width: 2.5rem; @@ -224,26 +226,26 @@ Styled.StepNumber = styled.div` color: var(--color-text-2); `; -Styled.Column = styled.div` +const $Column = styled.div` display: flex; flex-direction: column; `; -Styled.Title = styled.span` +const $Title = styled.span` color: var(--color-text-2); font: var(--font-medium-book); `; -Styled.Subtitle = styled.span` +const $Subtitle = styled.span` color: var(--color-text-0); `; -Styled.Link = styled(Link)` +const $Link = styled(Link)` --link-color: var(--color-accent); display: inline-block; `; -Styled.FormContainer = styled.div` +const $FormContainer = styled.div` min-width: 31.25rem; height: fit-content; border-radius: 1rem; diff --git a/src/pages/portfolio/AccountDetailsAndHistory.tsx b/src/pages/portfolio/AccountDetailsAndHistory.tsx index ca304e31f..5dab64ac0 100644 --- a/src/pages/portfolio/AccountDetailsAndHistory.tsx +++ b/src/pages/portfolio/AccountDetailsAndHistory.tsx @@ -1,28 +1,32 @@ import { useMemo, useState } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { shallowEqual, useSelector } from 'react-redux'; + import { TooltipContextType } from '@visx/xychart'; import BigNumber from 'bignumber.js'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import type { Nullable } from '@/constants/abacus'; import { OnboardingState } from '@/constants/account'; +import { ComplianceStates } from '@/constants/compliance'; import { STRING_KEYS } from '@/constants/localization'; import { NumberSign } from '@/constants/numbers'; -import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; -import { getOnboardingState, getSubaccount } from '@/state/accountSelectors'; -import { getSelectedLocale } from '@/state/localizationSelectors'; - import { Output, OutputType, ShowSign } from '@/components/Output'; import { TriangleIndicator } from '@/components/TriangleIndicator'; import { WithLabel } from '@/components/WithLabel'; - import { PnlChart, type PnlDatum } from '@/views/charts/PnlChart'; import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; +import { getOnboardingState, getSubaccount } from '@/state/accountSelectors'; +import { getSelectedLocale } from '@/state/localizationSelectors'; + import { isTruthy } from '@/lib/isTruthy'; import { MustBigNumber } from '@/lib/numbers'; @@ -46,7 +50,7 @@ export const usePortfolioValues = ({ timeStyle: 'short', }) : stringGetter({ key: STRING_KEYS.PORTFOLIO_VALUE }), - [activeDatum, stringGetter] + [activeDatum, selectedLocale, stringGetter] ); const accountEquity = useMemo( @@ -55,7 +59,7 @@ export const usePortfolioValues = ({ ); const earliestVisibleDatum = visibleData?.[0]; - const latestVisibleDatum = visibleData?.[visibleData?.length - 1]; + const latestVisibleDatum = visibleData?.[(visibleData?.length ?? 1) - 1]; const pnl = useMemo(() => { let pnlDiff; @@ -77,6 +81,7 @@ export const usePortfolioValues = ({ sign: fullTimeframeDiff.gte(0) ? NumberSign.Positive : NumberSign.Negative, }; } + return undefined; }, [activeDatum, earliestVisibleDatum, latestVisibleDatum]); return { @@ -84,18 +89,19 @@ export const usePortfolioValues = ({ accountEquity, pnlDiff: pnl?.pnlDiff, pnlDiffPercent: pnl?.pnlDiffPercent, - pnlDiffSign: pnl?.sign || NumberSign.Neutral, + pnlDiffSign: pnl?.sign ?? NumberSign.Neutral, }; }; export const AccountDetailsAndHistory = () => { const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); + const { complianceState } = useComplianceState(); const selectedLocale = useSelector(getSelectedLocale); const onboardingState = useSelector(getOnboardingState); const { buyingPower, equity, freeCollateral, leverage, marginUsage } = - useSelector(getSubaccount, shallowEqual) || {}; + useSelector(getSubaccount, shallowEqual) ?? {}; const [tooltipContext, setTooltipContext] = useState>(); @@ -136,44 +142,46 @@ export const AccountDetailsAndHistory = () => { ].filter(isTruthy); return ( - - - - + <$AccountDetailsAndHistory> + <$AccountValue> + <$WithLabel label={accountValueLabel}> + <$AccountEquity> - - + + <$PnlDiff isPositive={MustBigNumber(pnlDiff).gte(0)}> {pnlDiff && } {pnlDiffPercent && MustBigNumber(pnlDiffPercent).isFinite() && ( - + <$OutputInParentheses type={OutputType.Percent} value={pnlDiffPercent} /> )} - - - + + + {accountDetailsConfig.map(({ key, labelKey, type, value }) => ( - - + <$AccountDetail key={key} gridArea={key}> + <$WithLabel label={stringGetter({ key: labelKey })}> - - + + ))} - - {onboardingState !== OnboardingState.AccountConnected && ( - + <$EmptyChart> + {complianceState === ComplianceStates.READ_ONLY ? ( + <$EmptyCard>{stringGetter({ key: STRING_KEYS.BLOCKED_MESSAGE })} + ) : onboardingState !== OnboardingState.AccountConnected ? ( + <$EmptyCard>

    {stringGetter({ key: { @@ -183,18 +191,15 @@ export const AccountDetailsAndHistory = () => { })}

    -
    - )} - + + ) : null} + } /> -
    + ); }; - -const Styled: Record = {}; - -Styled.AccountDetailsAndHistory = styled.div<{ isSidebarOpen: boolean }>` +const $AccountDetailsAndHistory = styled.div<{ isSidebarOpen?: boolean }>` height: 100%; display: grid; @@ -216,7 +221,7 @@ Styled.AccountDetailsAndHistory = styled.div<{ isSidebarOpen: boolean }>` } `; -Styled.WithLabel = styled(WithLabel)` +const $WithLabel = styled(WithLabel)` --label-textColor: var(--color-text-0); label { @@ -224,7 +229,7 @@ Styled.WithLabel = styled(WithLabel)` } `; -Styled.AccountValue = styled.div` +const $AccountValue = styled.div` grid-area: PortfolioValue; padding: 1.25rem; @@ -234,12 +239,12 @@ Styled.AccountValue = styled.div` } `; -Styled.AccountEquity = styled.div` +const $AccountEquity = styled.div` font: var(--font-extra-book); color: var(--color-text-2); `; -Styled.PnlDiff = styled.div<{ isPositive: boolean }>` +const $PnlDiff = styled.div<{ isPositive: boolean }>` color: var(--color-negative); display: flex; flex-direction: row; @@ -253,7 +258,7 @@ Styled.PnlDiff = styled.div<{ isPositive: boolean }>` `} `; -Styled.OutputInParentheses = styled(Output)` +const $OutputInParentheses = styled(Output)` &:before { content: '('; } @@ -262,7 +267,7 @@ Styled.OutputInParentheses = styled(Output)` } `; -Styled.AccountDetail = styled.div<{ gridArea: string }>` +const $AccountDetail = styled.div<{ gridArea: string }>` grid-area: ${({ gridArea }) => gridArea}; padding: 1.25rem; @@ -270,7 +275,7 @@ Styled.AccountDetail = styled.div<{ gridArea: string }>` align-items: center; `; -Styled.PnlChart = styled(PnlChart)<{ pnlDiffSign: NumberSign }>` +const $PnlChart = styled(PnlChart)<{ pnlDiffSign: NumberSign }>` grid-area: Chart; background-color: var(--color-layer-2); @@ -282,12 +287,12 @@ Styled.PnlChart = styled(PnlChart)<{ pnlDiffSign: NumberSign }>` }[pnlDiffSign])}; `; -Styled.EmptyChart = styled.div` +const $EmptyChart = styled.div` display: grid; cursor: default; `; -Styled.OnboardingCard = styled.div` +const $EmptyCard = styled.div` width: 16.75rem; ${layoutMixins.column}; diff --git a/src/pages/portfolio/Fees.tsx b/src/pages/portfolio/Fees.tsx index bf1393fb2..0fc4c989d 100644 --- a/src/pages/portfolio/Fees.tsx +++ b/src/pages/portfolio/Fees.tsx @@ -1,12 +1,16 @@ import { useCallback, useMemo } from 'react'; + import { Nullable } from '@dydxprotocol/v4-abacus'; -import styled, { AnyStyledComponent } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { FeeTier } from '@/constants/abacus'; -import { FEE_DECIMALS } from '@/constants/numbers'; import { STRING_KEYS } from '@/constants/localization'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { FEE_DECIMALS } from '@/constants/numbers'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { tableMixins } from '@/styles/tableMixins'; @@ -43,69 +47,60 @@ export const Fees = () => { }, [userStats]); const AdditionalConditions = useCallback( - ({ - totalShare, - makerShare, - isAdditional, - }: { + (conditions: { totalShare: Nullable; makerShare: Nullable; isAdditional?: boolean; - }) => ( - - {!isAdditional && !totalShare && !makerShare && } - {!!totalShare && ( - - {isAdditional && stringGetter({ key: STRING_KEYS.AND })}{' '} - {stringGetter({ key: STRING_KEYS.EXCHANGE_MARKET_SHARE })}{' '} - {'>'}{' '} - - - )} - {!!makerShare && ( - - {isAdditional && stringGetter({ key: STRING_KEYS.AND })}{' '} - {stringGetter({ key: STRING_KEYS.MAKER_MARKET_SHARE })}{' '} - {'>'}{' '} - - - )} - - ), + }) => { + const { totalShare, makerShare, isAdditional } = conditions; + return ( + <$AdditionalConditions> + {!isAdditional && !totalShare && !makerShare && } + {!!totalShare && ( + <$AdditionalConditionsText> + {isAdditional && stringGetter({ key: STRING_KEYS.AND })}{' '} + {stringGetter({ key: STRING_KEYS.EXCHANGE_MARKET_SHARE })}{' '} + <$Highlighted>{'>'}{' '} + <$HighlightOutput type={OutputType.Percent} value={totalShare} fractionDigits={0} /> + + )} + {!!makerShare && ( + <$AdditionalConditionsText> + {isAdditional && stringGetter({ key: STRING_KEYS.AND })}{' '} + {stringGetter({ key: STRING_KEYS.MAKER_MARKET_SHARE })}{' '} + <$Highlighted>{'>'}{' '} + <$HighlightOutput type={OutputType.Percent} value={makerShare} fractionDigits={0} /> + + )} + + ); + }, [stringGetter] ); return ( {isNotTablet && } - - + <$FeesDetails layout="grid" items={[ { key: 'volume', label: ( - + <$CardLabel> {stringGetter({ key: STRING_KEYS.TRAILING_VOLUME })} {stringGetter({ key: STRING_KEYS._30D })} - + ), value: , }, ]} /> - row.tier} getRowAttributes={(row: FeeTier) => ({ 'data-yours': row.tier === userFeeTier, @@ -118,14 +113,14 @@ export const Fees = () => { label: stringGetter({ key: STRING_KEYS.TIER }), allowsSorting: false, renderCell: ({ tier }) => ( - - + <$Tier> + <$Output type={OutputType.Text} value={tier} /> {tier === userFeeTier && ( - + <$YouTag size={TagSize.Medium}> {stringGetter({ key: STRING_KEYS.YOU })} - + )} - + ), }, { @@ -133,14 +128,14 @@ export const Fees = () => { getCellValue: (row) => row.volume, label: stringGetter({ key: STRING_KEYS.VOLUME_30D }), allowsSorting: false, - renderCell: ({ symbol, volume, makerShare, totalShare }) => ( + renderCell: ({ symbol, volume: vol, makerShare, totalShare }) => ( <> {`${ symbol in EQUALITY_SYMBOL_MAP ? EQUALITY_SYMBOL_MAP[symbol as keyof typeof EQUALITY_SYMBOL_MAP] : symbol } `} - + <$HighlightOutput type={OutputType.CompactFiat} value={vol} /> {isTablet && AdditionalConditions({ totalShare, makerShare, isAdditional: true })} @@ -160,7 +155,7 @@ export const Fees = () => { label: stringGetter({ key: STRING_KEYS.MAKER }), allowsSorting: false, renderCell: ({ maker }) => ( - { label: stringGetter({ key: STRING_KEYS.TAKER }), allowsSorting: false, renderCell: ({ taker }) => ( - { ] as ColumnDef[] ).filter(isTruthy)} selectionBehavior="replace" + paginationBehavior="showAll" withOuterBorder={isNotTablet} withInnerBorders /> - + ); }; - -const Styled: Record = {}; - -Styled.ContentWrapper = styled.div` +const $ContentWrapper = styled.div` ${layoutMixins.flexColumn} gap: 1.5rem; max-width: 100vw; `; -Styled.AdditionalConditions = styled.div` - ${tableMixins.cellContentColumn} +const $AdditionalConditions = styled.div` + ${tableMixins.stackedWithSecondaryStyling} justify-content: end; color: var(--color-text-0); @@ -211,7 +204,7 @@ Styled.AdditionalConditions = styled.div` } `; -Styled.AdditionalConditionsText = styled.span` +const $AdditionalConditionsText = styled.span` display: flex; gap: 0.5ch; @@ -226,7 +219,7 @@ Styled.AdditionalConditionsText = styled.span` } `; -Styled.FeesDetails = styled(Details)` +const $FeesDetails = styled(Details)` gap: 1rem; @media ${breakpoints.notTablet} { @@ -261,12 +254,12 @@ Styled.FeesDetails = styled(Details)` } `; -Styled.TextRow = styled.div` +const $TextRow = styled.div` ${layoutMixins.inlineRow} gap: 0.25rem; `; -Styled.CardLabel = styled(Styled.TextRow)` +const $CardLabel = styled($TextRow)` font: var(--font-small-book); color: var(--color-text-1); @@ -280,7 +273,7 @@ Styled.CardLabel = styled(Styled.TextRow)` } `; -Styled.FeeTable = styled(Table)` +const $FeeTable = styled(Table)` --tableCell-padding: 0.5rem 1.5rem; --bordered-content-border-radius: 0.625rem; --table-cell-align: end; @@ -303,26 +296,26 @@ Styled.FeeTable = styled(Table)` } @media ${breakpoints.notTablet} { - --tableHeader-backgroundColor: var(--color-layer-1); + --tableStickyRow-backgroundColor: var(--color-layer-1); } -`; +` as typeof Table; -Styled.Output = styled(Output)` +const $Output = styled(Output)` color: var(--color-text-0); `; -Styled.Highlighted = styled.strong` +const $Highlighted = styled.strong` color: var(--color-text-1); `; -Styled.HighlightOutput = styled(Output)` +const $HighlightOutput = styled(Output)` color: var(--color-text-1); `; -Styled.Tier = styled(Styled.TextRow)` +const $Tier = styled($TextRow)` gap: 0.5rem; `; -Styled.YouTag = styled(Tag)` +const $YouTag = styled(Tag)` color: var(--color-text-1); `; diff --git a/src/pages/portfolio/History.tsx b/src/pages/portfolio/History.tsx index 78f85aa1d..e7c116c4d 100644 --- a/src/pages/portfolio/History.tsx +++ b/src/pages/portfolio/History.tsx @@ -1,14 +1,17 @@ import { Outlet } from 'react-router-dom'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { HistoryRoute } from '@/constants/routes'; -import { useBreakpoints, useStringGetter } from '@/hooks'; -import styled, { AnyStyledComponent } from 'styled-components'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { AttachedExpandingSection } from '@/components/ContentSection'; import { NavigationMenu } from '@/components/NavigationMenu'; +import { ExportHistoryDropdown } from '@/views/ExportHistoryDropdown'; export const History = () => { const stringGetter = useStringGetter(); @@ -17,8 +20,9 @@ export const History = () => { return ( {isNotTablet && ( - } items={[ { group: 'navigation', @@ -50,10 +54,13 @@ export const History = () => { ); }; - const Styled: Record = {}; -Styled.NavigationMenu = styled(NavigationMenu)` +Styled.ExportButton = styled(ExportHistoryDropdown)` + margin-left: auto; +`; + +const $NavigationMenu = styled(NavigationMenu)` --header-horizontal-padding: 1rem; ${layoutMixins.contentSectionDetached} diff --git a/src/pages/portfolio/Orders.tsx b/src/pages/portfolio/Orders.tsx index ef4d2a25d..8e1d399c5 100644 --- a/src/pages/portfolio/Orders.tsx +++ b/src/pages/portfolio/Orders.tsx @@ -1,12 +1,13 @@ import { useSelector } from 'react-redux'; import { STRING_KEYS } from '@/constants/localization'; -import { useBreakpoints, useStringGetter } from '@/hooks'; -import { OrdersTable, OrdersTableColumnKey } from '@/views/tables/OrdersTable'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { ContentSectionHeader } from '@/components/ContentSectionHeader'; import { AttachedExpandingSection } from '@/components/ContentSection'; +import { ContentSectionHeader } from '@/components/ContentSectionHeader'; +import { OrdersTable, OrdersTableColumnKey } from '@/views/tables/OrdersTable'; import { calculateIsAccountViewOnly } from '@/state/accountCalculators'; diff --git a/src/pages/portfolio/Overview.tsx b/src/pages/portfolio/Overview.tsx index 5e385d4c6..ac3a714af 100644 --- a/src/pages/portfolio/Overview.tsx +++ b/src/pages/portfolio/Overview.tsx @@ -1,20 +1,38 @@ import { useSelector } from 'react-redux'; -import styled, { AnyStyledComponent } from 'styled-components'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { AppRoute } from '@/constants/routes'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { AppRoute, PortfolioRoute } from '@/constants/routes'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { AttachedExpandingSection, DetachedSection } from '@/components/ContentSection'; import { ContentSectionHeader } from '@/components/ContentSectionHeader'; - import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/PositionsTable'; +import { + calculateShouldRenderActionsInPositionsTable, + calculateShouldRenderTriggersInPositionsTable, +} from '@/state/accountCalculators'; + +import { isTruthy } from '@/lib/isTruthy'; +import { testFlags } from '@/lib/testFlags'; + import { AccountDetailsAndHistory } from './AccountDetailsAndHistory'; export const Overview = () => { const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); + const navigate = useNavigate(); + + const showClosePositionAction = false; + + const shouldRenderTriggers = useSelector(calculateShouldRenderTriggersInPositionsTable); + const shouldRenderActions = useSelector( + calculateShouldRenderActionsInPositionsTable(showClosePositionAction) + ); return (
    @@ -22,7 +40,7 @@ export const Overview = () => { - + <$AttachedExpandingSection> { ] : [ PositionsTableColumnKey.Market, - PositionsTableColumnKey.Side, PositionsTableColumnKey.Size, - PositionsTableColumnKey.Leverage, - PositionsTableColumnKey.LiquidationAndOraclePrice, PositionsTableColumnKey.UnrealizedPnl, - PositionsTableColumnKey.RealizedPnl, + !testFlags.isolatedMargin && PositionsTableColumnKey.RealizedPnl, PositionsTableColumnKey.AverageOpenAndClose, - ] + PositionsTableColumnKey.LiquidationAndOraclePrice, + shouldRenderTriggers && PositionsTableColumnKey.Triggers, + shouldRenderActions && PositionsTableColumnKey.Actions, + ].filter(isTruthy) } currentRoute={AppRoute.Portfolio} - withGradientCardRows + navigateToOrders={() => + navigate(`${AppRoute.Portfolio}/${PortfolioRoute.Orders}`, { + state: { from: AppRoute.Portfolio }, + }) + } + showClosePositionAction={showClosePositionAction} withOuterBorder /> - +
    ); }; - -const Styled: Record = {}; - -Styled.AttachedExpandingSection = styled(AttachedExpandingSection)` +const $AttachedExpandingSection = styled(AttachedExpandingSection)` margin-top: 1rem; `; diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index 58aaeffe1..73047ad1b 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -1,24 +1,33 @@ import { lazy, Suspense } from 'react'; + import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; import { Navigate, Route, Routes } from 'react-router-dom'; +import styled from 'styled-components'; import { OnboardingState } from '@/constants/account'; import { ButtonAction } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { HistoryRoute, PortfolioRoute } from '@/constants/routes'; -import { useAccountBalance, useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks'; + +import { useAccountBalance } from '@/hooks/useAccountBalance'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; -import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; -import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable'; -import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable'; import { Button } from '@/components/Button'; import { Icon, IconName } from '@/components/Icon'; +import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { NavigationMenu } from '@/components/NavigationMenu'; import { Tag, TagType } from '@/components/Tag'; import { WithSidebar } from '@/components/WithSidebar'; +import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; +import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable'; +import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable'; import { getOnboardingState, getSubaccount, getTradeInfoNumbers } from '@/state/accountSelectors'; import { openDialog } from '@/state/dialogs'; @@ -26,7 +35,6 @@ import { openDialog } from '@/state/dialogs'; import { shortenNumberForDisplay } from '@/lib/numbers'; import { PortfolioNavMobile } from './PortfolioNavMobile'; -import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; const Overview = lazy(() => import('./Overview').then((module) => ({ default: module.Overview }))); const Positions = lazy(() => @@ -36,21 +44,24 @@ const Orders = lazy(() => import('./Orders').then((module) => ({ default: module const Fees = lazy(() => import('./Fees').then((module) => ({ default: module.Fees }))); const History = lazy(() => import('./History').then((module) => ({ default: module.History }))); -export default () => { +const PortfolioPage = () => { const dispatch = useDispatch(); const stringGetter = useStringGetter(); const { isTablet, isNotTablet } = useBreakpoints(); + const { complianceState } = useComplianceState(); + + const initialPageSize = 20; const onboardingState = useSelector(getOnboardingState); - const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; + const { freeCollateral } = useSelector(getSubaccount, shallowEqual) ?? {}; const { nativeTokenBalance } = useAccountBalance(); const { numTotalPositions, numTotalOpenOrders } = - useSelector(getTradeInfoNumbers, shallowEqual) || {}; + useSelector(getTradeInfoNumbers, shallowEqual) ?? {}; const numPositions = shortenNumberForDisplay(numTotalPositions); const numOrders = shortenNumberForDisplay(numTotalOpenOrders); - const usdcBalance = freeCollateral?.current || 0; + const usdcBalance = freeCollateral?.current ?? 0; useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO })); @@ -67,6 +78,7 @@ export default () => { path={HistoryRoute.Trades} element={ { /> } + element={ + + } /> } + element={ + + } /> } /> @@ -103,16 +125,16 @@ export default () => { ); return isTablet ? ( - + <$PortfolioMobile> {routesComponent} - + ) : ( - + <$NavigationMenu items={[ { group: 'views', @@ -121,9 +143,9 @@ export default () => { { value: PortfolioRoute.Overview, slotBefore: ( - + <$IconContainer> - + ), label: stringGetter({ key: STRING_KEYS.OVERVIEW }), href: PortfolioRoute.Overview, @@ -131,9 +153,9 @@ export default () => { { value: PortfolioRoute.Positions, slotBefore: ( - + <$IconContainer> - + ), label: ( <> @@ -149,9 +171,9 @@ export default () => { { value: PortfolioRoute.Orders, slotBefore: ( - + <$IconContainer> - + ), label: ( <> @@ -166,9 +188,9 @@ export default () => { { value: PortfolioRoute.Fees, slotBefore: ( - + <$IconContainer> - + ), label: stringGetter({ key: STRING_KEYS.FEES }), href: PortfolioRoute.Fees, @@ -176,9 +198,9 @@ export default () => { { value: PortfolioRoute.History, slotBefore: ( - + <$IconContainer> - + ), label: stringGetter({ key: STRING_KEYS.HISTORY }), href: PortfolioRoute.History, @@ -188,32 +210,35 @@ export default () => { ]} /> {onboardingState === OnboardingState.AccountConnected && ( - - - {usdcBalance > 0 && ( + <$Footer> + {complianceState === ComplianceStates.FULL_ACCESS && ( )} - {(usdcBalance > 0 || nativeTokenBalance.gt(0)) && ( + {usdcBalance > 0 && ( )} - + {complianceState === ComplianceStates.FULL_ACCESS && + (usdcBalance > 0 || nativeTokenBalance.gt(0)) && ( + + )} + )} - + ) } > @@ -222,21 +247,21 @@ export default () => { ); }; -const Styled: Record = {}; +export default PortfolioPage; -Styled.PortfolioMobile = styled.div` +const $PortfolioMobile = styled.div` min-height: 100%; ${layoutMixins.expandingColumnWithHeader} `; -Styled.SideBar = styled.div` +const $SideBar = styled.div` ${layoutMixins.flexColumn} justify-content: space-between; height: 100%; `; -Styled.Footer = styled.div` +const $Footer = styled.div` ${layoutMixins.row} flex-wrap: wrap; @@ -249,12 +274,12 @@ Styled.Footer = styled.div` } `; -Styled.NavigationMenu = styled(NavigationMenu)` +const $NavigationMenu = styled(NavigationMenu)` padding: 0.5rem; padding-top: 0; `; -Styled.IconContainer = styled.div` +const $IconContainer = styled.div` width: 1.5rem; height: 1.5rem; font-size: 0.75rem; diff --git a/src/pages/portfolio/PortfolioNavMobile.tsx b/src/pages/portfolio/PortfolioNavMobile.tsx index 0ce1a6583..855e9f224 100644 --- a/src/pages/portfolio/PortfolioNavMobile.tsx +++ b/src/pages/portfolio/PortfolioNavMobile.tsx @@ -1,12 +1,13 @@ import { useLocation, useNavigate } from 'react-router-dom'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; -import { AppRoute, HistoryRoute, PortfolioRoute } from '@/constants/routes'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; +import { AppRoute, HistoryRoute, PortfolioRoute } from '@/constants/routes'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; -import { BackButton } from '@/components/BackButton'; import { DropdownHeaderMenu } from '@/components/DropdownHeaderMenu'; export const PortfolioNavMobile = () => { @@ -60,7 +61,7 @@ export const PortfolioNavMobile = () => { const currentRoute = routeMap[pathname]; return ( - + <$MobilePortfolioHeader> value !== currentRoute?.value)} @@ -68,13 +69,10 @@ export const PortfolioNavMobile = () => { > {currentRoute?.label} - + ); }; - -const Styled: Record = {}; - -Styled.MobilePortfolioHeader = styled.div` +const $MobilePortfolioHeader = styled.div` ${layoutMixins.stickyHeader} ${layoutMixins.withOuterBorder} ${layoutMixins.row} diff --git a/src/pages/portfolio/Positions.tsx b/src/pages/portfolio/Positions.tsx index 59184703d..9265e641b 100644 --- a/src/pages/portfolio/Positions.tsx +++ b/src/pages/portfolio/Positions.tsx @@ -1,14 +1,35 @@ +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + import { STRING_KEYS } from '@/constants/localization'; import { AppRoute, PortfolioRoute } from '@/constants/routes'; -import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { AttachedExpandingSection } from '@/components/ContentSection'; import { ContentSectionHeader } from '@/components/ContentSectionHeader'; import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/PositionsTable'; +import { + calculateShouldRenderActionsInPositionsTable, + calculateShouldRenderTriggersInPositionsTable, +} from '@/state/accountCalculators'; + +import { isTruthy } from '@/lib/isTruthy'; +import { testFlags } from '@/lib/testFlags'; + export const Positions = () => { const stringGetter = useStringGetter(); const { isTablet, isNotTablet } = useBreakpoints(); + const navigate = useNavigate(); + + const showClosePositionAction = false; + + const shouldRenderTriggers = useSelector(calculateShouldRenderTriggersInPositionsTable); + const shouldRenderActions = useSelector( + calculateShouldRenderActionsInPositionsTable(showClosePositionAction) + ); return ( @@ -24,17 +45,25 @@ export const Positions = () => { ] : [ PositionsTableColumnKey.Market, - PositionsTableColumnKey.Side, PositionsTableColumnKey.Size, - PositionsTableColumnKey.Leverage, - PositionsTableColumnKey.LiquidationAndOraclePrice, + testFlags.isolatedMargin && PositionsTableColumnKey.Margin, PositionsTableColumnKey.UnrealizedPnl, - PositionsTableColumnKey.RealizedPnl, + !testFlags.isolatedMargin && PositionsTableColumnKey.RealizedPnl, + PositionsTableColumnKey.NetFunding, PositionsTableColumnKey.AverageOpenAndClose, - ] + PositionsTableColumnKey.LiquidationAndOraclePrice, + shouldRenderTriggers && PositionsTableColumnKey.Triggers, + shouldRenderActions && PositionsTableColumnKey.Actions, + ].filter(isTruthy) } currentRoute={`${AppRoute.Portfolio}/${PortfolioRoute.Positions}`} withOuterBorder={isNotTablet} + showClosePositionAction={showClosePositionAction} + navigateToOrders={() => + navigate(`${AppRoute.Portfolio}/${PortfolioRoute.Orders}`, { + state: { from: AppRoute.Portfolio }, + }) + } /> ); diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index e8dde6d00..d8b98d028 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -1,29 +1,27 @@ -import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { STRING_KEYS, - SupportedLocales, SUPPORTED_LOCALE_STRING_LABELS, + SupportedLocales, } from '@/constants/localization'; - import type { MenuItem } from '@/constants/menus'; import { DydxNetwork } from '@/constants/networks'; import { AppRoute, MobileSettingsRoute } from '@/constants/routes'; -import { useStringGetter, useSelectedNetwork } from '@/hooks'; +import { useSelectedNetwork } from '@/hooks/useSelectedNetwork'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { ComingSoonSpace } from '@/components/ComingSoon'; import { PageMenu } from '@/components/PageMenu'; import { PageMenuItemType } from '@/components/PageMenu/PageMenuItem'; +import { useNetworks } from '@/views/menus/useNetworks'; import { setSelectedLocale } from '@/state/localization'; - import { getSelectedLocale } from '@/state/localizationSelectors'; -import { useNetworks } from '@/views/menus/useNetworks'; - import { SettingsHeader } from './SettingsHeader'; -import { ComingSoonSpace } from '@/components/ComingSoon'; const SettingsPage = () => { const stringGetter = useStringGetter(); diff --git a/src/pages/settings/SettingsHeader.tsx b/src/pages/settings/SettingsHeader.tsx index 0faeffc6a..294feafd7 100644 --- a/src/pages/settings/SettingsHeader.tsx +++ b/src/pages/settings/SettingsHeader.tsx @@ -1,7 +1,8 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; -import { StringGetterFunction, STRING_KEYS } from '@/constants/localization'; +import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; import { AppRoute, MobileSettingsRoute } from '@/constants/routes'; + import { layoutMixins } from '@/styles/layoutMixins'; import { BackButton } from '@/components/BackButton'; @@ -39,16 +40,13 @@ export const SettingsHeader = ({ const currentRoute = routeMap[pathname]; return ( - + <$SettingsHeader> - {currentRoute?.label} - + <$Label>{currentRoute?.label} + ); }; - -const Styled: Record = {}; - -Styled.SettingsHeader = styled.header` +const $SettingsHeader = styled.header` --stickyArea-topHeight: var(--page-header-height-mobile); ${layoutMixins.stickyHeader} @@ -59,7 +57,7 @@ Styled.SettingsHeader = styled.header` background-color: var(--color-layer-2); `; -Styled.Label = styled.h1` +const $Label = styled.h1` padding: 0.5rem; font: var(--font-extra-medium); `; diff --git a/src/pages/token/Governance.tsx b/src/pages/token/Governance.tsx index 723f0ec4c..d22895a99 100644 --- a/src/pages/token/Governance.tsx +++ b/src/pages/token/Governance.tsx @@ -1,7 +1,9 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -11,31 +13,30 @@ import { ContentSectionHeader } from '@/components/ContentSectionHeader'; import { GovernancePanel } from './rewards/GovernancePanel'; import { NewMarketsPanel } from './rewards/NewMarketsPanel'; -export default () => { +const Governance = () => { const stringGetter = useStringGetter(); return ( - + <$HeaderSection> - + - - + <$ContentWrapper> + <$Row> - - + + ); }; +export default Governance; -const Styled: Record = {}; - -Styled.HeaderSection = styled.section` +const $HeaderSection = styled.section` ${layoutMixins.contentSectionDetached} @media ${breakpoints.tablet} { @@ -46,14 +47,14 @@ Styled.HeaderSection = styled.section` } `; -Styled.ContentWrapper = styled.div` +const $ContentWrapper = styled.div` ${layoutMixins.flexColumn} gap: 1.5rem; max-width: 80rem; padding: 0 1rem; `; -Styled.Row = styled.div` +const $Row = styled.div` gap: 1rem; display: grid; grid-template-columns: repeat(3, 1fr); diff --git a/src/pages/token/Token.tsx b/src/pages/token/Token.tsx index 6471f21c7..14d595bb0 100644 --- a/src/pages/token/Token.tsx +++ b/src/pages/token/Token.tsx @@ -1,10 +1,14 @@ import { Suspense, lazy } from 'react'; + import { Navigate, Route, Routes } from 'react-router-dom'; -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { TokenRoute } from '@/constants/routes'; -import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { Icon, IconName } from '@/components/Icon'; @@ -16,7 +20,7 @@ const RewardsPage = lazy(() => import('./rewards/RewardsPage')); const StakingPage = lazy(() => import('./staking/StakingPage')); const GovernancePage = lazy(() => import('./Governance')); -export default () => { +const Token = () => { const { isTablet } = useBreakpoints(); const stringGetter = useStringGetter(); @@ -35,8 +39,8 @@ export default () => { - + <$NavigationMenu items={[ { group: 'views', @@ -45,9 +49,9 @@ export default () => { { value: TokenRoute.TradingRewards, slotBefore: ( - + <$IconContainer> - + ), label: stringGetter({ key: STRING_KEYS.TRADING_REWARDS }), href: TokenRoute.TradingRewards, @@ -55,9 +59,9 @@ export default () => { { value: TokenRoute.StakingRewards, slotBefore: ( - + <$IconContainer> - + ), label: stringGetter({ key: STRING_KEYS.STAKING_REWARDS }), href: TokenRoute.StakingRewards, @@ -66,9 +70,9 @@ export default () => { { value: TokenRoute.Governance, slotBefore: ( - + <$IconContainer> - + ), label: stringGetter({ key: STRING_KEYS.GOVERNANCE }), href: TokenRoute.Governance, @@ -77,7 +81,7 @@ export default () => { }, ]} /> - + ) } > @@ -85,35 +89,21 @@ export default () => { ); }; +export default Token; -const Styled: Record = {}; - -Styled.SideBar = styled.div` +const $SideBar = styled.div` ${layoutMixins.flexColumn} justify-content: space-between; height: 100%; `; -Styled.Footer = styled.div` - ${layoutMixins.row} - flex-wrap: wrap; - - padding: 1rem; - - gap: 0.5rem; - - > button { - flex-grow: 1; - } -`; - -Styled.NavigationMenu = styled(NavigationMenu)` +const $NavigationMenu = styled(NavigationMenu)` padding: 0.5rem; padding-top: 0; `; -Styled.IconContainer = styled.div` +const $IconContainer = styled.div` width: 1.5rem; height: 1.5rem; font-size: 0.75rem; diff --git a/src/pages/token/rewards/DYDXBalancePanel.tsx b/src/pages/token/rewards/DYDXBalancePanel.tsx index b601783fd..3b4454ecf 100644 --- a/src/pages/token/rewards/DYDXBalancePanel.tsx +++ b/src/pages/token/rewards/DYDXBalancePanel.tsx @@ -1,14 +1,19 @@ import type { ElementType } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import { layoutMixins } from '@/styles/layoutMixins'; -import { useAccountBalance, useAccounts, useTokenConfigs, useStringGetter } from '@/hooks'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; -import { wallets, WalletType } from '@/constants/wallets'; +import { WalletType, wallets } from '@/constants/wallets'; + +import { useAccountBalance } from '@/hooks/useAccountBalance'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { Button } from '@/components/Button'; @@ -19,8 +24,8 @@ import { Panel } from '@/components/Panel'; import { Toolbar } from '@/components/Toolbar'; import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; -import { openDialog } from '@/state/dialogs'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; +import { openDialog } from '@/state/dialogs'; export const DYDXBalancePanel = ({ className }: { className?: string }) => { const dispatch = useDispatch(); @@ -35,12 +40,12 @@ export const DYDXBalancePanel = ({ className }: { className?: string }) => { - + <$Header> + <$Title> {chainTokenLabel} - - + + <$ActionButtons> {!canAccountTrade ? ( ) : ( @@ -53,27 +58,27 @@ export const DYDXBalancePanel = ({ className }: { className?: string }) => { {stringGetter({ key: STRING_KEYS.TRANSFER })} )} - - + + } > - - + <$WalletAndStakedBalance layout="grid" items={[ { key: 'wallet', label: ( - + <$Label>

    {stringGetter({ key: STRING_KEYS.WALLET })}

    - + <$IconContainer> - -
    + + ), value: , @@ -81,19 +86,19 @@ export const DYDXBalancePanel = ({ className }: { className?: string }) => { { key: 'staked', label: ( - + <$Label>

    {stringGetter({ key: STRING_KEYS.STAKED })}

    - + <$IconContainer> - -
    + + ), value: , }, ]} /> - { }, ]} /> -
    +
    ); }; - -const Styled: Record = {}; - -Styled.Header = styled.div` +const $Header = styled.div` ${layoutMixins.spacedRow} gap: 1rem; padding: var(--panel-paddingY) var(--panel-paddingX) 0; `; -Styled.Title = styled.h3` +const $Title = styled.h3` ${layoutMixins.inlineRow} font: var(--font-medium-book); color: var(--color-text-2); @@ -131,25 +133,19 @@ Styled.Title = styled.h3` } `; -Styled.ActionButtons = styled(Toolbar)` +const $ActionButtons = styled(Toolbar)` ${layoutMixins.inlineRow} --stickyArea-topHeight: max-content; gap: 0.5rem; padding: 0; `; -Styled.ReceiveButton = styled(Button)` - --button-textColor: var(--color-text-2); - --button-backgroundColor: var(--color-layer-5); - --button-border: solid var(--border-width) var(--color-layer-6); -`; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.flexColumn} gap: 0.75rem; `; -Styled.IconContainer = styled.div` +const $IconContainer = styled.div` ${layoutMixins.stack} height: 1.5rem; @@ -166,7 +162,7 @@ Styled.IconContainer = styled.div` } `; -Styled.WalletAndStakedBalance = styled(Details)` +const $WalletAndStakedBalance = styled(Details)` --details-item-backgroundColor: var(--color-layer-6); grid-template-columns: 1fr 1fr; @@ -191,14 +187,14 @@ Styled.WalletAndStakedBalance = styled(Details)` } `; -Styled.Label = styled.div` +const $Label = styled.div` ${layoutMixins.spacedRow} font: var(--font-base-book); color: var(--color-text-1); `; -Styled.TotalBalance = styled(Details)` +const $TotalBalance = styled(Details)` div { --scrollArea-height: auto; } diff --git a/src/pages/token/rewards/GovernancePanel.tsx b/src/pages/token/rewards/GovernancePanel.tsx index 6796a6a16..e277ee152 100644 --- a/src/pages/token/rewards/GovernancePanel.tsx +++ b/src/pages/token/rewards/GovernancePanel.tsx @@ -1,16 +1,17 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { ButtonAction, ButtonSize } from '@/constants/buttons'; -import { STRING_KEYS } from '@/constants/localization'; import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter, useURLConfigs } from '@/hooks'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; -import { Panel } from '@/components/Panel'; import { IconName } from '@/components/Icon'; import { IconButton } from '@/components/IconButton'; import { Link } from '@/components/Link'; +import { Panel } from '@/components/Panel'; import { openDialog } from '@/state/dialogs'; @@ -22,33 +23,28 @@ export const GovernancePanel = ({ className }: { className?: string }) => { return ( {stringGetter({ key: STRING_KEYS.GOVERNANCE })} - } + slotHeaderContent={<$Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}} slotRight={ - - + <$IconButton action={ButtonAction.Base} iconName={IconName.Arrow} size={ButtonSize.Small} /> - + } onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))} > - + <$Description> {stringGetter({ key: STRING_KEYS.GOVERNANCE_DESCRIPTION })} e.stopPropagation()}> {stringGetter({ key: STRING_KEYS.LEARN_MORE })} → - + ); }; - -const Styled: Record = {}; - -Styled.Description = styled.div` +const $Description = styled.div` color: var(--color-text-0); --link-color: var(--color-text-1); @@ -60,16 +56,16 @@ Styled.Description = styled.div` } `; -Styled.IconButton = styled(IconButton)` +const $IconButton = styled(IconButton)` color: var(--color-text-0); --color-border: var(--color-layer-6); `; -Styled.Arrow = styled.div` +const $Arrow = styled.div` padding-right: 1.5rem; `; -Styled.Title = styled.h3` +const $Title = styled.h3` font: var(--font-medium-book); color: var(--color-text-2); margin-bottom: -1rem; diff --git a/src/pages/token/rewards/LaunchIncentivesPanel.tsx b/src/pages/token/rewards/LaunchIncentivesPanel.tsx index d679a3640..e2cd9caf4 100644 --- a/src/pages/token/rewards/LaunchIncentivesPanel.tsx +++ b/src/pages/token/rewards/LaunchIncentivesPanel.tsx @@ -1,24 +1,27 @@ import { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; + import { useQuery } from 'react-query'; -import styled, { AnyStyledComponent } from 'styled-components'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; -import { STRING_KEYS } from '@/constants/localization'; import { ButtonAction } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; +import { TOKEN_DECIMALS } from '@/constants/numbers'; -import { ChaosLabsIcon } from '@/icons'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useQueryChaosLabsIncentives } from '@/hooks/useQueryChaosLabsIncentives'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { ChaosLabsIcon } from '@/icons'; import breakpoints from '@/styles/breakpoints'; -import { useAccounts, useBreakpoints, useStringGetter } from '@/hooks'; - import { layoutMixins } from '@/styles/layoutMixins'; -import { Panel } from '@/components/Panel'; import { Button } from '@/components/Button'; - -import { Output, OutputType } from '@/components/Output'; import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType } from '@/components/Output'; +import { Panel } from '@/components/Panel'; import { Tag, TagSize } from '@/components/Tag'; import { markLaunchIncentivesSeen } from '@/state/configs'; @@ -32,39 +35,39 @@ export const LaunchIncentivesPanel = ({ className }: { className?: string }) => useEffect(() => { dispatch(markLaunchIncentivesSeen()); - }, []); + }, [dispatch]); return isNotTablet ? ( - } slotRight={} > - + ) : ( - - + <$Panel className={className}> + <$Column> - - + + ); }; const LaunchIncentivesTitle = () => { const stringGetter = useStringGetter(); return ( - + <$Title> {stringGetter({ key: STRING_KEYS.LAUNCH_INCENTIVES_TITLE, params: { - FOR_V4: {stringGetter({ key: STRING_KEYS.FOR_V4 })}, + FOR_V4: <$ForV4>{stringGetter({ key: STRING_KEYS.FOR_V4 })}, }, })} - {stringGetter({ key: STRING_KEYS.NEW })} - + <$NewTag size={TagSize.Medium}>{stringGetter({ key: STRING_KEYS.NEW })} + ); }; @@ -98,40 +101,37 @@ const EstimatedRewards = () => { onError: (error: Error) => log('LaunchIncentives/fetchSeasonNumber', error), }); - const { data, isLoading } = useQuery({ - enabled: !!dydxAddress, - queryKey: `launch_incentives_rewards_${dydxAddress ?? ''}`, - queryFn: async () => { - if (!dydxAddress) return undefined; - const resp = await fetch(`https://cloud.chaoslabs.co/query/api/dydx/points/${dydxAddress}`); - return (await resp.json())?.incentivePoints; - }, - onError: (error: Error) => log('LaunchIncentives/fetchPoints', error), - }); + const { data, isLoading } = useQueryChaosLabsIncentives({ dydxAddress, season: seasonNumber }); + const { incentivePoints } = data ?? {}; return ( - - + <$EstimatedRewardsCard> + <$EstimatedRewardsCardContent>
    {stringGetter({ key: STRING_KEYS.ESTIMATED_REWARDS })} {seasonNumber !== undefined && ( - + <$Season> {stringGetter({ key: STRING_KEYS.LAUNCH_INCENTIVES_SEASON_NUM, params: { SEASON_NUMBER: seasonNumber }, })} - + )}
    - - - {data !== undefined && stringGetter({ key: STRING_KEYS.POINTS })} - -
    - - -
    + <$Points> + + {incentivePoints !== undefined && stringGetter({ key: STRING_KEYS.POINTS })} + + + + <$Image src="/rewards-stars.svg" /> + ); }; @@ -140,15 +140,15 @@ const LaunchIncentivesContent = () => { const dispatch = useDispatch(); return ( - - + <$Column> + <$Description> {stringGetter({ key: STRING_KEYS.LAUNCH_INCENTIVES_DESCRIPTION })}{' '} - - + + <$ChaosLabsLogo> {stringGetter({ key: STRING_KEYS.POWERED_BY_ALL_CAPS })} - - - + <$ButtonRow> + <$AboutButton action={ButtonAction.Base} onClick={() => { dispatch( @@ -161,8 +161,8 @@ const LaunchIncentivesContent = () => { slotRight={} > {stringGetter({ key: STRING_KEYS.ABOUT })} - - + <$Button action={ButtonAction.Primary} onClick={() => { dispatch( @@ -176,24 +176,21 @@ const LaunchIncentivesContent = () => { slotLeft={} > {stringGetter({ key: STRING_KEYS.LEADERBOARD })} - - - + + + ); }; - -const Styled: Record = {}; - -Styled.Panel = styled(Panel)` +const $Panel = styled(Panel)` background-color: var(--color-layer-3); width: 100%; `; -Styled.ForV4 = styled.span` +const $ForV4 = styled.span` color: var(--color-text-0); `; -Styled.Title = styled.h3` +const $Title = styled.h3` ${layoutMixins.inlineRow} font: var(--font-medium-book); color: var(--color-text-2); @@ -203,7 +200,7 @@ Styled.Title = styled.h3` } `; -Styled.Description = styled.div` +const $Description = styled.div` color: var(--color-text-0); --link-color: var(--color-text-1); @@ -218,7 +215,7 @@ Styled.Description = styled.div` } `; -Styled.ButtonRow = styled.div` +const $ButtonRow = styled.div` ${layoutMixins.inlineRow} gap: 0.75rem; margin-top: 0.5rem; @@ -228,22 +225,22 @@ Styled.ButtonRow = styled.div` } `; -Styled.Button = styled(Button)` +const $Button = styled(Button)` --button-padding: 0 1rem; `; -Styled.AboutButton = styled(Styled.Button)` +const $AboutButton = styled($Button)` --button-textColor: var(--color-text-2); --button-backgroundColor: var(--color-layer-6); --button-border: solid var(--border-width) var(--color-layer-7); `; -Styled.Column = styled.div` +const $Column = styled.div` ${layoutMixins.flexColumn} gap: 0.5rem; `; -Styled.EstimatedRewardsCard = styled.div` +const $EstimatedRewardsCard = styled.div` ${layoutMixins.spacedRow} padding: 1rem 1.25rem; min-width: 19rem; @@ -263,7 +260,7 @@ Styled.EstimatedRewardsCard = styled.div` } `; -Styled.EstimatedRewardsCardContent = styled.div` +const $EstimatedRewardsCardContent = styled.div` ${layoutMixins.flexColumn} gap: 1rem; height: 100%; @@ -280,16 +277,12 @@ Styled.EstimatedRewardsCardContent = styled.div` } `; -Styled.BackgroundDots = styled.img` - position: absolute; -`; - -Styled.Season = styled.span` +const $Season = styled.span` font: var(--font-small-book); color: var(--color-text-1); `; -Styled.Points = styled.span` +const $Points = styled.span` ${layoutMixins.inlineRow} gap: 0.25rem; font: var(--font-large-book); @@ -300,7 +293,7 @@ Styled.Points = styled.span` } `; -Styled.Image = styled.img` +const $Image = styled.img` position: relative; float: right; @@ -308,14 +301,14 @@ Styled.Image = styled.img` height: auto; `; -Styled.ChaosLabsLogo = styled.span` +const $ChaosLabsLogo = styled.span` display: flex; align-items: center; gap: 0.5em; font: var(--font-tiny-medium); `; -Styled.NewTag = styled(Tag)` +const $NewTag = styled(Tag)` color: var(--color-accent); background-color: var(--color-accent-faded); `; diff --git a/src/pages/token/rewards/MigratePanel.tsx b/src/pages/token/rewards/MigratePanel.tsx index 4593c8f40..ca0ac8286 100644 --- a/src/pages/token/rewards/MigratePanel.tsx +++ b/src/pages/token/rewards/MigratePanel.tsx @@ -1,20 +1,22 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import { useSelector } from 'react-redux'; +import styled from 'styled-components'; -import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; -import { STRING_KEYS } from '@/constants/localization'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; -import { useAccountBalance, useBreakpoints, useStringGetter } from '@/hooks'; +import { useAccountBalance } from '@/hooks/useAccountBalance'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; -import { Details } from '@/components/Details'; -import { Panel } from '@/components/Panel'; import { Button } from '@/components/Button'; +import { Details } from '@/components/Details'; +import { Icon, IconName } from '@/components/Icon'; import { Link } from '@/components/Link'; -import { IconName, Icon } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; +import { Panel } from '@/components/Panel'; import { VerticalSeparator } from '@/components/Separator'; import { Tag } from '@/components/Tag'; import { WithReceipt } from '@/components/WithReceipt'; @@ -42,14 +44,14 @@ export const MigratePanel = ({ className }: { className?: string }) => { }); return isNotTablet ? ( - {stringGetter({ key: STRING_KEYS.MIGRATE })}} + slotHeader={<$Title>{stringGetter({ key: STRING_KEYS.MIGRATE })}} slotRight={ - + <$MigrateAction>
    {stringGetter({ key: STRING_KEYS.AVAILABLE_TO_MIGRATE })}
    - + <$Token type={OutputType.Asset} value={tokenBalance} />
    {import.meta.env.VITE_TOKEN_MIGRATION_URI && ( )} -
    + } > - + <$Description> {stringGetter({ key: STRING_KEYS.MIGRATE_DESCRIPTION })} {stringGetter({ key: STRING_KEYS.LEARN_MORE })} → - -
    + + ) : ( - + <$MobileMigrateHeader>

    {stringGetter({ key: STRING_KEYS.MIGRATE })}

    - + <$VerticalSeparator /> {stringGetter({ key: STRING_KEYS.FROM_TO, params: { FROM: Ethereum, TO: dYdX Chain }, })} - + } > - - + <$WithReceipt slotReceipt={ - + <$InlineRow> {stringGetter({ key: STRING_KEYS.AVAILABLE_TO_MIGRATE })} DYDX - + ), value: , }, @@ -119,21 +121,18 @@ export const MigratePanel = ({ className }: { className?: string }) => { {stringGetter({ key: STRING_KEYS.MIGRATE_NOW })} )} - - + + <$InlineRow> {stringGetter({ key: STRING_KEYS.WANT_TO_LEARN })} {stringGetter({ key: STRING_KEYS.CLICK_HERE })} - - -
    + + + ); }; - -const Styled: Record = {}; - -Styled.MigratePanel = styled(Panel)` +const $MigratePanel = styled(Panel)` width: 100%; background-image: url('/dots-background.svg'); @@ -141,7 +140,7 @@ Styled.MigratePanel = styled(Panel)` background-repeat: no-repeat; `; -Styled.Title = styled.h3` +const $Title = styled.h3` font: var(--font-medium-book); color: var(--color-text-2); @@ -149,7 +148,7 @@ Styled.Title = styled.h3` margin-bottom: -0.5rem; `; -Styled.MigrateAction = styled.div` +const $MigrateAction = styled.div` ${layoutMixins.flexEqualColumns} align-items: center; gap: 1rem; @@ -162,12 +161,12 @@ Styled.MigrateAction = styled.div` border-radius: 0.75rem; `; -Styled.Token = styled(Output)` +const $Token = styled(Output)` font: var(--font-large-book); color: var(--color-text-2); `; -Styled.Description = styled.div` +const $Description = styled.div` color: var(--color-text-0); --link-color: var(--color-text-1); @@ -179,13 +178,13 @@ Styled.Description = styled.div` } `; -Styled.Column = styled.div` +const $Column = styled.div` ${layoutMixins.flexColumn} gap: 1rem; align-items: center; `; -Styled.MobileMigrateHeader = styled.div` +const $MobileMigrateHeader = styled.div` ${layoutMixins.inlineRow} gap: 1ch; @@ -214,7 +213,7 @@ Styled.MobileMigrateHeader = styled.div` } `; -Styled.VerticalSeparator = styled(VerticalSeparator)` +const $VerticalSeparator = styled(VerticalSeparator)` z-index: 1; && { @@ -222,16 +221,16 @@ Styled.VerticalSeparator = styled(VerticalSeparator)` } `; -Styled.Details = styled(Details)` +const $Details = styled(Details)` padding: 0.5rem 1rem; `; -Styled.WithReceipt = styled(WithReceipt)` +const $WithReceipt = styled(WithReceipt)` width: 100%; `; -Styled.InlineRow = styled.div` +const $InlineRow = styled.div` ${layoutMixins.inlineRow} color: var(--color-text-0); --link-color: var(--color-text-1); -`; \ No newline at end of file +`; diff --git a/src/pages/token/rewards/NewMarketsPanel.tsx b/src/pages/token/rewards/NewMarketsPanel.tsx index 26f0c2f65..11d287dd9 100644 --- a/src/pages/token/rewards/NewMarketsPanel.tsx +++ b/src/pages/token/rewards/NewMarketsPanel.tsx @@ -1,23 +1,25 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { isMainnet } from '@/constants/networks'; import { AppRoute, MarketsRoute } from '@/constants/routes'; -import { useStringGetter, useTokenConfigs } from '@/hooks'; -import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; import { useGovernanceVariables } from '@/hooks/useGovernanceVariables'; +import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; + +import { layoutMixins } from '@/styles/layoutMixins'; -import { Panel } from '@/components/Panel'; import { IconName } from '@/components/Icon'; import { IconButton } from '@/components/IconButton'; import { Output, OutputType } from '@/components/Output'; +import { Panel } from '@/components/Panel'; import { Tag } from '@/components/Tag'; import { MustBigNumber } from '@/lib/numbers'; -import { layoutMixins } from '@/styles/layoutMixins'; export const NewMarketsPanel = ({ className }: { className?: string }) => { const stringGetter = useStringGetter(); @@ -36,28 +38,28 @@ export const NewMarketsPanel = ({ className }: { className?: string }) => { + <$Title> {stringGetter({ key: STRING_KEYS.ADD_A_MARKET })} - {stringGetter({ key: STRING_KEYS.NEW })} - + <$NewTag>{stringGetter({ key: STRING_KEYS.NEW })} + } slotRight={ - - + <$IconButton action={ButtonAction.Base} iconName={IconName.Arrow} size={ButtonSize.Small} /> - + } onClick={() => navigate(`${AppRoute.Markets}/${MarketsRoute.New}`)} > - + <$Description> {stringGetter({ key: STRING_KEYS.NEW_MARKET_REWARDS_ENTRY_DESCRIPTION, params: { REQUIRED_NUM_TOKENS: ( - { NATIVE_TOKEN_DENOM: chainTokenLabel, }, })} - + ); }; - -const Styled: Record = {}; - -Styled.Description = styled.div` +const $Description = styled.div` color: var(--color-text-0); `; -Styled.IconButton = styled(IconButton)` +const $IconButton = styled(IconButton)` color: var(--color-text-0); --color-border: var(--color-layer-6); `; -Styled.Arrow = styled.div` +const $Arrow = styled.div` padding-right: 1.5rem; `; -Styled.Title = styled.h3` +const $Title = styled.h3` font: var(--font-medium-book); color: var(--color-text-2); margin-bottom: -1rem; ${layoutMixins.inlineRow} `; -Styled.Output = styled(Output)` +const $Output = styled(Output)` display: inline-block; `; -Styled.NewTag = styled(Tag)` +const $NewTag = styled(Tag)` color: var(--color-accent); background-color: var(--color-accent-faded); `; diff --git a/src/pages/token/rewards/RewardHistoryPanel.tsx b/src/pages/token/rewards/RewardHistoryPanel.tsx index d98f51539..9112af815 100644 --- a/src/pages/token/rewards/RewardHistoryPanel.tsx +++ b/src/pages/token/rewards/RewardHistoryPanel.tsx @@ -1,16 +1,16 @@ import { useCallback, useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; -import { useStringGetter } from '@/hooks'; +import styled from 'styled-components'; import { HISTORICAL_TRADING_REWARDS_PERIODS, HistoricalTradingRewardsPeriod, HistoricalTradingRewardsPeriods, } from '@/constants/abacus'; - -import { isMainnet } from '@/constants/networks'; import { STRING_KEYS } from '@/constants/localization'; +import { isMainnet } from '@/constants/networks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import breakpoints from '@/styles/breakpoints'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -33,23 +33,20 @@ export const RewardHistoryPanel = () => { abacusStateManager.getHistoricalTradingRewardPeriod() || HistoricalTradingRewardsPeriod.WEEKLY ); - const onSelectPeriod = useCallback( - (periodName: string) => { - const selectedPeriod = - HISTORICAL_TRADING_REWARDS_PERIODS[ - periodName as keyof typeof HISTORICAL_TRADING_REWARDS_PERIODS - ]; - setSelectedPeriod(selectedPeriod); - abacusStateManager.setHistoricalTradingRewardPeriod(selectedPeriod); - }, - [setSelectedPeriod, selectedPeriod] - ); + const onSelectPeriod = useCallback((periodName: string) => { + const thisSelectedPeriod = + HISTORICAL_TRADING_REWARDS_PERIODS[ + periodName as keyof typeof HISTORICAL_TRADING_REWARDS_PERIODS + ]; + setSelectedPeriod(thisSelectedPeriod); + abacusStateManager.setHistoricalTradingRewardPeriod(thisSelectedPeriod); + }, []); return ( - + <$Header> + <$Title>

    {stringGetter({ key: STRING_KEYS.REWARD_HISTORY })}

    @@ -58,7 +55,7 @@ export const RewardHistoryPanel = () => { key: STRING_KEYS.REWARD_HISTORY_DESCRIPTION, params: { REWARDS_HISTORY_START_DATE: ( - { }, })} -
    + { value={selectedPeriod.name} onValueChange={onSelectPeriod} /> - + } >
    ); }; - -const Styled: Record = {}; - -Styled.Header = styled.div` +const $Header = styled.div` ${layoutMixins.spacedRow} padding: 1rem 1rem 0; @@ -107,7 +101,7 @@ Styled.Header = styled.div` } `; -Styled.Title = styled.div` +const $Title = styled.div` ${layoutMixins.column} color: var(--color-text-0); font: var(--font-small-book); @@ -118,11 +112,6 @@ Styled.Title = styled.div` } `; -Styled.Content = styled.div` - ${layoutMixins.flexColumn} - gap: 0.75rem; -`; - -Styled.Output = styled(Output)` +const $Output = styled(Output)` display: inline; `; diff --git a/src/pages/token/rewards/RewardsHelpPanel.tsx b/src/pages/token/rewards/RewardsHelpPanel.tsx index 6696e375d..478b6eaa1 100644 --- a/src/pages/token/rewards/RewardsHelpPanel.tsx +++ b/src/pages/token/rewards/RewardsHelpPanel.tsx @@ -1,55 +1,54 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; -import { breakpoints } from '@/styles'; import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; + +import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; -import { useStringGetter } from '@/hooks'; import { Accordion } from '@/components/Accordion'; import { Link } from '@/components/Link'; import { Panel } from '@/components/Panel'; -const REWARDS_LEARN_MORE_LINK = ''; // to be configured - export const RewardsHelpPanel = () => { const stringGetter = useStringGetter(); + const { tradingRewardsLearnMore } = useURLConfigs(); return ( - + <$Header>

    {stringGetter({ key: STRING_KEYS.HELP })}

    - {REWARDS_LEARN_MORE_LINK && ( - + {tradingRewardsLearnMore && ( + {stringGetter({ key: STRING_KEYS.LEARN_MORE })} )} - + } > -
    + ); }; - -const Styled: Record = {}; - -Styled.HelpCard = styled(Panel)` +const $HelpCard = styled(Panel)` --panel-content-paddingX: 0; --panel-content-paddingY: 0; width: 100%; @@ -60,7 +59,7 @@ Styled.HelpCard = styled(Panel)` text-align: start; `; -Styled.Header = styled.div` +const $Header = styled.div` ${layoutMixins.spacedRow} gap: 1ch; @@ -78,7 +77,3 @@ Styled.Header = styled.div` color: var(--color-text-2); } `; - -Styled.Link = styled(Link)` - display: inline-flex; -`; diff --git a/src/pages/token/rewards/RewardsPage.tsx b/src/pages/token/rewards/RewardsPage.tsx index 2935d767b..dbf241db2 100644 --- a/src/pages/token/rewards/RewardsPage.tsx +++ b/src/pages/token/rewards/RewardsPage.tsx @@ -1,11 +1,11 @@ -import styled, { AnyStyledComponent, css } from 'styled-components'; import { useNavigate } from 'react-router-dom'; +import styled, { css } from 'styled-components'; -import { isMainnet } from '@/constants/networks'; import { STRING_KEYS } from '@/constants/localization'; import { AppRoute } from '@/constants/routes'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -14,22 +14,18 @@ import { BackButton } from '@/components/BackButton'; import { DetachedSection } from '@/components/ContentSection'; import { ContentSectionHeader } from '@/components/ContentSectionHeader'; -import { testFlags } from '@/lib/testFlags'; - import { DYDXBalancePanel } from './DYDXBalancePanel'; import { LaunchIncentivesPanel } from './LaunchIncentivesPanel'; import { MigratePanel } from './MigratePanel'; +import { RewardHistoryPanel } from './RewardHistoryPanel'; import { RewardsHelpPanel } from './RewardsHelpPanel'; import { TradingRewardsSummaryPanel } from './TradingRewardsSummaryPanel'; -import { RewardHistoryPanel } from './RewardHistoryPanel'; const RewardsPage = () => { const stringGetter = useStringGetter(); const { isTablet, isNotTablet } = useBreakpoints(); const navigate = useNavigate(); - const showTradingRewards = testFlags.showTradingRewards || !isMainnet; - return (
    {isTablet && ( @@ -39,35 +35,30 @@ const RewardsPage = () => { /> )} - - {import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet && } + <$GridLayout showMigratePanel={import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet}> + {import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet && <$MigratePanel />} {isTablet ? ( - + <$LaunchIncentivesPanel /> ) : ( <> - - + <$LaunchIncentivesPanel /> + <$DYDXBalancePanel /> )} - {showTradingRewards && ( - - - {isTablet && } - - - )} + <$TradingRewardsColumn> + + {isTablet && } + + {isNotTablet && ( - + <$OtherColumn> - + )} - +
    ); @@ -75,20 +66,7 @@ const RewardsPage = () => { export default RewardsPage; -const Styled: Record = {}; - -Styled.MobileHeader = styled.header` - ${layoutMixins.contentSectionDetachedScrollable} - ${layoutMixins.stickyHeader} - z-index: 2; - padding: 1.25rem 0; - - font: var(--font-large-medium); - color: var(--color-text-2); - background-color: var(--color-layer-2); -`; - -Styled.GridLayout = styled.div<{ showTradingRewards?: boolean; showMigratePanel?: boolean }>` +const $GridLayout = styled.div<{ showMigratePanel?: boolean }>` --gap: 1.5rem; display: grid; grid-template-columns: 2fr 1fr; @@ -99,8 +77,8 @@ Styled.GridLayout = styled.div<{ showTradingRewards?: boolean; showMigratePanel? gap: var(--gap); } - ${({ showTradingRewards, showMigratePanel }) => - showTradingRewards && showMigratePanel + ${({ showMigratePanel }) => + showMigratePanel ? css` grid-template-areas: 'migrate migrate' @@ -108,25 +86,11 @@ Styled.GridLayout = styled.div<{ showTradingRewards?: boolean; showMigratePanel? 'balance balance' 'rewards other'; ` - : showTradingRewards - ? css` - grid-template-areas: - 'incentives balance' - 'rewards other'; - ` - : showMigratePanel - ? css` - grid-template-areas: - 'migrate migrate' - 'incentives incentives' - 'balance balance' - 'other other'; - ` : css` grid-template-areas: 'incentives balance' - 'other other'; - `}; + 'rewards other'; + `} @media ${breakpoints.notTablet} { padding: 1rem; @@ -138,48 +102,30 @@ Styled.GridLayout = styled.div<{ showTradingRewards?: boolean; showMigratePanel? width: calc(100vw - 2rem); margin: 0 auto; - ${({ showTradingRewards }) => - showTradingRewards - ? css` - grid-template-areas: - 'incentives' - 'rewards'; - ` - : css` - grid-template-areas: 'incentives'; - `} + grid-template-areas: + 'incentives' + 'rewards'; } `; -Styled.MigratePanel = styled(MigratePanel)` +const $MigratePanel = styled(MigratePanel)` grid-area: migrate; `; -Styled.LaunchIncentivesPanel = styled(LaunchIncentivesPanel)` +const $LaunchIncentivesPanel = styled(LaunchIncentivesPanel)` grid-area: incentives; `; -Styled.DYDXBalancePanel = styled(DYDXBalancePanel)` +const $DYDXBalancePanel = styled(DYDXBalancePanel)` grid-area: balance; `; -Styled.TradingRewardsColumn = styled.div` +const $TradingRewardsColumn = styled.div` grid-area: rewards; ${layoutMixins.flexColumn} `; -Styled.OtherColumn = styled.div<{ showTradingRewards?: boolean }>` +const $OtherColumn = styled.div` grid-area: other; ${layoutMixins.flexColumn} - - ${({ showTradingRewards }) => - !showTradingRewards && - css` - display: grid; - grid-template-columns: repeat(3, 1fr); - - > section:last-of-type { - grid-column: 1 / -1; - } - `} `; diff --git a/src/pages/token/rewards/TradingRewardsSummaryPanel.tsx b/src/pages/token/rewards/TradingRewardsSummaryPanel.tsx index c76940a29..662a18613 100644 --- a/src/pages/token/rewards/TradingRewardsSummaryPanel.tsx +++ b/src/pages/token/rewards/TradingRewardsSummaryPanel.tsx @@ -1,9 +1,13 @@ import { useEffect } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter, useTokenConfigs } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; + import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; @@ -29,29 +33,27 @@ export const TradingRewardsSummaryPanel = () => { return !currentWeekTradingReward ? null : ( {stringGetter({ key: STRING_KEYS.TRADING_REWARDS_SUMMARY })} - } + slotHeader={<$Header>{stringGetter({ key: STRING_KEYS.TRADING_REWARDS_SUMMARY })}} > - - + <$TradingRewardsDetails layout="grid" items={[ { key: 'week', label: ( - + <$Label>

    {stringGetter({ key: STRING_KEYS.THIS_WEEK })}

    -
    + ), value: ( - + <$Column> } + slotRight={<$AssetIcon symbol={chainTokenLabel} />} type={OutputType.Asset} value={currentWeekTradingReward.amount} /> - + <$TimePeriod> { value={currentWeekTradingReward.endedAtInMilliseconds} timeOptions={{ useUTC: true }} /> - - + + ), }, // TODO(@aforaleka): add all-time when supported ]} /> -
    +
    ); }; - -const Styled: Record = {}; - -Styled.Header = styled.div` +const $Header = styled.div` padding: var(--panel-paddingY) var(--panel-paddingX) 0; font: var(--font-medium-book); color: var(--color-text-2); `; -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.flexColumn} gap: 0.75rem; `; -Styled.TradingRewardsDetails = styled(Details)` +const $TradingRewardsDetails = styled(Details)` --details-item-backgroundColor: var(--color-layer-6); grid-template-columns: 1fr; // TODO(@aforaleka): change to 1fr 1fr when all-time is supported @@ -111,14 +110,14 @@ Styled.TradingRewardsDetails = styled(Details)` } `; -Styled.Label = styled.div` +const $Label = styled.div` ${layoutMixins.spacedRow} font: var(--font-base-book); color: var(--color-text-1); `; -Styled.TimePeriod = styled.div` +const $TimePeriod = styled.div` ${layoutMixins.inlineRow} &, output { @@ -127,11 +126,11 @@ Styled.TimePeriod = styled.div` } `; -Styled.Column = styled.div` +const $Column = styled.div` ${layoutMixins.flexColumn} gap: 0.33rem; `; -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` margin-left: 0.5ch; `; diff --git a/src/pages/token/staking/StakingPage.tsx b/src/pages/token/staking/StakingPage.tsx index 91b909687..a2906946c 100644 --- a/src/pages/token/staking/StakingPage.tsx +++ b/src/pages/token/staking/StakingPage.tsx @@ -1,44 +1,45 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { DetachedSection } from '@/components/ContentSection'; import { ContentSectionHeader } from '@/components/ContentSectionHeader'; -import { useStringGetter } from '@/hooks'; +import { DYDXBalancePanel } from '../rewards/DYDXBalancePanel'; import { StakingPanel } from './StakingPanel'; import { StrideStakingPanel } from './StrideStakingPanel'; -import { DYDXBalancePanel } from '../rewards/DYDXBalancePanel'; -import { STRING_KEYS } from '@/constants/localization'; -export default () => { +const StakingPage = () => { const stringGetter = useStringGetter(); return ( - + <$HeaderSection> - + - - - + <$ContentWrapper> + <$Row> + <$InnerRow> - + - - + + ); }; +export default StakingPage; -const Styled: Record = {}; - -Styled.HeaderSection = styled.section` +const $HeaderSection = styled.section` ${layoutMixins.contentSectionDetached} @media ${breakpoints.tablet} { @@ -49,20 +50,20 @@ Styled.HeaderSection = styled.section` } `; -Styled.ContentWrapper = styled.div` +const $ContentWrapper = styled.div` ${layoutMixins.flexColumn} gap: 1.5rem; max-width: 80rem; padding: 0 1rem; `; -Styled.Row = styled.div` +const $Row = styled.div` gap: 1rem; display: grid; grid-template-columns: 2fr 1fr; `; -Styled.InnerRow = styled.div` +const $InnerRow = styled.div` gap: 1rem; display: grid; grid-template-columns: 1fr 1fr; diff --git a/src/pages/token/staking/StakingPanel.tsx b/src/pages/token/staking/StakingPanel.tsx index 1b934c7f5..4df9abebc 100644 --- a/src/pages/token/staking/StakingPanel.tsx +++ b/src/pages/token/staking/StakingPanel.tsx @@ -1,15 +1,14 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; -import { STRING_KEYS } from '@/constants/localization'; import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter, useURLConfigs } from '@/hooks'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; -import { Panel } from '@/components/Panel'; -import { IconName } from '@/components/Icon'; -import { IconButton } from '@/components/IconButton'; import { Link } from '@/components/Link'; +import { Panel } from '@/components/Panel'; import { openDialog } from '@/state/dialogs'; @@ -19,29 +18,26 @@ export const StakingPanel = ({ className }: { className?: string }) => { const { stakingLearnMore } = useURLConfigs(); return ( - - {stringGetter({ key: STRING_KEYS.STAKE_WITH_KEPLR })} - - + <$Header> + <$Title>{stringGetter({ key: STRING_KEYS.STAKE_WITH_KEPLR })} + <$Img src="/third-party/keplr.png" alt={stringGetter({ key: STRING_KEYS.KEPLR })} /> + } onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))} > - + <$Description> {stringGetter({ key: STRING_KEYS.STAKING_DESCRIPTION })} e.stopPropagation()}> {stringGetter({ key: STRING_KEYS.LEARN_MORE })} → - - + + ); }; - -const Styled: Record = {}; - -Styled.Panel = styled(Panel)` +const $Panel = styled(Panel)` align-items: start; header { @@ -50,7 +46,7 @@ Styled.Panel = styled(Panel)` } `; -Styled.Header = styled.div` +const $Header = styled.div` display: flex; flex-direction: row; justify-content: space-between; @@ -58,18 +54,18 @@ Styled.Header = styled.div` width: 100%; `; -Styled.Title = styled.h3` +const $Title = styled.h3` font: var(--font-medium-book); color: var(--color-text-2); `; -Styled.Img = styled.img` +const $Img = styled.img` width: 2rem; height: 2rem; margin-left: 0.5rem; `; -Styled.Description = styled.div` +const $Description = styled.div` color: var(--color-text-0); --link-color: var(--color-text-1); @@ -80,8 +76,3 @@ Styled.Description = styled.div` } } `; - -Styled.IconButton = styled(IconButton)` - color: var(--color-text-0); - --color-border: var(--color-layer-6); -`; diff --git a/src/pages/token/staking/StrideStakingPanel.tsx b/src/pages/token/staking/StrideStakingPanel.tsx index e100523c8..042bdbce7 100644 --- a/src/pages/token/staking/StrideStakingPanel.tsx +++ b/src/pages/token/staking/StrideStakingPanel.tsx @@ -1,11 +1,13 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; -import { STRING_KEYS } from '@/constants/localization'; import { DialogTypes } from '@/constants/dialogs'; -import { useStringGetter, useTokenConfigs, useURLConfigs } from '@/hooks'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; -import { IconButton } from '@/components/IconButton'; import { Link } from '@/components/Link'; import { Panel } from '@/components/Panel'; import { Tag } from '@/components/Tag'; @@ -19,20 +21,20 @@ export const StrideStakingPanel = ({ className }: { className?: string }) => { const { chainTokenLabel } = useTokenConfigs(); return ( - - + <$Header> + <$Title> {stringGetter({ key: STRING_KEYS.LIQUID_STAKE_W_STRIDE })} {stringGetter({ key: STRING_KEYS.NEW })} - - - + + <$Img src="/third-party/stride.png" alt="Stride" /> + } onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavStride }))} > - + <$Description> {stringGetter({ key: STRING_KEYS.LIQUID_STAKE_STRIDE_DESCRIPTION, params: { TOKEN_DENOM: chainTokenLabel }, @@ -40,14 +42,11 @@ export const StrideStakingPanel = ({ className }: { className?: string }) => { e.stopPropagation()}> {stringGetter({ key: STRING_KEYS.LEARN_MORE })} → - - + + ); }; - -const Styled: Record = {}; - -Styled.Panel = styled(Panel)` +const $Panel = styled(Panel)` align-items: start; header { @@ -56,7 +55,7 @@ Styled.Panel = styled(Panel)` } `; -Styled.Header = styled.div` +const $Header = styled.div` display: flex; flex-direction: row; justify-content: space-between; @@ -64,7 +63,7 @@ Styled.Header = styled.div` width: 100%; `; -Styled.Title = styled.h3` +const $Title = styled.h3` font: var(--font-medium-book); color: var(--color-text-2); @@ -73,13 +72,13 @@ Styled.Title = styled.h3` gap: 0.5ch; `; -Styled.Img = styled.img` +const $Img = styled.img` width: 2rem; height: 2rem; margin-left: 0.5rem; `; -Styled.Description = styled.div` +const $Description = styled.div` color: var(--color-text-0); --link-color: var(--color-text-1); @@ -90,8 +89,3 @@ Styled.Description = styled.div` } } `; - -Styled.IconButton = styled(IconButton)` - color: var(--color-text-0); - --color-border: var(--color-layer-6); -`; diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index 602467928..50f8a3e4a 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -1,10 +1,14 @@ -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; + import { shallowEqual, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; +import { AppRoute } from '@/constants/routes'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { AssetIcon } from '@/components/AssetIcon'; import { CollapsibleTabs } from '@/components/CollapsibleTabs'; @@ -12,30 +16,32 @@ import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; import { MobileTabs } from '@/components/Tabs'; import { Tag, TagType } from '@/components/Tag'; import { ToggleGroup } from '@/components/ToggleGroup'; - +import { PositionInfo } from '@/views/PositionInfo'; import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; -// import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable'; import { OrdersTable, OrdersTableColumnKey } from '@/views/tables/OrdersTable'; import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/PositionsTable'; import { calculateHasUncommittedOrders, calculateIsAccountViewOnly, + calculateShouldRenderActionsInPositionsTable, + calculateShouldRenderTriggersInPositionsTable, } from '@/state/accountCalculators'; - import { getCurrentMarketTradeInfoNumbers, getHasUnseenFillUpdates, getHasUnseenOrderUpdates, getTradeInfoNumbers, } from '@/state/accountSelectors'; - +import { getDefaultToAllMarketsInPositionsOrdersFills } from '@/state/configsSelectors'; import { getCurrentMarketAssetId, getCurrentMarketId } from '@/state/perpetualsSelectors'; +import { getSimpleStyledOutputType } from '@/lib/genericFunctionalComponentUtils'; import { isTruthy } from '@/lib/isTruthy'; import { shortenNumberForDisplay } from '@/lib/numbers'; +import { testFlags } from '@/lib/testFlags'; -import { PositionInfo } from '@/views/PositionInfo'; +import { UnopenedIsolatedPositions } from './UnopenedIsolatedPositions'; enum InfoSection { Position = 'Position', @@ -56,30 +62,56 @@ type ElementProps = { export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); + const navigate = useNavigate(); const { isTablet } = useBreakpoints(); - const [view, setView] = useState(PanelView.CurrentMarket); + const allMarkets = useSelector(getDefaultToAllMarketsInPositionsOrdersFills); + const [view, setView] = useState( + allMarkets ? PanelView.AllMarkets : PanelView.CurrentMarket + ); + const [tab, setTab] = useState(InfoSection.Position); + const currentMarketId = useSelector(getCurrentMarketId); const currentMarketAssetId = useSelector(getCurrentMarketAssetId); - const { numTotalPositions, numTotalOpenOrders, numTotalFills, numTotalFundingPayments } = + const { numTotalPositions, numTotalOpenOrders, numTotalFills } = useSelector(getTradeInfoNumbers, shallowEqual) || {}; - const { numOpenOrders, numFills, numFundingPayments } = + const { numOpenOrders, numFills } = useSelector(getCurrentMarketTradeInfoNumbers, shallowEqual) || {}; + const showClosePositionAction = true; + const hasUnseenOrderUpdates = useSelector(getHasUnseenOrderUpdates); const hasUnseenFillUpdates = useSelector(getHasUnseenFillUpdates); const isAccountViewOnly = useSelector(calculateIsAccountViewOnly); + const shouldRenderTriggers = useSelector(calculateShouldRenderTriggersInPositionsTable); + const shouldRenderActions = useSelector( + calculateShouldRenderActionsInPositionsTable(showClosePositionAction) + ); const isWaitingForOrderToIndex = useSelector(calculateHasUncommittedOrders); const showCurrentMarket = isTablet || view === PanelView.CurrentMarket; const fillsTagNumber = shortenNumberForDisplay(showCurrentMarket ? numFills : numTotalFills); - const ordersTagNumber = shortenNumberForDisplay( showCurrentMarket ? numOpenOrders : numTotalOpenOrders ); + const initialPageSize = 10; + + const onViewOrders = useCallback( + (market: string) => { + navigate(`${AppRoute.Trade}/${market}`, { + state: { + from: AppRoute.Trade, + }, + }); + setView(PanelView.CurrentMarket); + setTab(InfoSection.Orders); + }, + [navigate] + ); + const tabItems = useMemo( () => [ { @@ -90,10 +122,11 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions), - content: showCurrentMarket ? ( + content: isTablet ? ( ) : ( { ] : [ PositionsTableColumnKey.Market, - PositionsTableColumnKey.Side, PositionsTableColumnKey.Size, - PositionsTableColumnKey.Leverage, - PositionsTableColumnKey.LiquidationAndOraclePrice, + testFlags.isolatedMargin && PositionsTableColumnKey.Margin, PositionsTableColumnKey.UnrealizedPnl, - PositionsTableColumnKey.RealizedPnl, + !testFlags.isolatedMargin && PositionsTableColumnKey.RealizedPnl, + PositionsTableColumnKey.NetFunding, PositionsTableColumnKey.AverageOpenAndClose, - ] + PositionsTableColumnKey.LiquidationAndOraclePrice, + shouldRenderTriggers && PositionsTableColumnKey.Triggers, + shouldRenderActions && PositionsTableColumnKey.Actions, + ].filter(isTruthy) } + showClosePositionAction={showClosePositionAction} + initialPageSize={initialPageSize} onNavigate={() => setView(PanelView.CurrentMarket)} + navigateToOrders={onViewOrders} /> ), }, @@ -122,7 +160,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { label: stringGetter({ key: STRING_KEYS.ORDERS }), slotRight: isWaitingForOrderToIndex ? ( - + <$LoadingSpinner /> ) : ( ordersTagNumber && ( @@ -148,6 +186,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { !isAccountViewOnly && OrdersTableColumnKey.Actions, ].filter(isTruthy) } + initialPageSize={initialPageSize} /> ), }, @@ -186,6 +225,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { columnWidths={{ [FillsTableColumnKey.TypeAmount]: '100%', }} + initialPageSize={initialPageSize} /> ), }, @@ -202,6 +242,8 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { // ), // }, ], + // TODO - not sure if it's necessary but lots of the actual deps are missing from the deps list + // eslint-disable-next-line react-hooks/exhaustive-deps [ stringGetter, currentMarketId, @@ -216,56 +258,72 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { ] ); + const slotBottom = { + [InfoSection.Position]: testFlags.isolatedMargin && ( + <$UnopenedIsolatedPositions onViewOrders={onViewOrders} /> + ), + [InfoSection.Orders]: null, + [InfoSection.Fills]: null, + [InfoSection.Payments]: null, + }[tab]; + return isTablet ? ( ) : ( - , - label: currentMarketAssetId, - } - : { label: stringGetter({ key: STRING_KEYS.MARKET }) }), - }, - ]} - value={view} - onValueChange={setView} - onInteraction={() => { - setIsOpen?.(true); - }} - /> - } - items={tabItems} - /> + <> + <$CollapsibleTabs + defaultTab={InfoSection.Position} + tab={tab} + setTab={setTab} + defaultOpen={isOpen} + onOpenChange={setIsOpen} + slotToolbar={ + , + label: currentMarketAssetId, + } + : { label: stringGetter({ key: STRING_KEYS.MARKET }) }), + }, + ]} + value={view} + onValueChange={setView} + onInteraction={() => { + setIsOpen?.(true); + }} + /> + } + tabItems={tabItems} + /> + {isOpen && slotBottom} + ); }; -const Styled: Record = {}; - -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` font-size: 1.5em; `; +const collapsibleTabsType = getSimpleStyledOutputType(CollapsibleTabs); -Styled.CollapsibleTabs = styled(CollapsibleTabs)` +const $CollapsibleTabs = styled(CollapsibleTabs)` --tableHeader-backgroundColor: var(--color-layer-3); header { background-color: var(--color-layer-2); } -`; +` as typeof collapsibleTabsType; -Styled.LoadingSpinner = styled(LoadingSpinner)` +const $LoadingSpinner = styled(LoadingSpinner)` --spinner-width: 1rem; `; +const $UnopenedIsolatedPositions = styled(UnopenedIsolatedPositions)` + margin-top: auto; +`; diff --git a/src/pages/trade/InnerPanel.tsx b/src/pages/trade/InnerPanel.tsx index 3061abe37..11f5bd37f 100644 --- a/src/pages/trade/InnerPanel.tsx +++ b/src/pages/trade/InnerPanel.tsx @@ -1,21 +1,22 @@ import { useState } from 'react'; + import { useSelector } from 'react-redux'; import { TradeInputField } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; - -import { getSelectedLocale } from '@/state/localizationSelectors'; -import abacusStateManager from '@/lib/abacus'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { Tabs } from '@/components/Tabs'; - import { MarketDetails } from '@/views/MarketDetails'; import { MarketLinks } from '@/views/MarketLinks'; -import { TvChart } from '@/views/charts/TvChart'; import { DepthChart } from '@/views/charts/DepthChart'; import { FundingChart } from '@/views/charts/FundingChart'; +import { TvChart } from '@/views/charts/TvChart'; + +import { getSelectedLocale } from '@/state/localizationSelectors'; + +import abacusStateManager from '@/lib/abacus'; enum Tab { Price = 'Price', diff --git a/src/pages/trade/MarketSelectorAndStats.tsx b/src/pages/trade/MarketSelectorAndStats.tsx index 65a5e002c..920195606 100644 --- a/src/pages/trade/MarketSelectorAndStats.tsx +++ b/src/pages/trade/MarketSelectorAndStats.tsx @@ -1,12 +1,11 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; import { VerticalSeparator } from '@/components/Separator'; - -import { MarketsDropdown } from '@/views/MarketsDropdown'; import { MarketStatsDetails } from '@/views/MarketStatsDetails'; +import { MarketsDropdown } from '@/views/MarketsDropdown'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketId } from '@/state/perpetualsSelectors'; @@ -16,19 +15,16 @@ export const MarketSelectorAndStats = ({ className }: { className?: string }) => const currentMarketId = useSelector(getCurrentMarketId); return ( - + <$Container className={className}> - + ); }; - -const Styled: Record = {}; - -Styled.Container = styled.div` +const $Container = styled.div` ${layoutMixins.container} display: grid; diff --git a/src/pages/trade/MobileBottomPanel.tsx b/src/pages/trade/MobileBottomPanel.tsx index 0ae965b2e..7578c76f5 100644 --- a/src/pages/trade/MobileBottomPanel.tsx +++ b/src/pages/trade/MobileBottomPanel.tsx @@ -1,8 +1,8 @@ import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; -import { MobileTabs } from '@/components/Tabs'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { MobileTabs } from '@/components/Tabs'; import { MarketDetails } from '@/views/MarketDetails'; import { MarketStatsDetails } from '@/views/MarketStatsDetails'; diff --git a/src/pages/trade/MobileTopPanel.tsx b/src/pages/trade/MobileTopPanel.tsx index 0e72f30e1..49624ac38 100644 --- a/src/pages/trade/MobileTopPanel.tsx +++ b/src/pages/trade/MobileTopPanel.tsx @@ -1,25 +1,26 @@ import { useState } from 'react'; -import { useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; + import { Trigger } from '@radix-ui/react-tabs'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; import { Icon, IconName } from '@/components/Icon'; import { Tabs } from '@/components/Tabs'; import { ToggleButton } from '@/components/ToggleButton'; - -import { getSelectedLocale } from '@/state/localizationSelectors'; - import { AccountInfo } from '@/views/AccountInfo'; import { DepthChart } from '@/views/charts/DepthChart'; -import { Orderbook } from '@/views/tables/Orderbook'; -import { LiveTrades } from '@/views/tables/LiveTrades'; import { FundingChart } from '@/views/charts/FundingChart'; import { TvChart } from '@/views/charts/TvChart'; +import { LiveTrades } from '@/views/tables/LiveTrades'; +import { Orderbook } from '@/views/tables/Orderbook'; + +import { getSelectedLocale } from '@/state/localizationSelectors'; enum Tab { Account = 'Account', @@ -27,15 +28,16 @@ enum Tab { Depth = 'Depth', Funding = 'Funding', OrderBook = 'OrderBook', + // eslint-disable-next-line @typescript-eslint/no-shadow LiveTrades = 'LiveTrades', } const TabButton = ({ value, label, icon }: { value: Tab; label: string; icon: IconName }) => ( - + <$TabButton> {label} - + ); @@ -47,7 +49,7 @@ export const MobileTopPanel = () => { const items = [ { - content: , + content: <$AccountInfo />, label: stringGetter({ key: STRING_KEYS.WALLET }), value: Tab.Account, icon: IconName.Coins, @@ -73,9 +75,9 @@ export const MobileTopPanel = () => { }, { content: ( - + <$ScrollableTableContainer> - + ), label: stringGetter({ key: STRING_KEYS.ORDERBOOK_SHORT }), value: Tab.OrderBook, @@ -83,9 +85,9 @@ export const MobileTopPanel = () => { }, { content: ( - + <$ScrollableTableContainer> - + ), label: stringGetter({ key: STRING_KEYS.RECENT }), value: Tab.LiveTrades, @@ -94,7 +96,7 @@ export const MobileTopPanel = () => { ]; return ( - ({ @@ -108,10 +110,7 @@ export const MobileTopPanel = () => { /> ); }; - -const Styled: Record = {}; - -Styled.Tabs = styled(Tabs)` +const $Tabs = styled(Tabs)` --scrollArea-height: 20rem; --stickyArea0-background: var(--color-layer-2); --tabContent-height: calc(20rem - 2rem - var(--tabs-currentHeight)); @@ -129,9 +128,9 @@ Styled.Tabs = styled(Tabs)` gap: 0.5rem; } } -`; +` as typeof Tabs; -Styled.TabButton = styled(ToggleButton)` +const $TabButton = styled(ToggleButton)` padding: 0 0.5rem; span { @@ -155,11 +154,11 @@ Styled.TabButton = styled(ToggleButton)` } `; -Styled.AccountInfo = styled(AccountInfo)` +const $AccountInfo = styled(AccountInfo)` --account-info-section-height: var(--tabContent-height); `; -Styled.ScrollableTableContainer = styled.div` +const $ScrollableTableContainer = styled.div` ${layoutMixins.scrollArea} --scrollArea-height: var(--tabContent-height); --stickyArea0-topGap: 0px; diff --git a/src/pages/trade/Trade.tsx b/src/pages/trade/Trade.tsx index 96ca26846..986a54db9 100644 --- a/src/pages/trade/Trade.tsx +++ b/src/pages/trade/Trade.tsx @@ -1,35 +1,33 @@ import { useRef, useState } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; + import { useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; + +import { TradeLayouts } from '@/constants/layout'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useCurrentMarketId } from '@/hooks/useCurrentMarketId'; +import { usePageTitlePriceUpdates } from '@/hooks/usePageTitlePriceUpdates'; +import { useTradeFormInputs } from '@/hooks/useTradeFormInputs'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; -import { TradeLayouts } from '@/constants/layout'; - -import { - useBreakpoints, - useCurrentMarketId, - usePageTitlePriceUpdates, - useTradeFormInputs, -} from '@/hooks'; +import { DetachedSection } from '@/components/ContentSection'; +import { AccountInfo } from '@/views/AccountInfo'; +import { TradeBox } from '@/views/TradeBox'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; import { getSelectedTradeLayout } from '@/state/layoutSelectors'; -import { DetachedSection } from '@/components/ContentSection'; - -import { TradeHeaderMobile } from './TradeHeaderMobile'; +import { HorizontalPanel } from './HorizontalPanel'; import { InnerPanel } from './InnerPanel'; import { MarketSelectorAndStats } from './MarketSelectorAndStats'; -import { VerticalPanel } from './VerticalPanel'; -import { HorizontalPanel } from './HorizontalPanel'; import { MobileBottomPanel } from './MobileBottomPanel'; import { MobileTopPanel } from './MobileTopPanel'; import { TradeDialogTrigger } from './TradeDialogTrigger'; - -import { AccountInfo } from '@/views/AccountInfo'; -import { TradeBox } from '@/views/TradeBox'; +import { TradeHeaderMobile } from './TradeHeaderMobile'; +import { VerticalPanel } from './VerticalPanel'; const TradePage = () => { const tradePageRef = useRef(null); @@ -45,7 +43,7 @@ const TradePage = () => { useTradeFormInputs(); return isTablet ? ( - + <$TradeLayoutMobile>
    @@ -63,42 +61,39 @@ const TradePage = () => {
    {canAccountTrade && } -
    + ) : ( - - + <$Top> - + - + <$SideSection gridArea="Side"> - + - + <$GridSection gridArea="Vertical"> - + - + <$GridSection gridArea="Inner"> - + - + <$GridSection gridArea="Horizontal"> - - + + ); }; export default TradePage; - -const Styled: Record = {}; - -Styled.TradeLayout = styled.article<{ +const $TradeLayout = styled.article<{ tradeLayout: TradeLayouts; isHorizontalPanelOpen: boolean; }>` @@ -214,7 +209,7 @@ Styled.TradeLayout = styled.article<{ } `; -Styled.TradeLayoutMobile = styled.article` +const $TradeLayoutMobile = styled.article` ${layoutMixins.contentContainerPage} min-height: 100%; @@ -232,14 +227,14 @@ Styled.TradeLayoutMobile = styled.article` } `; -Styled.Top = styled.header` +const $Top = styled.header` grid-area: Top; `; -Styled.GridSection = styled.section<{ gridArea: string }>` +const $GridSection = styled.section<{ gridArea: string }>` grid-area: ${({ gridArea }) => gridArea}; `; -Styled.SideSection = styled(Styled.GridSection)` +const $SideSection = styled($GridSection)` grid-template-rows: auto minmax(0, 1fr); `; diff --git a/src/pages/trade/TradeDialogTrigger.tsx b/src/pages/trade/TradeDialogTrigger.tsx index e746142b5..be98537e8 100644 --- a/src/pages/trade/TradeDialogTrigger.tsx +++ b/src/pages/trade/TradeDialogTrigger.tsx @@ -1,13 +1,14 @@ import { useState } from 'react'; -import { useSelector, shallowEqual } from 'react-redux'; -import styled, { AnyStyledComponent } from 'styled-components'; + +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { ORDER_TYPE_STRINGS } from '@/constants/trade'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { useStringGetter } from '@/hooks'; +import { layoutMixins } from '@/styles/layoutMixins'; import { Icon, IconName } from '@/components/Icon'; import { OrderSideTag } from '@/components/OrderSideTag'; @@ -26,8 +27,8 @@ export const TradeDialogTrigger = () => { const currentTradeData = useSelector(getInputTradeData, shallowEqual); - const { side, type, summary } = currentTradeData || {}; - const { total } = summary || {}; + const { side, type, summary } = currentTradeData ?? {}; + const { total } = summary ?? {}; const selectedTradeType = getSelectedTradeType(type); const selectedOrderSide = getSelectedOrderSide(side); @@ -38,35 +39,27 @@ export const TradeDialogTrigger = () => { isOpen={isOpen} setIsOpen={setIsOpen} slotTrigger={ - + <$TradeDialogTrigger hasSummary={hasSummary}> {hasSummary ? ( - - + <$TradeSummary> + <$TradeType> {stringGetter({ key: ORDER_TYPE_STRINGS[selectedTradeType].orderTypeKey })} - - - + + <$Output type={OutputType.Fiat} value={total} showSign={ShowSign.None} useGrouping /> + ) : ( stringGetter({ key: STRING_KEYS.TAP_TO_TRADE }) )} - - + <$Icon iconName={IconName.Caret} /> + } /> ); }; - -const Styled: Record = {}; - -Styled.TradeDialogTrigger = styled.div<{ hasSummary?: boolean }>` +const $TradeDialogTrigger = styled.div<{ hasSummary?: boolean }>` ${layoutMixins.stickyFooter} ${layoutMixins.spacedRow} @@ -81,21 +74,21 @@ Styled.TradeDialogTrigger = styled.div<{ hasSummary?: boolean }>` cursor: pointer; `; -Styled.TradeSummary = styled.div` +const $TradeSummary = styled.div` ${layoutMixins.rowColumn} font: var(--font-medium-book); `; -Styled.TradeType = styled.div` +const $TradeType = styled.div` ${layoutMixins.inlineRow} `; -Styled.Output = styled(Output)` +const $Output = styled(Output)` color: var(--color-text-2); font: var(--font-large-book); `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` rotate: 0.5turn; width: 1.5rem; height: 1.5rem; diff --git a/src/pages/trade/TradeHeaderMobile.tsx b/src/pages/trade/TradeHeaderMobile.tsx index 3b895ce7c..d955982e1 100644 --- a/src/pages/trade/TradeHeaderMobile.tsx +++ b/src/pages/trade/TradeHeaderMobile.tsx @@ -1,20 +1,20 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; import { AppRoute } from '@/constants/routes'; -import { layoutMixins } from '@/styles/layoutMixins'; -import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; -import { getCurrentMarketData } from '@/state/perpetualsSelectors'; +import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { BackButton } from '@/components/BackButton'; import { Output, OutputType } from '@/components/Output'; +import { MidMarketPrice } from '@/views/MidMarketPrice'; -import { MustBigNumber } from '@/lib/numbers'; +import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; +import { getCurrentMarketData } from '@/state/perpetualsSelectors'; -import { MidMarketPrice } from '@/views/MidMarketPrice'; +import { MustBigNumber } from '@/lib/numbers'; export const TradeHeaderMobile = () => { const { name, id } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; @@ -24,31 +24,28 @@ export const TradeHeaderMobile = () => { useSelector(getCurrentMarketData, shallowEqual) ?? {}; return ( - + <$Header> navigate(AppRoute.Markets)} /> - - - + <$MarketName> + <$AssetIcon symbol={id} /> + <$Name>

    {name}

    {market} -
    -
    + + - + <$Right> - - -
    + + ); }; - -const Styled: Record = {}; - -Styled.Header = styled.header` +const $Header = styled.header` ${layoutMixins.contentSectionDetachedScrollable} ${layoutMixins.stickyHeader} @@ -64,16 +61,16 @@ Styled.Header = styled.header` background-color: var(--color-layer-2); `; -Styled.MarketName = styled.div` +const $MarketName = styled.div` ${layoutMixins.inlineRow} gap: 1ch; `; -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` font-size: 2.5rem; `; -Styled.Name = styled.div` +const $Name = styled.div` ${layoutMixins.rowColumn} h3 { @@ -86,14 +83,14 @@ Styled.Name = styled.div` } `; -Styled.Right = styled.div` +const $Right = styled.div` margin-left: auto; ${layoutMixins.rowColumn} justify-items: flex-end; `; -Styled.PriceChange = styled(Output)<{ isNegative?: boolean }>` +const $PriceChange = styled(Output)<{ isNegative?: boolean }>` font: var(--font-small-book); color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)}; `; diff --git a/src/pages/trade/UnopenedIsolatedPositions.tsx b/src/pages/trade/UnopenedIsolatedPositions.tsx new file mode 100644 index 000000000..4f3de4809 --- /dev/null +++ b/src/pages/trade/UnopenedIsolatedPositions.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; + +import styled from 'styled-components'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; +import { PotentialPositionCard } from '@/components/PotentialPositionCard'; + +type UnopenedIsolatedPositionsProps = { + className?: string; + onViewOrders: (marketId: string) => void; +}; + +export const UnopenedIsolatedPositions = ({ + className, + onViewOrders, +}: UnopenedIsolatedPositionsProps) => { + const [isOpen, setIsOpen] = useState(false); + return ( + <$UnopenedIsolatedPositions className={className} isOpen={isOpen}> + <$Button isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}> + Unopened Isolated Positions + + + + {isOpen && ( + <$Cards> + + + + + + + + + + + )} + + ); +}; + +const $UnopenedIsolatedPositions = styled.div<{ isOpen?: boolean }>` + overflow: auto; + border-top: var(--border); + ${({ isOpen }) => isOpen && 'height: 100%;'} +`; +const $Button = styled(Button)<{ isOpen?: boolean }>` + position: sticky; + top: 0; + gap: 1rem; + backdrop-filter: blur(4px) contrast(1.01); + background-color: transparent; + border: none; + margin: 0 1rem; + + ${({ isOpen }) => + isOpen && + ` + svg { + transform: rotate(180deg); + } + `} +`; +const $Cards = styled.div` + ${layoutMixins.flexWrap} + gap: 1rem; + scroll-snap-align: none; + padding: 0 1rem 1rem; +`; diff --git a/src/pages/trade/VerticalPanel.tsx b/src/pages/trade/VerticalPanel.tsx index 7a7af7dde..1b4a766de 100644 --- a/src/pages/trade/VerticalPanel.tsx +++ b/src/pages/trade/VerticalPanel.tsx @@ -3,10 +3,9 @@ import { useState } from 'react'; import { TradeLayouts } from '@/constants/layout'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { Tabs } from '@/components/Tabs'; - import { CanvasOrderbook } from '@/views/CanvasOrderbook'; import { LiveTrades } from '@/views/tables/LiveTrades'; @@ -29,8 +28,8 @@ export const VerticalPanel = ({ tradeLayout }: { tradeLayout: TradeLayouts }) => { - setValue(value); + onValueChange={(v: Tab) => { + setValue(v); }} items={[ { diff --git a/src/polyfills.ts b/src/polyfills.ts index 6784beadc..5d1d0df84 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,9 +1,8 @@ import { Buffer } from 'buffer'; -// @ts-ignore -globalThis.process ??= { env: {} }; // Minimal process polyfill -globalThis.global ??= globalThis; -globalThis.Buffer ??= Buffer; +globalThis.process = globalThis.process || { env: {} }; // Minimal process polyfill +globalThis.global = globalThis.global || globalThis; +globalThis.Buffer = globalThis.Buffer || Buffer; declare global { interface WindowEventMap { diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 000000000..e7575c153 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1 @@ +export * from './numia'; diff --git a/src/services/numia.ts b/src/services/numia.ts new file mode 100644 index 000000000..267d97b52 --- /dev/null +++ b/src/services/numia.ts @@ -0,0 +1,23 @@ +type GetChainRevenueRequest = { + startDate: Date; + endDate: Date; +}; + +type ChainRevenue = { + labels: string; + tx_fees: number; + trading_fees: number; + total: number; +}; + +export const getChainRevenue = async ({ startDate, endDate }: GetChainRevenueRequest) => { + const url = new URL(`${import.meta.env.VITE_NUMIA_BASE_URL}/dydx/tokenomics/chain_revenue`); + + url.searchParams.set('start_date', startDate.toISOString().split('T')[0]); + url.searchParams.set('end_date', endDate.toISOString().split('T')[0]); + + const response = await fetch(url); + const data = await response.json(); + + return data as ChainRevenue[]; +}; diff --git a/src/state/_store.ts b/src/state/_store.ts index 2e2b93a58..36b7b1478 100644 --- a/src/state/_store.ts +++ b/src/state/_store.ts @@ -1,20 +1,21 @@ import { configureStore } from '@reduxjs/toolkit'; +// TODO - fix cycle +// eslint-disable-next-line import/no-cycle import abacusStateManager from '@/lib/abacus'; -import appMiddleware from './appMiddleware'; -import localizationMiddleware from './localizationMiddleware'; - import { accountSlice } from './account'; import { appSlice } from './app'; +import appMiddleware from './appMiddleware'; import { assetsSlice } from './assets'; import { configsSlice } from './configs'; import { dialogsSlice } from './dialogs'; import { inputsSlice } from './inputs'; import { layoutSlice } from './layout'; import { localizationSlice } from './localization'; -import { perpetualsSlice } from './perpetuals'; +import localizationMiddleware from './localizationMiddleware'; import { notificationsSlice } from './notifications'; +import { perpetualsSlice } from './perpetuals'; export const commandMenuSlices = [layoutSlice, localizationSlice]; diff --git a/src/state/account.ts b/src/state/account.ts index 57d7a54db..f93c4a4a4 100644 --- a/src/state/account.ts +++ b/src/state/account.ts @@ -2,22 +2,30 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import type { AccountBalance, - SubaccountFill, + Compliance, + HistoricalPnlPeriods, Nullable, + SubAccountHistoricalPNLs, Subaccount, + SubaccountFill, SubaccountFills, SubaccountFundingPayments, - Wallet, SubaccountOrder, SubaccountTransfers, - HistoricalPnlPeriods, - SubAccountHistoricalPNLs, - UsageRestriction, TradingRewards, + UsageRestriction, + Wallet, } from '@/constants/abacus'; - import { OnboardingGuard, OnboardingState } from '@/constants/account'; import { LocalStorageKey } from '@/constants/localStorage'; +import { STRING_KEYS } from '@/constants/localization'; +import { + CancelOrderStatuses, + PlaceOrderStatuses, + type LocalCancelOrderData, + type LocalPlaceOrderData, + type TradeTypes, +} from '@/constants/trade'; import { WalletType } from '@/constants/wallets'; import { getLocalStorage } from '@/lib/localStorage'; @@ -44,8 +52,11 @@ export type AccountState = { latestOrder?: Nullable; historicalPnlPeriod?: HistoricalPnlPeriods; uncommittedOrderClientIds: number[]; + localPlaceOrders: LocalPlaceOrderData[]; + localCancelOrders: LocalCancelOrderData[]; restriction?: Nullable; + compliance?: Compliance; }; const initialState: AccountState = { @@ -82,9 +93,12 @@ const initialState: AccountState = { latestOrder: undefined, uncommittedOrderClientIds: [], historicalPnlPeriod: undefined, + localPlaceOrders: [], + localCancelOrders: [], // Restriction restriction: undefined, + compliance: undefined, }; export const accountSlice = createSlice({ @@ -99,10 +113,27 @@ export const accountSlice = createSlice({ state.fills != null && (action.payload ?? []).some((fill: SubaccountFill) => !existingFillIds.includes(fill.id)); + const filledOrderIds = (action.payload ?? []).map((fill: SubaccountFill) => fill.orderId); + return { ...state, fills: action.payload, hasUnseenFillUpdates: state.hasUnseenFillUpdates || hasNewFillUpdates, + localPlaceOrders: hasNewFillUpdates + ? state.localPlaceOrders.map((order) => + order.submissionStatus < PlaceOrderStatuses.Filled && + order.orderId && + filledOrderIds.includes(order.orderId) + ? { + ...order, + submissionStatus: PlaceOrderStatuses.Filled, + } + : order + ) + : state.localPlaceOrders, + submittedCanceledOrders: hasNewFillUpdates + ? state.localCancelOrders.filter((order) => !filledOrderIds.includes(order.orderId)) + : state.localCancelOrders, }; }, setFundingPayments: (state, action: PayloadAction) => { @@ -112,18 +143,27 @@ export const accountSlice = createSlice({ state.transfers = action.payload; }, setLatestOrder: (state, action: PayloadAction>) => { - const { clientId } = action.payload ?? {}; + const { clientId, id } = action.payload ?? {}; state.latestOrder = action.payload; if (clientId) { state.uncommittedOrderClientIds = state.uncommittedOrderClientIds.filter( - (id) => id !== clientId + (uncommittedClientId) => uncommittedClientId !== clientId + ); + state.localPlaceOrders = state.localPlaceOrders.map((order) => + order.clientId === clientId && order.submissionStatus < PlaceOrderStatuses.Placed + ? { + ...order, + orderId: id, + submissionStatus: PlaceOrderStatuses.Placed, + } + : order ); } }, clearOrder: (state, action: PayloadAction) => ({ ...state, - clearedOrderIds: [...(state.clearedOrderIds || []), action.payload], + clearedOrderIds: [...(state.clearedOrderIds ?? []), action.payload], }), setOnboardingGuard: ( state, @@ -147,6 +187,9 @@ export const accountSlice = createSlice({ setRestrictionType: (state, action: PayloadAction>) => { state.restriction = action.payload; }, + setCompliance: (state, action: PayloadAction) => { + state.compliance = action.payload; + }, setSubaccount: (state, action: PayloadAction>) => { const existingOrderIds = state.subaccount?.orders ? state.subaccount.orders.toArray().map((order) => order.id) @@ -184,12 +227,61 @@ export const accountSlice = createSlice({ setTradingRewards: (state, action: PayloadAction) => { state.tradingRewards = action.payload; }, - addUncommittedOrderClientId: (state, action: PayloadAction) => { - state.uncommittedOrderClientIds.push(action.payload); + placeOrderSubmitted: ( + state, + action: PayloadAction<{ marketId: string; clientId: number; orderType: TradeTypes }> + ) => { + state.localPlaceOrders.push({ + ...action.payload, + submissionStatus: PlaceOrderStatuses.Submitted, + }); + state.uncommittedOrderClientIds.push(action.payload.clientId); }, - removeUncommittedOrderClientId: (state, action: PayloadAction) => { + placeOrderFailed: ( + state, + action: PayloadAction<{ clientId: number; errorStringKey: string }> + ) => { + state.localPlaceOrders = state.localPlaceOrders.map((order) => + order.clientId === action.payload.clientId + ? { + ...order, + errorStringKey: action.payload.errorStringKey, + } + : order + ); state.uncommittedOrderClientIds = state.uncommittedOrderClientIds.filter( - (id) => id !== action.payload + (id) => id !== action.payload.clientId + ); + }, + placeOrderTimeout: (state, action: PayloadAction) => { + if (state.uncommittedOrderClientIds.includes(action.payload)) { + placeOrderFailed({ + clientId: action.payload, + errorStringKey: STRING_KEYS.SOMETHING_WENT_WRONG, + }); + } + }, + cancelOrderSubmitted: (state, action: PayloadAction) => { + state.localCancelOrders.push({ + orderId: action.payload, + submissionStatus: CancelOrderStatuses.Submitted, + }); + }, + cancelOrderConfirmed: (state, action: PayloadAction) => { + state.localCancelOrders = state.localCancelOrders.map((order) => + order.orderId === action.payload + ? { ...order, submissionStatus: CancelOrderStatuses.Canceled } + : order + ); + }, + cancelOrderFailed: ( + state, + action: PayloadAction<{ orderId: string; errorStringKey: string }> + ) => { + state.localCancelOrders.map((order) => + order.orderId === action.payload.orderId + ? { ...order, errorStringKey: action.payload.errorStringKey } + : order ); }, }, @@ -205,6 +297,7 @@ export const { setOnboardingState, setHistoricalPnl, setRestrictionType, + setCompliance, setSubaccount, setWallet, viewedFills, @@ -212,6 +305,10 @@ export const { setBalances, setStakingBalances, setTradingRewards, - addUncommittedOrderClientId, - removeUncommittedOrderClientId, + placeOrderSubmitted, + placeOrderFailed, + placeOrderTimeout, + cancelOrderSubmitted, + cancelOrderConfirmed, + cancelOrderFailed, } = accountSlice.actions; diff --git a/src/state/accountCalculators.ts b/src/state/accountCalculators.ts index e78b4a983..fbbcf7c6e 100644 --- a/src/state/accountCalculators.ts +++ b/src/state/accountCalculators.ts @@ -1,6 +1,7 @@ import { createSelector } from 'reselect'; import { OnboardingState, OnboardingSteps } from '@/constants/account'; +import { ENVIRONMENT_CONFIG_MAP, type DydxNetwork } from '@/constants/networks'; import { getOnboardingGuards, @@ -8,6 +9,9 @@ import { getSubaccountId, getUncommittedOrderClientIds, } from '@/state/accountSelectors'; +import { getSelectedNetwork } from '@/state/appSelectors'; + +import { testFlags } from '@/lib/testFlags'; export const calculateOnboardingStep = createSelector( [getOnboardingState, getOnboardingGuards], @@ -84,3 +88,26 @@ export const calculateIsAccountLoading = createSelector( ); } ); + +/** + * @description calculate whether positions table should render triggers column + */ +export const calculateShouldRenderTriggersInPositionsTable = createSelector( + [calculateIsAccountViewOnly, getSelectedNetwork], + (isAccountViewOnly: boolean, selectedNetwork: DydxNetwork) => + !isAccountViewOnly && ENVIRONMENT_CONFIG_MAP[selectedNetwork].featureFlags.isSlTpEnabled +); + +/** + * @description calculate whether positions table should render actions column + */ +export const calculateShouldRenderActionsInPositionsTable = (isCloseActionShown: boolean) => + createSelector( + [calculateIsAccountViewOnly, calculateShouldRenderTriggersInPositionsTable], + (isAccountViewOnly: boolean, areTriggersRendered: boolean) => { + const hasActionsInColumn = testFlags.isolatedMargin + ? isCloseActionShown + : areTriggersRendered || isCloseActionShown; + return !isAccountViewOnly && hasActionsInColumn; + } + ); diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts index e7390eb0a..d7cee566e 100644 --- a/src/state/accountSelectors.ts +++ b/src/state/accountSelectors.ts @@ -3,24 +3,25 @@ import { OrderSide } from '@dydxprotocol/v4-client-js'; import { createSelector } from 'reselect'; import { - type AbacusOrderStatuses, - type SubaccountOrder, - type SubaccountFill, - type SubaccountFundingPayment, + AbacusOrderSide, AbacusOrderStatus, AbacusPositionSide, - ORDER_SIDES, HistoricalTradingReward, HistoricalTradingRewardsPeriod, + ORDER_SIDES, + type AbacusOrderStatuses, + type SubaccountFill, + type SubaccountFundingPayment, + type SubaccountOrder, } from '@/constants/abacus'; - import { OnboardingState } from '@/constants/account'; -import { getHydratedTradingData } from '@/lib/orders'; -import type { RootState } from './_store'; +import { getHydratedTradingData, isStopLossOrder, isTakeProfitOrder } from '@/lib/orders'; +import { getHydratedPositionData } from '@/lib/positions'; -import { getCurrentMarketId, getPerpetualMarkets } from './perpetualsSelectors'; +import type { RootState } from './_store'; import { getAssets } from './assetsSelectors'; +import { getCurrentMarketId, getPerpetualMarkets } from './perpetualsSelectors'; /** * @param state @@ -54,6 +55,22 @@ export const getSubaccountHistoricalPnl = (state: RootState) => state.account?.h export const getOpenPositions = (state: RootState) => state.account.subaccount?.openPositions?.toArray(); +/** + * @param marketId + * @returns user's position details with the given marketId + */ + +export const getPositionDetails = (marketId: string) => + createSelector( + [getExistingOpenPositions, getAssets, getPerpetualMarkets], + (positions, assets, perpetualMarkets) => { + const matchingPosition = positions?.find((position) => position.id === marketId); + return matchingPosition + ? getHydratedPositionData({ data: matchingPosition, assets, perpetualMarkets }) + : undefined; + } + ); + /** * @param state * @returns list of a subaccount's open positions, excluding the ones in draft, i.e. with NONE position side. @@ -62,6 +79,11 @@ export const getExistingOpenPositions = createSelector([getOpenPositions], (allO allOpenPositions?.filter((position) => position.side.current !== AbacusPositionSide.NONE) ); +export const getOpenPositionFromId = (marketId: string) => + createSelector([getOpenPositions], (allOpenPositions) => + allOpenPositions?.find(({ id }) => id === marketId) + ); + /** * @param state * @returns AccountPositions of the current market @@ -104,22 +126,140 @@ export const getSubaccountUnclearedOrders = createSelector( /** * @param state - * @returns list of orders that are in the open status + * @returns Record of SubaccountOrders indexed by marketId + */ +export const getMarketOrders = (state: RootState): { [marketId: string]: SubaccountOrder[] } => { + const orders = getSubaccountUnclearedOrders(state); + return (orders ?? []).reduce((marketOrders, order) => { + marketOrders[order.marketId] ??= []; + marketOrders[order.marketId].push(order); + return marketOrders; + }, {} as { [marketId: string]: SubaccountOrder[] }); +}; + +/** + * @param state + * @returns SubaccountOrders of the current market + */ +export const getCurrentMarketOrders = createSelector( + [getCurrentMarketId, getMarketOrders], + (currentMarketId, marketOrders): SubaccountOrder[] => + !currentMarketId ? [] : marketOrders[currentMarketId] +); + +/** + * @param state + * @returns list of orders that have not been filled or cancelled */ export const getSubaccountOpenOrders = createSelector([getSubaccountOrders], (orders) => + orders?.filter( + (order) => + order.status !== AbacusOrderStatus.filled && order.status !== AbacusOrderStatus.cancelled + ) +); + +/** + * @param state + * @returns order with the specified id + */ +export const getOrderById = (orderId: string) => + createSelector([getSubaccountOrders], (orders) => orders?.find((order) => order.id === orderId)); + +/** + * @param state + * @returns order with the specified client id + */ +export const getOrderByClientId = (orderClientId: number) => + createSelector([getSubaccountOrders], (orders) => + orders?.find((order) => order.clientId === orderClientId) + ); + +/** + * @param state + * @returns first matching fill with the specified order client id + */ +export const getFillByClientId = (orderClientId: number) => + createSelector([getSubaccountFills, getOrderByClientId(orderClientId)], (fills, order) => + fills?.find((fill) => fill.orderId === order?.id) + ); + +/** + * @param state + * @returns Record of SubaccountOrders that have not been filled or cancelled, indexed by marketId + */ +export const getMarketSubaccountOpenOrders = ( + state: RootState +): { + [marketId: string]: SubaccountOrder[]; +} => { + const orders = getSubaccountOpenOrders(state); + return (orders ?? []).reduce((marketOrders, order) => { + marketOrders[order.marketId] ??= []; + marketOrders[order.marketId].push(order); + return marketOrders; + }, {} as { [marketId: string]: SubaccountOrder[] }); +}; + +/** + * @param state + * @returns list of conditional orders that have not been filled or cancelled for all subaccount positions + */ +export const getSubaccountConditionalOrders = (isSlTpLimitOrdersEnabled: boolean) => + createSelector( + [getMarketSubaccountOpenOrders, getOpenPositions], + (openOrdersByMarketId, positions) => { + const stopLossOrders: SubaccountOrder[] = []; + const takeProfitOrders: SubaccountOrder[] = []; + + positions?.forEach((position) => { + const orderSideForConditionalOrder = + position?.side?.current === AbacusPositionSide.LONG + ? AbacusOrderSide.sell + : AbacusOrderSide.buy; + + const conditionalOrders = openOrdersByMarketId[position.id]; + + conditionalOrders?.forEach((order: SubaccountOrder) => { + if ( + order.side === orderSideForConditionalOrder && + isStopLossOrder(order, isSlTpLimitOrdersEnabled) + ) { + stopLossOrders.push(order); + } else if ( + order.side === orderSideForConditionalOrder && + isTakeProfitOrder(order, isSlTpLimitOrdersEnabled) + ) { + takeProfitOrders.push(order); + } + }); + }); + + return { stopLossOrders, takeProfitOrders }; + } + ); + +/** + * @param state + * @returns list of orders that are in the open status + */ +export const getSubaccountOpenStatusOrders = createSelector([getSubaccountOrders], (orders) => orders?.filter((order) => order.status === AbacusOrderStatus.open) ); -export const getSubaccountOpenOrdersBySideAndPrice = createSelector( - [getSubaccountOpenOrders], +export const getSubaccountOrderSizeBySideAndPrice = createSelector( + [getSubaccountOpenStatusOrders], (openOrders = []) => { - const ordersBySide: Partial>> = {}; - openOrders.forEach((order) => { + const orderSizeBySideAndPrice: Partial>> = {}; + openOrders.forEach((order: SubaccountOrder) => { const side = ORDER_SIDES[order.side.name]; - const byPrice = (ordersBySide[side] ??= {}); - byPrice[order.price] = order; + const byPrice = (orderSizeBySideAndPrice[side] ??= {}); + if (byPrice[order.price]) { + byPrice[order.price] += order.size; + } else { + byPrice[order.price] = order.size; + } }); - return ordersBySide; + return orderSizeBySideAndPrice; } ); @@ -142,6 +282,16 @@ export const getLatestOrderStatus = createSelector( export const getUncommittedOrderClientIds = (state: RootState) => state.account.uncommittedOrderClientIds; +/** + * @returns a list of locally placed orders for the current FE session + */ +export const getLocalPlaceOrders = (state: RootState) => state.account.localPlaceOrders; + +/** + * @returns a list of locally canceled orders for the current FE session + */ +export const getLocalCancelOrders = (state: RootState) => state.account.localCancelOrders; + /** * @param orderId * @returns order details with the given orderId @@ -157,29 +307,6 @@ export const getOrderDetails = (orderId: string) => } ); -/** - * @param state - * @returns Record of SubaccountOrders indexed by marketId - */ -export const getMarketOrders = (state: RootState): { [marketId: string]: SubaccountOrder[] } => { - const orders = getSubaccountUnclearedOrders(state); - return (orders ?? []).reduce((marketOrders, order) => { - marketOrders[order.marketId] ??= []; - marketOrders[order.marketId].push(order); - return marketOrders; - }, {} as { [marketId: string]: SubaccountOrder[] }); -}; - -/** - * @param state - * @returns SubaccountOrders of the current market - */ -export const getCurrentMarketOrders = createSelector( - [getCurrentMarketId, getMarketOrders], - (currentMarketId, marketOrders): SubaccountOrder[] => - !currentMarketId ? [] : marketOrders[currentMarketId] -); - /** * @param state * @returns list of fills for the currently connected subaccount @@ -393,3 +520,18 @@ export const getUsageRestriction = (state: RootState) => state.account.restricti * @returns RestrictionType from the current session */ export const getRestrictionType = (state: RootState) => state.account.restriction?.restriction; + +/** + * @returns compliance status of the current session + */ +export const getComplianceStatus = (state: RootState) => state.account.compliance?.status; + +/** + * @returns compliance status of the current session + */ +export const getComplianceUpdatedAt = (state: RootState) => state.account.compliance?.updatedAt; + +/** + * @returns compliance geo of the current session + */ +export const getGeo = (state: RootState) => state.account.compliance?.geo; diff --git a/src/state/app.ts b/src/state/app.ts index d29bfc63f..ba7f9ca8b 100644 --- a/src/state/app.ts +++ b/src/state/app.ts @@ -10,10 +10,12 @@ import { validateAgainstAvailableEnvironments } from '@/lib/network'; export interface AppState { apiState: AbacusApiState | undefined; pageLoaded: boolean; + initializationError?: string; selectedNetwork: DydxNetwork; } const initialState: AppState = { + initializationError: undefined, apiState: undefined, pageLoaded: false, selectedNetwork: getLocalStorage({ @@ -40,8 +42,16 @@ export const appSlice = createSlice({ ...state, selectedNetwork: action.payload, }), + setInitializationError: (state: AppState, action: PayloadAction) => { + state.initializationError = action.payload; + }, }, }); -export const { initializeLocalization, initializeWebsocket, setApiState, setSelectedNetwork } = - appSlice.actions; +export const { + initializeLocalization, + initializeWebsocket, + setApiState, + setSelectedNetwork, + setInitializationError, +} = appSlice.actions; diff --git a/src/state/appMiddleware.ts b/src/state/appMiddleware.ts index 61ff70b04..f71641aaf 100644 --- a/src/state/appMiddleware.ts +++ b/src/state/appMiddleware.ts @@ -1,17 +1,12 @@ import type { PayloadAction } from '@reduxjs/toolkit'; -import { AbacusApiStatus } from '@/constants/abacus'; -import { DialogTypes } from '@/constants/dialogs'; -import { isDev } from '@/constants/networks'; - -import { setApiState, setSelectedNetwork } from '@/state/app'; +import { setSelectedNetwork } from '@/state/app'; import { resetPerpetualsState } from '@/state/perpetuals'; +// TODO - fix cycle +// eslint-disable-next-line import/no-cycle import abacusStateManager from '@/lib/abacus'; -import { openDialog } from './dialogs'; -import { getActiveDialog } from './dialogsSelectors'; - export default (store: any) => (next: any) => async (action: PayloadAction) => { next(action); @@ -23,21 +18,6 @@ export default (store: any) => (next: any) => async (action: PayloadAction) abacusStateManager.switchNetwork(payload); break; } - case setApiState.type: { - const { status } = payload ?? {}; - const { type: activeDialogType } = getActiveDialog(store.getState()) ?? {}; - - if (status !== AbacusApiStatus.NORMAL && activeDialogType !== DialogTypes.ExchangeOffline) { - store.dispatch( - openDialog({ - type: DialogTypes.ExchangeOffline, - dialogProps: { preventClose: !isDev }, - }) - ); - } - - break; - } default: { break; } diff --git a/src/state/appSelectors.ts b/src/state/appSelectors.ts index 2351386c7..272c68fde 100644 --- a/src/state/appSelectors.ts +++ b/src/state/appSelectors.ts @@ -1,8 +1,10 @@ import { DydxChainId, ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; + import type { RootState } from './_store'; export const getApiState = (state: RootState) => state.app.apiState; export const getSelectedNetwork = (state: RootState) => state.app.selectedNetwork; +export const getInitializationError = (state: RootState) => state.app.initializationError; export const getSelectedDydxChainId = (state: RootState) => ENVIRONMENT_CONFIG_MAP[state.app.selectedNetwork].dydxChainId as DydxChainId; diff --git a/src/state/assetsSelectors.ts b/src/state/assetsSelectors.ts index 9d4b8e3fb..196b460cb 100644 --- a/src/state/assetsSelectors.ts +++ b/src/state/assetsSelectors.ts @@ -1,5 +1,4 @@ import type { RootState } from './_store'; - import { getCurrentMarketData } from './perpetualsSelectors'; /** diff --git a/src/state/configs.ts b/src/state/configs.ts index d4e797788..3117486a5 100644 --- a/src/state/configs.ts +++ b/src/state/configs.ts @@ -1,5 +1,5 @@ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import type { kollections } from '@dydxprotocol/v4-abacus'; +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import type { Configs, FeeDiscount, FeeTier, NetworkConfigs, Nullable } from '@/constants/abacus'; import { LocalStorageKey } from '@/constants/localStorage'; @@ -23,6 +23,11 @@ export enum AppColorMode { RedUp = 'RedUp', } +export enum OtherPreference { + DisplayAllMarketsDefault = 'DisplayAllMarketsDefault', + GasToken = 'GasToken', +} + export interface ConfigsState { appThemeSetting: AppThemeSetting; appColorMode: AppColorMode; @@ -30,6 +35,7 @@ export interface ConfigsState { feeDiscounts?: FeeDiscount[]; network?: NetworkConfigs; hasSeenLaunchIncentives: boolean; + defaultToAllMarketsInPositionsOrdersFills: boolean; } const initialState: ConfigsState = { @@ -48,12 +54,26 @@ const initialState: ConfigsState = { key: LocalStorageKey.HasSeenLaunchIncentives, defaultValue: false, }), + defaultToAllMarketsInPositionsOrdersFills: getLocalStorage({ + key: LocalStorageKey.DefaultToAllMarketsInPositionsOrdersFills, + defaultValue: true, + }), }; export const configsSlice = createSlice({ name: 'Inputs', initialState, reducers: { + setDefaultToAllMarketsInPositionsOrdersFills: ( + state: ConfigsState, + { payload }: PayloadAction + ) => { + setLocalStorage({ + key: LocalStorageKey.DefaultToAllMarketsInPositionsOrdersFills, + value: payload, + }); + state.defaultToAllMarketsInPositionsOrdersFills = payload; + }, setAppThemeSetting: (state: ConfigsState, { payload }: PayloadAction) => { setLocalStorage({ key: LocalStorageKey.SelectedTheme, value: payload }); state.appThemeSetting = payload; @@ -73,5 +93,10 @@ export const configsSlice = createSlice({ }, }); -export const { setAppThemeSetting, setAppColorMode, setConfigs, markLaunchIncentivesSeen } = - configsSlice.actions; +export const { + setDefaultToAllMarketsInPositionsOrdersFills, + setAppThemeSetting, + setAppColorMode, + setConfigs, + markLaunchIncentivesSeen, +} = configsSlice.actions; diff --git a/src/state/configsSelectors.ts b/src/state/configsSelectors.ts index 25cc4d158..3ac7b1bdb 100644 --- a/src/state/configsSelectors.ts +++ b/src/state/configsSelectors.ts @@ -1,5 +1,5 @@ import type { RootState } from './_store'; -import { AppTheme, AppThemeSystemSetting, AppThemeSetting } from './configs'; +import { AppTheme, AppThemeSetting, AppThemeSystemSetting } from './configs'; export const getAppThemeSetting = (state: RootState): AppThemeSetting => state.configs.appThemeSetting; @@ -23,3 +23,6 @@ export const getFeeDiscounts = (state: RootState) => state.configs.feeDiscounts; export const getHasSeenLaunchIncentives = (state: RootState) => state.configs.hasSeenLaunchIncentives; + +export const getDefaultToAllMarketsInPositionsOrdersFills = (state: RootState) => + state.configs.defaultToAllMarketsInPositionsOrdersFills; diff --git a/src/state/inputs.ts b/src/state/inputs.ts index c168c12af..13dd45bc2 100644 --- a/src/state/inputs.ts +++ b/src/state/inputs.ts @@ -2,14 +2,14 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import assign from 'lodash/assign'; import type { + ClosePositionInputs, InputError, Inputs, Nullable, TradeInputs, - ClosePositionInputs, TransferInputs, + TriggerOrdersInputs, } from '@/constants/abacus'; - import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade'; type TradeFormInputs = typeof CLEARED_TRADE_INPUTS & typeof CLEARED_SIZE_INPUTS; @@ -20,6 +20,7 @@ export interface InputsState { tradeFormInputs: TradeFormInputs; tradeInputs?: Nullable; closePositionInputs?: Nullable; + triggerOrdersInputs?: Nullable; transferInputs?: Nullable; } @@ -39,7 +40,8 @@ export const inputsSlice = createSlice({ initialState, reducers: { setInputs: (state, action: PayloadAction>) => { - const { current, errors, trade, closePosition, transfer } = action.payload || {}; + const { current, errors, trade, closePosition, transfer, triggerOrders } = + action.payload ?? {}; return { ...state, @@ -51,6 +53,7 @@ export const inputsSlice = createSlice({ ...transfer, isCctp: !!transfer?.isCctp, } as Nullable, + triggerOrdersInputs: triggerOrders, }; }, diff --git a/src/state/inputsSelectors.ts b/src/state/inputsSelectors.ts index 5b88f64b9..ba3672335 100644 --- a/src/state/inputsSelectors.ts +++ b/src/state/inputsSelectors.ts @@ -27,6 +27,17 @@ export const getTradeSide = (state: RootState) => state.inputs.tradeInputs?.side */ export const getInputTradeOptions = (state: RootState) => state.inputs.tradeInputs?.options; +/** + * @returns The selected MarginMode in TradeInputs. 'CROSS' or 'ISOLATED' + */ +export const getInputTradeMarginMode = (state: RootState) => state.inputs.tradeInputs?.marginMode; + +/** + * @returns The specified targetLeverage for the next placed order + */ +export const getInputTradeTargetLeverage = (state: RootState) => + state.inputs.tradeInputs?.targetLeverage; + /** * @param state * @returns ValidationErrors of the current Input type (Trade or Transfer) @@ -35,7 +46,7 @@ export const getInputErrors = (state: RootState) => state.inputs.inputErrors; /** * @param state - * @returns trade or closePosition transfer, depending on which form was last edited. + * @returns trade, closePosition, transfer, or triggerOrders depending on which form was last edited. */ export const getCurrentInput = (state: RootState) => state.inputs.current; @@ -57,6 +68,12 @@ export const getClosePositionInputErrors = (state: RootState) => { return currentInput === 'closePosition' ? getInputErrors(state) : []; }; +/** + * @param state + * @returns ClosePositionInputs + */ +export const getInputClosePositionData = (state: RootState) => state.inputs.closePositionInputs; + /** * @param state * @returns input errors for Transfer @@ -74,9 +91,18 @@ export const getTransferInputs = (state: RootState) => state.inputs.transferInpu /** * @param state - * @returns ClosePositionInputs + * @returns input errors for TriggerOrders */ -export const getInputClosePositionData = (state: RootState) => state.inputs.closePositionInputs; +export const getTriggerOrdersInputErrors = (state: RootState) => { + const currentInput = state.inputs.current; + return currentInput === 'triggerOrders' ? getInputErrors(state) : []; +}; + +/** + * @param state + * @returns TriggerOrdersInputs + */ +export const getTriggerOrdersInputs = (state: RootState) => state.inputs.triggerOrdersInputs; /** * @returns Data needed for the TradeForm (price, size, summary, input render options, and errors/input validation) @@ -86,7 +112,7 @@ export const useTradeFormData = () => { createSelector( [getInputTradeData, getInputTradeOptions, getTradeInputErrors], (tradeData, tradeOptions, tradeErrors) => { - const { price, size, summary } = tradeData || {}; + const { price, size, summary } = tradeData ?? {}; const { needsLimitPrice, @@ -99,7 +125,7 @@ export const useTradeFormData = () => { postOnlyTooltip, reduceOnlyTooltip, timeInForceOptions, - } = tradeOptions || {}; + } = tradeOptions ?? {}; return { price, diff --git a/src/state/layoutSelectors.ts b/src/state/layoutSelectors.ts index a17002a21..e7841cf26 100644 --- a/src/state/layoutSelectors.ts +++ b/src/state/layoutSelectors.ts @@ -1,4 +1,5 @@ import { TradeLayouts } from '@/constants/layout'; + import type { RootState } from './_store'; /** diff --git a/src/state/localization.ts b/src/state/localization.ts index b7cbefbe4..1a525613d 100644 --- a/src/state/localization.ts +++ b/src/state/localization.ts @@ -1,5 +1,5 @@ -import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import { EN_LOCALE_DATA, LocaleData, SupportedLocales } from '@/constants/localization'; diff --git a/src/state/localizationMiddleware.ts b/src/state/localizationMiddleware.ts index db30e92a8..09f188000 100644 --- a/src/state/localizationMiddleware.ts +++ b/src/state/localizationMiddleware.ts @@ -1,17 +1,17 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; import { LOCALE_DATA, NOTIFICATIONS, SupportedLocale, TOOLTIPS, } from '@dydxprotocol/v4-localization'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { LocalStorageKey } from '@/constants/localStorage'; import { - type LocaleData, SUPPORTED_BASE_TAGS_LOCALE_MAPPING, SupportedLocales, + type LocaleData, } from '@/constants/localization'; -import { LocalStorageKey } from '@/constants/localStorage'; import { initializeLocalization } from '@/state/app'; import { setLocaleData, setLocaleLoaded, setSelectedLocale } from '@/state/localization'; diff --git a/src/state/localizationSelectors.ts b/src/state/localizationSelectors.ts index 7981b204b..94977a76b 100644 --- a/src/state/localizationSelectors.ts +++ b/src/state/localizationSelectors.ts @@ -1,7 +1,12 @@ -import { createSelector } from 'reselect'; import _ from 'lodash'; +import { createSelector } from 'reselect'; -import { EN_LOCALE_DATA, LocaleData, SupportedLocales } from '@/constants/localization'; +import { + EN_LOCALE_DATA, + LocaleData, + StringGetterFunction, + SupportedLocales, +} from '@/constants/localization'; import formatString from '@/lib/formatString'; @@ -31,18 +36,23 @@ export const getSelectedLocale = (state: RootState): SupportedLocales => * @param state * @returns */ -export const getStringGetterForLocaleData = (localeData: LocaleData) => { - return ({ - key, - params = {}, - }: { - key: any; - params?: { [key: string]: string | React.ReactNode }; - }): string | Array => { +export const getStringGetterForLocaleData = ( + localeData: LocaleData, + isLocaleLoaded: boolean +): StringGetterFunction => { + // @ts-expect-error TODO: formatString return doesn't match StringGetterFunction + return (props) => { // Fallback to english whenever a key doesn't exist for other languages - const formattedString: string = _.get(localeData, key) || _.get(EN_LOCALE_DATA, key) || ''; + if (isLocaleLoaded) { + const formattedString: string = + localeData || EN_LOCALE_DATA + ? _.get(localeData, props.key) || _.get(EN_LOCALE_DATA, props.key) + : ''; + + return formatString(formattedString, props?.params); + } - return formatString(formattedString, params); + return ''; }; }; @@ -51,6 +61,6 @@ export const getStringGetterForLocaleData = (localeData: LocaleData) => { * @returns */ export const getLocaleStringGetter = createSelector( - [getSelectedLocaleData], + [getSelectedLocaleData, getIsLocaleLoaded], getStringGetterForLocaleData ); diff --git a/src/state/notifications.ts b/src/state/notifications.ts index 3f2ba4c83..86b0b68e3 100644 --- a/src/state/notifications.ts +++ b/src/state/notifications.ts @@ -1,5 +1,5 @@ -import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import { AbacusNotification } from '@/constants/abacus'; diff --git a/src/state/perpetuals.ts b/src/state/perpetuals.ts index 4a9f675ae..d49f3d4e7 100644 --- a/src/state/perpetuals.ts +++ b/src/state/perpetuals.ts @@ -1,17 +1,16 @@ -import merge from 'lodash/merge'; import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import merge from 'lodash/merge'; import type { + MarketHistoricalFunding, MarketOrderbook, MarketTrade, Nullable, PerpetualMarket, - MarketHistoricalFunding, } from '@/constants/abacus'; - import { Candle, RESOLUTION_MAP } from '@/constants/candles'; import { LocalStorageKey } from '@/constants/localStorage'; -import { DEFAULT_MARKETID } from '@/constants/markets'; +import { DEFAULT_MARKETID, MarketFilters } from '@/constants/markets'; import { getLocalStorage } from '@/lib/localStorage'; import { processOrderbookToCreateMap } from '@/lib/orderbookHelpers'; @@ -35,6 +34,7 @@ export interface PerpetualsState { } >; historicalFundings: Record; + marketFilter: MarketFilters; } const initialState: PerpetualsState = { @@ -45,6 +45,7 @@ const initialState: PerpetualsState = { orderbooks: undefined, orderbooksMap: undefined, historicalFundings: {}, + marketFilter: MarketFilters.ALL, }; const MAX_NUM_LIVE_TRADES = 100; @@ -53,6 +54,9 @@ export const perpetualsSlice = createSlice({ name: 'Perpetuals', initialState, reducers: { + setMarketFilter: (state: PerpetualsState, action: PayloadAction) => { + state.marketFilter = action.payload; + }, setCurrentMarketId: (state: PerpetualsState, action: PayloadAction) => { state.currentMarketId = action.payload; }, @@ -164,4 +168,5 @@ export const { setTvChartResolution, setHistoricalFundings, resetPerpetualsState, + setMarketFilter, } = perpetualsSlice.actions; diff --git a/src/state/perpetualsCalculators.ts b/src/state/perpetualsCalculators.ts index 09fa68cf8..be6756725 100644 --- a/src/state/perpetualsCalculators.ts +++ b/src/state/perpetualsCalculators.ts @@ -1,14 +1,13 @@ import { createSelector } from 'reselect'; +import type { MarketHistoricalFunding } from '@/constants/abacus'; import { FundingDirection } from '@/constants/markets'; import { - getCurrentMarketNextFundingRate, getCurrentMarketHistoricalFundings, + getCurrentMarketNextFundingRate, } from '@/state/perpetualsSelectors'; -import type { MarketHistoricalFunding } from '@/constants/abacus'; - export const calculateFundingRateHistory = createSelector( [getCurrentMarketHistoricalFundings, getCurrentMarketNextFundingRate], (historicalFundings, nextFundingRate) => { diff --git a/src/state/perpetualsSelectors.ts b/src/state/perpetualsSelectors.ts index f83fe78c8..e04e440c6 100644 --- a/src/state/perpetualsSelectors.ts +++ b/src/state/perpetualsSelectors.ts @@ -6,6 +6,11 @@ import { mapCandle } from '@/lib/tradingView/utils'; import type { RootState } from './_store'; +/** + * @returns current market filter applied inside the markets page + */ +export const getMarketFilter = (state: RootState) => state.perpetuals.marketFilter; + /** * @returns marketId of the market the user is currently viewing */ @@ -15,7 +20,7 @@ export const getCurrentMarketId = (state: RootState) => state.perpetuals.current * @returns assetId of the currentMarket */ export const getCurrentMarketAssetId = (state: RootState) => { - const currentMarketId = getCurrentMarketId(state) || ''; + const currentMarketId = getCurrentMarketId(state) ?? ''; return state.perpetuals?.markets?.[currentMarketId]?.assetId; }; @@ -81,7 +86,7 @@ export const getOrderbooks = (state: RootState) => state.perpetuals.orderbooks; export const getCurrentMarketOrderbook = (state: RootState) => { const orderbookData = getOrderbooks(state); const currentMarketId = getCurrentMarketId(state); - return orderbookData?.[currentMarketId || '']; + return orderbookData?.[currentMarketId ?? '']; }; /** @@ -90,7 +95,7 @@ export const getCurrentMarketOrderbook = (state: RootState) => { export const getCurrentMarketOrderbookMap = (state: RootState) => { const orderbookMap = state.perpetuals.orderbooksMap; const currentMarketId = getCurrentMarketId(state); - return orderbookMap?.[currentMarketId || '']; + return orderbookMap?.[currentMarketId ?? '']; }; /** @@ -159,3 +164,10 @@ export const getCurrentMarketNextFundingRate = createSelector( [getCurrentMarketData], (marketData) => marketData?.perpetual?.nextFundingRate ); + +/** + * @param marketId + * @returns sparkline data for specified marketId + */ +export const getPerpetualMarketSparklineData = (marketId: string) => (state: RootState) => + getPerpetualMarkets(state)?.[marketId]?.perpetual?.line?.toArray() ?? []; diff --git a/src/styles/formMixins.ts b/src/styles/formMixins.ts index 841b4f26b..cb955c816 100644 --- a/src/styles/formMixins.ts +++ b/src/styles/formMixins.ts @@ -5,8 +5,8 @@ import { type ThemeProps, } from 'styled-components'; -import { layoutMixins } from './layoutMixins'; import breakpoints from './breakpoints'; +import { layoutMixins } from './layoutMixins'; export const formMixins: Record< string, diff --git a/src/styles/globalStyle.ts b/src/styles/globalStyle.ts index 6038aa6d6..8395ba4d9 100644 --- a/src/styles/globalStyle.ts +++ b/src/styles/globalStyle.ts @@ -3,9 +3,12 @@ import { createGlobalStyle } from 'styled-components'; export const GlobalStyle = createGlobalStyle` :root { --color-white: ${({ theme }) => theme.white}; + --color-black: ${({ theme }) => theme.black}; --color-green: ${({ theme }) => theme.green}; --color-red: ${({ theme }) => theme.red}; + --color-white-faded: ${({ theme }) => theme.whiteFaded}; + --color-layer-0: ${({ theme }) => theme.layer0}; --color-layer-1: ${({ theme }) => theme.layer1}; --color-layer-2: ${({ theme }) => theme.layer2}; diff --git a/src/styles/layoutMixins.ts b/src/styles/layoutMixins.ts index fdc2be43e..943652c2b 100644 --- a/src/styles/layoutMixins.ts +++ b/src/styles/layoutMixins.ts @@ -6,10 +6,207 @@ import { type ThemeProps, } from 'styled-components'; -export const layoutMixins: Record< - string, - FlattenSimpleInterpolation | FlattenInterpolation> -> = { +const withOuterBorder = css` + box-shadow: 0 0 0 var(--border-width) var(--border-color); +`; + +// A standalone column (shrinks to fit container) +const flexColumn = css` + display: flex; + flex-direction: column; + min-width: 0; +`; + +// A content container column +const contentContainer = css` + /* Params */ + --content-container-width: 100%; + --content-max-width: 100%; + + /* Overrides */ + --bordered-content-outer-container-width: var(--content-container-width); + --bordered-content-max-width: var(--content-max-width); + + /* Rules */ + ${() => flexColumn} + isolation: isolate; + /* $ {() => layoutMixins.scrollArea} */ +`; + +// Use as a descendant of layoutMixins.scrollArea +const scrollSnapItem = css` + scroll-snap-align: start; + + scroll-margin-top: var(--stickyArea-totalInsetTop); + scroll-margin-bottom: var(--stickyArea-totalInsetBottom); + scroll-margin-left: var(--stickyArea-totalInsetLeft); + scroll-margin-right: var(--stickyArea-totalInsetRight); +`; + +// Creates a scrollable container that can contain sticky and/or scroll-snapped descendants. +const scrollArea = css` + /* Params */ + --scrollArea-height: 100%; + --scrollArea-width: 100%; + + /* Rules */ + + isolation: isolate; + + height: var(--scrollArea-height); + + position: relative; + overflow: auto; + + /* scroll-snap-type: both proximity; */ + + @media (prefers-reduced-motion: no-preference) { + scroll-behavior: smooth; + } +`; + +// Use within contentContainer or contentContainerPage +const contentSection = css` + ${() => scrollSnapItem} +`; + +// Section that defines its own horizontal scrollArea and does not scroll with the outer scrollArea +// Use within contentContainer or contentContainerPage +const contentSectionDetached = css` + ${() => contentSection} + ${() => stickyLeft} + + max-width: min(var(--content-container-width), var(--content-max-width)); + transition: max-width 0.3s var(--ease-out-expo); +`; + +// An item within a horizontally scrollable container that is unaffected by the horizontal scroll position. +// Use when a sibling is horizontally overflowing (e.g. a table with many columns). +const stickyLeft = css` + z-index: 1; + + position: sticky; + left: var(--stickyArea-totalInsetLeft, 0px); + + transition: left 0.3s var(--ease-out-expo); +`; + +const sticky = css` + /* Params */ + --stickyArea-totalInsetTop: ; + --stickyArea-totalInsetBottom: ; + --stickyArea-totalInsetLeft: ; + --stickyArea-totalInsetRight: ; + + z-index: 1; + + position: sticky; + inset: 0; + top: var(--stickyArea-totalInsetTop, 0px); + bottom: var(--stickyArea-totalInsetBottom, 0px); + left: var(--stickyArea-totalInsetLeft, 0px); + right: var(--stickyArea-totalInsetRight, 0px); + + backdrop-filter: blur(10px); +`; + +/** + * A container for positioning sticky items using simulated padding, width, height and gap properties. + * Use on layoutMixins.scrollArea or as a descendant of layoutMixins.scrollArea + */ +const stickyArea = css` + /* Params */ + --stickyArea-paddingTop: ; + --stickyArea-height: var(--scrollArea-height, 100%); + --stickyArea-topHeight: ; + --stickyArea-topGap: ; + --stickyArea-bottomGap: ; + --stickyArea-paddingBottom: ; + --stickyArea-bottomHeight: ; + --stickyArea-paddingLeft: ; + + --stickyArea-width: var(--scrollArea-width, 100%); + --stickyArea-leftWidth: ; + --stickyArea-leftGap: ; + --stickyArea-paddingRight: ; + --stickyArea-rightGap: ; + --stickyArea-rightWidth: ; + + --stickyArea-background: ; + + /* Computed */ + --stickyArea-totalInsetTop: var(--stickyArea-paddingTop); + --stickyArea-totalInsetBottom: var(--stickyArea-paddingBottom); + --stickyArea-innerHeight: calc( + var(--stickyArea-height, 100%) - + ( + var(--stickyArea-paddingTop, 0px) + var(--stickyArea-topHeight, 0px) + + var(--stickyArea-topGap, 0px) + ) - + ( + var(--stickyArea-paddingBottom, 0px) + var(--stickyArea-bottomHeight, 0px) + + var(--stickyArea-bottomGap, 0px) + ) + ); + + --stickyArea-totalInsetLeft: var(--stickyArea-paddingLeft); + --stickyArea-totalInsetRight: var(--stickyArea-paddingRight); + --stickyArea-innerWidth: calc( + var(--stickyArea-width, 100%) - + ( + var(--stickyArea-paddingLeft, 0px) + var(--stickyArea-leftWidth, 0px) + + var(--stickyArea-leftGap, 0px) + ) - + ( + var(--stickyArea-paddingRight, 0px) + var(--stickyArea-rightWidth, 0px) + + var(--stickyArea-rightGap, 0px) + ) + ); + + /* Rules */ + /* scroll-padding-top: var(--stickyArea-topHeight); +scroll-padding-bottom: var(--stickyArea-bottomHeight); */ + /* scroll-padding-top: var(--stickyArea-totalInsetTop); +scroll-padding-bottom: var(--stickyArea-totalInsetBottom); */ + /* scroll-padding-block-end: 4rem; */ + + /* Firefox: opaque background required for backdrop-filter to work */ + background: var(--stickyArea-background, var(--color-layer-2)); +`; + +// Use as a descendant of layoutMixins.stickyArea +const stickyFooter = css` + ${() => sticky} + min-height: var(--stickyArea-bottomHeight); + flex-shrink: 0; + + ${() => scrollSnapItem} +`; + +// Use with layoutMixins.stickyFooter +const withStickyFooterBackdrop = css` + /* Params */ + --stickyFooterBackdrop-outsetY: ; + --stickyFooterBackdrop-outsetX: ; + + /* Rules */ + backdrop-filter: none; + + &:before { + content: ''; + + z-index: -1; + position: absolute; + inset: calc(-1 * var(--stickyFooterBackdrop-outsetY, 0px)) + calc(-1 * var(--stickyFooterBackdrop-outsetX, 0px)); + + background: linear-gradient(transparent, var(--stickyArea-background)); + + pointer-events: none; + } +`; + +export const layoutMixins = { // A standalone row row: css` display: flex; @@ -39,12 +236,7 @@ export const layoutMixins: Record< grid-template-columns: minmax(0, 1fr); `, - // A standalone column (shrinks to fit container) - flexColumn: css` - display: flex; - flex-direction: column; - min-width: 0; - `, + flexColumn, // A column as a child of a row rowColumn: css` @@ -53,6 +245,11 @@ export const layoutMixins: Record< min-width: max-content; `, + flexWrap: css` + display: flex; + flex-wrap: wrap; + `, + // A column with a fixed header and expanding content expandingColumnWithHeader: css` // Expand if within a flexColumn @@ -168,7 +365,7 @@ export const layoutMixins: Record< inset: 0; background: inherit; - ${() => layoutMixins.withOuterBorder} + ${() => withOuterBorder} pointer-events: none; } @@ -209,26 +406,12 @@ export const layoutMixins: Record< } */ `, - // A content container column - contentContainer: css` - /* Params */ - --content-container-width: 100%; - --content-max-width: 100%; - - /* Overrides */ - --bordered-content-outer-container-width: var(--content-container-width); - --bordered-content-max-width: var(--content-max-width); - - /* Rules */ - ${() => layoutMixins.flexColumn} - isolation: isolate; - /* $ {() => layoutMixins.scrollArea} */ - `, + contentContainer, // A content container with dynamic padding and children with a max width of --content-max-width contentContainerPage: css` /* Overrides */ - ${() => layoutMixins.contentContainer} + ${() => contentContainer} --content-container-width: 100vw; --content-max-width: var(--default-page-content-max-width); @@ -241,8 +424,7 @@ export const layoutMixins: Record< ); /* Rules */ - ${() => layoutMixins.stickyHeaderArea} - ${() => layoutMixins.scrollSnapItem} + ${() => scrollSnapItem} min-height: 100%; /* height: max-content; */ @@ -258,65 +440,28 @@ export const layoutMixins: Record< `, // Section - // Use within contentContainer or contentContainerPage - contentSection: css` - ${() => layoutMixins.scrollSnapItem} - `, + contentSection, // Section containing horizontally-overflowing content that scrolls with the outer scrollArea // Use within contentContainer or contentContainerPage contentSectionAttached: css` - ${() => layoutMixins.contentSection} + ${() => contentSection} min-width: max-content; /* max-width: none; */ `, - // Section that defines its own horizontal scrollArea and does not scroll with the outer scrollArea - // Use within contentContainer or contentContainerPage - contentSectionDetached: css` - ${() => layoutMixins.contentSection} - ${() => layoutMixins.stickyLeft} - - max-width: min(var(--content-container-width), var(--content-max-width)); - transition: max-width 0.3s var(--ease-out-expo); - `, + contentSectionDetached, // Section that defines its own horizontal scrollArea and does not scroll with the outer scrollArea // Use within contentContainer or contentContainerPage contentSectionDetachedScrollable: css` - ${() => layoutMixins.contentSectionDetached} - ${() => layoutMixins.scrollArea} + ${() => contentSectionDetached} + ${() => scrollArea} `, - sticky: css` - /* Params */ - --stickyArea-totalInsetTop: ; - --stickyArea-totalInsetBottom: ; - --stickyArea-totalInsetLeft: ; - --stickyArea-totalInsetRight: ; + sticky, - z-index: 1; - - position: sticky; - inset: 0; - top: var(--stickyArea-totalInsetTop, 0px); - bottom: var(--stickyArea-totalInsetBottom, 0px); - left: var(--stickyArea-totalInsetLeft, 0px); - right: var(--stickyArea-totalInsetRight, 0px); - - backdrop-filter: blur(10px); - `, - - // An item within a horizontally scrollable container that is unaffected by the horizontal scroll position. - // Use when a sibling is horizontally overflowing (e.g. a table with many columns). - stickyLeft: css` - z-index: 1; - - position: sticky; - left: var(--stickyArea-totalInsetLeft, 0px); - - transition: left 0.3s var(--ease-out-expo); - `, + stickyLeft, stickyRight: css` z-index: 1; @@ -327,91 +472,9 @@ export const layoutMixins: Record< transition: right 0.3s var(--ease-out-expo); `, - // Creates a scrollable container that can contain sticky and/or scroll-snapped descendants. - scrollArea: css` - /* Params */ - --scrollArea-height: 100%; - --scrollArea-width: 100%; - - /* Rules */ - - isolation: isolate; - - height: var(--scrollArea-height); - - position: relative; - overflow: auto; - - /* scroll-snap-type: both proximity; */ - - @media (prefers-reduced-motion: no-preference) { - scroll-behavior: smooth; - } - `, - - /** - * A container for positioning sticky items using simulated padding, width, height and gap properties. - * Use on layoutMixins.scrollArea or as a descendant of layoutMixins.scrollArea - */ - stickyArea: css` - /* Params */ - --stickyArea-paddingTop: ; - --stickyArea-height: var(--scrollArea-height, 100%); - --stickyArea-topHeight: ; - --stickyArea-topGap: ; - --stickyArea-bottomGap: ; - --stickyArea-paddingBottom: ; - --stickyArea-bottomHeight: ; - --stickyArea-paddingLeft: ; - - --stickyArea-width: var(--scrollArea-width, 100%); - --stickyArea-leftWidth: ; - --stickyArea-leftGap: ; - --stickyArea-paddingRight: ; - --stickyArea-rightGap: ; - --stickyArea-rightWidth: ; - - --stickyArea-background: ; - - /* Computed */ - --stickyArea-totalInsetTop: var(--stickyArea-paddingTop); - --stickyArea-totalInsetBottom: var(--stickyArea-paddingBottom); - --stickyArea-innerHeight: calc( - var(--stickyArea-height, 100%) - - ( - var(--stickyArea-paddingTop, 0px) + var(--stickyArea-topHeight, 0px) + - var(--stickyArea-topGap, 0px) - ) - - ( - var(--stickyArea-paddingBottom, 0px) + var(--stickyArea-bottomHeight, 0px) + - var(--stickyArea-bottomGap, 0px) - ) - ); - - --stickyArea-totalInsetLeft: var(--stickyArea-paddingLeft); - --stickyArea-totalInsetRight: var(--stickyArea-paddingRight); - --stickyArea-innerWidth: calc( - var(--stickyArea-width, 100%) - - ( - var(--stickyArea-paddingLeft, 0px) + var(--stickyArea-leftWidth, 0px) + - var(--stickyArea-leftGap, 0px) - ) - - ( - var(--stickyArea-paddingRight, 0px) + var(--stickyArea-rightWidth, 0px) + - var(--stickyArea-rightGap, 0px) - ) - ); + scrollArea, - /* Rules */ - /* scroll-padding-top: var(--stickyArea-topHeight); - scroll-padding-bottom: var(--stickyArea-bottomHeight); */ - /* scroll-padding-top: var(--stickyArea-totalInsetTop); - scroll-padding-bottom: var(--stickyArea-totalInsetBottom); */ - /* scroll-padding-block-end: 4rem; */ - - /* Firefox: opaque background required for backdrop-filter to work */ - background: var(--stickyArea-background, var(--color-layer-2)); - `, + stickyArea, /** * A sticky area that may contain nested sticky areas. @@ -445,7 +508,7 @@ export const layoutMixins: Record< --stickyArea0-totalInsetRight: var(--stickyArea0-paddingRight); /* Rules */ - ${() => layoutMixins.stickyArea} + ${() => stickyArea} --stickyArea-height: var(--stickyArea0-height); --stickyArea-totalInsetTop: var(--stickyArea0-totalInsetTop); @@ -531,7 +594,7 @@ export const layoutMixins: Record< ); /* Rules */ - ${() => layoutMixins.stickyArea} + ${() => stickyArea} --stickyArea-height: var(--stickyArea1-height); --stickyArea-totalInsetTop: var(--stickyArea1-totalInsetTop); @@ -616,7 +679,7 @@ export const layoutMixins: Record< ); /* Rules */ - ${() => layoutMixins.stickyArea} + ${() => stickyArea} --stickyArea-height: var(--stickyArea2-height); --stickyArea-totalInsetTop: var(--stickyArea2-totalInsetTop); @@ -701,7 +764,7 @@ export const layoutMixins: Record< ); /* Rules */ - ${() => layoutMixins.stickyArea} + ${() => stickyArea} --stickyArea-height: var(--stickyArea3-height); --stickyArea-totalInsetTop: var(--stickyArea3-totalInsetTop); @@ -730,58 +793,20 @@ export const layoutMixins: Record< // Use as a descendant of layoutMixins.stickyArea stickyHeader: css` - ${() => layoutMixins.sticky} + ${() => sticky} min-height: var(--stickyArea-topHeight); flex-shrink: 0; - ${() => layoutMixins.scrollSnapItem} + ${() => scrollSnapItem} `, - // Use as a descendant of layoutMixins.stickyArea - stickyFooter: css` - ${() => layoutMixins.sticky} - min-height: var(--stickyArea-bottomHeight); - flex-shrink: 0; + stickyFooter, - ${() => layoutMixins.scrollSnapItem} - `, + scrollSnapItem, - // Use as a descendant of layoutMixins.scrollArea - scrollSnapItem: css` - scroll-snap-align: start; - - scroll-margin-top: var(--stickyArea-totalInsetTop); - scroll-margin-bottom: var(--stickyArea-totalInsetBottom); - scroll-margin-left: var(--stickyArea-totalInsetLeft); - scroll-margin-right: var(--stickyArea-totalInsetRight); - `, - - // Use with layoutMixins.stickyFooter - withStickyFooterBackdrop: css` - /* Params */ - --stickyFooterBackdrop-outsetY: ; - --stickyFooterBackdrop-outsetX: ; - - /* Rules */ - backdrop-filter: none; + withStickyFooterBackdrop, - &:before { - content: ''; - - z-index: -1; - position: absolute; - inset: calc(-1 * var(--stickyFooterBackdrop-outsetY, 0px)) - calc(-1 * var(--stickyFooterBackdrop-outsetX, 0px)); - - background: linear-gradient(transparent, var(--stickyArea-background)); - - pointer-events: none; - } - `, - - withOuterBorder: css` - box-shadow: 0 0 0 var(--border-width) var(--border-color); - `, + withOuterBorder, // Show "borders" between and around grid/flex items using gap + box-shadow // Apply to element with display: grid or display: flex @@ -792,7 +817,7 @@ export const layoutMixins: Record< gap: var(--border-width); > * { - ${() => layoutMixins.withOuterBorder} + ${() => withOuterBorder} } `, @@ -845,7 +870,7 @@ export const layoutMixins: Record< inset: 0; border-radius: var(--computed-radius); - ${() => layoutMixins.withOuterBorder} + ${() => withOuterBorder} pointer-events: none; } @@ -897,8 +922,8 @@ export const layoutMixins: Record< align-items: start; > :last-child { - ${() => layoutMixins.stickyFooter} - ${() => layoutMixins.withStickyFooterBackdrop} + ${() => stickyFooter} + ${() => withStickyFooterBackdrop} } `, @@ -916,4 +941,4 @@ export const layoutMixins: Record< min-height: 100%; place-items: center; `, -}; +} satisfies Record>>; diff --git a/src/styles/popoverMixins.ts b/src/styles/popoverMixins.ts index 4945d2ae2..b269d3ef1 100644 --- a/src/styles/popoverMixins.ts +++ b/src/styles/popoverMixins.ts @@ -1,4 +1,10 @@ -import { css, keyframes } from 'styled-components'; +import { + FlattenInterpolation, + FlattenSimpleInterpolation, + ThemeProps, + css, + keyframes, +} from 'styled-components'; import { layoutMixins } from './layoutMixins'; @@ -236,4 +242,4 @@ export const popoverMixins = { color: var(--item-checked-textColor, var(--trigger-textColor, inherit)); } `, -}; +} satisfies Record>>; diff --git a/src/styles/styled.d.ts b/src/styles/styled.d.ts new file mode 100644 index 000000000..d28135ee8 --- /dev/null +++ b/src/styles/styled.d.ts @@ -0,0 +1,5 @@ +import { ThemeColorBase } from '@/constants/styles/colors'; + +declare module 'styled-components' { + export interface DefaultTheme extends ThemeColorBase {} +} diff --git a/src/styles/tableMixins.ts b/src/styles/tableMixins.ts index 0b9e5b40c..9e84ceaee 100644 --- a/src/styles/tableMixins.ts +++ b/src/styles/tableMixins.ts @@ -16,7 +16,10 @@ export const tableMixins: Record< ${layoutMixins.row} gap: 0.5em; - color: var(--color-text-1); + --primary-content-color: var(--color-text-1); + --secondary-content-color: var(--color-text-0); + + color: var(--primary-content-color); text-align: var(--table-cell-currentAlign); justify-content: var(--table-cell-currentAlign); `, @@ -24,16 +27,22 @@ export const tableMixins: Record< /** Use as a direct child of tableMixins.cellContent */ cellContentColumn: css` ${layoutMixins.rowColumn} + gap: 0.125rem; - color: var(--color-text-1); + color: var(--primary-content-color); > * { justify-content: var(--table-cell-currentAlign); } + `, + + cellContentColumnSecondary: css` + ${() => tableMixins.cellContentColumn} + gap: 0; > :nth-child(2) { font: var(--font-mini-book); - color: var(--color-text-0); + color: var(--secondary-content-color); margin-top: 0.125rem; } `, @@ -41,12 +50,13 @@ export const tableMixins: Record< /** Use as a direct child of
    @@ -49,13 +49,10 @@ export const TextStory: Story = () => ( ))} - + ); - -const Styled: Record = {}; - -Styled.Table = styled.table` +const $Table = styled.table` width: 100%; text-align: left; border-collapse: separate; diff --git a/src/styles/themes.ts b/src/styles/themes.ts index cf5d54bdf..ba939ad7e 100644 --- a/src/styles/themes.ts +++ b/src/styles/themes.ts @@ -1,13 +1,18 @@ -import { AppTheme, AppColorMode } from '@/state/configs'; -import type { Theme, ThemeColorBase } from '@/constants/styles/colors'; import { BrightnessFilterToken, ColorToken, OpacityToken } from '@/constants/styles/base'; +import type { Theme, ThemeColorBase } from '@/constants/styles/colors'; + +import { AppColorMode, AppTheme } from '@/state/configs'; + import { generateFadedColorVariant } from '@/lib/styles'; const ClassicThemeBase: ThemeColorBase = { + black: ColorToken.Black, white: ColorToken.White, green: ColorToken.Green1, red: ColorToken.Red2, + whiteFaded: generateFadedColorVariant(ColorToken.White, OpacityToken.Opacity16), + layer0: ColorToken.GrayBlue7, layer1: ColorToken.GrayBlue6, layer2: ColorToken.GrayBlue5, @@ -64,10 +69,13 @@ const ClassicThemeBase: ThemeColorBase = { }; const DarkThemeBase: ThemeColorBase = { + black: ColorToken.Black, white: ColorToken.White, green: ColorToken.Green0, red: ColorToken.Red0, + whiteFaded: generateFadedColorVariant(ColorToken.White, OpacityToken.Opacity16), + layer0: ColorToken.Black, layer1: ColorToken.DarkGray11, layer2: ColorToken.DarkGray13, @@ -124,10 +132,13 @@ const DarkThemeBase: ThemeColorBase = { }; const LightThemeBase: ThemeColorBase = { + black: ColorToken.Black, white: ColorToken.White, green: ColorToken.Green2, red: ColorToken.Red1, + whiteFaded: generateFadedColorVariant(ColorToken.White, OpacityToken.Opacity16), + layer0: ColorToken.White, layer1: ColorToken.LightGray6, layer2: ColorToken.White, diff --git a/src/styles/tradeViewMixins.ts b/src/styles/tradeViewMixins.ts index fa6449810..537bb3eec 100644 --- a/src/styles/tradeViewMixins.ts +++ b/src/styles/tradeViewMixins.ts @@ -10,13 +10,31 @@ export const tradeViewMixins: Record< FlattenSimpleInterpolation | FlattenInterpolation> > = { horizontalTable: css` - --tableCell-padding: 0.5rem 1rem; - --tableHeader-backgroundColor: var(--color-layer-2); + --tableCell-padding: 0.5rem 0.25rem; + --tableStickyRow-backgroundColor: var(--color-layer-2); --tableRow-backgroundColor: var(--color-layer-2); font: var(--font-mini-book); + thead, + tbody { + tr { + td:first-of-type, + th:first-of-type { + --tableCell-padding: 0.5rem 0.25rem 0.5rem 1rem; + } + td:last-of-type, + th:last-of-type { + --tableCell-padding: 0.5rem 1rem 0.5rem 0.25rem; + } + } + } + tbody { font: var(--font-small-book); } + + tfoot { + --tableCell-padding: 0.5rem 1rem 0.5rem 1rem; + } `, }; diff --git a/src/views/AccountInfo.tsx b/src/views/AccountInfo.tsx index e9ee3246d..5de62e587 100644 --- a/src/views/AccountInfo.tsx +++ b/src/views/AccountInfo.tsx @@ -1,9 +1,11 @@ -import styled, { AnyStyledComponent, css } from 'styled-components'; import { useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { OnboardingState } from '@/constants/account'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; @@ -23,11 +25,11 @@ export const AccountInfo: React.FC = ({ className }: StyleProps) => { const canViewAccountInfo = useSelector(calculateCanViewAccount); return ( - + <$AccountInfoSectionContainer className={className} showAccountInfo={canViewAccountInfo}> {onboardingState === OnboardingState.AccountConnected || canViewAccountInfo ? ( ) : ( - + <$DisconnectedAccountInfoContainer>

    {stringGetter({ key: { @@ -37,15 +39,12 @@ export const AccountInfo: React.FC = ({ className }: StyleProps) => { })}

    -
    + )} -
    + ); }; - -const Styled: Record = {}; - -Styled.DisconnectedAccountInfoContainer = styled.div` +const $DisconnectedAccountInfoContainer = styled.div` margin: auto; ${layoutMixins.column} @@ -59,7 +58,7 @@ Styled.DisconnectedAccountInfoContainer = styled.div` } `; -Styled.AccountInfoSectionContainer = styled.div<{ showAccountInfo?: boolean }>` +const $AccountInfoSectionContainer = styled.div<{ showAccountInfo?: boolean }>` ${layoutMixins.column} height: var(--account-info-section-height); min-height: var(--account-info-section-height); diff --git a/src/views/AccountInfo/AccountInfoConnectedState.tsx b/src/views/AccountInfo/AccountInfoConnectedState.tsx index b6b7dbbf2..8c42ca932 100644 --- a/src/views/AccountInfo/AccountInfoConnectedState.tsx +++ b/src/views/AccountInfo/AccountInfoConnectedState.tsx @@ -1,12 +1,17 @@ -import styled, { type AnyStyledComponent, css } from 'styled-components'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import type { Nullable, TradeState } from '@/constants/abacus'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; +import { DydxChainAsset } from '@/constants/wallets'; -import { useAccounts, useBreakpoints, useStringGetter } from '@/hooks'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -14,15 +19,15 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; import { Details } from '@/components/Details'; import { Icon, IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; import { MarginUsageRing } from '@/components/MarginUsageRing'; import { Output, OutputType } from '@/components/Output'; import { UsageBars } from '@/components/UsageBars'; import { WithTooltip } from '@/components/WithTooltip'; -import { openDialog } from '@/state/dialogs'; - import { calculateIsAccountLoading } from '@/state/accountCalculators'; import { getSubaccount } from '@/state/accountSelectors'; +import { openDialog } from '@/state/dialogs'; import { getInputErrors } from '@/state/inputsSelectors'; import { getCurrentMarketId } from '@/state/perpetualsSelectors'; @@ -49,6 +54,7 @@ export const AccountInfoConnectedState = () => { const dispatch = useDispatch(); const { isTablet } = useBreakpoints(); + const { complianceState } = useComplianceState(); const { dydxAccounts } = useAccounts(); @@ -59,7 +65,7 @@ export const AccountInfoConnectedState = () => { const listOfErrors = inputErrors?.map(({ code }: { code: string }) => code); - const { buyingPower, equity, marginUsage, leverage } = subAccount || {}; + const { buyingPower, equity, marginUsage, leverage } = subAccount ?? {}; const hasDiff = (marginUsage?.postOrder !== null && @@ -70,42 +76,60 @@ export const AccountInfoConnectedState = () => { const showHeader = !hasDiff && !isTablet; return ( - + <$ConnectedAccountInfoContainer $showHeader={showHeader}> {!showHeader ? null : ( - + <$Header> {stringGetter({ key: STRING_KEYS.ACCOUNT })} - - + <$Button state={{ isDisabled: !dydxAccounts }} onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))} - shape={ButtonShape.Pill} + shape={ButtonShape.Rectangle} size={ButtonSize.XSmall} > {stringGetter({ key: STRING_KEYS.WITHDRAW })} - - dispatch(openDialog({ type: DialogTypes.Deposit }))} - shape={ButtonShape.Pill} - size={ButtonSize.XSmall} - > - {stringGetter({ key: STRING_KEYS.DEPOSIT })} - - - + + {complianceState === ComplianceStates.FULL_ACCESS && ( + <> + <$Button + state={{ isDisabled: !dydxAccounts }} + onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))} + shape={ButtonShape.Rectangle} + size={ButtonSize.XSmall} + > + {stringGetter({ key: STRING_KEYS.DEPOSIT })} + + + <$IconButton + shape={ButtonShape.Square} + iconName={IconName.Send} + onClick={() => + dispatch( + openDialog({ + type: DialogTypes.Transfer, + dialogProps: { selectedAsset: DydxChainAsset.USDC }, + }) + ) + } + /> + + + )} + + )} - - {!showHeader && !isTablet && ( - + {!showHeader && !isTablet && complianceState === ComplianceStates.FULL_ACCESS && ( + <$CornerButton state={{ isDisabled: !dydxAccounts }} onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))} > - + <$CircleContainer> - - + + )} - { label: stringGetter({ key: STRING_KEYS.LEVERAGE }), type: OutputType.Multiple, value: leverage, - slotRight: , + slotRight: <$UsageBars value={getUsageValue(leverage)} />, }, { key: AccountInfoItem.Equity, @@ -171,14 +195,14 @@ export const AccountInfoConnectedState = () => { key, label: ( - + <$WithUsage> {label} - {hasError ? : slotRight} - + {hasError ? <$CautionIcon iconName={IconName.CautionCircle} /> : slotRight} + ), value: [AccountInfoItem.Leverage, AccountInfoItem.Equity].includes(key) ? ( - + <$Output type={type} value={value?.current} /> ) : ( { showHeader={showHeader} isLoading={isLoading} /> - - + + ); }; - -const Styled: Record = {}; - -Styled.Stack = styled.div` +const $Stack = styled.div` ${layoutMixins.stack} `; -Styled.CornerButton = styled(Button)` +const $CornerButton = styled(Button)` ${layoutMixins.withOuterBorder} z-index: 1; place-self: start end; @@ -221,7 +242,7 @@ Styled.CornerButton = styled(Button)` } `; -Styled.CircleContainer = styled.div` +const $CircleContainer = styled.div` display: inline-flex; align-items: center; @@ -230,11 +251,11 @@ Styled.CircleContainer = styled.div` border-radius: 50%; `; -Styled.CautionIcon = styled(Icon)` +const $CautionIcon = styled(Icon)` color: var(--color-error); `; -Styled.WithUsage = styled.div` +const $WithUsage = styled.div` ${layoutMixins.row} & > :last-child { @@ -246,7 +267,7 @@ Styled.WithUsage = styled.div` } `; -Styled.Details = styled(Details)<{ showHeader?: boolean }>` +const $Details = styled(Details)<{ showHeader?: boolean }>` ${layoutMixins.withOuterAndInnerBorders} clip-path: inset(0.5rem 1px); @@ -270,37 +291,33 @@ Styled.Details = styled(Details)<{ showHeader?: boolean }>` } `; -Styled.UsageBars = styled(UsageBars)` +const $UsageBars = styled(UsageBars)` margin-top: -0.125rem; `; -Styled.Output = styled(Output)<{ isNegative?: boolean }>` +const $Output = styled(Output)<{ isNegative?: boolean }>` color: var(--color-text-1); font: var(--font-base-book); `; -Styled.Header = styled.header` +const $Header = styled.header` ${layoutMixins.spacedRow} font: var(--font-small-book); padding: 0 1.25rem; `; -Styled.TransferButtons = styled.div` +const $TransferButtons = styled.div` ${layoutMixins.inlineRow} gap: 1rem; `; -Styled.ConnectedAccountInfoContainer = styled.div<{ $showHeader?: boolean }>` +const $ConnectedAccountInfoContainer = styled.div<{ $showHeader?: boolean }>` ${layoutMixins.column} @media ${breakpoints.notTablet} { ${layoutMixins.withOuterAndInnerBorders} } - @media ${breakpoints.tablet} { - ${layoutMixins.withInnerBorder} - } - ${({ $showHeader }) => $showHeader && css` @@ -308,7 +325,7 @@ Styled.ConnectedAccountInfoContainer = styled.div<{ $showHeader?: boolean }>` `} `; -Styled.Button = styled(Button)` +const $Button = styled(Button)` margin-right: -0.3rem; svg { @@ -316,3 +333,8 @@ Styled.Button = styled(Button)` height: 1.25em; } `; + +const $IconButton = styled(IconButton)` + --button-padding: 0 0.25rem; + --button-border: solid var(--border-width) var(--color-layer-6); +`; diff --git a/src/views/AccountInfo/AccountInfoDiffOutput.tsx b/src/views/AccountInfo/AccountInfoDiffOutput.tsx index 15f8921ac..7e00b6de4 100644 --- a/src/views/AccountInfo/AccountInfoDiffOutput.tsx +++ b/src/views/AccountInfo/AccountInfoDiffOutput.tsx @@ -1,4 +1,4 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import type { Nullable, TradeState } from '@/constants/abacus'; import { NumberSign } from '@/constants/numbers'; @@ -21,7 +21,7 @@ export const AccountInfoDiffOutput = ({ hasError, isPositive, type, value }: Ele const hasDiffPostOrder = isNumber(postOrderValue) && currentValue !== postOrderValue; return ( - ); }; - -const Styled: Record = {}; - -Styled.DiffOutput = styled(DiffOutput)<{ withDiff?: boolean }>` +const $DiffOutput = styled(DiffOutput)<{ withDiff?: boolean }>` --diffOutput-valueWithDiff-font: var(--font-small-book); --diffOutput-gap: 0.125rem; font: var(--font-base-book); diff --git a/src/views/CanvasOrderbook/OrderbookRow.tsx b/src/views/CanvasOrderbook/OrderbookRow.tsx index 31a3b698c..5d4bd579a 100644 --- a/src/views/CanvasOrderbook/OrderbookRow.tsx +++ b/src/views/CanvasOrderbook/OrderbookRow.tsx @@ -1,11 +1,14 @@ import { forwardRef } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; + +import { BigNumber } from 'bignumber.js'; +import styled, { css } from 'styled-components'; import type { Nullable } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; import { TOKEN_DECIMALS } from '@/constants/numbers'; import { ORDERBOOK_ROW_HEIGHT } from '@/constants/orderbook'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { Output, OutputType } from '@/components/Output'; import { WithTooltip } from '@/components/WithTooltip'; @@ -15,7 +18,7 @@ type StyleProps = { }; type ElementProps = { - spread?: Nullable; + spread?: Nullable; spreadPercent?: Nullable; tickSizeDecimals?: Nullable; }; @@ -41,7 +44,7 @@ export const SpreadRow = forwardRef( const stringGetter = useStringGetter(); return ( - + <$SpreadRow ref={ref} side={side}> {stringGetter({ key: STRING_KEYS.ORDERBOOK_SPREAD })} @@ -53,14 +56,11 @@ export const SpreadRow = forwardRef( - + ); } ); - -const Styled: Record = {}; - -Styled.SpreadRow = styled(OrderbookRow)<{ side?: 'top' | 'bottom' }>` +const $SpreadRow = styled(OrderbookRow)<{ side?: 'top' | 'bottom' }>` height: 2rem; border-top: var(--border); border-bottom: var(--border); diff --git a/src/views/CanvasOrderbook/index.tsx b/src/views/CanvasOrderbook/index.tsx index fa0442a20..9a7d224b1 100644 --- a/src/views/CanvasOrderbook/index.tsx +++ b/src/views/CanvasOrderbook/index.tsx @@ -1,11 +1,11 @@ import { forwardRef, useCallback, useMemo, useRef } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; + import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { Nullable, type PerpetualMarketOrderbookLevel } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; import { ORDERBOOK_HEIGHT, ORDERBOOK_MAX_ROWS_PER_SIDE } from '@/constants/orderbook'; -import { useStringGetter } from '@/hooks'; import { useCalculateOrderbookData, @@ -13,27 +13,25 @@ import { useDrawOrderbook, useSpreadRowScrollListener, } from '@/hooks/Orderbook'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { Canvas } from '@/components/Canvas'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { Tag } from '@/components/Tag'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; -import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors'; import { setTradeFormInputs } from '@/state/inputs'; import { getCurrentInput } from '@/state/inputsSelectors'; +import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors'; import { OrderbookRow, SpreadRow } from './OrderbookRow'; type ElementProps = { maxRowsPerSide?: number; - layout?: 'vertical' | 'horizontal'; }; type StyleProps = { - hideHeader?: boolean; histogramSide?: 'left' | 'right'; - className?: string; }; export const CanvasOrderbook = forwardRef( @@ -42,7 +40,7 @@ export const CanvasOrderbook = forwardRef( histogramSide = 'right', maxRowsPerSide = ORDERBOOK_MAX_ROWS_PER_SIDE, }: ElementProps & StyleProps, - ref + ref: React.ForwardedRef ) => { const { asks, bids, hasOrderbook, histogramRange, spread, spreadPercent } = useCalculateOrderbookData({ @@ -60,23 +58,23 @@ export const CanvasOrderbook = forwardRef( * Slice asks and bids to maxRowsPerSide using empty rows */ const { asksSlice, bidsSlice } = useMemo(() => { - let asksSlice: Array = []; + let newAsksSlice: Array = []; const emptyAskRows = asks.length < maxRowsPerSide ? new Array(maxRowsPerSide - asks.length).fill(undefined) : []; - asksSlice = [...emptyAskRows, ...asks.reverse()]; + newAsksSlice = [...emptyAskRows, ...asks.reverse()]; - let bidsSlice: Array = []; + let newBidsSlice: Array = []; const emptyBidRows = bids.length < maxRowsPerSide ? new Array(maxRowsPerSide - bids.length).fill(undefined) : []; - bidsSlice = [...bids, ...emptyBidRows]; + newBidsSlice = [...bids, ...emptyBidRows]; return { - asksSlice, - bidsSlice, + asksSlice: newAsksSlice, + bidsSlice: newBidsSlice, }; }, [asks, bids]); @@ -122,9 +120,9 @@ export const CanvasOrderbook = forwardRef( }); return ( - - - + <$OrderbookContainer ref={ref}> + <$OrderbookContent $isLoading={!hasOrderbook}> + <$Header> {stringGetter({ key: STRING_KEYS.SIZE })} {id && {id}} @@ -132,10 +130,10 @@ export const CanvasOrderbook = forwardRef( {stringGetter({ key: STRING_KEYS.PRICE })} USD {stringGetter({ key: STRING_KEYS.MINE })} - + {displaySide === 'top' && ( - )} - - - + <$OrderbookWrapper ref={orderbookRef}> + <$OrderbookSideContainer $side="asks"> + <$HoverRows $bottom> {asksSlice.map((row: PerpetualMarketOrderbookLevel | undefined, idx) => row ? ( - { @@ -156,12 +155,13 @@ export const CanvasOrderbook = forwardRef( }} /> ) : ( - + // eslint-disable-next-line react/no-array-index-key + <$Row key={idx} /> ) )} - - - + + <$OrderbookCanvas ref={asksCanvasRef} width="100%" height="100%" /> + - - + <$OrderbookSideContainer $side="bids"> + <$HoverRows> {bidsSlice.map((row: PerpetualMarketOrderbookLevel | undefined, idx) => row ? ( - ) : ( - + // eslint-disable-next-line react/no-array-index-key + <$Row key={idx} /> ) )} - - - - + + <$OrderbookCanvas ref={bidsCanvasRef} width="100%" height="100%" /> + + {displaySide === 'bottom' && ( - )} - + {!hasOrderbook && } - + ); } ); - -const Styled: Record = {}; - -Styled.OrderbookContainer = styled.div` +const $OrderbookContainer = styled.div` display: flex; flex: 1 1 0%; flex-direction: column; overflow: hidden; `; -Styled.OrderbookContent = styled.div<{ $isLoading?: boolean }>` +const $OrderbookContent = styled.div<{ $isLoading?: boolean }>` max-height: 100%; display: flex; flex-direction: column; @@ -225,20 +224,20 @@ Styled.OrderbookContent = styled.div<{ $isLoading?: boolean }>` ${({ $isLoading }) => $isLoading && 'flex: 1;'} `; -Styled.Header = styled(OrderbookRow)` +const $Header = styled(OrderbookRow)` height: 2rem; border-bottom: var(--border); color: var(--color-text-0); `; -Styled.OrderbookWrapper = styled.div` +const $OrderbookWrapper = styled.div` overflow-y: auto; display: flex; flex-direction: column; flex: 1 1 0%; `; -Styled.OrderbookSideContainer = styled.div<{ $side: 'bids' | 'asks' }>` +const $OrderbookSideContainer = styled.div<{ $side: 'bids' | 'asks' }>` min-height: ${ORDERBOOK_HEIGHT}px; ${({ $side }) => css` --accent-color: ${$side === 'bids' ? 'var(--color-positive)' : 'var(--color-negative)'}; @@ -246,7 +245,7 @@ Styled.OrderbookSideContainer = styled.div<{ $side: 'bids' | 'asks' }>` position: relative; `; -Styled.OrderbookCanvas = styled(Canvas)` +const $OrderbookCanvas = styled(Canvas)` width: 100%; height: 100%; position: absolute; @@ -256,14 +255,14 @@ Styled.OrderbookCanvas = styled(Canvas)` font-feature-settings: var(--fontFeature-monoNumbers); `; -Styled.HoverRows = styled.div<{ $bottom?: boolean }>` +const $HoverRows = styled.div<{ $bottom?: boolean }>` position: absolute; width: 100%; ${({ $bottom }) => $bottom && 'bottom: 0;'} `; -Styled.Row = styled(OrderbookRow)<{ onClick?: () => void }>` +const $Row = styled(OrderbookRow)<{ onClick?: () => void }>` ${({ onClick }) => onClick ? css` @@ -279,6 +278,6 @@ Styled.Row = styled(OrderbookRow)<{ onClick?: () => void }>` `} `; -Styled.SpreadRow = styled(SpreadRow)` +const $SpreadRow = styled(SpreadRow)` position: absolute; `; diff --git a/src/views/ExchangeBillboards.tsx b/src/views/ExchangeBillboards.tsx index d6ce3450a..9665d6502 100644 --- a/src/views/ExchangeBillboards.tsx +++ b/src/views/ExchangeBillboards.tsx @@ -1,145 +1,165 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; -import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { TokenRoute } from '@/constants/routes'; + +import { usePerpetualMarketsStats } from '@/hooks/usePerpetualMarketsStats'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; + import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; +import { Button } from '@/components/Button'; import { Output, OutputType } from '@/components/Output'; - -import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; +import { Tag } from '@/components/Tag'; +import { SparklineChart } from '@/components/visx/SparklineChart'; type ExchangeBillboardsProps = { - isSearching: boolean; - searchQuery: string; className?: string; }; -export const ExchangeBillboards: React.FC = ({ - isSearching, - searchQuery, - className, -}) => { +export const ExchangeBillboards: React.FC = () => { const stringGetter = useStringGetter(); - const { isTablet } = useBreakpoints(); - - let volume24HUSDC = 0; - let totalTrades24H = 0; - let openInterestUSDC = 0; - - const perpetualMarkets = useSelector(getPerpetualMarkets, shallowEqual) ?? {}; - - Object.values(perpetualMarkets) - .filter(Boolean) - .forEach(({ oraclePrice, perpetual }) => { - const { volume24H, trades24H, openInterest = 0 } = perpetual || {}; - volume24HUSDC += volume24H ?? 0; - totalTrades24H += trades24H ?? 0; - if (oraclePrice) openInterestUSDC += openInterest * oraclePrice; - }); + const { chainTokenLabel } = useTokenConfigs(); + const { + stats: { volume24HUSDC, openInterestUSDC, feesEarned }, + feesEarnedChart, + } = usePerpetualMarketsStats(); return ( - + <$MarketBillboardsWrapper> {[ { key: 'volume', - labelKey: isTablet ? STRING_KEYS.VOLUME_24H : STRING_KEYS.TRADING_VOLUME, + labelKey: STRING_KEYS.TRADING_VOLUME, + tagKey: STRING_KEYS._24H, value: volume24HUSDC || undefined, fractionDigits: 0, - type: isTablet ? OutputType.CompactFiat : OutputType.Fiat, - subLabelKey: !isTablet && STRING_KEYS.TRADING_VOLUME_LABEL, + type: OutputType.Fiat, }, { key: 'open-interest', labelKey: STRING_KEYS.OPEN_INTEREST, + tagKey: STRING_KEYS.CURRENT, value: openInterestUSDC || undefined, fractionDigits: 0, - type: isTablet ? OutputType.CompactFiat : OutputType.Fiat, - subLabelKey: !isTablet && STRING_KEYS.OPEN_INTEREST_LABEL, + type: OutputType.Fiat, }, { - key: 'trades', - labelKey: isTablet ? STRING_KEYS.TRADES_24H : STRING_KEYS.TRADES, - value: totalTrades24H || undefined, - type: isTablet ? OutputType.CompactNumber : OutputType.Number, - subLabelKey: !isTablet && STRING_KEYS.TRADES_LABEL, + key: 'fee-earned-stakers', + labelKey: STRING_KEYS.EARNED_BY_STAKERS, + tagKey: STRING_KEYS._24H, + value: feesEarned, + type: OutputType.Fiat, + chartData: feesEarnedChart, + linkLabelKey: STRING_KEYS.LEARN_MORE_ARROW, + link: `${chainTokenLabel}/${TokenRoute.StakingRewards}`, + slotLeft: '~', }, - ].map(({ key, labelKey, value, fractionDigits, type, subLabelKey }) => ( - - - - {subLabelKey &&

    {stringGetter({ key: subLabelKey })}

    } -
    - ))} -
    + ].map( + ({ + key, + labelKey, + tagKey, + value, + fractionDigits, + type, + chartData, + link, + linkLabelKey, + slotLeft, + }) => ( + <$BillboardContainer key={key}> + <$BillboardStat> + <$BillboardTitle> + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + {stringGetter({ key: tagKey })} + + <$Output + useGrouping + withBaseFont + fractionDigits={fractionDigits} + type={type} + value={value} + slotLeft={slotLeft} + /> + {link && linkLabelKey ? ( + <$BillboardLink + href={link} + size={ButtonSize.Small} + type={ButtonType.Link} + action={ButtonAction.Navigation} + > + {stringGetter({ key: linkLabelKey })} + + ) : null} + + {chartData ? ( + <$BillboardChart> + datum.x} + yAccessor={(datum) => datum.y} + positive + /> + + ) : ( + false + )} + + ) + )} + ); }; -const Styled: Record = {}; - -Styled.MarketBillboardsWrapper = styled.div` +const $MarketBillboardsWrapper = styled.div` + ${layoutMixins.column} + gap: 1rem; +`; +const $BillboardContainer = styled.div` ${layoutMixins.row} + flex: 1; + justify-content: space-between; - gap: 1.75rem; - - padding: 1rem; - - @media ${breakpoints.tablet} { - gap: 0.625rem; + background-color: var(--color-layer-3); + padding: 1.5rem; + border-radius: 0.625rem; +`; +const $BillboardChart = styled.div` + width: 130px; + height: 40px; +`; +const $BillboardLink = styled(Button)` + --button-textColor: var(--color-accent); + --button-height: unset; + --button-padding: 0; + justify-content: flex-start; +`; +const $BillboardTitle = styled.div` + ${layoutMixins.row} - padding: 0 1rem; - } + gap: 0.375rem; `; +const $BillboardStat = styled.div` + ${layoutMixins.column} -Styled.BillboardContainer = styled.div` - ${layoutMixins.rowColumn} - width: 17.5rem; + gap: 0.5rem; label { - margin-bottom: 1rem; - } - - p { - margin-top: 0.25rem; - } - - label, - p { - font: var(--font-base-book); color: var(--color-text-0); + font: var(--font-base-medium); } - &:not(:last-child) { - border-right: solid var(--border-width) var(--color-border); - } - - @media ${breakpoints.tablet} { - padding: 0.625rem 0.75rem; - - background-color: var(--color-layer-3); - border-radius: 0.625rem; - - &:not(:last-child) { - border-right: none; - } - - label { - margin-bottom: 0.5rem; - color: var(--color-text-1); - - font: var(--font-mini-book); - } + output { + color: var(--color-text-1); + font: var(--font-large-medium); } `; - -Styled.Output = styled(Output)` +const $Output = styled(Output)` font: var(--font-extra-book); color: var(--color-text-2); diff --git a/src/views/ExportHistoryDropdown.tsx b/src/views/ExportHistoryDropdown.tsx new file mode 100644 index 000000000..2f0c67217 --- /dev/null +++ b/src/views/ExportHistoryDropdown.tsx @@ -0,0 +1,264 @@ +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; +import { STRING_KEYS } from '@/constants/localization'; + +import { shallowEqual, useSelector } from 'react-redux'; +import { getSubaccountFills, getSubaccountTransfers } from '@/state/accountSelectors'; +import { useCallback, useMemo, useState } from 'react'; +import { OutputType, formatNumber, formatTimestamp } from '@/components/Output'; +import { MustBigNumber } from '@/lib/numbers'; +import { OrderSide } from '@dydxprotocol/v4-client-js'; +import { getSelectedLocale } from '@/state/localizationSelectors'; +import { exportCSV } from '@/lib/csv'; +import { useLocaleSeparators, useStringGetter } from '@/hooks'; +import { ButtonAction, ButtonSize } from '@/constants/buttons'; +import { DropdownMenu, DropdownMenuProps } from '@/components/DropdownMenu'; +import { Checkbox } from '@/components/Checkbox'; +import styled, { AnyStyledComponent } from 'styled-components'; +import { track } from '@/lib/analytics'; +import { AnalyticsEvent } from '@/constants/analytics'; + +export const ExportHistoryDropdown = (props: DropdownMenuProps) => { + const { items = [], ...rest } = props; + const selectedLocale = useSelector(getSelectedLocale); + const stringGetter = useStringGetter(); + const allTransfers = useSelector(getSubaccountTransfers, shallowEqual) ?? []; + const allFills = useSelector(getSubaccountFills, shallowEqual) ?? []; + const { decimal: localeDecimalSeparator, group: localeGroupSeparator } = useLocaleSeparators(); + const [checkedTrades, setCheckedTrades] = useState(true); + const [checkedTransfers, setCheckedTransfers] = useState(true); + + const trades = useMemo( + () => + allFills.map((fill) => { + const { sign: feeSign, formattedString: feeString } = formatNumber({ + type: OutputType.Fiat, + value: fill.fee, + localeDecimalSeparator, + localeGroupSeparator, + }); + + const { sign: totalSign, formattedString: totalString } = formatNumber({ + type: OutputType.Fiat, + value: MustBigNumber(fill.price).times(fill.size), + localeDecimalSeparator, + localeGroupSeparator, + }); + + const { displayString } = formatTimestamp({ + type: OutputType.DateTime, + value: fill.createdAtMilliseconds, + locale: selectedLocale, + }); + + const sideKey = { + [OrderSide.BUY]: STRING_KEYS.BUY, + [OrderSide.SELL]: STRING_KEYS.SELL, + }[fill.side.rawValue]; + + return { + type: fill.resources.typeStringKey && stringGetter({ key: fill.resources.typeStringKey }), + liquidity: + fill.resources.liquidityStringKey && + stringGetter({ key: fill.resources.liquidityStringKey }), + time: displayString, + amount: fill.size, + fee: feeSign ? `${feeSign}${feeString}` : feeString, + total: totalSign ? `${totalSign}${totalString}` : totalString, + market: fill.marketId, + side: sideKey + ? stringGetter({ + key: sideKey, + }) + : '', + }; + }), + [allFills, stringGetter, localeDecimalSeparator, localeGroupSeparator] + ); + + const transfers = useMemo( + () => + allTransfers.map((transfer) => { + const { sign, formattedString } = formatNumber({ + type: OutputType.Fiat, + value: transfer.amount, + localeDecimalSeparator, + localeGroupSeparator, + }); + + const { displayString } = formatTimestamp({ + type: OutputType.DateTime, + value: transfer.updatedAtMilliseconds, + locale: selectedLocale, + }); + + return { + time: displayString, + action: + transfer.resources.typeStringKey && + stringGetter({ key: transfer.resources.typeStringKey }), + sender: transfer.fromAddress, + recipient: transfer.toAddress, + amount: sign ? `${sign}${formattedString}` : formattedString, + transaction: transfer.transactionHash, + }; + }), + [allTransfers, stringGetter, localeDecimalSeparator, localeGroupSeparator] + ); + + const exportTrades = useCallback(() => { + exportCSV(trades, { + filename: 'trades', + columnHeaders: [ + { + key: 'time', + displayLabel: stringGetter({ key: STRING_KEYS.TIME }), + }, + { + key: 'market', + displayLabel: stringGetter({ key: STRING_KEYS.MARKET }), + }, + { + key: 'side', + displayLabel: stringGetter({ key: STRING_KEYS.SIDE }), + }, + { + key: 'amount', + displayLabel: stringGetter({ key: STRING_KEYS.AMOUNT }), + }, + { + key: 'total', + displayLabel: stringGetter({ key: STRING_KEYS.TOTAL }), + }, + { + key: 'fee', + displayLabel: stringGetter({ key: STRING_KEYS.FEE }), + }, + { + key: 'type', + displayLabel: stringGetter({ key: STRING_KEYS.TYPE }), + }, + { + key: 'liquidity', + displayLabel: stringGetter({ key: STRING_KEYS.LIQUIDITY }), + }, + ], + }); + }, [trades, stringGetter]); + + const exportTransfers = useCallback(() => { + exportCSV(transfers, { + filename: 'transfers', + columnHeaders: [ + { + key: 'time', + displayLabel: stringGetter({ key: STRING_KEYS.TIME }), + }, + { + key: 'action', + displayLabel: stringGetter({ key: STRING_KEYS.ACTION }), + }, + { + key: 'sender', + displayLabel: stringGetter({ key: STRING_KEYS.TRANSFER_SENDER }), + }, + { + key: 'recipient', + displayLabel: stringGetter({ key: STRING_KEYS.TRANSFER_RECIPIENT }), + }, + { + key: 'amount', + displayLabel: stringGetter({ key: STRING_KEYS.AMOUNT }), + }, + { + key: 'transaction', + displayLabel: stringGetter({ key: STRING_KEYS.TRANSACTION }), + }, + ], + }); + }, [transfers, stringGetter]); + + const exportData = useCallback(() => { + if (checkedTrades) { + exportTrades(); + } + + if (checkedTransfers) { + exportTransfers(); + } + + track(AnalyticsEvent.ExportDownloadClick, { trades: checkedTrades, transfers: checkedTransfers }) + }, [checkedTrades, checkedTransfers, exportTrades, exportTransfers]); + + return ( + { + if (open) { + track(AnalyticsEvent.ExportButtonClick); + } + }} + items={[ + ...items, + { + label: ( + { + setCheckedTrades(!checkedTrades); + + track(AnalyticsEvent.ExportTradesCheckboxClick, { value: !checkedTrades }); + }} + /> + ), + value: 'trades', + onSelect: (e) => e.preventDefault(), + }, + { + label: ( + { + setCheckedTransfers(!checkedTransfers); + + track(AnalyticsEvent.ExportTransfersCheckboxClick, { value: !checkedTrades }); + }} + /> + ), + value: 'transfers', + onSelect: (e) => e.preventDefault(), + }, + { + label: ( + + {stringGetter({ key: STRING_KEYS.DOWNLOAD })} + + ), + value: 'download', + onSelect: exportData, + }, + ]} + > + + {stringGetter({ key: STRING_KEYS.EXPORT })} + + ); +}; + +const Styled: Record = {}; + +Styled.Button = styled(Button)` + width: 100%; +`; diff --git a/src/views/MarketDetails.tsx b/src/views/MarketDetails.tsx index 46ccad4ca..706bca7bb 100644 --- a/src/views/MarketDetails.tsx +++ b/src/views/MarketDetails.tsx @@ -1,15 +1,20 @@ +import BigNumber from 'bignumber.js'; import { shallowEqual, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; -import { STRING_KEYS, type StringKey } from '@/constants/localization'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { Button } from '@/components/Button'; import { Details } from '@/components/Details'; +import { DiffOutput } from '@/components/DiffOutput'; import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; @@ -23,8 +28,8 @@ import { MarketLinks } from './MarketLinks'; export const MarketDetails: React.FC = () => { const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); - const { configs, market } = useSelector(getCurrentMarketData, shallowEqual) || {}; - const { id, name, resources } = useSelector(getCurrentMarketAssetData, shallowEqual) || {}; + const { configs, market } = useSelector(getCurrentMarketData, shallowEqual) ?? {}; + const { id, name, resources } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; if (!configs) return null; @@ -32,6 +37,7 @@ export const MarketDetails: React.FC = () => { tickSize, stepSize, initialMarginFraction, + effectiveInitialMarginFraction, maintenanceMarginFraction, minOrderSize, stepSizeDecimals, @@ -44,7 +50,11 @@ export const MarketDetails: React.FC = () => { secondaryDescriptionKey, websiteLink, whitepaperLink, - } = resources || {}; + } = resources ?? {}; + + const preferEIMF = Boolean( + effectiveInitialMarginFraction && initialMarginFraction !== effectiveInitialMarginFraction + ); const items = [ { @@ -97,9 +107,14 @@ export const MarketDetails: React.FC = () => { label: stringGetter({ key: STRING_KEYS.MAXIMUM_LEVERAGE }), tooltip: 'maximum-leverage', value: ( - ), @@ -116,30 +131,39 @@ export const MarketDetails: React.FC = () => { key: 'initial-margin-fraction', label: stringGetter({ key: STRING_KEYS.INITIAL_MARGIN_FRACTION }), tooltip: 'initial-margin-fraction', - value: , + value: ( + + ), }, ]; return ( - - - - + <$MarketDetails> + <$Header> + <$WrapRow> + <$MarketTitle> {name} - - {isTablet && } - + + {isTablet && <$MarketLinks />} + - + <$MarketDescription> {primaryDescriptionKey &&

    {stringGetter({ key: `APP.${primaryDescriptionKey}` })}

    } {secondaryDescriptionKey && (

    {stringGetter({ key: `APP.${secondaryDescriptionKey}` })}

    )} -
    + {!isTablet && ( - + <$Buttons> {whitepaperLink && ( )} - + )} -
    + - -
    + <$Details items={items} withSeparators /> + ); }; -const Styled: Record = {}; - -Styled.MarketDetails = styled.div` +const $MarketDetails = styled.div` margin: auto; width: 100%; @@ -204,20 +226,17 @@ Styled.MarketDetails = styled.div` padding: 0 clamp(0.5rem, 7.5%, 2.5rem); } `; - -Styled.Header = styled.header` +const $Header = styled.header` ${layoutMixins.column} gap: 1.25rem; `; - -Styled.WrapRow = styled.div` +const $WrapRow = styled.div` ${layoutMixins.row} gap: 0.5rem; flex-wrap: wrap; `; - -Styled.MarketTitle = styled.h3` +const $MarketTitle = styled.h3` ${layoutMixins.row} font: var(--font-large-medium); gap: 0.5rem; @@ -227,12 +246,10 @@ Styled.MarketTitle = styled.h3` height: 2.25rem; } `; - -Styled.MarketLinks = styled(MarketLinks)` +const $MarketLinks = styled(MarketLinks)` place-self: start end; `; - -Styled.MarketDescription = styled.div` +const $MarketDescription = styled.div` ${layoutMixins.column} gap: 0.5em; @@ -244,15 +261,13 @@ Styled.MarketDescription = styled.div` } } `; - -Styled.Buttons = styled.div` +const $Buttons = styled.div` ${layoutMixins.row} flex-wrap: wrap; gap: 0.5rem; overflow-x: auto; `; - -Styled.Details = styled(Details)` +const $Details = styled(Details)` font: var(--font-mini-book); `; diff --git a/src/views/MarketFilter.tsx b/src/views/MarketFilter.tsx index 6768a4f6c..22ed19162 100644 --- a/src/views/MarketFilter.tsx +++ b/src/views/MarketFilter.tsx @@ -1,11 +1,18 @@ -import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styled, { css } from 'styled-components'; +import { ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { MarketFilters, MARKET_FILTER_LABELS } from '@/constants/markets'; +import { MARKET_FILTER_LABELS, MarketFilters } from '@/constants/markets'; +import { AppRoute, MarketsRoute } from '@/constants/routes'; -import { useStringGetter } from '@/hooks'; +import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { InputType } from '@/components/Input'; +import { breakpoints } from '@/styles'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Button } from '@/components/Button'; import { SearchInput } from '@/components/SearchInput'; import { ToggleGroup } from '@/components/ToggleGroup'; @@ -14,20 +21,29 @@ export const MarketFilter = ({ filters, onChangeFilter, onSearchTextChange, - withoutSearch, + hideNewMarketButton, + compactLayout = false, + searchPlaceholderKey = STRING_KEYS.MARKET_SEARCH_PLACEHOLDER, }: { selectedFilter: MarketFilters; filters: MarketFilters[]; onChangeFilter: (filter: MarketFilters) => void; onSearchTextChange?: (filter: string) => void; - withoutSearch?: boolean; + hideNewMarketButton?: boolean; + searchPlaceholderKey?: string; + compactLayout?: boolean; }) => { const stringGetter = useStringGetter(); - const [isSearch, setIsSearch] = useState(false); + const navigate = useNavigate(); + const { hasPotentialMarketsData } = usePotentialMarkets(); return ( - <> - {!isSearch && ( + <$MarketFilter $compactLayout={compactLayout}> + + <$ToggleGroupContainer $compactLayout={compactLayout}> ({ label: stringGetter({ key: MARKET_FILTER_LABELS[value] }), @@ -36,16 +52,50 @@ export const MarketFilter = ({ value={selectedFilter} onValueChange={onChangeFilter} /> - )} - - {!withoutSearch && ( - - )} - + {hasPotentialMarketsData && !hideNewMarketButton && ( + + )} + + ); }; +const $MarketFilter = styled.div<{ $compactLayout: boolean }>` + display: flex; + flex-direction: ${({ $compactLayout }) => ($compactLayout ? 'row-reverse' : 'column')}; + justify-content: space-between; + gap: 0.75rem; + flex: 1; + overflow: hidden; + + ${({ $compactLayout }) => + $compactLayout && + css` + @media ${breakpoints.mobile} { + flex-direction: column; + } + `} +`; + +const $ToggleGroupContainer = styled.div<{ $compactLayout: boolean }>` + ${layoutMixins.row} + justify-content: space-between; + overflow-x: auto; + + ${({ $compactLayout }) => + $compactLayout && + css` + & button { + --button-toggle-off-backgroundColor: ${({ theme }) => theme.toggleBackground}; + --button-toggle-off-textColor: ${({ theme }) => theme.textSecondary}; + --border-color: ${({ theme }) => theme.layer6}; + --button-height: 2rem; + --button-padding: 0 0.625rem; + --button-font: var(--font-small-book); + } + `} +`; diff --git a/src/views/MarketLinks.tsx b/src/views/MarketLinks.tsx index be38805f6..03af602bc 100644 --- a/src/views/MarketLinks.tsx +++ b/src/views/MarketLinks.tsx @@ -1,7 +1,8 @@ import { shallowEqual, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { ButtonType } from '@/constants/buttons'; + import { layoutMixins } from '@/styles/layoutMixins'; import { IconName } from '@/components/Icon'; @@ -10,8 +11,8 @@ import { IconButton } from '@/components/IconButton'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; export const MarketLinks = () => { - const { resources } = useSelector(getCurrentMarketAssetData, shallowEqual) || {}; - const { coinMarketCapsLink, websiteLink, whitepaperLink } = resources || {}; + const { resources } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; + const { coinMarketCapsLink, websiteLink, whitepaperLink } = resources ?? {}; const linkItems = [ { @@ -32,18 +33,15 @@ export const MarketLinks = () => { ].filter(({ href }) => href); return ( - + <$MarketLinks> {linkItems.map( ({ key, href, icon }) => href && )} - + ); }; - -const Styled: Record = {}; - -Styled.MarketLinks = styled.div` +const $MarketLinks = styled.div` ${layoutMixins.row} margin-left: auto; diff --git a/src/views/MarketStatsDetails.tsx b/src/views/MarketStatsDetails.tsx index 562debd95..6d7ffbf81 100644 --- a/src/views/MarketStatsDetails.tsx +++ b/src/views/MarketStatsDetails.tsx @@ -1,29 +1,34 @@ import { useEffect, useRef } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; + import { shallowEqual, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { LARGE_TOKEN_DECIMALS, TINY_PERCENT_DECIMALS } from '@/constants/numbers'; -import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { Details } from '@/components/Details'; +import { DiffOutput } from '@/components/DiffOutput'; import { Output, OutputType } from '@/components/Output'; import { VerticalSeparator } from '@/components/Separator'; import { TriangleIndicator } from '@/components/TriangleIndicator'; +import { WithTooltip } from '@/components/WithTooltip'; +import { NextFundingTimer } from '@/views/NextFundingTimer'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; - import { getCurrentMarketConfig, getCurrentMarketData, getCurrentMarketMidMarketPrice, } from '@/state/perpetualsSelectors'; -import { MustBigNumber } from '@/lib/numbers'; +import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers'; -import { NextFundingTimer } from '@/views/NextFundingTimer'; import { MidMarketPrice } from './MidMarketPrice'; type ElementProps = { @@ -38,6 +43,7 @@ enum MarketStats { Volume24H = 'Volume24H', Trades24H = 'Trades24H', NextFunding = 'NextFunding', + MaxLeverage = 'MaxLeverage', } const defaultMarketStatistics = Object.values(MarketStats); @@ -46,7 +52,8 @@ export const MarketStatsDetails = ({ showMidMarketPrice = true }: ElementProps) const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; - const { tickSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) ?? {}; + const { tickSizeDecimals, initialMarginFraction, effectiveInitialMarginFraction } = + useSelector(getCurrentMarketConfig, shallowEqual) ?? {}; const midMarketPrice = useSelector(getCurrentMarketMidMarketPrice); const lastMidMarketPrice = useRef(midMarketPrice); const currentMarketData = useSelector(getCurrentMarketData, shallowEqual); @@ -68,6 +75,7 @@ export const MarketStatsDetails = ({ showMidMarketPrice = true }: ElementProps) [MarketStats.PriceChange24H]: priceChange24H, [MarketStats.Trades24H]: trades24H, [MarketStats.Volume24H]: volume24H, + [MarketStats.MaxLeverage]: undefined, // needs more complex logic }; const labelMap = { @@ -78,106 +86,48 @@ export const MarketStatsDetails = ({ showMidMarketPrice = true }: ElementProps) [MarketStats.PriceChange24H]: stringGetter({ key: STRING_KEYS.CHANGE_24H }), [MarketStats.Trades24H]: stringGetter({ key: STRING_KEYS.TRADES_24H }), [MarketStats.Volume24H]: stringGetter({ key: STRING_KEYS.VOLUME_24H }), + [MarketStats.MaxLeverage]: ( + + {stringGetter({ key: STRING_KEYS.MAXIMUM_LEVERAGE })} + + ), }; return ( - + <$MarketDetailsItems> {showMidMarketPrice && ( - + <$MidMarketPrice> - + )} - ({ key: stat, label: labelMap[stat], tooltip: stat, - // value: {valueMap[stat]?.toString()}, - value: (() => { - const value = valueMap[stat]; - const valueBN = MustBigNumber(value); - - const color = valueBN.isNegative() ? 'var(--color-negative)' : 'var(--color-positive)'; - - switch (stat) { - case MarketStats.OraclePrice: { - return ( - - ); - } - case MarketStats.OpenInterest: { - return ( - - ); - } - case MarketStats.Funding1H: { - return ( - - ); - } - case MarketStats.NextFunding: { - return ; - } - case MarketStats.PriceChange24H: { - return ( - - {!isLoading && } - - {!isLoading && ( - - )} - - ); - } - case MarketStats.Trades24H: { - return ; - } - case MarketStats.Volume24H: { - // $ with no decimals - return ; - } - default: { - // Default renderer - return ; - } - } - })(), + value: ( + + ), }))} isLoading={isLoading} layout={isTablet ? 'grid' : 'rowColumns'} withSeparators={!isTablet} /> - + ); }; - -const Styled: Record = {}; - -Styled.MarketDetailsItems = styled.div` +const $MarketDetailsItems = styled.div` @media ${breakpoints.notTablet} { ${layoutMixins.scrollArea} ${layoutMixins.row} @@ -192,7 +142,7 @@ Styled.MarketDetailsItems = styled.div` } `; -Styled.Details = styled(Details)` +const $Details = styled(Details)` font: var(--font-mini-book); @media ${breakpoints.tablet} { @@ -206,7 +156,7 @@ Styled.Details = styled(Details)` } `; -Styled.MidMarketPrice = styled.div` +const $MidMarketPrice = styled.div` ${layoutMixins.sticky} ${layoutMixins.row} font: var(--font-medium-medium); @@ -217,7 +167,7 @@ Styled.MidMarketPrice = styled.div` gap: 1rem; `; -Styled.Output = styled(Output)<{ color?: string }>` +const $Output = styled(Output)<{ color?: string }>` ${layoutMixins.row} ${({ color }) => @@ -227,7 +177,7 @@ Styled.Output = styled(Output)<{ color?: string }>` `} `; -Styled.RowSpan = styled.span<{ color?: string }>` +const $RowSpan = styled.span<{ color?: string }>` ${layoutMixins.row} ${({ color }) => @@ -242,3 +192,96 @@ Styled.RowSpan = styled.span<{ color?: string }>` gap: 0.25rem; `; + +const DetailsItem = ({ + value, + stat, + tickSizeDecimals, + id, + isLoading, + priceChange24HPercent, + initialMarginFraction, + effectiveInitialMarginFraction, +}: { + value: number | null | undefined; + stat: MarketStats; + tickSizeDecimals: number | null | undefined; + id: string; + isLoading: boolean; + priceChange24HPercent: number | null | undefined; + initialMarginFraction: number | null | undefined; + effectiveInitialMarginFraction: number | null | undefined; +}) => { + const valueBN = MustBigNumber(value); + + const color = valueBN.isNegative() ? 'var(--color-negative)' : 'var(--color-positive)'; + + switch (stat) { + case MarketStats.OraclePrice: { + return <$Output type={OutputType.Fiat} value={value} fractionDigits={tickSizeDecimals} />; + } + case MarketStats.OpenInterest: { + return ( + <$Output + type={OutputType.Number} + value={value} + tag={id} + fractionDigits={LARGE_TOKEN_DECIMALS} + /> + ); + } + case MarketStats.Funding1H: { + return ( + <$Output + type={OutputType.Percent} + value={value} + color={color} + fractionDigits={TINY_PERCENT_DECIMALS} + /> + ); + } + case MarketStats.NextFunding: { + return ; + } + case MarketStats.PriceChange24H: { + return ( + <$RowSpan color={!isLoading ? color : undefined}> + {!isLoading && } + <$Output type={OutputType.Fiat} value={valueBN.abs()} fractionDigits={tickSizeDecimals} /> + {!isLoading && ( + <$Output + type={OutputType.Percent} + value={MustBigNumber(priceChange24HPercent).abs()} + withParentheses + /> + )} + + ); + } + case MarketStats.Trades24H: { + return <$Output type={OutputType.Number} value={value} fractionDigits={0} />; + } + case MarketStats.Volume24H: { + // $ with no decimals + return <$Output type={OutputType.Fiat} value={value} fractionDigits={0} />; + } + case MarketStats.MaxLeverage: { + return ( + + ); + } + default: { + // Default renderer + return <$Output type={OutputType.Text} value={value} />; + } + } +}; diff --git a/src/views/MarketsDropdown.tsx b/src/views/MarketsDropdown.tsx index 899894467..654e5def2 100644 --- a/src/views/MarketsDropdown.tsx +++ b/src/views/MarketsDropdown.tsx @@ -1,18 +1,20 @@ -import { memo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; +import { Key, memo, useMemo, useState } from 'react'; + import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import styled, { css, keyframes } from 'styled-components'; import { ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { MarketFilters, type MarketData } from '@/constants/markets'; import { AppRoute, MarketsRoute } from '@/constants/routes'; -import { useStringGetter } from '@/hooks'; + import { useMarketsData } from '@/hooks/useMarketsData'; import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { popoverMixins } from '@/styles/popoverMixins'; import { layoutMixins } from '@/styles/layoutMixins'; +import { popoverMixins } from '@/styles/popoverMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { Button } from '@/components/Button'; @@ -29,7 +31,7 @@ import { MustBigNumber } from '@/lib/numbers'; import { MarketFilter } from './MarketFilter'; -const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string) => void }) => { +const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: Key) => void }) => { const [filter, setFilter] = useState(MarketFilters.ALL); const stringGetter = useStringGetter(); const selectedLocale = useSelector(getSelectedLocale); @@ -38,18 +40,85 @@ const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string const navigate = useNavigate(); const { hasPotentialMarketsData } = usePotentialMarkets(); + const columns = useMemo( + () => + [ + { + columnKey: 'market', + getCellValue: (row) => row.market, + label: stringGetter({ key: STRING_KEYS.MARKET }), + renderCell: ({ assetId, id, isNew }) => ( + <$MarketName isFavorited={false}> + {/* TRCL-1693 */} + +

    {id}

    + {assetId} + {isNew && {stringGetter({ key: STRING_KEYS.NEW })}} + + ), + }, + { + columnKey: 'oraclePrice', + getCellValue: (row) => row.oraclePrice, + label: stringGetter({ key: STRING_KEYS.PRICE }), + renderCell: ({ oraclePrice, tickSizeDecimals }) => ( + <$Output type={OutputType.Fiat} value={oraclePrice} fractionDigits={tickSizeDecimals} /> + ), + }, + { + columnKey: 'priceChange24HPercent', + getCellValue: (row) => row.priceChange24HPercent, + label: stringGetter({ key: STRING_KEYS._24H }), + renderCell: ({ priceChange24HPercent }) => ( + <$InlineRow> + {!priceChange24HPercent ? ( + <$Output type={OutputType.Text} value={null} /> + ) : ( + <$PriceChangeOutput + type={OutputType.Percent} + value={priceChange24HPercent} + isNegative={MustBigNumber(priceChange24HPercent).isNegative()} + /> + )} + + ), + }, + { + columnKey: 'volume24H', + getCellValue: (row) => row.volume24H, + label: stringGetter({ key: STRING_KEYS.VOLUME }), + renderCell: ({ volume24H }) => ( + <$Output type={OutputType.CompactFiat} value={volume24H} locale={selectedLocale} /> + ), + }, + { + columnKey: 'openInterest', + getCellValue: (row) => row.openInterestUSDC, + label: stringGetter({ key: STRING_KEYS.OPEN_INTEREST }), + renderCell: (row) => ( + <$Output + type={OutputType.CompactFiat} + value={row.openInterestUSDC} + locale={selectedLocale} + /> + ), + }, + ] as ColumnDef[], + [stringGetter, selectedLocale] + ); + return ( <> - + <$Toolbar> - - - + <$ScrollArea> + <$Table withInnerBorders data={filteredMarkets} getRowKey={(row: MarketData) => row.id} @@ -59,86 +128,34 @@ const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string direction: 'descending', }} label={stringGetter({ key: STRING_KEYS.MARKETS })} - columns={ - [ - { - columnKey: 'market', - getCellValue: (row) => row.market, - label: stringGetter({ key: STRING_KEYS.MARKET }), - renderCell: ({ assetId, id }) => ( - - {/* TRCL-1693 */} - -

    {id}

    - {assetId} -
    - ), - }, - { - columnKey: 'oraclePrice', - getCellValue: (row) => row.oraclePrice, - label: stringGetter({ key: STRING_KEYS.PRICE }), - renderCell: ({ oraclePrice, tickSizeDecimals }) => ( - - ), - }, - { - columnKey: 'priceChange24HPercent', - getCellValue: (row) => row.priceChange24HPercent, - label: stringGetter({ key: STRING_KEYS._24H }), - renderCell: ({ priceChange24HPercent }) => ( - - {!priceChange24HPercent ? ( - - ) : ( - - )} - - ), - }, - { - columnKey: 'volume24H', - getCellValue: (row) => row.volume24H, - label: stringGetter({ key: STRING_KEYS.VOLUME }), - renderCell: ({ volume24H }) => ( - - ), - }, - { - columnKey: 'openInterest', - getCellValue: (row) => row.openInterestUSDC, - label: stringGetter({ key: STRING_KEYS.OPEN_INTEREST }), - renderCell: (row) => ( - - ), - }, - ] as ColumnDef[] - } + columns={columns} + initialPageSize={15} slotEmpty={ - -

    - {stringGetter({ - key: STRING_KEYS.QUERY_NOT_FOUND, - params: { QUERY: searchFilter ?? '' }, - })} -

    -

    {stringGetter({ key: STRING_KEYS.MARKET_SEARCH_DOES_NOT_EXIST_YET })}

    + <$MarketNotFound> + {filter === MarketFilters.NEW && !searchFilter ? ( + <> +

    + {stringGetter({ + key: STRING_KEYS.QUERY_NOT_FOUND, + params: { QUERY: stringGetter({ key: STRING_KEYS.NEW }) }, + })} +

    + {hasPotentialMarketsData && ( +

    {stringGetter({ key: STRING_KEYS.ADD_DETAILS_TO_LAUNCH_MARKET })}

    + )} + + ) : ( + <> +

    + {stringGetter({ + key: STRING_KEYS.QUERY_NOT_FOUND, + params: { QUERY: searchFilter ?? '' }, + })} +

    +

    {stringGetter({ key: STRING_KEYS.MARKET_SEARCH_DOES_NOT_EXIST_YET })}

    + + )} + {hasPotentialMarketsData && (
    )} -
    + } /> -
    + ); }; -export const MarketsDropdown: React.FC<{ currentMarketId?: string; symbol: string | null }> = memo( - ({ currentMarketId, symbol = '' }) => { +export const MarketsDropdown = memo( + ({ currentMarketId, symbol = '' }: { currentMarketId?: string; symbol: string | null }) => { const [isOpen, setIsOpen] = useState(false); const stringGetter = useStringGetter(); const navigate = useNavigate(); return ( - + <$TriggerContainer $isOpen={isOpen}> {isOpen ? (

    {stringGetter({ key: STRING_KEYS.SELECT_MARKET })}

    ) : ( @@ -180,28 +197,26 @@ export const MarketsDropdown: React.FC<{ currentMarketId?: string; symbol: strin )}

    {stringGetter({ key: isOpen ? STRING_KEYS.TAP_TO_CLOSE : STRING_KEYS.ALL_MARKETS })} - +

    - + } triggerType={TriggerType.MarketDropdown} > { + onRowAction={(market: Key) => { navigate(`${AppRoute.Trade}/${market}`); setIsOpen(false); }} /> -
    + ); } ); -const Styled: Record = {}; - -Styled.MarketName = styled.div<{ isFavorited: boolean }>` +const $MarketName = styled.div<{ isFavorited: boolean }>` ${layoutMixins.row} gap: 0.5rem; @@ -222,7 +237,7 @@ Styled.MarketName = styled.div<{ isFavorited: boolean }>` `} `; -Styled.TriggerContainer = styled.div<{ $isOpen: boolean }>` +const $TriggerContainer = styled.div<{ $isOpen: boolean }>` --marketsDropdown-width: var(--sidebar-width); width: var(--sidebar-width); @@ -261,24 +276,22 @@ Styled.TriggerContainer = styled.div<{ $isOpen: boolean }>` } `; -Styled.DropdownIcon = styled.span` +const $DropdownIcon = styled.span` margin-left: auto; display: inline-flex; transition: transform 0.3s var(--ease-out-expo); font-size: 0.375rem; - - ${Styled.Trigger}[data-state='open'] & { - transform: scaleY(-1); - } `; -Styled.Popover = styled(Popover)` +const $Popover = styled(Popover)` ${popoverMixins.popover} --popover-item-height: 3.375rem; --popover-backgroundColor: var(--color-layer-2); - --stickyArea-topHeight: var(--popover-item-height); + --stickyArea-topHeight: 6.125rem; + + --toolbar-height: var(--stickyArea-topHeight); height: calc( 100vh - var(--page-header-height) - var(--market-info-row-height) - var(--page-footer-height) @@ -314,19 +327,21 @@ Styled.Popover = styled(Popover)` } `; -Styled.Toolbar = styled(Toolbar)` +const $Toolbar = styled(Toolbar)` ${layoutMixins.stickyHeader} - height: var(--stickyArea-topHeight); - + height: var(--toolbar-height); + gap: 0.5rem; border-bottom: solid var(--border-width) var(--color-border); `; -Styled.ScrollArea = styled.div` +const $ScrollArea = styled.div` ${layoutMixins.scrollArea} - height: calc(100% - var(--popover-item-height)); + height: calc(100% - var(--toolbar-height)); `; -Styled.Table = styled(Table)` +const $Table = styled(Table)` + --tableCell-padding: 0.5rem 1rem; + thead { --stickyArea-totalInsetTop: 0px; --stickyArea-totalInsetBottom: 0px; @@ -335,25 +350,34 @@ Styled.Table = styled(Table)` } } + tfoot { + --stickyArea-totalInsetTop: 0px; + --stickyArea-totalInsetBottom: 3px; + + tr { + height: var(--stickyArea-bottomHeight); + } + } + tr { height: var(--popover-item-height); } -`; +` as typeof Table; -Styled.InlineRow = styled.div` +const $InlineRow = styled.div` ${layoutMixins.inlineRow} `; -Styled.Output = styled(Output)<{ isNegative?: boolean }>` +const $Output = styled(Output)<{ isNegative?: boolean }>` color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)}; color: var(--color-text-2); `; -Styled.PriceChangeOutput = styled(Output)<{ isNegative?: boolean }>` +const $PriceChangeOutput = styled(Output)<{ isNegative?: boolean }>` color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)}; `; -Styled.MarketNotFound = styled.div` +const $MarketNotFound = styled.div` ${layoutMixins.column} justify-content: center; align-items: center; diff --git a/src/views/MarketsStats.tsx b/src/views/MarketsStats.tsx new file mode 100644 index 000000000..5538620cf --- /dev/null +++ b/src/views/MarketsStats.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react'; + +import styled from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; +import { MarketFilters, MarketSorting } from '@/constants/markets'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { breakpoints } from '@/styles'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Tag } from '@/components/Tag'; +import { ToggleGroup } from '@/components/ToggleGroup'; + +import { ExchangeBillboards } from './ExchangeBillboards'; +import { MarketsCompactTable } from './tables/MarketsCompactTable'; + +interface MarketsStatsProps { + className?: string; +} + +export const MarketsStats = (props: MarketsStatsProps) => { + const { className } = props; + const stringGetter = useStringGetter(); + const [sorting, setSorting] = useState(MarketSorting.GAINERS); + + return ( + <$MarketsStats className={className}> + + <$Section> + <$SectionHeader> + <$RecentlyListed> + {stringGetter({ key: STRING_KEYS.RECENTLY_LISTED })} + <$NewTag>{stringGetter({ key: STRING_KEYS.NEW })} + + + + + <$Section> + <$SectionHeader> +

    {stringGetter({ key: STRING_KEYS.BIGGEST_MOVERS })}

    + {stringGetter({ key: STRING_KEYS._24H })} + + <$ToggleGroupContainer> + + + + + + + ); +}; + +const $MarketsStats = styled.section` + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + + @media ${breakpoints.desktopSmall} { + padding-left: 1rem; + padding-right: 1rem; + } + + @media ${breakpoints.tablet} { + ${layoutMixins.column} + } +`; +const $Section = styled.div` + background: var(--color-layer-3); + border-radius: 0.625rem; +`; +const $RecentlyListed = styled.h4` + display: flex; + align-items: center; + gap: 0.375rem; +`; +const $NewTag = styled(Tag)` + background-color: var(--color-accent-faded); + color: var(--color-accent); + text-transform: uppercase; +`; +const $ToggleGroupContainer = styled.div` + ${layoutMixins.row} + margin-left: auto; + + & button { + --button-toggle-off-backgroundColor: var(--color-layer-3); + --button-toggle-off-textColor: var(--color-text-1); + --border-color: var(--color-layer-6); + --button-height: 1.75rem; + --button-padding: 0 0.75rem; + --button-font: var(--font-mini-book); + } +`; +const $SectionHeader = styled.div` + ${layoutMixins.row} + + justify-content: space-between; + padding: 1.125rem 1.5rem; + gap: 0.375rem; + height: 4rem; + + & h4 { + font: var(--font-base-medium); + color: var(--color-text-2); + } +`; diff --git a/src/views/MidMarketPrice.tsx b/src/views/MidMarketPrice.tsx index 6a7c43ba6..d4ffdeb60 100644 --- a/src/views/MidMarketPrice.tsx +++ b/src/views/MidMarketPrice.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; + import { shallowEqual, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { Nullable } from '@/constants/abacus'; @@ -9,7 +10,10 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { LoadingDots } from '@/components/Loading/LoadingDots'; import { Output, OutputType } from '@/components/Output'; -import { getCurrentMarketConfig, getCurrentMarketMidMarketPrice } from '@/state/perpetualsSelectors'; +import { + getCurrentMarketConfig, + getCurrentMarketMidMarketPrice, +} from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; @@ -22,7 +26,8 @@ const getMidMarketPriceColor = ({ }) => { if (MustBigNumber(midMarketPrice).lt(MustBigNumber(lastMidMarketPrice))) { return 'var(--color-negative)'; - } else if (MustBigNumber(midMarketPrice).gt(MustBigNumber(lastMidMarketPrice))) { + } + if (MustBigNumber(midMarketPrice).gt(MustBigNumber(lastMidMarketPrice))) { return 'var(--color-positive)'; } @@ -44,7 +49,7 @@ export const MidMarketPrice = () => { }, [midMarketPrice]); return midMarketPrice !== undefined ? ( - { ); }; - -const Styled: Record = {}; - -Styled.Output = styled(Output)<{ color?: string }>` +const $Output = styled(Output)<{ color?: string }>` ${layoutMixins.row} ${({ color }) => diff --git a/src/views/NextFundingTimer.tsx b/src/views/NextFundingTimer.tsx index 5781121a1..1d1a4eb50 100644 --- a/src/views/NextFundingTimer.tsx +++ b/src/views/NextFundingTimer.tsx @@ -1,9 +1,11 @@ -import { useState, useCallback } from 'react'; -import { useInterval } from '@/hooks'; +import { useCallback, useState } from 'react'; + +import { useInterval } from '@/hooks/useInterval'; -import { getTimeTillNextUnit, formatSeconds } from '@/lib/timeUtils'; import { Output, OutputType } from '@/components/Output'; +import { formatSeconds, getTimeTillNextUnit } from '@/lib/timeUtils'; + export const NextFundingTimer = () => { const [secondsLeft, setSecondsLeft] = useState(); @@ -16,11 +18,7 @@ export const NextFundingTimer = () => { return ( ); }; diff --git a/src/views/OrderStatusIcon.tsx b/src/views/OrderStatusIcon.tsx index 2dbac7050..127c33312 100644 --- a/src/views/OrderStatusIcon.tsx +++ b/src/views/OrderStatusIcon.tsx @@ -1,65 +1,20 @@ import styled from 'styled-components'; -import { AbacusOrderStatus } from '@/constants/abacus'; - -import { - OrderCanceledIcon, - OrderFilledIcon, - OrderOpenIcon, - OrderPartiallyFilledIcon, - OrderPendingIcon, -} from '@/icons'; - import { Icon } from '@/components/Icon'; +import { getOrderStatusInfo } from '@/lib/orders'; + type ElementProps = { status: string; - totalFilled: number; }; type StyleProps = { className?: string; }; -export const OrderStatusIcon = ({ className, status, totalFilled }: ElementProps & StyleProps) => { - const { iconComponent, color } = { - [AbacusOrderStatus.open.rawValue]: - totalFilled > 0 - ? { - iconComponent: OrderPartiallyFilledIcon, - color: 'var(--color-warning)', - } - : { - iconComponent: OrderOpenIcon, - color: 'var(--color-text-2)', - }, - [AbacusOrderStatus.partiallyFilled.rawValue]: { - iconComponent: OrderPartiallyFilledIcon, - color: 'var(--color-warning)', - }, - [AbacusOrderStatus.filled.rawValue]: { - iconComponent: OrderFilledIcon, - color: 'var(--color-success)', - }, - [AbacusOrderStatus.cancelled.rawValue]: { - iconComponent: OrderCanceledIcon, - color: 'var(--color-error)', - }, - [AbacusOrderStatus.canceling.rawValue]: { - iconComponent: OrderPendingIcon, - color: 'var(--color-error)', - }, - [AbacusOrderStatus.pending.rawValue]: { - iconComponent: OrderPendingIcon, - color: 'var(--color-text-2)', - }, - [AbacusOrderStatus.untriggered.rawValue]: { - iconComponent: OrderPendingIcon, - color: 'var(--color-text-2)', - }, - }[status]; - - return <$Icon className={className} iconComponent={iconComponent} color={color} />; +export const OrderStatusIcon = ({ className, status }: ElementProps & StyleProps) => { + const { statusIcon, statusIconColor } = getOrderStatusInfo({ status }); + return <$Icon className={className} iconName={statusIcon} color={statusIconColor} />; }; const $Icon = styled(Icon)<{ color: string }>` diff --git a/src/views/PositionInfo.tsx b/src/views/PositionInfo.tsx index 1df8cb60f..e3b5390bd 100644 --- a/src/views/PositionInfo.tsx +++ b/src/views/PositionInfo.tsx @@ -1,18 +1,18 @@ -import styled, { type AnyStyledComponent, css } from 'styled-components'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { type Nullable } from '@/constants/abacus'; import { DialogTypes, TradeBoxDialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { NumberSign, USD_DECIMALS } from '@/constants/numbers'; -import { breakpoints } from '@/styles'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; -import { DetachedSection, DetachedScrollableSection } from '@/components/ContentSection'; +import { DetachedScrollableSection, DetachedSection } from '@/components/ContentSection'; import { Details } from '@/components/Details'; import { DiffOutput } from '@/components/DiffOutput'; import { Output, OutputType, ShowSign } from '@/components/Output'; @@ -21,9 +21,9 @@ import { ToggleButton } from '@/components/ToggleButton'; import { calculateIsAccountLoading } from '@/state/accountCalculators'; import { getCurrentMarketPositionData } from '@/state/accountSelectors'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; -import { getActiveDialog, getActiveTradeBoxDialog } from '@/state/dialogsSelectors'; -import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; import { closeDialogInTradeBox, openDialog, openDialogInTradeBox } from '@/state/dialogs'; +import { getActiveTradeBoxDialog } from '@/state/dialogsSelectors'; +import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; import abacusStateManager from '@/lib/abacus'; import { BIG_NUMBERS, isNumber, MustBigNumber } from '@/lib/numbers'; @@ -62,15 +62,13 @@ export const PositionInfo = ({ showNarrowVariation }: { showNarrowVariation?: bo const currentMarketAssetData = useSelector(getCurrentMarketAssetData, shallowEqual); const currentMarketConfigs = useSelector(getCurrentMarketConfig, shallowEqual); - const activeDialog = useSelector(getActiveDialog, shallowEqual); - const activeTradeBoxDialog = useSelector(getActiveTradeBoxDialog, shallowEqual); + const activeTradeBoxDialog = useSelector(getActiveTradeBoxDialog); const currentMarketPosition = useSelector(getCurrentMarketPositionData, shallowEqual); const isLoading = useSelector(calculateIsAccountLoading); - const { stepSizeDecimals, tickSizeDecimals } = currentMarketConfigs || {}; - const { id } = currentMarketAssetData || {}; - const { type: dialogType } = activeDialog || {}; - const { type: tradeBoxDialogType } = activeTradeBoxDialog || {}; + const { stepSizeDecimals, tickSizeDecimals } = currentMarketConfigs ?? {}; + const { id } = currentMarketAssetData ?? {}; + const { type: tradeBoxDialogType } = activeTradeBoxDialog ?? {}; const { adjustedImf, @@ -117,7 +115,7 @@ export const PositionInfo = ({ showNarrowVariation }: { showNarrowVariation?: bo }, ]; - const { current: currentSize, postOrder: postOrderSize } = size || {}; + const { current: currentSize, postOrder: postOrderSize } = size ?? {}; const leverageBN = MustBigNumber(leverage?.current); const newLeverageBN = MustBigNumber(leverage?.postOrder); const maxLeverage = BIG_NUMBERS.ONE.div(MustBigNumber(adjustedImf?.postOrder)); @@ -188,7 +186,7 @@ export const PositionInfo = ({ showNarrowVariation }: { showNarrowVariation?: bo label: STRING_KEYS.LIQUIDATION_PRICE, tooltip: 'liquidation-price', tooltipParams: { - SYMBOL: id || '', + SYMBOL: id ?? '', }, fractionDigits: tickSizeDecimals, hasInvalidNewValue: Boolean(newLeverageIsInvalid), @@ -222,7 +220,7 @@ export const PositionInfo = ({ showNarrowVariation }: { showNarrowVariation?: bo : MustBigNumber(realizedPnl?.current).lt(0) ? NumberSign.Negative : NumberSign.Neutral, - value: realizedPnl?.current || undefined, + value: realizedPnl?.current ?? undefined, withBaseFont: true, }, ]; @@ -247,58 +245,54 @@ export const PositionInfo = ({ showNarrowVariation }: { showNarrowVariation?: bo label: stringGetter({ key: label }), tooltip, tooltipParams, - value: ( - <> - {useDiffOutput ? ( - - ) : ( - - ) - } - withBaseFont={withBaseFont} - /> - )} - + value: useDiffOutput ? ( + <$DiffOutput + type={type} + value={value} + newValue={newValue} + fractionDigits={fractionDigits} + hasInvalidNewValue={hasInvalidNewValue} + layout={isTablet ? 'row' : 'column'} + sign={sign} + showSign={showSign} + withBaseFont={withBaseFont} + withDiff={isNumber(newValue) && value !== newValue} + /> + ) : ( + <$Output + type={type} + value={value} + fractionDigits={fractionDigits} + showSign={showSign} + sign={sign} + slotRight={ + percentValue && ( + <$Output + type={OutputType.Percent} + value={percentValue} + sign={sign} + showSign={showSign} + withParentheses + withBaseFont={withBaseFont} + margin="0 0 0 0.5ch" + /> + ) + } + withBaseFont={withBaseFont} + /> ), }); const actions = ( - + <$Actions> {isTablet ? ( - dispatch(openDialog({ type: DialogTypes.ClosePosition }))} > {stringGetter({ key: STRING_KEYS.CLOSE_POSITION })} - + ) : ( - { dispatch( @@ -312,70 +306,70 @@ export const PositionInfo = ({ showNarrowVariation }: { showNarrowVariation?: bo }} > {stringGetter({ key: STRING_KEYS.CLOSE_POSITION })} - + )} - + ); if (showNarrowVariation) { return ( - - + <$MobilePositionInfo> + <$DetachedSection> - - + - - + <$MobileDetails items={[mainFieldsContent[2], mainFieldsContent[3]].map(createDetailItem)} layout="rowColumns" withSeparators isLoading={isLoading} /> - + - {!hasNoPositionInMarket && {actions}} + {!hasNoPositionInMarket && <$DetachedSection>{actions}} - - + <$MobileDetails items={detailFieldsContent.map(createDetailItem)} withSeparators isLoading={isLoading} /> - - + + ); } return ( - + <$PositionInfo>
    -
    - - + ); }; - -const Styled: Record = {}; - -Styled.DiffOutput = styled(DiffOutput)` +const $DiffOutput = styled(DiffOutput)` --diffOutput-gap: 0.125rem; --diffOutput-value-color: var(--color-text-2); --diffOutput-valueWithDiff-color: var(--color-text-0); @@ -409,7 +400,7 @@ Styled.DiffOutput = styled(DiffOutput)` justify-items: inherit; `; -Styled.PrimaryDetails = styled(Details)` +const $PrimaryDetails = styled(Details)` font: var(--font-mini-book); --details-value-font: var(--font-base-book); @@ -425,12 +416,12 @@ Styled.PrimaryDetails = styled(Details)` } `; -Styled.SecondaryDetails = styled(Details)` +const $SecondaryDetails = styled(Details)` font: var(--font-mini-book); --details-value-font: var(--font-small-book); `; -Styled.MobileDetails = styled(Details)` +const $MobileDetails = styled(Details)` font: var(--font-small-book); --details-value-font: var(--font-medium-medium); @@ -441,7 +432,7 @@ Styled.MobileDetails = styled(Details)` } `; -Styled.Actions = styled.footer` +const $Actions = styled.footer` display: flex; flex-wrap: wrap; gap: 0.5rem; @@ -455,13 +446,15 @@ Styled.Actions = styled.footer` } `; -Styled.Output = styled(Output)<{ sign: NumberSign; smallText?: boolean; margin?: string }>` +const $Output = styled(Output)<{ sign?: NumberSign; smallText?: boolean; margin?: string }>` color: ${({ sign }) => - ({ - [NumberSign.Positive]: `var(--color-positive)`, - [NumberSign.Negative]: `var(--color-negative)`, - [NumberSign.Neutral]: `var(--color-text-2)`, - }[sign])}; + sign == null + ? undefined + : { + [NumberSign.Positive]: `var(--color-positive)`, + [NumberSign.Negative]: `var(--color-negative)`, + [NumberSign.Neutral]: `var(--color-text-2)`, + }[sign]}; ${({ smallText }) => smallText && @@ -473,7 +466,7 @@ Styled.Output = styled(Output)<{ sign: NumberSign; smallText?: boolean; margin?: ${({ margin }) => margin && `margin: ${margin};`} `; -Styled.PositionInfo = styled.div` +const $PositionInfo = styled.div` margin: 0 auto; width: 100%; @@ -505,25 +498,25 @@ Styled.PositionInfo = styled.div` } `; -Styled.DetachedSection = styled(DetachedSection)` +const $DetachedSection = styled(DetachedSection)` padding: 0 1.5rem; position: relative; `; -Styled.DetachedScrollableSection = styled(DetachedScrollableSection)` +const $DetachedScrollableSection = styled(DetachedScrollableSection)` padding: 0 1.5rem; `; -Styled.MobilePositionInfo = styled.div` +const $MobilePositionInfo = styled.div` ${layoutMixins.column} gap: 1rem; - > ${Styled.DetachedSection}:nth-child(1) { + > ${$DetachedSection}:nth-child(1) { display: flex; gap: 1rem; flex-wrap: wrap; - > ${() => Styled.PositionTile} { + > ${() => $PositionTile} { flex: 2 9rem; // Icon + Tags @@ -532,35 +525,35 @@ Styled.MobilePositionInfo = styled.div` } } - > ${Styled.MobileDetails} { + > ${$MobileDetails} { flex: 1 9rem; } } - > ${Styled.DetachedScrollableSection}:nth-child(2) { + > ${$DetachedScrollableSection}:nth-child(2) { // Profit/Loss Section - > ${Styled.MobileDetails} { + > ${$MobileDetails} { margin: 0 -1rem; } } - > ${Styled.DetachedSection}:nth-last-child(1) { + > ${$DetachedSection}:nth-last-child(1) { // Other Details Section - > ${Styled.MobileDetails} { + > ${$MobileDetails} { margin: 0 -0.25rem; --details-value-font: var(--font-base-book); } } `; -Styled.PositionTile = styled(PositionTile)``; +const $PositionTile = styled(PositionTile)``; -Styled.ClosePositionButton = styled(Button)` +const $ClosePositionButton = styled(Button)` --button-border: solid var(--border-width) var(--color-border-red); --button-textColor: var(--color-red); `; -Styled.ClosePositionToggleButton = styled(ToggleButton)` +const $ClosePositionToggleButton = styled(ToggleButton)` --button-border: solid var(--border-width) var(--color-border-red); --button-toggle-off-textColor: var(--color-red); --button-toggle-on-textColor: var(--color-red); diff --git a/src/views/PositionTile.stories.tsx b/src/views/PositionTile.stories.tsx index 58834f595..b215cf648 100644 --- a/src/views/PositionTile.stories.tsx +++ b/src/views/PositionTile.stories.tsx @@ -1,22 +1,21 @@ import type { Story } from '@ladle/react'; -import { StoryWrapper } from '.ladle/components'; +import styled from 'styled-components'; -import styled, { AnyStyledComponent } from 'styled-components'; import { breakpoints } from '@/styles'; import { PositionTile } from './PositionTile'; +// eslint and prettier seem to conflict on ordering or something +// eslint-disable-next-line import/order +import { StoryWrapper } from '.ladle/components'; export const PositionTileStory: Story[0]> = (args) => ( - + <$PositionInfoContainer> - + ); - -const Styled: Record = {}; - -Styled.PositionInfoContainer = styled.div` +const $PositionInfoContainer = styled.div` display: grid; height: 4.625rem; margin: auto; diff --git a/src/views/PositionTile.tsx b/src/views/PositionTile.tsx index 4642fd53f..8da7f8302 100644 --- a/src/views/PositionTile.tsx +++ b/src/views/PositionTile.tsx @@ -1,18 +1,19 @@ +import styled, { css } from 'styled-components'; + import { NumberSign, TOKEN_DECIMALS } from '@/constants/numbers'; import { PositionSide } from '@/constants/trade'; -import { isNumber, MustBigNumber } from '@/lib/numbers'; -import { hasPositionSideChanged } from '@/lib/tradeData'; +import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; -import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; import { DiffArrow } from '@/components/DiffArrow'; +import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; import { Output, OutputType, ShowSign } from '@/components/Output'; import { PositionSideTag } from '@/components/PositionSideTag'; import { TagSize } from '@/components/Tag'; -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { isNumber, MustBigNumber } from '@/lib/numbers'; +import { hasPositionSideChanged } from '@/lib/tradeData'; type ElementProps = { currentSize?: number | null; @@ -49,15 +50,15 @@ export const PositionTile = ({ currentPositionSide === PositionSide.None && newPositionSide === PositionSide.None; return ( -
    - {showNarrowVariation && } - + {showNarrowVariation && <$AssetIcon symbol={symbol} />} + <$PositionTags> {hasSizeDiff && newPositionSide && currentPositionSide !== newPositionSide && ( <> @@ -65,22 +66,22 @@ export const PositionTile = ({ )} - +
    {!hasNoCurrentOrPostOrderPosition && ( - - + <$Output type={OutputType.Number} tag={!hasSizeDiff && symbol} value={currentSize} - fractionDigits={stepSizeDecimals || TOKEN_DECIMALS} + fractionDigits={stepSizeDecimals ?? TOKEN_DECIMALS} showSign={ShowSign.None} smallText={hasSizeDiff} withBaseFont /> {hasSizeDiff ? ( - + <$PostOrderSizeRow> - - + ) : ( - )} - + )} - {isLoading && } -
    + {isLoading && <$LoadingSpinner />} + ); }; - -const Styled: Record = {}; - -Styled.PositionTags = styled.div` +const $PositionTags = styled.div` ${layoutMixins.inlineRow} `; -Styled.PositionSizes = styled.div<{ showNarrowVariation?: boolean }>` +const $PositionSizes = styled.div<{ showNarrowVariation?: boolean }>` display: flex; flex-direction: column; align-items: end; @@ -143,14 +141,7 @@ Styled.PositionSizes = styled.div<{ showNarrowVariation?: boolean }>` `} `; -Styled.Output = styled(Output)<{ sign: NumberSign; smallText?: boolean; margin?: string }>` - color: ${({ sign }) => - ({ - [NumberSign.Positive]: `var(--color-positive)`, - [NumberSign.Negative]: `var(--color-negative)`, - [NumberSign.Neutral]: `var(--color-text-2)`, - }[sign])}; - +const $Output = styled(Output)<{ smallText?: boolean; margin?: string }>` ${({ smallText }) => smallText && css` @@ -161,7 +152,7 @@ Styled.Output = styled(Output)<{ sign: NumberSign; smallText?: boolean; margin?: ${({ margin }) => margin && `margin: ${margin};`} `; -Styled.PositionTile = styled.div<{ +const $PositionTile = styled.div<{ newPositionSide?: PositionSide; positionSide?: PositionSide; positionSideHasChanged?: Boolean; @@ -235,14 +226,14 @@ Styled.PositionTile = styled.div<{ `}; `; -Styled.PostOrderSizeRow = styled.div` +const $PostOrderSizeRow = styled.div` ${layoutMixins.inlineRow} `; -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` font-size: 2.25rem; `; -Styled.LoadingSpinner = styled(LoadingSpinner)` +const $LoadingSpinner = styled(LoadingSpinner)` color: var(--color-text-0); `; diff --git a/src/views/TradeBox.tsx b/src/views/TradeBox.tsx index 221dc8623..8d9c03b6d 100644 --- a/src/views/TradeBox.tsx +++ b/src/views/TradeBox.tsx @@ -1,18 +1,19 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; -import { STRING_KEYS } from '@/constants/localization'; import { TradeBoxDialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { useStringGetter } from '@/hooks'; +import { layoutMixins } from '@/styles/layoutMixins'; import { Dialog, DialogPlacement } from '@/components/Dialog'; import { ClosePositionForm } from '@/views/forms/ClosePositionForm'; +import { SelectMarginModeForm } from '@/views/forms/SelectMarginModeForm'; +import { closeDialogInTradeBox, openDialogInTradeBox } from '@/state/dialogs'; import { getActiveTradeBoxDialog } from '@/state/dialogsSelectors'; -import { openDialogInTradeBox, closeDialogInTradeBox } from '@/state/dialogs'; import abacusStateManager from '@/lib/abacus'; @@ -36,13 +37,19 @@ export const TradeBox = () => { abacusStateManager.clearClosePositionInputValues({ shouldFocusOnTradeInput: true }); }, }, + [TradeBoxDialogTypes.SelectMarginMode]: { + title: stringGetter({ key: STRING_KEYS.MARGIN_MODE }), + content: ( + dispatch(closeDialogInTradeBox())} /> + ), + }, }[activeDialog.type]; return ( - + <$TradeBox> - { @@ -56,14 +63,11 @@ export const TradeBox = () => { {...activeDialog?.dialogProps} > {activeDialogConfig?.content} - - + + ); }; - -const Styled: Record = {}; - -Styled.TradeBox = styled.section` +const $TradeBox = styled.section` --tradeBox-content-paddingTop: 1rem; --tradeBox-content-paddingRight: 1rem; --tradeBox-content-paddingBottom: 1rem; @@ -75,16 +79,16 @@ Styled.TradeBox = styled.section` ${layoutMixins.stack} `; -Styled.Dialog = styled(Dialog)` +const $Dialog = styled(Dialog)` --dialog-backgroundColor: var(--color-layer-2); - --dialog-paddingX: 1.5rem; + --dialog-paddingX: 1.25rem; --dialog-header-paddingTop: 1.25rem; --dialog-header-paddingBottom: 0.25rem; --dialog-content-paddingTop: 1rem; - --dialog-content-paddingRight: 1.5rem; - --dialog-content-paddingBottom: 1.25rem; - --dialog-content-paddingLeft: 1.5rem; + --dialog-content-paddingRight: 1rem; + --dialog-content-paddingBottom: 1rem; + --dialog-content-paddingLeft: 1rem; `; diff --git a/src/views/TradeBoxOrderView.tsx b/src/views/TradeBoxOrderView.tsx index be0deead8..f4bd6aeb4 100644 --- a/src/views/TradeBoxOrderView.tsx +++ b/src/views/TradeBoxOrderView.tsx @@ -1,15 +1,18 @@ import { useCallback } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + import { shallowEqual, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import styled from 'styled-components'; import { TradeInputField } from '@/constants/abacus'; -import { TradeTypes } from '@/constants/trade'; import { STRING_KEYS, StringKey } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; +import { TradeTypes } from '@/constants/trade'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; -import { Tabs } from '@/components/Tabs'; +import { TabItem, Tabs } from '@/components/Tabs'; import { getInputTradeData, getInputTradeOptions } from '@/state/inputsSelectors'; @@ -18,7 +21,10 @@ import { isTruthy } from '@/lib/isTruthy'; import { TradeForm } from './forms/TradeForm'; -const useTradeTypeOptions = () => { +const useTradeTypeOptions = (): { + tradeTypeItems: TabItem[]; + selectedTradeType: TradeTypes; +} => { const stringGetter = useStringGetter(); const selectedTradeType = useSelector( createSelector( @@ -44,11 +50,12 @@ const useTradeTypeOptions = () => { // All conditional orders labeled under "Stop Order" allTradeTypeItems?.length && { label: stringGetter({ key: STRING_KEYS.STOP_ORDER_SHORT }), + value: '', subitems: allTradeTypeItems ?.map( ({ value, label }) => - value && { - value: value as TradeTypes, + value != null && { + value, label, } ) @@ -60,7 +67,7 @@ const useTradeTypeOptions = () => { }; export const TradeBoxOrderView = () => { - const onTradeTypeChange = useCallback((tradeType?: TradeTypes) => { + const onTradeTypeChange = useCallback((tradeType?: string) => { if (tradeType) { abacusStateManager.clearTradeInputValues(); abacusStateManager.setTradeValue({ value: tradeType, field: TradeInputField.type }); @@ -70,27 +77,25 @@ export const TradeBoxOrderView = () => { const { selectedTradeType, tradeTypeItems } = useTradeTypeOptions(); return ( - + <$Container> - + } fullWidthTabs /> ); }; -const Styled: Record = {}; - -Styled.Container = styled.div` +const $Container = styled.div` ${layoutMixins.scrollArea} `; -Styled.Tabs = styled(Tabs)` +const $Tabs = styled(Tabs)` overflow: hidden; -`; +` as typeof Tabs; diff --git a/src/views/charts/DepthChart/Tooltip.tsx b/src/views/charts/DepthChart/Tooltip.tsx index 3d4b0a3e3..5ff95783a 100644 --- a/src/views/charts/DepthChart/Tooltip.tsx +++ b/src/views/charts/DepthChart/Tooltip.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; + import { OrderSide } from '@dydxprotocol/v4-client-js'; import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'; import { shallowEqual, useSelector } from 'react-redux'; @@ -12,12 +13,12 @@ import { } from '@/constants/charts'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; import { useOrderbookValuesForDepthChart } from '@/hooks/Orderbook/useOrderbookValues'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { TooltipContent } from '@/components/visx/TooltipContent'; import { Details } from '@/components/Details'; import { Output, OutputType } from '@/components/Output'; +import { TooltipContent } from '@/components/visx/TooltipContent'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; @@ -38,13 +39,13 @@ export const DepthChartTooltipContent = ({ tickSizeDecimals, tooltipData, }: DepthChartTooltipProps) => { - const { nearestDatum } = tooltipData || {}; + const { nearestDatum } = tooltipData ?? {}; const stringGetter = useStringGetter(); const { spread, spreadPercent, midMarketPrice } = useOrderbookValuesForDepthChart(); const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; const priceImpact = useMemo(() => { - if (nearestDatum) { + if (nearestDatum && midMarketPrice) { const depthChartSeries = nearestDatum.key as DepthChartSeries; return { @@ -57,6 +58,7 @@ export const DepthChartTooltipContent = ({ [DepthChartSeries.MidMarket]: undefined, }[depthChartSeries]; } + return undefined; }, [nearestDatum, chartPointAtPointer.price]); @@ -200,8 +202,8 @@ export const DepthChartTooltipContent = ({ useGrouping type={OutputType.Fiat} value={ - nearestDatum - ? nearestDatum?.datum.price * nearestDatum?.datum.depth + nearestDatum != null + ? nearestDatum.datum.price * nearestDatum.datum.depth : undefined } /> diff --git a/src/views/charts/DepthChart/index.tsx b/src/views/charts/DepthChart/index.tsx index b1d210174..8cde56213 100644 --- a/src/views/charts/DepthChart/index.tsx +++ b/src/views/charts/DepthChart/index.tsx @@ -1,7 +1,22 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import styled, { AnyStyledComponent, css, keyframes } from 'styled-components'; -import { useSelector, shallowEqual } from 'react-redux'; + import { OrderSide } from '@dydxprotocol/v4-client-js'; +import { curveStepAfter } from '@visx/curve'; +import { LinearGradient } from '@visx/gradient'; +import { Point } from '@visx/point'; +import { + AreaSeries, + Axis, + DataProvider, + EventEmitterProvider, // AnimatedAxis, + Grid, // AnimatedGrid, + LineSeries, // AnimatedAreaSeries, + buildChartTheme, + darkTheme, + type EventHandlerParams, +} from '@visx/xychart'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled, { keyframes } from 'styled-components'; import { DepthChartDatum, @@ -11,32 +26,17 @@ import { } from '@/constants/charts'; import { StringGetterFunction } from '@/constants/localization'; -import { useBreakpoints } from '@/hooks'; import { useOrderbookValuesForDepthChart } from '@/hooks/Orderbook/useOrderbookValues'; - -import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; -import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; - -import { XYChartWithPointerEvents } from '@/components/visx/XYChartWithPointerEvents'; -import { - Axis, // AnimatedAxis, - Grid, // AnimatedGrid, - LineSeries, - AreaSeries, // AnimatedAreaSeries, - buildChartTheme, - darkTheme, - DataProvider, - EventEmitterProvider, - type EventHandlerParams, -} from '@visx/xychart'; -import { LinearGradient } from '@visx/gradient'; -import { curveStepAfter } from '@visx/curve'; -import { Point } from '@visx/point'; -import Tooltip from '@/components/visx/XYChartTooltipWithBounds'; -import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { OutputType } from '@/components/Output'; +import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput'; +import Tooltip from '@/components/visx/XYChartTooltipWithBounds'; +import { XYChartWithPointerEvents } from '@/components/visx/XYChartWithPointerEvents'; + +import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; +import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; @@ -119,25 +119,26 @@ export const DepthChart = ({ if (!(zoomDomain && midMarketPrice && asks.length && bids.length)) return { domain: [0, 0] as const, range: [0, 0] as const }; - const domain = [ + const newDomain = [ clamp(midMarketPrice - zoomDomain, 0, highestBid.price), clamp(midMarketPrice + zoomDomain, lowestAsk.price, highestAsk.price), ] as const; - const range = [ + const newRange = [ 0, [...bids, ...asks] - .filter((datum) => datum.price >= domain[0] && datum.price <= domain[1]) + .filter((datum) => datum.price >= newDomain[0] && datum.price <= newDomain[1]) .map((datum) => datum.depth) .reduce((a, b) => Math.max(a, b), 0), ] as const; - return { domain, range }; + return { domain: newDomain, range: newRange }; }, [orderbook, zoomDomain]); const getChartPoint = useCallback( (point: Point | EventHandlerParams) => { - let price, size; + let price; + let size; if (point instanceof Point) { const { x, y } = point as Point; price = x; @@ -167,7 +168,7 @@ export const DepthChart = ({ const onDepthChartZoom = ({ deltaY, wheelDelta = deltaY, - }: WheelEvent & { wheelDelta?: number }) => { + }: React.WheelEvent & { wheelDelta?: number }) => { setZoomDomain( clamp( Math.max( @@ -181,7 +182,7 @@ export const DepthChart = ({ }; return ( - + <$Container onWheel={onDepthChartZoom}> point && onChartClick?.(getChartPoint(point))} onPointerMove={(point) => point && setChartPointAtPointer(getChartPoint(point))} - onPointerPressedChange={(isPointerPressed) => setIsPointerPressed(isPointerPressed)} + onPointerPressedChange={(pointerPressed) => setIsPointerPressed(pointerPressed)} > ( - (isEditingOrder || tooltipData!.nearestDatum?.datum.depth) && ( - - + ); }; - -const Styled: Record = {}; - -Styled.Container = styled.div` +const $Container = styled.div` width: 0; min-width: 100%; height: 0; @@ -430,11 +428,11 @@ Styled.Container = styled.div` } `; -Styled.XAxisLabelOutput = styled(AxisLabelOutput)` +const $XAxisLabelOutput = styled(AxisLabelOutput)` box-shadow: 0 0 0.5rem var(--color-layer-2); `; -Styled.YAxisLabelOutput = styled(AxisLabelOutput)` +const $YAxisLabelOutput = styled(AxisLabelOutput)` --axisLabel-offset: 0.5rem; [data-side='left'] & { diff --git a/src/views/charts/FundingChart/Tooltip.tsx b/src/views/charts/FundingChart/Tooltip.tsx index 5e5a87a8b..1efc83c00 100644 --- a/src/views/charts/FundingChart/Tooltip.tsx +++ b/src/views/charts/FundingChart/Tooltip.tsx @@ -1,13 +1,14 @@ import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'; -import { TooltipContent } from '@/components/visx/TooltipContent'; import { FundingRateResolution, type FundingChartDatum } from '@/constants/charts'; import { STRING_KEYS } from '@/constants/localization'; import { FundingDirection } from '@/constants/markets'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { Details, DetailsItem } from '@/components/Details'; import { Output, OutputType, ShowSign } from '@/components/Output'; +import { TooltipContent } from '@/components/visx/TooltipContent'; type FundingChartTooltipProps = { fundingRateView: FundingRateResolution; @@ -19,7 +20,7 @@ export const FundingChartTooltipContent = ({ latestDatum, tooltipData, }: FundingChartTooltipProps) => { - const { nearestDatum } = tooltipData || {}; + const { nearestDatum } = tooltipData ?? {}; const stringGetter = useStringGetter(); const tooltipDatum = nearestDatum?.datum ?? latestDatum; diff --git a/src/views/charts/FundingChart/index.tsx b/src/views/charts/FundingChart/index.tsx index 136a22cf9..185de7679 100644 --- a/src/views/charts/FundingChart/index.tsx +++ b/src/views/charts/FundingChart/index.tsx @@ -1,28 +1,31 @@ import { useState } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; + import { curveMonotoneX, curveStepAfter } from '@visx/curve'; +import type { TooltipContextType } from '@visx/xychart'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { ButtonSize } from '@/constants/buttons'; import { FundingRateResolution, type FundingChartDatum } from '@/constants/charts'; import { STRING_KEYS } from '@/constants/localization'; import { FundingDirection } from '@/constants/markets'; import { SMALL_PERCENT_DECIMALS, TINY_PERCENT_DECIMALS } from '@/constants/numbers'; -import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; -import { Output, OutputType } from '@/components/Output'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; +import { Output, OutputType } from '@/components/Output'; import { ToggleGroup } from '@/components/ToggleGroup'; - -import { TimeSeriesChart } from '@/components/visx/TimeSeriesChart'; import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput'; -import type { TooltipContextType } from '@visx/xychart'; +import { TimeSeriesChart } from '@/components/visx/TimeSeriesChart'; import { calculateFundingRateHistory } from '@/state/perpetualsCalculators'; import { MustBigNumber } from '@/lib/numbers'; + import { FundingChartTooltipContent } from './Tooltip'; const FUNDING_RATE_TIME_RESOLUTION = 60 * 60 * 1000; // 1 hour @@ -91,13 +94,13 @@ export const FundingChart = ({ selectedLocale }: ElementProps) => { renderXAxisLabel={({ tooltipData }) => { const tooltipDatum = tooltipData!.nearestDatum?.datum ?? latestDatum; - return ; + return <$XAxisLabelOutput type={OutputType.DateTime} value={tooltipDatum.time} />; }} renderYAxisLabel={({ tooltipData }) => { const tooltipDatum = tooltipData!.nearestDatum?.datum ?? latestDatum; return ( - { latestDatum={latestDatum} /> )} - onTooltipContext={(tooltipContext) => setTooltipContext(tooltipContext)} + onTooltipContext={(ttContext) => setTooltipContext(ttContext)} minZoomDomain={FUNDING_RATE_TIME_RESOLUTION * 4} numGridLines={1} slotEmpty={} > - + <$FundingRateToggle> ({ value: rate as FundingRateResolution, @@ -137,15 +140,15 @@ export const FundingChart = ({ selectedLocale }: ElementProps) => { [FundingRateResolution.Annualized]: stringGetter({ key: STRING_KEYS.ANNUALIZED, }), - }[rate] || '', + }[rate] ?? '', }))} value={fundingRateView} onValueChange={setFundingRateView} size={ButtonSize.XSmall} /> - + - + <$CurrentFundingRate isShowing={!tooltipContext?.tooltipOpen}>

    { { @@ -161,27 +164,24 @@ export const FundingChart = ({ selectedLocale }: ElementProps) => { }[fundingRateView] }

    - -
    + ); }; - -const Styled: Record = {}; - -Styled.FundingRateToggle = styled.div` +const $FundingRateToggle = styled.div` place-self: start end; isolation: isolate; margin: 1rem; `; -Styled.CurrentFundingRate = styled.div<{ isShowing?: boolean }>` +const $CurrentFundingRate = styled.div<{ isShowing?: boolean }>` place-self: start center; padding: clamp(1.5rem, 9rem - 15%, 4rem); pointer-events: none; @@ -193,16 +193,6 @@ Styled.CurrentFundingRate = styled.div<{ isShowing?: boolean }>` text-align: center; - /* Hover-based */ - /* - transition: opacity var(--ease-out-expo) 0.25s 0.3s; - - ${Styled.TimeSeriesChart}:hover ${Styled.FundingRateToggle}:not(:hover) + & { - opacity: 0; - transition-delay: 0s; - } - */ - /* Tooltip state-based */ transition: opacity var(--ease-out-expo) 0.25s; ${({ isShowing }) => @@ -223,15 +213,15 @@ Styled.CurrentFundingRate = styled.div<{ isShowing?: boolean }>` } `; -Styled.Output = styled(Output)<{ isNegative?: boolean }>` +const $Output = styled(Output)<{ isNegative?: boolean }>` color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)}; `; -Styled.XAxisLabelOutput = styled(AxisLabelOutput)` +const $XAxisLabelOutput = styled(AxisLabelOutput)` box-shadow: 0 0 0.5rem var(--color-layer-2); `; -Styled.YAxisLabelOutput = styled(AxisLabelOutput)` +const $YAxisLabelOutput = styled(AxisLabelOutput)` --axisLabel-offset: 0.5rem; [data-side='left'] & { diff --git a/src/views/charts/PnlChart.tsx b/src/views/charts/PnlChart.tsx index df65430b8..d011c7d06 100644 --- a/src/views/charts/PnlChart.tsx +++ b/src/views/charts/PnlChart.tsx @@ -1,25 +1,23 @@ -import { type ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; -import { useSelector, shallowEqual } from 'react-redux'; -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { curveLinear /*, curveMonotoneX*/ } from '@visx/curve'; -import debounce from 'lodash/debounce'; +import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; + +import { curveLinear } from '@visx/curve'; +import type { TooltipContextType } from '@visx/xychart'; +import _, { debounce } from 'lodash'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { + HISTORICAL_PNL_PERIODS, HistoricalPnlPeriod, HistoricalPnlPeriods, - HISTORICAL_PNL_PERIODS, } from '@/constants/abacus'; import { timeUnits } from '@/constants/time'; -import { breakpoints } from '@/styles'; -import { useBreakpoints, useNow } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useNow } from '@/hooks/useNow'; -import { Output } from '@/components/Output'; import { ToggleGroup } from '@/components/ToggleGroup'; - -import type { TooltipContextType } from '@visx/xychart'; import { TimeSeriesChart } from '@/components/visx/TimeSeriesChart'; -import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput'; import { getSubaccount, @@ -58,6 +56,10 @@ const MS_FOR_PERIOD = { [HistoricalPnlPeriod.Period90d.name]: 90 * timeUnits.day, }; +const zoomDomainDefaultValues = new Set(Object.values(MS_FOR_PERIOD)); +const getPeriodFromName = (periodName: string) => + HISTORICAL_PNL_PERIODS[periodName as keyof typeof HISTORICAL_PNL_PERIODS]; + const DARK_CHART_BACKGROUND_URL = '/chart-dots-background-dark.svg'; const LIGHT_CHART_BACKGROUND_URL = '/chart-dots-background-light.svg'; @@ -83,57 +85,42 @@ export const PnlChart = ({ }: PnlChartProps) => { const { isTablet } = useBreakpoints(); const appTheme = useSelector(getAppTheme); - const { equity } = useSelector(getSubaccount, shallowEqual) || {}; + const { equity } = useSelector(getSubaccount, shallowEqual) ?? {}; const now = useNow({ intervalMs: timeUnits.minute }); // Chart data const pnlData = useSelector(getSubaccountHistoricalPnl, shallowEqual); const subaccountId = useSelector(getSubaccountId, shallowEqual); - const [minimumRequestedZoomDomain, setMinimumRequestedZoomDomain] = useState(-Infinity); + const [periodOptions, setPeriodOptions] = useState([ + HistoricalPnlPeriod.Period1d, + ]); const [selectedPeriod, setSelectedPeriod] = useState( - abacusStateManager.getHistoricalPnlPeriod() || HistoricalPnlPeriod.Period1d + HistoricalPnlPeriod.Period1d ); - /** - * Default period in Abacus to 90d so that we can work with a larger dataset - */ + const [isZooming, setIsZooming] = useState(false); + + // Fetch 90d data once in Abacus for the chart useEffect(() => { abacusStateManager.setHistoricalPnlPeriod(HistoricalPnlPeriod.Period90d); - }, [pnlData]); - - const onSelectPeriod = useCallback( - (periodName: string) => { - setSelectedPeriod( - HISTORICAL_PNL_PERIODS[ - (periodName as keyof typeof HISTORICAL_PNL_PERIODS) || selectedPeriod.name - ] - ); - }, - [setSelectedPeriod, selectedPeriod] - ); + }, []); + + const onSelectPeriod = (periodName: string) => setSelectedPeriod(getPeriodFromName(periodName)); + // Unselect selected period in toggle if user zooms in/out const onZoomSnap = useCallback( - debounce( - ({ zoomDomain }: { zoomDomain?: number }) => - zoomDomain && setMinimumRequestedZoomDomain(zoomDomain), - 500 - ), - [setMinimumRequestedZoomDomain] + debounce(({ zoomDomain }: { zoomDomain?: number }) => { + if (zoomDomain) { + setIsZooming(!zoomDomainDefaultValues.has(zoomDomain)); + } + }, 200), + [] ); - useEffect(() => { - const smallestRequestedPeriod = Object.entries(MS_FOR_PERIOD).find( - ([, milliseconds]) => milliseconds >= minimumRequestedZoomDomain - )?.[0]; - - if (smallestRequestedPeriod && smallestRequestedPeriod !== selectedPeriod.name) { - setSelectedPeriod( - HISTORICAL_PNL_PERIODS[smallestRequestedPeriod as keyof typeof MS_FOR_PERIOD] - ); - } - }, [minimumRequestedZoomDomain]); + // Snap back to default zoom domain according to selected period + const onToggleInteract = () => setIsZooming(false); const lastPnlTick = pnlData?.[pnlData.length - 1]; @@ -154,27 +141,56 @@ export const PnlChart = ({ (datum) => ({ id: datum.createdAtMilliseconds, - subaccountId: subaccountId, + subaccountId, equity: Number(datum.equity), totalPnl: Number(datum.totalPnl), netTransfers: Number(datum.netTransfers), createdAt: new Date(datum.createdAtMilliseconds).valueOf(), side: { [-1]: PnlSide.Loss, - [0]: PnlSide.Flat, - [1]: PnlSide.Profit, + 0: PnlSide.Flat, + 1: PnlSide.Profit, }[Math.sign(datum.equity)], } as PnlDatum) ) : [], - [pnlData, equity, selectedPeriod, now] + [pnlData, equity?.current, now] ); + // Include period option if oldest pnl is older than the previous option + // e.g. oldest pnl is 31 days old -> show 90d option + const getPeriodOptions = (oldestPnlMs: number): HistoricalPnlPeriods[] => + Object.entries(MS_FOR_PERIOD).reduce( + (acc: HistoricalPnlPeriods[], [, ms], i, arr) => { + if (oldestPnlMs < now - ms) { + const nextPeriod = _.get(arr, [i + 1, 0]); + if (nextPeriod) { + acc.push(getPeriodFromName(nextPeriod)); + } + } + return acc; + }, + [HistoricalPnlPeriod.Period1d] + ); + + const oldestPnlCreatedAt = pnlData?.[0]?.createdAtMilliseconds; + + useEffect(() => { + if (oldestPnlCreatedAt) { + const options = getPeriodOptions(oldestPnlCreatedAt); + setPeriodOptions(options); + + // default to show 7d period if there's enough data + if (options[options.length - 1] === HistoricalPnlPeriod.Period7d) + setSelectedPeriod(HistoricalPnlPeriod.Period7d); + } + }, [oldestPnlCreatedAt]); + const chartBackground = appTheme === AppTheme.Light ? LIGHT_CHART_BACKGROUND_URL : DARK_CHART_BACKGROUND_URL; return ( - + <$Container className={className} chartBackground={chartBackground}> - + <$PeriodToggle> ({ - value: period, - label: formatRelativeTime(MS_FOR_PERIOD[period], { + items={periodOptions.map((period) => ({ + value: period.name, + label: formatRelativeTime(MS_FOR_PERIOD[period.name], { locale: selectedLocale, relativeToTimestamp: 0, largestUnit: 'day', }), }))} - value={selectedPeriod.name} + value={isZooming ? '' : selectedPeriod.name} onValueChange={onSelectPeriod} + onInteraction={onToggleInteract} /> - + - + ); }; -const Styled: Record = {}; - -Styled.Container = styled.div<{ chartBackground: string }>` +const $Container = styled.div<{ chartBackground: string }>` position: relative; background: url(${({ chartBackground }) => chartBackground}) no-repeat center center; `; -Styled.PeriodToggle = styled.div` +const $PeriodToggle = styled.div` place-self: start end; isolation: isolate; margin: 1rem; `; - -Styled.SignedOutput = styled(Output)<{ side: PnlSide }>` - ${({ side }) => - ({ - [PnlSide.Loss]: css` - /* --output-sign-color: var(--color-negative); */ - color: var(--color-negative); - `, - [PnlSide.Profit]: css` - /* --output-sign-color: var(--color-positive); */ - color: var(--color-positive); - `, - [PnlSide.Flat]: css``, - }[side])}; -`; - -Styled.XAxisLabelOutput = styled(AxisLabelOutput)` - box-shadow: 0 0 0.5rem var(--color-layer-2); -`; - -Styled.YAxisLabelOutput = styled(AxisLabelOutput)` - --axisLabel-offset: 0.5rem; - - [data-side='left'] & { - translate: calc(-50% - var(--axisLabel-offset)) 0; - - @media ${breakpoints.mobile} { - translate: calc(50% + var(--axisLabel-offset)) 0; - } - } - - [data-side='right'] & { - translate: calc(-50% - var(--axisLabel-offset)) 0; - } -`; diff --git a/src/views/charts/TvChart.tsx b/src/views/charts/TvChart.tsx index 45b4455f8..7567ef3b2 100644 --- a/src/views/charts/TvChart.tsx +++ b/src/views/charts/TvChart.tsx @@ -1,8 +1,7 @@ import { useRef, useState } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; - import type { ResolutionString } from 'public/tradingview/charting_library'; +import styled, { css } from 'styled-components'; import type { TvWidget } from '@/constants/tvchart'; @@ -13,10 +12,10 @@ import { useTradingViewTheme, } from '@/hooks/tradingView'; -import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; - import { layoutMixins } from '@/styles/layoutMixins'; +import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; + export const TvChart = () => { const [isChartReady, setIsChartReady] = useState(false); @@ -37,17 +36,14 @@ export const TvChart = () => { useTradingViewTheme({ tvWidget, isWidgetReady, chartLines }); return ( - + <$PriceChart isChartReady={isChartReady}> {!isChartReady && }
    - + ); }; - -const Styled: Record = {}; - -Styled.PriceChart = styled.div<{ isChartReady?: boolean }>` +const $PriceChart = styled.div<{ isChartReady?: boolean }>` ${layoutMixins.stack} height: 100%; diff --git a/src/views/dialogs/AdjustIsolatedMarginDialog.tsx b/src/views/dialogs/AdjustIsolatedMarginDialog.tsx new file mode 100644 index 000000000..489574db3 --- /dev/null +++ b/src/views/dialogs/AdjustIsolatedMarginDialog.tsx @@ -0,0 +1,43 @@ +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import type { SubaccountPosition } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Dialog } from '@/components/Dialog'; + +import { getOpenPositionFromId } from '@/state/accountSelectors'; + +import { AdjustIsolatedMarginForm } from '../forms/AdjustIsolatedMarginForm'; + +type ElementProps = { + positionId: SubaccountPosition['id']; + setIsOpen?: (open: boolean) => void; +}; + +export const AdjustIsolatedMarginDialog = ({ positionId, setIsOpen }: ElementProps) => { + const stringGetter = useStringGetter(); + const subaccountPosition = useSelector(getOpenPositionFromId(positionId), shallowEqual); + + return ( + } + title={stringGetter({ key: STRING_KEYS.ADJUST_ISOLATED_MARGIN })} + > + <$Content> + + + + ); +}; +const $Content = styled.div` + ${layoutMixins.column} + gap: 1rem; +`; diff --git a/src/views/dialogs/AdjustTargetLeverageDialog.tsx b/src/views/dialogs/AdjustTargetLeverageDialog.tsx new file mode 100644 index 000000000..259d79ae3 --- /dev/null +++ b/src/views/dialogs/AdjustTargetLeverageDialog.tsx @@ -0,0 +1,35 @@ +import styled from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Dialog } from '@/components/Dialog'; + +import { AdjustTargetLeverageForm } from '../forms/AdjustTargetLeverageForm'; + +type ElementProps = { + setIsOpen?: (open: boolean) => void; +}; + +export const AdjustTargetLeverageDialog = ({ setIsOpen }: ElementProps) => { + const stringGetter = useStringGetter(); + + return ( + + <$Content> + setIsOpen?.(false)} /> + + + ); +}; +const $Content = styled.div` + ${layoutMixins.column} + gap: 1rem; +`; diff --git a/src/views/dialogs/ClosePositionDialog.tsx b/src/views/dialogs/ClosePositionDialog.tsx index f75a2d937..86faae4ca 100644 --- a/src/views/dialogs/ClosePositionDialog.tsx +++ b/src/views/dialogs/ClosePositionDialog.tsx @@ -1,21 +1,25 @@ import { useState } from 'react'; + import { shallowEqual, useSelector } from 'react-redux'; -import styled, { AnyStyledComponent, css } from 'styled-components'; +import styled, { css } from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { MobilePlaceOrderSteps } from '@/constants/trade'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; -import { ClosePositionForm } from '@/views/forms/ClosePositionForm'; import { Dialog, DialogPlacement } from '@/components/Dialog'; import { GreenCheckCircle } from '@/components/GreenCheckCircle'; +import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType } from '@/components/Output'; import { Ring } from '@/components/Ring'; import { VerticalSeparator } from '@/components/Separator'; import { MidMarketPrice } from '@/views/MidMarketPrice'; -import { Output, OutputType } from '@/components/Output'; +import { ClosePositionForm } from '@/views/forms/ClosePositionForm'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketData } from '@/state/perpetualsSelectors'; @@ -50,27 +54,29 @@ export const ClosePositionDialog = ({ setIsOpen }: ElementProps) => { }, [MobilePlaceOrderSteps.PreviewOrder]: { title: ( - - {stringGetter({ key: STRING_KEYS.PREVIEW_ORDER_TITLE })} - + <$PreviewTitle>{stringGetter({ key: STRING_KEYS.PREVIEW_ORDER_TITLE })} ), description: stringGetter({ key: STRING_KEYS.PREVIEW_ORDER_DESCRIPTION }), }, [MobilePlaceOrderSteps.PlacingOrder]: { title: stringGetter({ key: STRING_KEYS.PLACING_ORDER_TITLE }), description: stringGetter({ key: STRING_KEYS.PLACING_ORDER_DESCRIPTION }), - slotIcon: , + slotIcon: <$Ring withAnimation value={0.25} />, + }, + [MobilePlaceOrderSteps.PlaceOrderFailed]: { + title: stringGetter({ key: STRING_KEYS.PLACE_ORDER_FAILED }), + description: stringGetter({ key: STRING_KEYS.PLACE_ORDER_FAILED_DESCRIPTION }), + slotIcon: <$WarningIcon iconName={IconName.Warning} />, }, - // TODO(@aforaleka): add error state if trade didn't actually go through [MobilePlaceOrderSteps.Confirmation]: { title: stringGetter({ key: STRING_KEYS.CONFIRMED_TITLE }), description: stringGetter({ key: STRING_KEYS.CONFIRMED_DESCRIPTION }), - slotIcon: , + slotIcon: <$GreenCheckCircle />, }, }; return ( - { setIsOpen?.(isOpen); @@ -85,7 +91,7 @@ export const ClosePositionDialog = ({ setIsOpen }: ElementProps) => { currentStep={currentStep} > - + ); }; @@ -95,26 +101,23 @@ const CloseOrderHeader = () => { useSelector(getCurrentMarketData, shallowEqual) ?? {}; return ( - + <$CloseOrderHeader>

    {stringGetter({ key: STRING_KEYS.CLOSE })}

    - - + <$Right> + <$MarketDetails> - - - - -
    + + <$VerticalSeparator /> + + ); }; - -const Styled: Record = {}; - -Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>` +const $Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>` --dialog-backgroundColor: var(--color-layer-2); --dialog-header-height: 1rem; --dialog-content-paddingTop: 1.5rem; @@ -130,42 +133,47 @@ Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>` `} `; -Styled.Ring = styled(Ring)` +const $Ring = styled(Ring)` --ring-color: var(--color-accent); `; -Styled.GreenCheckCircle = styled(GreenCheckCircle)` +const $GreenCheckCircle = styled(GreenCheckCircle)` --icon-size: 2rem; `; -Styled.CloseOrderHeader = styled.div` +const $CloseOrderHeader = styled.div` ${layoutMixins.spacedRow} `; -Styled.Right = styled.div` +const $Right = styled.div` ${layoutMixins.inlineRow} gap: 1rem; margin-right: 0.5rem; `; -Styled.MarketDetails = styled.div` +const $MarketDetails = styled.div` ${layoutMixins.rowColumn} justify-items: flex-end; font: var(--font-medium-medium); `; -Styled.PriceChange = styled(Output)<{ isNegative?: boolean }>` +const $PriceChange = styled(Output)<{ isNegative?: boolean }>` font: var(--font-base-book); color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)}; `; -Styled.VerticalSeparator = styled(VerticalSeparator)` +const $VerticalSeparator = styled(VerticalSeparator)` && { height: 3rem; } `; -Styled.PreviewTitle = styled.div` +const $PreviewTitle = styled.div` ${layoutMixins.inlineRow} height: var(--dialog-icon-size); `; + +const $WarningIcon = styled(Icon)` + color: var(--color-warning); + font-size: 1.5rem; +`; diff --git a/src/views/dialogs/ComplianceConfigDialog.tsx b/src/views/dialogs/ComplianceConfigDialog.tsx new file mode 100644 index 000000000..763d7d931 --- /dev/null +++ b/src/views/dialogs/ComplianceConfigDialog.tsx @@ -0,0 +1,125 @@ +import { useMemo } from 'react'; + +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import type { Compliance } from '@/constants/abacus'; +import { ComplianceStatus } from '@/constants/abacus'; +import { ButtonAction } from '@/constants/buttons'; +import { BLOCKED_COUNTRIES, CountryCodes, OFAC_SANCTIONED_COUNTRIES } from '@/constants/geo'; +import { MenuGroup } from '@/constants/menus'; + +import { useAccounts } from '@/hooks/useAccounts'; +import { useDydxClient } from '@/hooks/useDydxClient'; + +import { Button } from '@/components/Button'; +import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu'; +import { Switch } from '@/components/Switch'; + +import { setCompliance } from '@/state/account'; +import { getComplianceStatus, getGeo } from '@/state/accountSelectors'; + +const complianceStatusOptions = [ + { status: ComplianceStatus.COMPLIANT, label: 'Compliant' }, + { status: ComplianceStatus.BLOCKED, label: 'Blocked' }, + { status: ComplianceStatus.CLOSE_ONLY, label: 'Close Only' }, + { status: ComplianceStatus.FIRST_STRIKE, label: 'First Strike' }, + { status: ComplianceStatus.FIRST_STRIKE_CLOSE_ONLY, label: 'First Strike Close Only' }, +]; + +export const usePreferenceMenu = () => { + const dispatch = useDispatch(); + + const complianceStatus = useSelector(getComplianceStatus, shallowEqual); + const geo = useSelector(getGeo, shallowEqual); + const geoRestricted = Boolean( + geo && [...BLOCKED_COUNTRIES, ...OFAC_SANCTIONED_COUNTRIES].includes(geo as CountryCodes) + ); + + const notificationSection = useMemo( + (): MenuGroup => ({ + group: 'status', + groupLabel: 'Simulate Compliance Status', + items: complianceStatusOptions.map(({ status, label }) => ({ + value: status.name, + label, + onSelect: () => + dispatch(setCompliance({ geo, status, updatedAt: new Date().toString() } as Compliance)), + slotAfter: ( + null} // Assuming the onChange logic is to be defined or is unnecessary + /> + ), + })), + }), + [complianceStatus, setCompliance] + ); + + const otherSection = useMemo( + (): MenuGroup => ({ + group: 'Geo', + items: [ + { + value: 'RestrictGeo', + label: 'Simulate Restricted Geo', + slotAfter: ( + null} /> + ), + onSelect: () => { + dispatch( + geoRestricted + ? setCompliance({ geo: '', status: complianceStatus } as Compliance) + : setCompliance({ geo: 'US', status: complianceStatus } as Compliance) + ); + }, + }, + ], + }), + [geoRestricted] + ); + + return [otherSection, notificationSection]; +}; + +type ElementProps = { + setIsOpen: (open: boolean) => void; +}; + +export const ComplianceConfigDialog = ({ setIsOpen }: ElementProps) => { + const preferenceItems = usePreferenceMenu(); + const complianceStatus = useSelector(getComplianceStatus, shallowEqual); + + const { dydxAddress } = useAccounts(); + const { compositeClient } = useDydxClient(); + + const submit = async () => { + const endpoint = `${compositeClient?.indexerClient.config.restEndpoint}/v4/compliance/setStatus`; + if (dydxAddress) { + await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ address: dydxAddress, status: complianceStatus?.name }), + }); + } + }; + + return ( + <$ComboboxDialogMenu + isOpen + title="Compliance Settings (Dev Only)" + items={preferenceItems} + setIsOpen={setIsOpen} + > + + + ); +}; +const $ComboboxDialogMenu = styled(ComboboxDialogMenu)` + --dialog-content-paddingBottom: 0.5rem; +` as typeof ComboboxDialogMenu; diff --git a/src/views/dialogs/DepositDialog.tsx b/src/views/dialogs/DepositDialog.tsx index f46022d06..0863db0a4 100644 --- a/src/views/dialogs/DepositDialog.tsx +++ b/src/views/dialogs/DepositDialog.tsx @@ -1,5 +1,7 @@ import { STRING_KEYS } from '@/constants/localization'; -import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { Dialog, DialogPlacement } from '@/components/Dialog'; diff --git a/src/views/dialogs/DepositDialog/DepositDialogContent.tsx b/src/views/dialogs/DepositDialog/DepositDialogContent.tsx index bfd9046b2..2f8321a04 100644 --- a/src/views/dialogs/DepositDialog/DepositDialogContent.tsx +++ b/src/views/dialogs/DepositDialog/DepositDialogContent.tsx @@ -1,9 +1,11 @@ import { useEffect, useState } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; + +import styled from 'styled-components'; import { TransferInputField, TransferType } from '@/constants/abacus'; import { AnalyticsEvent } from '@/constants/analytics'; import { isMainnet } from '@/constants/networks'; + import { layoutMixins } from '@/styles/layoutMixins'; import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm'; @@ -34,7 +36,7 @@ export const DepositDialogContent = ({ onDeposit }: ElementProps) => { }, []); return ( - + <$Content> {isMainnet || !showFaucet ? ( { @@ -51,17 +53,14 @@ export const DepositDialogContent = ({ onDeposit }: ElementProps) => { /> )} {!isMainnet && ( - setShowFaucet(!showFaucet)}> + <$TextToggle onClick={() => setShowFaucet(!showFaucet)}> {showFaucet ? 'Show deposit form' : 'Show test faucet'} - + )} - + ); }; - -const Styled: Record = {}; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.flexColumn} gap: 1rem; @@ -70,7 +69,7 @@ Styled.Content = styled.div` } `; -Styled.TextToggle = styled.div` +const $TextToggle = styled.div` ${layoutMixins.stickyFooter} --stickyArea-bottomHeight: 0; diff --git a/src/views/dialogs/DetailsDialog/FillDetailsDialog.tsx b/src/views/dialogs/DetailsDialog/FillDetailsDialog.tsx index 0ba26c987..62336bbb7 100644 --- a/src/views/dialogs/DetailsDialog/FillDetailsDialog.tsx +++ b/src/views/dialogs/DetailsDialog/FillDetailsDialog.tsx @@ -1,9 +1,10 @@ -import { useSelector } from 'react-redux'; -import styled, { AnyStyledComponent } from 'styled-components'; import { DateTime } from 'luxon'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { AssetIcon } from '@/components/AssetIcon'; import { type DetailsItem } from '@/components/Details'; @@ -94,16 +95,13 @@ export const FillDetailsDialog = ({ fillId, setIsOpen }: ElementProps) => { return ( } + slotIcon={<$AssetIcon symbol={asset?.id} />} title={resources.typeStringKey && stringGetter({ key: resources.typeStringKey })} items={detailItems} setIsOpen={setIsOpen} /> ); }; - -const Styled: Record = {}; - -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` font-size: 1em; `; diff --git a/src/views/dialogs/DetailsDialog/OrderDetailsDialog.tsx b/src/views/dialogs/DetailsDialog/OrderDetailsDialog.tsx index 904d15d54..dafa31f24 100644 --- a/src/views/dialogs/DetailsDialog/OrderDetailsDialog.tsx +++ b/src/views/dialogs/DetailsDialog/OrderDetailsDialog.tsx @@ -1,37 +1,32 @@ -import { useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import styled, { AnyStyledComponent, css } from 'styled-components'; - -import { useStringGetter, useSubaccount } from '@/hooks'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { AbacusOrderStatus, AbacusOrderTypes, type Nullable } from '@/constants/abacus'; import { ButtonAction } from '@/constants/buttons'; import { STRING_KEYS, type StringKey } from '@/constants/localization'; +import { CancelOrderStatuses } from '@/constants/trade'; + +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useSubaccount } from '@/hooks/useSubaccount'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { Button } from '@/components/Button'; import { type DetailsItem } from '@/components/Details'; import { DetailsDialog } from '@/components/DetailsDialog'; -import { Icon } from '@/components/Icon'; import { OrderSideTag } from '@/components/OrderSideTag'; import { Output, OutputType } from '@/components/Output'; +import { OrderStatusIcon } from '@/views/OrderStatusIcon'; import { type OrderTableRow } from '@/views/tables/OrdersTable'; +import { clearOrder } from '@/state/account'; import { calculateIsAccountViewOnly } from '@/state/accountCalculators'; -import { getOrderDetails } from '@/state/accountSelectors'; +import { getLocalCancelOrders, getOrderDetails } from '@/state/accountSelectors'; import { getSelectedLocale } from '@/state/localizationSelectors'; -import { clearOrder } from '@/state/account'; - import { MustBigNumber } from '@/lib/numbers'; - -import { - isOrderStatusClearable, - isMarketOrderType, - relativeTimeString, - getStatusIconInfo, -} from '@/lib/orders'; +import { isMarketOrderType, isOrderStatusClearable, relativeTimeString } from '@/lib/orders'; type ElementProps = { orderId: string; @@ -43,9 +38,13 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { const dispatch = useDispatch(); const selectedLocale = useSelector(getSelectedLocale); const isAccountViewOnly = useSelector(calculateIsAccountViewOnly); - + const localCancelOrders = useSelector(getLocalCancelOrders, shallowEqual); const { cancelOrder } = useSubaccount(); + const localCancelOrder = localCancelOrders.find((order) => order.orderId === orderId); + const isOrderCanceling = + localCancelOrder && localCancelOrder.submissionStatus < CancelOrderStatuses.Canceled; + const { asset, cancelReason, @@ -65,27 +64,21 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { trailingPercent, triggerPrice, type, - } = (useSelector(getOrderDetails(orderId)) as OrderTableRow) || {}; - const [isPlacingCancel, setIsPlacingCancel] = useState(false); - - const { statusIcon, statusIconColor, statusStringKey } = getStatusIconInfo({ - status, - totalFilled, - }); + } = (useSelector(getOrderDetails(orderId)) as OrderTableRow) ?? {}; const renderOrderPrice = ({ - type, - price, - tickSizeDecimals, + type: innerType, + price: innerPrice, + tickSizeDecimals: innerTickSizeDecimals, }: { type?: AbacusOrderTypes; price?: Nullable; tickSizeDecimals: number; }) => - isMarketOrderType(type) ? ( + isMarketOrderType(innerType) ? ( stringGetter({ key: STRING_KEYS.MARKET_PRICE_SHORT }) ) : ( - + ); const renderOrderTime = ({ timeInMs }: { timeInMs: Nullable }) => @@ -106,16 +99,12 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { key: 'status', label: stringGetter({ key: STRING_KEYS.STATUS }), value: ( - - - - {statusStringKey - ? stringGetter({ key: statusStringKey }) - : resources.statusStringKey - ? stringGetter({ key: resources.statusStringKey }) - : undefined} - - + <$Row> + + <$Status> + {resources.statusStringKey && stringGetter({ key: resources.statusStringKey })} + + ), }, { @@ -182,9 +171,8 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { }, ].filter((item) => Boolean(item.value)) as DetailsItem[]; - const onCancelClick = async () => { - setIsPlacingCancel(true); - await cancelOrder({ orderId, onError: () => setIsPlacingCancel(false) }); + const onCancelClick = () => { + cancelOrder({ orderId }); }; const onClearClick = () => { @@ -194,7 +182,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { return ( } + slotIcon={<$AssetIcon symbol={asset?.id} />} title={!resources.typeStringKey ? '' : stringGetter({ key: resources.typeStringKey })} slotFooter={ isAccountViewOnly ? null : isOrderStatusClearable(status) ? ( @@ -203,8 +191,8 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { - - + + ); }; - -const Styled: Record = {}; - -Styled.ButtonRow = styled.div` +const $ButtonRow = styled.div` ${layoutMixins.row} gap: 0.5rem; justify-content: end; `; -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; `; diff --git a/src/views/dialogs/DisplaySettingsDialog.tsx b/src/views/dialogs/DisplaySettingsDialog.tsx index 61f5e1261..aa2876f61 100644 --- a/src/views/dialogs/DisplaySettingsDialog.tsx +++ b/src/views/dialogs/DisplaySettingsDialog.tsx @@ -1,29 +1,28 @@ +import { Indicator, Item, Root } from '@radix-ui/react-radio-group'; import { useDispatch, useSelector } from 'react-redux'; -import styled, { AnyStyledComponent, css } from 'styled-components'; +import styled, { css } from 'styled-components'; -import { Root, Item, Indicator } from '@radix-ui/react-radio-group'; - -import { useStringGetter } from '@/hooks'; +import { STRING_KEYS } from '@/constants/localization'; -import { - AppTheme, - type AppThemeSetting, - AppThemeSystemSetting, - AppColorMode, - setAppThemeSetting, - setAppColorMode, -} from '@/state/configs'; -import { getAppTheme, getAppThemeSetting, getAppColorMode } from '@/state/configsSelectors'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; import { Themes } from '@/styles/themes'; -import { STRING_KEYS } from '@/constants/localization'; - import { Dialog } from '@/components/Dialog'; import { Icon, IconName } from '@/components/Icon'; import { HorizontalSeparatorFiller } from '@/components/Separator'; +import { + AppColorMode, + AppTheme, + AppThemeSystemSetting, + setAppColorMode, + setAppThemeSetting, + type AppThemeSetting, +} from '@/state/configs'; +import { getAppColorMode, getAppThemeSetting } from '@/state/configsSelectors'; + type ElementProps = { setIsOpen: (open: boolean) => void; }; @@ -33,21 +32,20 @@ export const DisplaySettingsDialog = ({ setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); const currentThemeSetting: AppThemeSetting = useSelector(getAppThemeSetting); - const currentTheme: AppTheme = useSelector(getAppTheme); const currentColorMode: AppColorMode = useSelector(getAppColorMode); const sectionHeader = (heading: string) => { return ( - + <$Header> {heading} - + ); }; const themePanels = () => { return ( - + <$AppThemeRoot value={currentThemeSetting}> {[ { themeSetting: AppTheme.Classic, @@ -78,7 +76,7 @@ export const DisplaySettingsDialog = ({ setIsOpen }: ElementProps) => { const textColor = Themes[theme][currentColorMode].textPrimary; return ( - { dispatch(setAppThemeSetting(themeSetting)); }} > - + <$AppThemeHeader textcolor={textColor}> {stringGetter({ key: label })} - - - - - - + + <$Image src="/chart-bars.svg" /> + <$CheckIndicator> + <$CheckIcon iconName={IconName.Check} /> + + ); })} - + ); }; const colorModeOptions = () => { return ( - + <$ColorPreferenceRoot value={currentColorMode}> {[ { colorMode: AppColorMode.GreenUp, @@ -114,32 +112,32 @@ export const DisplaySettingsDialog = ({ setIsOpen }: ElementProps) => { label: STRING_KEYS.RED_IS_UP, }, ].map(({ colorMode, label }) => ( - { dispatch(setAppColorMode(colorMode)); }} > - - - + <$ArrowIconContainer> + <$ArrowIcon iconName={IconName.Arrow} direction="up" color={colorMode === AppColorMode.GreenUp ? 'green' : 'red'} /> - - + {stringGetter({ key: label })} - - - + + <$DotIndicator $selected={currentColorMode === colorMode} /> + ))} - + ); }; @@ -149,45 +147,42 @@ export const DisplaySettingsDialog = ({ setIsOpen }: ElementProps) => { setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.DISPLAY_SETTINGS })} > - + <$Section> {sectionHeader(stringGetter({ key: STRING_KEYS.THEME }))} {themePanels()} - - + + <$Section> {sectionHeader(stringGetter({ key: STRING_KEYS.DIRECTION_COLOR_PREFERENCE }))} {colorModeOptions()} - + ); }; - -const Styled: Record = {}; - const gridStyle = css` display: grid; gap: 1.5rem; `; -Styled.Section = styled.div` +const $Section = styled.div` ${gridStyle} padding: 1rem 0; `; -Styled.Header = styled.header` +const $Header = styled.header` ${layoutMixins.inlineRow} `; -Styled.AppThemeRoot = styled(Root)` +const $AppThemeRoot = styled(Root)` ${gridStyle} grid-template-columns: 1fr 1fr; `; -Styled.ColorPreferenceRoot = styled(Root)` +const $ColorPreferenceRoot = styled(Root)` ${gridStyle} grid-template-columns: 1fr; `; -Styled.Item = styled(Item)` +const $Item = styled(Item)` --border-color: var(--color-border); --item-padding: 0.75rem; @@ -201,7 +196,7 @@ Styled.Item = styled(Item)` padding: var(--item-padding); `; -Styled.ColorPreferenceItem = styled(Styled.Item)` +const $ColorPreferenceItem = styled($Item)` &[data-state='checked'] { background-color: var(--color-layer-4); } @@ -210,7 +205,7 @@ Styled.ColorPreferenceItem = styled(Styled.Item)` justify-content: space-between; `; -Styled.AppThemeItem = styled(Styled.Item)<{ backgroundcolor: string; gridcolor: string }>` +const $AppThemeItem = styled($Item)<{ backgroundcolor: string; gridcolor: string }>` ${({ backgroundcolor, gridcolor }) => css` --themePanel-backgroundColor: ${backgroundcolor}; --themePanel-gridColor: ${gridcolor}; @@ -244,51 +239,51 @@ Styled.AppThemeItem = styled(Styled.Item)<{ backgroundcolor: string; gridcolor: } `; -Styled.AppThemeHeader = styled.h3<{ textcolor: string }>` +const $AppThemeHeader = styled.h3<{ textcolor: string }>` ${({ textcolor }) => css` color: ${textcolor}; `} z-index: 1; `; -Styled.Image = styled.img` +const $Image = styled.img` width: 100%; height: auto; z-index: 1; `; -Styled.ColorPreferenceLabel = styled.div` +const $ColorPreferenceLabel = styled.div` ${layoutMixins.inlineRow}; gap: 1ch; `; -Styled.ArrowIconContainer = styled.div` +const $ArrowIconContainer = styled.div` ${layoutMixins.column} - gap: 0.5ch; + gap: 0.25rem; svg { - height: 0.75em; - width: 0.75em; + height: 0.875em; + width: 0.875em; } `; -Styled.ArrowIcon = styled(Icon)<{ direction: 'up' | 'down'; color: 'green' | 'red' }>` +const $ArrowIcon = styled(Icon)<{ direction: 'up' | 'down'; color: 'green' | 'red' }>` ${({ direction }) => ({ - ['up']: css` + up: css` transform: rotate(-90deg); `, - ['down']: css` + down: css` transform: rotate(90deg); `, }[direction])} ${({ color }) => ({ - ['green']: css` + green: css` color: var(--color-green); `, - ['red']: css` + red: css` color: var(--color-red); `, }[color])} @@ -307,7 +302,7 @@ const indicatorStyle = css` justify-content: center; `; -Styled.DotIndicator = styled.div<{ $selected: boolean }>` +const $DotIndicator = styled.div<{ $selected: boolean }>` ${indicatorStyle} --background-color: var(--color-layer-2); --border-color: var(--color-border); @@ -332,7 +327,7 @@ Styled.DotIndicator = styled.div<{ $selected: boolean }>` border: solid var(--border-width) var(--border-color); `; -Styled.CheckIndicator = styled(Indicator)` +const $CheckIndicator = styled(Indicator)` ${indicatorStyle} position: absolute; bottom: var(--item-padding); @@ -342,7 +337,7 @@ Styled.CheckIndicator = styled(Indicator)` color: var(--color-text-button); `; -Styled.CheckIcon = styled(Icon)` +const $CheckIcon = styled(Icon)` width: var(--icon-size); height: var(--icon-size); `; diff --git a/src/views/dialogs/ExchangeOfflineDialog.tsx b/src/views/dialogs/ExchangeOfflineDialog.tsx index d25a8f3cb..bc7f9ea29 100644 --- a/src/views/dialogs/ExchangeOfflineDialog.tsx +++ b/src/views/dialogs/ExchangeOfflineDialog.tsx @@ -1,22 +1,23 @@ import { useEffect } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { AbacusApiStatus } from '@/constants/abacus'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { isDev } from '@/constants/networks'; -import { useApiState, useStringGetter } from '@/hooks'; +import { useApiState } from '@/hooks/useApiState'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { Dialog } from '@/components/Dialog'; -import { Link } from '@/components/Link'; import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu'; -import { closeDialog } from '@/state/dialogs'; - import { getSelectedNetwork } from '@/state/appSelectors'; +import { closeDialog } from '@/state/dialogs'; import { getActiveDialog } from '@/state/dialogsSelectors'; type ElementProps = { @@ -44,22 +45,14 @@ export const ExchangeOfflineDialog = ({ preventClose, setIsOpen }: ElementProps) setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.UNAVAILABLE })} > - -

    {statusErrorMessage}

    + <$Content> +

    {statusErrorMessage?.body}

    {isDev && } -
    + ); }; - -const Styled: Record = {}; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; `; - -Styled.Link = styled(Link)` - display: contents; - --link-color: var(--color-accent); -`; diff --git a/src/views/dialogs/ExternalLinkDialog.tsx b/src/views/dialogs/ExternalLinkDialog.tsx index 231cb24fd..fe2ee2ab6 100644 --- a/src/views/dialogs/ExternalLinkDialog.tsx +++ b/src/views/dialogs/ExternalLinkDialog.tsx @@ -1,15 +1,17 @@ import type { ReactNode } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; + +import styled from 'styled-components'; import { ButtonAction, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; -import { Button } from '@/components/Button'; -import { Dialog } from '@/components/Dialog'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; +import { Button } from '@/components/Button'; +import { Dialog } from '@/components/Dialog'; + type ElementProps = { buttonText?: ReactNode; link: string; @@ -37,20 +39,17 @@ export const ExternalLinkDialog = ({ linkDescription ?? stringGetter({ key: STRING_KEYS.LEAVING_WEBSITE_DESCRIPTION }) } > - + <$Content> {slotContent}

    {stringGetter({ key: STRING_KEYS.LEAVING_WEBSITE_DISCLAIMER })}.

    -
    + ); }; - -const Styled: Record = {}; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.flexColumn} gap: 1rem; diff --git a/src/views/dialogs/ExternalNavKeplrDialog.tsx b/src/views/dialogs/ExternalNavKeplrDialog.tsx index d1622c6e2..cbda6a5ef 100644 --- a/src/views/dialogs/ExternalNavKeplrDialog.tsx +++ b/src/views/dialogs/ExternalNavKeplrDialog.tsx @@ -1,11 +1,17 @@ import { useCallback } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; + import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; -import { useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; import { Dialog, DialogPlacement } from '@/components/Dialog'; @@ -14,8 +20,6 @@ import { IconButton } from '@/components/IconButton'; import { closeDialog, openDialog } from '@/state/dialogs'; -import { layoutMixins } from '@/styles/layoutMixins'; - type ElementProps = { setIsOpen: (open: boolean) => void; }; @@ -53,12 +57,8 @@ export const ExternalNavKeplrDialog = ({ setIsOpen }: ElementProps) => { title={stringGetter({ key: STRING_KEYS.HAVE_YOU_EXPORTED })} placement={isTablet ? DialogPlacement.FullScreen : DialogPlacement.Default} > - - + <$Content> + <$Button type={ButtonType.Button} size={ButtonSize.XLarge} onClick={onExternalNavDialog}> {stringGetter({ key: STRING_KEYS.NAVIGATE_TO_KEPLR, @@ -68,18 +68,14 @@ export const ExternalNavKeplrDialog = ({ setIsOpen }: ElementProps) => { })} - - + - + <$Button type={ButtonType.Link} size={ButtonSize.XLarge} href={accountExportLearnMore}> {stringGetter({ key: STRING_KEYS.LEARN_TO_EXPORT, @@ -89,32 +85,18 @@ export const ExternalNavKeplrDialog = ({ setIsOpen }: ElementProps) => { })} - - - + + ); }; -const Styled: Record = {}; - -Styled.TextToggle = styled.div` - ${layoutMixins.stickyFooter} - color: var(--color-accent); - cursor: pointer; - - margin-top: auto; - - &:hover { - text-decoration: underline; - } -`; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.stickyArea0} --stickyArea0-bottomHeight: 2rem; --stickyArea0-bottomGap: 1rem; @@ -124,7 +106,7 @@ Styled.Content = styled.div` gap: 1rem; `; -Styled.Button = styled(Button)` +const $Button = styled(Button)` --button-font: var(--font-base-book); --button-padding: 0 1.5rem; @@ -133,7 +115,7 @@ Styled.Button = styled(Button)` justify-content: space-between; `; -Styled.IconButton = styled(IconButton)` +const $IconButton = styled(IconButton)` color: var(--color-text-0); --color-border: var(--color-layer-6); `; diff --git a/src/views/dialogs/ExternalNavStrideDialog.tsx b/src/views/dialogs/ExternalNavStrideDialog.tsx index 2511f8b3a..96cea6573 100644 --- a/src/views/dialogs/ExternalNavStrideDialog.tsx +++ b/src/views/dialogs/ExternalNavStrideDialog.tsx @@ -1,11 +1,16 @@ import { useCallback } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; + import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; -import { useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; + import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; @@ -32,10 +37,10 @@ export const ExternalNavStrideDialog = ({ setIsOpen }: ElementProps) => { type: DialogTypes.ExternalLink, dialogProps: { buttonText: ( - + <$Span> {stringGetter({ key: STRING_KEYS.LIQUID_STAKE_ON_STRIDE })} - + ), link: strideZoneApp, title: stringGetter({ key: STRING_KEYS.LIQUID_STAKING_AND_LEAVING }), @@ -57,12 +62,8 @@ export const ExternalNavStrideDialog = ({ setIsOpen }: ElementProps) => { title={stringGetter({ key: STRING_KEYS.HAVE_YOU_EXPORTED })} placement={isTablet ? DialogPlacement.FullScreen : DialogPlacement.Default} > - - + <$Content> + <$Button type={ButtonType.Button} size={ButtonSize.XLarge} onClick={openExternalNavDialog}> {stringGetter({ key: STRING_KEYS.NAVIGATE_TO_STRIDE, @@ -72,18 +73,14 @@ export const ExternalNavStrideDialog = ({ setIsOpen }: ElementProps) => { })} - - + - + <$Button type={ButtonType.Link} size={ButtonSize.XLarge} href={accountExportLearnMore}> {stringGetter({ key: STRING_KEYS.LEARN_TO_EXPORT, @@ -93,32 +90,18 @@ export const ExternalNavStrideDialog = ({ setIsOpen }: ElementProps) => { })} - - - + + ); }; -const Styled: Record = {}; - -Styled.TextToggle = styled.div` - ${layoutMixins.stickyFooter} - color: var(--color-accent); - cursor: pointer; - - margin-top: auto; - - &:hover { - text-decoration: underline; - } -`; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.stickyArea0} --stickyArea0-bottomHeight: 2rem; --stickyArea0-bottomGap: 1rem; @@ -128,7 +111,7 @@ Styled.Content = styled.div` gap: 1rem; `; -Styled.Button = styled(Button)` +const $Button = styled(Button)` --button-font: var(--font-base-book); --button-padding: 0 1.5rem; @@ -137,12 +120,12 @@ Styled.Button = styled(Button)` justify-content: space-between; `; -Styled.IconButton = styled(IconButton)` +const $IconButton = styled(IconButton)` color: var(--color-text-0); --color-border: var(--color-layer-6); `; -Styled.Span = styled.span` +const $Span = styled.span` display: flex; align-items: center; gap: 0.5ch; diff --git a/src/views/dialogs/GeoComplianceDialog.tsx b/src/views/dialogs/GeoComplianceDialog.tsx new file mode 100644 index 000000000..f65f98c6e --- /dev/null +++ b/src/views/dialogs/GeoComplianceDialog.tsx @@ -0,0 +1,154 @@ +import { useState } from 'react'; + +import styled from 'styled-components'; + +import { ComplianceAction, Nullable, ParsingError } from '@/constants/abacus'; +import { ButtonAction } from '@/constants/buttons'; +import { COUNTRIES_MAP } from '@/constants/geo'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { formMixins } from '@/styles/formMixins'; + +import { Button } from '@/components/Button'; +import { Checkbox } from '@/components/Checkbox'; +import { Dialog, DialogPlacement } from '@/components/Dialog'; +import { SearchSelectMenu } from '@/components/SearchSelectMenu'; +import { WithReceipt } from '@/components/WithReceipt'; + +import abacusStateManager from '@/lib/abacus'; +import { isBlockedGeo } from '@/lib/compliance'; +import { log } from '@/lib/telemetry'; + +type ElementProps = { + setIsOpen?: (open: boolean) => void; +}; + +const CountrySelector = ({ + label, + selectedCountry, + onSelect, +}: { + label: string; + selectedCountry: string; + onSelect: (country: string) => void; +}) => { + const stringGetter = useStringGetter(); + + const countriesList = Object.keys(COUNTRIES_MAP).map((country) => ({ + value: country, + label: country, + onSelect: () => onSelect(country), + })); + + return ( + + <$SelectedCountry> + {selectedCountry || stringGetter({ key: STRING_KEYS.SELECT_A_COUNTRY })} + + + ); +}; + +export const GeoComplianceDialog = ({ setIsOpen }: ElementProps) => { + const stringGetter = useStringGetter(); + + const [residence, setResidence] = useState(''); + const [hasAcknowledged, setHasAcknowledged] = useState(false); + + const [showForm, setShowForm] = useState(false); + const { isMobile } = useBreakpoints(); + + const submit = async () => { + const action = + residence && isBlockedGeo(COUNTRIES_MAP[residence]) + ? ComplianceAction.INVALID_SURVEY + : ComplianceAction.VALID_SURVEY; + + const callback = (success: boolean, parsingError?: Nullable) => { + if (success) { + setIsOpen?.(false); + } else { + log('useWithdrawalInfo/getWithdrawalCapacityByDenom', new Error(parsingError?.message)); + } + }; + abacusStateManager.triggerCompliance(action, callback); + }; + + return ( + + {showForm ? ( + <$Form> + + <$WithReceipt + slotReceipt={ + <$CheckboxContainer> + + + } + > + + + + ) : ( + <$Form> +

    {stringGetter({ key: STRING_KEYS.COMPLIANCE_BODY_FIRST_OFFENSE_1 })}

    +

    {stringGetter({ key: STRING_KEYS.COMPLIANCE_BODY_FIRST_OFFENSE_2 })}

    + + + )} +
    + ); +}; +const $Form = styled.form` + ${formMixins.transfersForm} +`; + +const $SelectedCountry = styled.div` + text-align: start; +`; + +const $CheckboxContainer = styled.div` + padding: 1rem; + color: var(--color-text-0); +`; + +const $WithReceipt = styled(WithReceipt)` + --withReceipt-backgroundColor: var(--color-layer-2); +`; diff --git a/src/views/dialogs/GlobalCommandDialog.tsx b/src/views/dialogs/GlobalCommandDialog.tsx index e984ec17d..62f6f2d2c 100644 --- a/src/views/dialogs/GlobalCommandDialog.tsx +++ b/src/views/dialogs/GlobalCommandDialog.tsx @@ -1,6 +1,7 @@ -import { useCommandMenu } from '@/hooks'; -import { useGlobalCommands } from '@/views/menus/useGlobalCommands'; +import { useCommandMenu } from '@/hooks/useCommandMenu'; + import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu'; +import { useGlobalCommands } from '@/views/menus/useGlobalCommands'; export const GlobalCommandDialog = () => { const { isCommandMenuOpen, setIsCommandMenuOpen, closeCommandMenu } = useCommandMenu(); diff --git a/src/views/dialogs/HelpDialog.tsx b/src/views/dialogs/HelpDialog.tsx index 156303228..52af4f46d 100644 --- a/src/views/dialogs/HelpDialog.tsx +++ b/src/views/dialogs/HelpDialog.tsx @@ -1,25 +1,33 @@ import { useMemo } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter, useURLConfigs } from '@/hooks'; +import { MenuConfig } from '@/constants/menus'; + +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; + +import { breakpoints } from '@/styles'; import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu'; import { Icon, IconName } from '@/components/Icon'; import { isTruthy } from '@/lib/isTruthy'; -import { breakpoints } from '@/styles'; type ElementProps = { setIsOpen: (open: boolean) => void; }; +const latestCommit = import.meta.env.VITE_LAST_ORIGINAL_COMMIT; +const latestVersion = import.meta.env.VITE_LAST_TAG; + export const HelpDialog = ({ setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); const { help: helpCenter, community } = useURLConfigs(); const HELP_ITEMS = useMemo( - () => [ + (): MenuConfig => [ { group: 'help-items', items: [ @@ -28,7 +36,9 @@ export const HelpDialog = ({ setIsOpen }: ElementProps) => { label: stringGetter({ key: STRING_KEYS.HELP_CENTER }), description: stringGetter({ key: STRING_KEYS.HELP_CENTER_DESCRIPTION }), onSelect: () => { - helpCenter && globalThis.open(helpCenter, '_blank'); + if (helpCenter) { + globalThis.open(helpCenter, '_blank'); + } setIsOpen(false); }, slotBefore: , @@ -48,7 +58,9 @@ export const HelpDialog = ({ setIsOpen }: ElementProps) => { label: stringGetter({ key: STRING_KEYS.COMMUNITY }), description: stringGetter({ key: STRING_KEYS.COMMUNITY_DESCRIPTION }), onSelect: () => { - community && globalThis.open(community, '_blank'); + if (community) { + globalThis.open(community, '_blank'); + } setIsOpen(false); }, slotBefore: , @@ -60,19 +72,33 @@ export const HelpDialog = ({ setIsOpen }: ElementProps) => { ); return ( - + {latestCommit && ( + + Release - {`${latestCommit.substring(0, 7)}`} + + )} + {latestVersion && ( + + Version -{' '} + {`${latestVersion.split(`release/v`).at(-1)}`} + + )} + + ) : undefined + } /> ); }; - -const Styled: Record = {}; - -Styled.ComboboxDialogMenu = styled(ComboboxDialogMenu)` +const $ComboboxDialogMenu = styled(ComboboxDialogMenu)` --dialog-content-paddingTop: 1rem; --dialog-content-paddingBottom: 1rem; --comboxDialogMenu-item-gap: 1rem; @@ -81,3 +107,12 @@ Styled.ComboboxDialogMenu = styled(ComboboxDialogMenu)` --dialog-width: var(--dialog-small-width); } `; + +const $Footer = styled.div` + display: flex; + flex-direction: column; + + color: var(--color-text-0); + cursor: default; + user-select: text; +`; diff --git a/src/views/dialogs/ManageFundsDialog.tsx b/src/views/dialogs/ManageFundsDialog.tsx index 3db65193c..cd58e45c9 100644 --- a/src/views/dialogs/ManageFundsDialog.tsx +++ b/src/views/dialogs/ManageFundsDialog.tsx @@ -1,15 +1,16 @@ import { shallowEqual, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { TransferInputField, TransferType } from '@/constants/abacus'; -import { STRING_KEYS } from '@/constants/localization'; import { ButtonSize } from '@/constants/buttons'; -import { useStringGetter } from '@/hooks'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { Dialog, DialogPlacement } from '@/components/Dialog'; import { ToggleGroup } from '@/components/ToggleGroup'; -import { TransferForm } from '@/views/forms/TransferForm'; import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm'; +import { TransferForm } from '@/views/forms/TransferForm'; import { getTransferInputs } from '@/state/inputsSelectors'; @@ -24,7 +25,7 @@ type ElementProps = { export const ManageFundsDialog = ({ setIsOpen, selectedTransferType }: ElementProps) => { const stringGetter = useStringGetter(); - const { type } = useSelector(getTransferInputs, shallowEqual) || {}; + const { type } = useSelector(getTransferInputs, shallowEqual) ?? {}; const currentType = type?.rawValue ?? selectedTransferType ?? TransferType.deposit.rawValue; const closeDialog = () => setIsOpen?.(false); @@ -48,12 +49,12 @@ export const ManageFundsDialog = ({ setIsOpen, selectedTransferType }: ElementPr }; return ( - {transferTypeConfig[currentType].component} - + ); }; -const Styled: Record = {}; - -Styled.Dialog = styled(Dialog)` +const $Dialog = styled(Dialog)` --dialog-content-paddingTop: 1.5rem; `; -Styled.ToggleGroup = styled(ToggleGroup)` +const $ToggleGroup = styled(ToggleGroup)` overflow-x: auto; button { diff --git a/src/views/dialogs/MnemonicExportDialog.tsx b/src/views/dialogs/MnemonicExportDialog.tsx index 3b7c3462c..adf3f1547 100644 --- a/src/views/dialogs/MnemonicExportDialog.tsx +++ b/src/views/dialogs/MnemonicExportDialog.tsx @@ -1,17 +1,21 @@ import { useState } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; + +import styled, { css } from 'styled-components'; import { AlertType } from '@/constants/alerts'; import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { useAccounts, useStringGetter } from '@/hooks'; + +import { useAccounts } from '@/hooks/useAccounts'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import breakpoints from '@/styles/breakpoints'; import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; +import { Checkbox } from '@/components/Checkbox'; import { CopyButton } from '@/components/CopyButton'; import { Dialog } from '@/components/Dialog'; -import { Checkbox } from '@/components/Checkbox'; import { Icon, IconName } from '@/components/Icon'; import { TimeoutButton } from '@/components/TimeoutButton'; import { ToggleButton } from '@/components/ToggleButton'; @@ -44,16 +48,16 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => { const content = { [MnemonicExportStep.AcknowledgeRisk]: ( <> - - + <$WaitingSpan> + <$CautionIconContainer> - +

    {stringGetter({ key: STRING_KEYS.SECRET_PHRASE_RISK })}

    -
    - + <$WithReceipt slotReceipt={ - + <$CheckboxContainer> { key: STRING_KEYS.SECRET_PHRASE_RISK_ACK, })} /> - + } > { > {stringGetter({ key: STRING_KEYS.REVEAL_SECRET_PHRASE })} - + ), [MnemonicExportStep.DisplayMnemonic]: ( <> - + <$AlertMessage type={AlertType.Error}> {stringGetter({ key: STRING_KEYS.NEVER_SHARE_PHRASE })} - - + + <$RevealControls> {stringGetter({ key: isShowing ? STRING_KEYS.NOT_READY : STRING_KEYS.READY })} @@ -95,19 +99,20 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => { key: !isShowing ? STRING_KEYS.SHOW_PHRASE : STRING_KEYS.HIDE_PHRASE, })} - + setIsShowing(!isShowing)}> - + <$WordList isShowing={isShowing} onClick={() => setIsShowing(!isShowing)}> + <$List> {mnemonic?.split(' ').map((word: string, i: number) => ( - + // eslint-disable-next-line react/no-array-index-key + <$Word key={i}> {isShowing ? word : '*****'} - + ))} - + {stringGetter({ key: STRING_KEYS.CLICK_TO_SHOW })} - + } > @@ -117,26 +122,23 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => { }[currentStep]; return ( - - {content} - + <$Content>{content} + ); }; - -const Styled: Record = {}; - -Styled.WaitingSpan = styled.span` +const $WaitingSpan = styled.span` ${layoutMixins.row} gap: 1rem; color: var(--color-text-1); `; -Styled.CautionIconContainer = styled.div` +const $CautionIconContainer = styled.div` ${layoutMixins.stack} min-width: 2.5rem; height: 2.5rem; @@ -159,21 +161,21 @@ Styled.CautionIconContainer = styled.div` } `; -Styled.WithReceipt = styled(WithReceipt)` +const $WithReceipt = styled(WithReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); `; -Styled.CheckboxContainer = styled.div` +const $CheckboxContainer = styled.div` padding: 1rem; color: var(--color-text-0); `; -Styled.AlertMessage = styled(AlertMessage)` +const $AlertMessage = styled(AlertMessage)` font: var(--font-base-book); margin: 0; `; -Styled.RevealControls = styled.div` +const $RevealControls = styled.div` ${layoutMixins.spacedRow} svg { @@ -181,7 +183,7 @@ Styled.RevealControls = styled.div` } `; -Styled.WordList = styled.div<{ isShowing?: boolean }>` +const $WordList = styled.div<{ isShowing?: boolean }>` ${layoutMixins.stack} transition: 0.2s; cursor: pointer; @@ -215,7 +217,7 @@ Styled.WordList = styled.div<{ isShowing?: boolean }>` } `; -Styled.List = styled.ol` +const $List = styled.ol` display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.25rem; @@ -223,7 +225,7 @@ Styled.List = styled.ol` counter-reset: word; `; -Styled.Word = styled.li` +const $Word = styled.li` font: var(--font-base-book); font-family: var(--fontFamily-monospace); @@ -241,13 +243,13 @@ Styled.Word = styled.li` } `; -Styled.Dialog = styled(Dialog)` +const $Dialog = styled(Dialog)` @media ${breakpoints.notMobile} { --dialog-width: 30rem; } `; -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; `; diff --git a/src/views/dialogs/MobileDownloadDialog.tsx b/src/views/dialogs/MobileDownloadDialog.tsx index 6a4aff121..0c1889d6d 100644 --- a/src/views/dialogs/MobileDownloadDialog.tsx +++ b/src/views/dialogs/MobileDownloadDialog.tsx @@ -1,11 +1,13 @@ -import styled, { AnyStyledComponent, css } from 'styled-components'; +import styled, { css } from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; import { Dialog } from '@/components/Dialog'; import { QrCode } from '@/components/QrCode'; -import { useStringGetter } from '@/hooks'; -import { STRING_KEYS } from '@/constants/localization'; type ElementProps = { setIsOpen: (open: boolean) => void; @@ -24,28 +26,26 @@ type ElementProps = { // for testing only // export const mobileAppUrl = "http://example.com"; -let mobileAppUrl: string | undefined | null = undefined; +let mobileAppUrl: string | undefined | null; export const getMobileAppUrl = () => { if (!mobileAppUrl) { mobileAppUrl = - // for testing to verify is retrieved by name, QR code should show "@dYdX" as value - // document.querySelector('meta[name="twitter:creator"]')?.getAttribute('content') ?? - document.querySelector('meta[name="smartbanner:button-url-apple"]')?.getAttribute('content') ?? - document.querySelector('meta[name="smartbanner:button-url-google"]')?.getAttribute('content'); + // for testing to verify is retrieved by name, QR code should show "@dYdX" as value + // document.querySelector('meta[name="twitter:creator"]')?.getAttribute('content') ?? + document + .querySelector('meta[name="smartbanner:button-url-apple"]') + ?.getAttribute('content') ?? + document.querySelector('meta[name="smartbanner:button-url-google"]')?.getAttribute('content'); } return mobileAppUrl; -} - -const MobileQrCode = ({ - url, -}: { - url: string; -}) => { +}; + +const MobileQrCode = ({ url }: { url: string }) => { return ( - + <$QrCodeContainer isShowing> - + ); }; @@ -55,22 +55,19 @@ MobileDownloadDialog should only been shown on desktop when mobileAppUrl has val export const MobileDownloadDialog = ({ setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); - const content = ( - - ); + const content = ; return ( - - {content} + + <$Content>{content} ); }; - -const Styled: Record = {}; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; @@ -89,13 +86,7 @@ Styled.Content = styled.div` } `; -Styled.WaitingSpan = styled.span` - strong { - color: var(--color-warning); - } -`; - -Styled.QrCodeContainer = styled.figure<{ isShowing: boolean }>` +const $QrCodeContainer = styled.figure<{ isShowing: boolean }>` ${layoutMixins.stack} overflow: hidden; diff --git a/src/views/dialogs/MobileSignInDialog.tsx b/src/views/dialogs/MobileSignInDialog.tsx index 3b9b9786d..4dfa8f872 100644 --- a/src/views/dialogs/MobileSignInDialog.tsx +++ b/src/views/dialogs/MobileSignInDialog.tsx @@ -1,19 +1,24 @@ import { useMemo, useState } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; + import { AES } from 'crypto-js'; +import styled, { css } from 'styled-components'; import { AlertType } from '@/constants/alerts'; import { ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { useAccounts, useStringGetter } from '@/hooks'; + +import { useAccounts } from '@/hooks/useAccounts'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; import { Dialog } from '@/components/Dialog'; import { Icon, IconName } from '@/components/Icon'; +import { QrCode } from '@/components/QrCode'; import { TimeoutButton } from '@/components/TimeoutButton'; import { ToggleButton } from '@/components/ToggleButton'; -import { QrCode } from '@/components/QrCode'; + import { log } from '@/lib/telemetry'; type ElementProps = { @@ -55,10 +60,10 @@ const MobileQrCode = ({ const encryptedData = AES.encrypt(JSON.stringify(data), encryptionKey).toString(); return ( - + <$QrCodeContainer isShowing={isShowing} onClick={onClick}> {stringGetter({ key: STRING_KEYS.CLICK_TO_SHOW })} - + ); }; @@ -80,12 +85,12 @@ export const MobileSignInDialog = ({ setIsOpen }: ElementProps) => { const content = { [MobileSignInState.Waiting]: ( <> - + <$WaitingSpan>

    {stringGetter({ key: STRING_KEYS.DESCRIPTION_ABOUT_TO_TRANSFER })}

    {stringGetter({ key: STRING_KEYS.DESCRIPTION_NEVER_SHARE })}

    -
    + setCurrentState(MobileSignInState.Scanning)} timeoutInSeconds={8} @@ -140,14 +145,11 @@ export const MobileSignInDialog = ({ setIsOpen }: ElementProps) => { return ( - {content} + <$Content>{content} ); }; - -const Styled: Record = {}; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; @@ -166,13 +168,13 @@ Styled.Content = styled.div` } `; -Styled.WaitingSpan = styled.span` +const $WaitingSpan = styled.span` strong { color: var(--color-warning); } `; -Styled.QrCodeContainer = styled.figure<{ isShowing: boolean }>` +const $QrCodeContainer = styled.figure<{ isShowing: boolean }>` ${layoutMixins.stack} overflow: hidden; diff --git a/src/views/dialogs/NewMarketAgreementDialog.tsx b/src/views/dialogs/NewMarketAgreementDialog.tsx index 80e26bcf8..b0bf6048a 100644 --- a/src/views/dialogs/NewMarketAgreementDialog.tsx +++ b/src/views/dialogs/NewMarketAgreementDialog.tsx @@ -1,10 +1,13 @@ import { useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + +import styled from 'styled-components'; import { ButtonAction } from '@/constants/buttons'; -import { AppRoute, BASE_ROUTE } from '@/constants/routes'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; +import { AppRoute, BASE_ROUTE } from '@/constants/routes'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import breakpoints from '@/styles/breakpoints'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -24,25 +27,25 @@ export const NewMarketAgreementDialog = ({ acceptTerms, setIsOpen }: ElementProp const stringGetter = useStringGetter(); return ( - - + <$Content>

    {stringGetter({ key: STRING_KEYS.NEW_MARKET_PROPOSAL_AGREEMENT, params: { DOCUMENTATION_LINK: ( - + <$Link href="https://docs.dydx.community/dydx-governance/voting-and-governance/governance-process"> {stringGetter({ key: STRING_KEYS.WEBSITE }).toLowerCase()} - + ), TERMS_OF_USE: ( - + <$Link href={`${BASE_ROUTE}${AppRoute.Terms}`}> {stringGetter({ key: STRING_KEYS.TERMS_OF_USE })} - + ), }, })} @@ -54,7 +57,7 @@ export const NewMarketAgreementDialog = ({ acceptTerms, setIsOpen }: ElementProp id="acknowledgement-checkbox" label={stringGetter({ key: STRING_KEYS.I_HAVE_READ_AND_AGREE })} /> - + <$ButtonRow> @@ -68,21 +71,18 @@ export const NewMarketAgreementDialog = ({ acceptTerms, setIsOpen }: ElementProp > {stringGetter({ key: STRING_KEYS.CONTINUE })} - - - + + + ); }; - -const Styled: Record = {}; - -Styled.Dialog = styled(Dialog)` +const $Dialog = styled(Dialog)` @media ${breakpoints.notMobile} { --dialog-width: 30rem; } `; -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; @@ -93,12 +93,12 @@ Styled.Content = styled.div` } `; -Styled.Link = styled(Link)` +const $Link = styled(Link)` --link-color: var(--color-accent); display: inline-block; `; -Styled.ButtonRow = styled.div` +const $ButtonRow = styled.div` display: grid; grid-template-columns: 1fr 2fr; gap: 1rem; diff --git a/src/views/dialogs/NewMarketMessageDetailsDialog.tsx b/src/views/dialogs/NewMarketMessageDetailsDialog.tsx index a899a768e..8490771d6 100644 --- a/src/views/dialogs/NewMarketMessageDetailsDialog.tsx +++ b/src/views/dialogs/NewMarketMessageDetailsDialog.tsx @@ -1,13 +1,16 @@ import { useMemo, useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + import { utils } from '@dydxprotocol/v4-client-js'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; +import { MenuItem } from '@/constants/menus'; import { isMainnet } from '@/constants/networks'; -import { PotentialMarketItem } from '@/constants/potentialMarkets'; -import { useGovernanceVariables, useStringGetter, useTokenConfigs } from '@/hooks'; -import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { NewMarketProposal } from '@/constants/potentialMarkets'; + +import { useGovernanceVariables } from '@/hooks/useGovernanceVariables'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; import { Details } from '@/components/Details'; import { Dialog } from '@/components/Dialog'; @@ -20,7 +23,7 @@ import { MustBigNumber } from '@/lib/numbers'; type ElementProps = { preventClose?: boolean; setIsOpen?: (open: boolean) => void; - assetData?: PotentialMarketItem; + assetData: NewMarketProposal; clobPairId?: number; liquidityTier?: string; }; @@ -41,20 +44,28 @@ export const NewMarketMessageDetailsDialog = ({ setIsOpen, }: ElementProps) => { const [codeToggleGroup, setCodeToggleGroup] = useState(CodeToggleGroup.CREATE_ORACLE); - const { exchangeConfigs } = usePotentialMarkets(); - const { baseAsset } = assetData ?? {}; + const { baseAsset, params, title } = assetData ?? {}; + const { + ticker, + marketType, + exchangeConfigJson, + minExchanges, + minPriceChange, + atomicResolution, + quantumConversionExponent, + stepBaseQuantums, + subticksPerTick, + } = params ?? {}; const { newMarketProposal } = useGovernanceVariables(); const stringGetter = useStringGetter(); const { chainTokenDecimals, chainTokenLabel } = useTokenConfigs(); const initialDepositAmountDecimals = isMainnet ? 0 : chainTokenDecimals; const exchangeConfig = useMemo(() => { - return baseAsset ? exchangeConfigs?.[baseAsset] : undefined; + return baseAsset ? exchangeConfigJson : undefined; }, [baseAsset]); - const ticker = useMemo(() => `${baseAsset}-USD`, [baseAsset]); - - const toggleGroupItems: Parameters[0]['items'] = useMemo(() => { + const toggleGroupItems: MenuItem[] = useMemo(() => { return [ { value: CodeToggleGroup.CREATE_ORACLE, @@ -86,8 +97,8 @@ export const NewMarketMessageDetailsDialog = ({ setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.MESSAGE_DETAILS })} > - - + <$Tabs items={toggleGroupItems} value={codeToggleGroup} onValueChange={setCodeToggleGroup} @@ -95,8 +106,8 @@ export const NewMarketMessageDetailsDialog = ({ { { [CodeToggleGroup.CREATE_ORACLE]: ( - - + <$Details layout="column" items={[ { @@ -112,42 +123,44 @@ export const NewMarketMessageDetailsDialog = ({ { key: 'min-exchanges', label: 'min_exchanges', - value: `${assetData?.minExchanges}`, + value: `${minExchanges}`, }, { key: 'min-price-change-ppm', label: 'min_price_change_ppm', - value: `${assetData?.minPriceChangePpm}`, + value: `${minPriceChange}`, }, ]} /> - - exchange_config_json{' '} - {exchangeConfig && {exchangeConfig.length}} - - {'['} - {exchangeConfig?.map((exchange) => { - return ( - - {'{'} - {Object.keys(exchange).map((key) => ( - - {key}: {exchange[key as keyof typeof exchange]} - - ))} - {'},'} - - ); - })} - {']'} - + <$ExchangeConfigs> + <$Text0> + exchange_config_json + {exchangeConfig && <$Tag type={TagType.Number}>{exchangeConfig.length}} + + [ + {exchangeConfig?.map((exchange) => { + return ( + <$ExchangeObject + key={exchange.exchangeName} + style={{ padding: 0, margin: 0, paddingLeft: '0.5rem' }} + > + {'{'} + {Object.keys(exchange).map((key) => ( + <$Line key={key}> + {key}: {exchange[key as keyof typeof exchange]} + + ))} + {'},'} + + ); + })} + ] + + ), [CodeToggleGroup.MSG_CREATE_PERPETUAL]: ( - - + <$Details layout="column" items={[ { @@ -168,7 +181,7 @@ export const NewMarketMessageDetailsDialog = ({ { key: 'atomic_resolution', label: 'atomic_resolution', - value: `${assetData?.atomicResolution}`, + value: `${atomicResolution}`, }, { key: 'default_funding_ppm', @@ -180,13 +193,18 @@ export const NewMarketMessageDetailsDialog = ({ label: 'liquidity_tier', value: liquidityTier, }, + { + key: 'market_type', + label: 'market_type', + value: marketType ?? 'PERPETUAL_MARKET_TYPE_CROSS', + }, ]} /> - + ), [CodeToggleGroup.MSG_CREATE_CLOB_PAIR]: ( - - + <$Details layout="column" items={[ { @@ -202,17 +220,17 @@ export const NewMarketMessageDetailsDialog = ({ { key: 'quantum_conversion_exponent', label: 'quantum_conversion_exponent', - value: `${assetData?.quantumConversionExponent}`, + value: `${quantumConversionExponent}`, }, { key: 'step_base_quantums', label: 'step_base_quantums', - value: `${assetData?.stepBaseQuantum}`, + value: `${stepBaseQuantums}`, }, { key: 'subticks_per_tick', label: 'subticks_per_tick', - value: `${assetData?.subticksPerTick}`, + value: `${subticksPerTick}`, }, { key: 'status', @@ -221,11 +239,11 @@ export const NewMarketMessageDetailsDialog = ({ }, ]} /> - + ), [CodeToggleGroup.MSG_DELAY_MESSAGE]: ( - - + <$Details layout="column" items={[ { @@ -241,8 +259,8 @@ export const NewMarketMessageDetailsDialog = ({ }, ]} /> -

    MSG_UPDATE_CLOB_PAIR
    - MSG_UPDATE_CLOB_PAIR
    + <$Details layout="column" items={[ { @@ -258,17 +276,17 @@ export const NewMarketMessageDetailsDialog = ({ { key: 'quantum_conversion_exponent', label: 'quantum_conversion_exponent', - value: `${assetData?.quantumConversionExponent}`, + value: `${quantumConversionExponent}`, }, { key: 'step_base_quantums', label: 'step_base_quantums', - value: `${assetData?.stepBaseQuantum}`, + value: `${stepBaseQuantums}`, }, { key: 'subticks_per_tick', label: 'subticks_per_tick', - value: `${assetData?.subticksPerTick}`, + value: `${subticksPerTick}`, }, { key: 'status', @@ -277,48 +295,52 @@ export const NewMarketMessageDetailsDialog = ({ }, ]} /> - + ), [CodeToggleGroup.MSG_SUBMIT_PROPOSAL]: ( - - title: - {utils.getGovAddNewMarketTitle(ticker)} - - initial_deposit_amount: - - { - - } - - - summary: - - {utils.getGovAddNewMarketSummary(ticker, newMarketProposal.delayBlocks)} - - + <$Code> + <$Details + items={[ + { + key: 'title', + label: 'title', + value: title, + }, + { + key: 'initial_deposit_amount', + label: 'initial_deposit_amount', + value: ( + + ), + }, + { + key: 'summary', + label: 'summary', + value: ( + <$Summary> + {utils.getGovAddNewMarketSummary(ticker, newMarketProposal.delayBlocks)} + + ), + }, + ]} + /> + ), }[codeToggleGroup] } - + ); }; -const Styled: Record = {}; - -Styled.Content = styled.div` - ${layoutMixins.column} - gap: 0; -`; - -Styled.ProposedMessageDetails = styled.div` +const $ProposedMessageDetails = styled.div` display: flex; flex-direction: column; gap: 1rem; @@ -328,15 +350,27 @@ Styled.ProposedMessageDetails = styled.div` border-radius: 10px; `; -Styled.Tabs = styled(ToggleGroup)` +const $Tabs = styled(ToggleGroup)` overflow-x: auto; +` as typeof ToggleGroup; + +const $Details = styled(Details)` + --details-item-height: 1.5rem; +`; + +const $ExchangeConfigs = styled.div` + margin-top: 0.5rem; `; -Styled.Text0 = styled.span` +const $Text0 = styled.span` color: var(--color-text-0); `; -Styled.Code = styled.div` +const $Tag = styled(Tag)` + margin: 0 0.5ch; +`; + +const $Code = styled.div` height: 16.25rem; overflow: auto; display: block; @@ -349,18 +383,15 @@ Styled.Code = styled.div` gap: 0rem; `; -Styled.ExchangeObject = styled.div` +const $ExchangeObject = styled.div` padding: 1rem; `; -Styled.Details = styled(Details)` - --details-item-height: 1.5rem; -`; - -Styled.Line = styled.pre` +const $Line = styled.pre` margin-left: 1rem; `; -Styled.Description = styled.p` - margin-bottom: 1rem; +const $Summary = styled.p` + text-align: justify; + margin-left: 0.5rem; `; diff --git a/src/views/dialogs/OnboardingDialog.tsx b/src/views/dialogs/OnboardingDialog.tsx index 54fb939c2..838d7ce2c 100644 --- a/src/views/dialogs/OnboardingDialog.tsx +++ b/src/views/dialogs/OnboardingDialog.tsx @@ -1,16 +1,17 @@ -import { type ElementType, useState, useEffect } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; +import { useEffect, useState, type ElementType } from 'react'; +import { useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; + +import { EvmDerivedAccountStatus, OnboardingSteps } from '@/constants/account'; import { AnalyticsEvent } from '@/constants/analytics'; import { STRING_KEYS } from '@/constants/localization'; import { isMainnet } from '@/constants/networks'; -import { EvmDerivedAccountStatus, OnboardingSteps } from '@/constants/account'; -import { wallets } from '@/constants/wallets'; +import { WalletType, wallets } from '@/constants/wallets'; -import { calculateOnboardingStep } from '@/state/accountCalculators'; - -import { useSelector } from 'react-redux'; -import { useAccounts, useBreakpoints, useStringGetter } from '@/hooks'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -19,15 +20,16 @@ import { Dialog, DialogPlacement } from '@/components/Dialog'; import { GreenCheckCircle } from '@/components/GreenCheckCircle'; import { Icon } from '@/components/Icon'; import { Ring } from '@/components/Ring'; - import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm'; +import { calculateOnboardingStep } from '@/state/accountCalculators'; + import { track } from '@/lib/analytics'; +import { DepositForm } from '../forms/AccountManagementForms/DepositForm'; import { AcknowledgeTerms } from './OnboardingDialog/AcknowledgeTerms'; import { ChooseWallet } from './OnboardingDialog/ChooseWallet'; import { GenerateKeys } from './OnboardingDialog/GenerateKeys'; -import { DepositForm } from '../forms/AccountManagementForms/DepositForm'; type ElementProps = { setIsOpen?: (open: boolean) => void; @@ -39,7 +41,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); const { isMobile } = useBreakpoints(); - const { disconnect, walletType } = useAccounts(); + const { selectWalletType, disconnect, walletType } = useAccounts(); const currentOnboardingStep = useSelector(calculateOnboardingStep); @@ -54,8 +56,15 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => { setIsOpen?.(open); }; + const onChooseWallet = (wType: WalletType) => { + if (wType === WalletType.Privy) { + setIsOpenFromDialog(false); + } + selectWalletType(wType); + }; + return ( - { title: stringGetter({ key: STRING_KEYS.CONNECT_YOUR_WALLET }), description: 'Select your wallet from these supported options.', children: ( - - - + <$Content> + + ), }, [OnboardingSteps.KeyDerivation]: { @@ -74,27 +83,25 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => { [EvmDerivedAccountStatus.NotDerived]: walletType && ( ), - [EvmDerivedAccountStatus.Deriving]: , - [EvmDerivedAccountStatus.EnsuringDeterminism]: ( - - ), + [EvmDerivedAccountStatus.Deriving]: <$Ring withAnimation value={0.25} />, + [EvmDerivedAccountStatus.EnsuringDeterminism]: <$Ring withAnimation value={0.25} />, [EvmDerivedAccountStatus.Derived]: , }[derivationStatus], title: stringGetter({ key: STRING_KEYS.SIGN_MESSAGE }), description: stringGetter({ key: STRING_KEYS.SIGNATURE_CREATES_COSMOS_WALLET }), children: ( - + <$Content> - + ), width: '23rem', }, [OnboardingSteps.AcknowledgeTerms]: { title: stringGetter({ key: STRING_KEYS.ACKNOWLEDGE_TERMS }), children: ( - + <$Content> setIsOpenFromDialog?.(false)} /> - + ), width: '30rem', }, @@ -102,7 +109,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => { title: stringGetter({ key: STRING_KEYS.DEPOSIT }), description: !isMainnet && 'Test funds will be sent directly to your dYdX account.', children: ( - + <$Content> {isMainnet ? ( { @@ -116,7 +123,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => { }} /> )} - + ), }, }[currentOnboardingStep])} @@ -124,15 +131,12 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => { /> ); }; - -const Styled: Record = {}; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.flexColumn} gap: 1rem; `; -Styled.Dialog = styled(Dialog)<{ width?: string }>` +const $Dialog = styled(Dialog)<{ width?: string }>` @media ${breakpoints.notMobile} { ${({ width }) => width && @@ -144,7 +148,7 @@ Styled.Dialog = styled(Dialog)<{ width?: string }>` --dialog-icon-size: 1.25rem; `; -Styled.Ring = styled(Ring)` +const $Ring = styled(Ring)` width: 1.25rem; height: 1.25rem; --ring-color: var(--color-accent); diff --git a/src/views/dialogs/OnboardingDialog/AcknowledgeTerms.tsx b/src/views/dialogs/OnboardingDialog/AcknowledgeTerms.tsx index c1210b71c..b42469302 100644 --- a/src/views/dialogs/OnboardingDialog/AcknowledgeTerms.tsx +++ b/src/views/dialogs/OnboardingDialog/AcknowledgeTerms.tsx @@ -1,13 +1,14 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; -import { useAccounts, useStringGetter } from '@/hooks'; - -import { AppRoute, BASE_ROUTE } from '@/constants/routes'; -import { STRING_KEYS } from '@/constants/localization'; import { ButtonAction } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { AppRoute, BASE_ROUTE } from '@/constants/routes'; + +import { useAccounts } from '@/hooks/useAccounts'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { layoutMixins } from '@/styles/layoutMixins'; import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; import { Link } from '@/components/Link'; @@ -34,19 +35,19 @@ export const AcknowledgeTerms = ({ onClose, onContinue }: ElementProps) => { key: STRING_KEYS.TOS_TITLE, params: { TERMS_LINK: ( - + <$Link href={`${BASE_ROUTE}${AppRoute.Terms}`}> {stringGetter({ key: STRING_KEYS.TERMS_OF_USE })} - + ), PRIVACY_POLICY_LINK: ( - + <$Link href={`${BASE_ROUTE}${AppRoute.Privacy}`}> {stringGetter({ key: STRING_KEYS.PRIVACY_POLICY })} - + ), }, })}

    - + <$TOS>
    • {stringGetter({ key: STRING_KEYS.TOS_LINE1 })}
    • {stringGetter({ key: STRING_KEYS.TOS_LINE2 })}
    • @@ -54,22 +55,19 @@ export const AcknowledgeTerms = ({ onClose, onContinue }: ElementProps) => {
    • {stringGetter({ key: STRING_KEYS.TOS_LINE4 })}
    • {stringGetter({ key: STRING_KEYS.TOS_LINE5 })}
    -
    - - + + <$Footer> + <$Button onClick={onClose} action={ButtonAction.Base}> {stringGetter({ key: STRING_KEYS.CLOSE })} - - + + <$Button onClick={onAcknowledgement} action={ButtonAction.Primary}> {stringGetter({ key: STRING_KEYS.I_AGREE })} - - + + ); }; - -const Styled: Record = {}; - -Styled.Link = styled(Link)` +const $Link = styled(Link)` display: inline-block; color: var(--color-accent); @@ -78,7 +76,7 @@ Styled.Link = styled(Link)` } `; -Styled.TOS = styled.section` +const $TOS = styled.section` background-color: var(--color-layer-4); padding: 1rem 1rem 1rem 2rem; border-radius: 0.875rem; @@ -88,13 +86,13 @@ Styled.TOS = styled.section` } `; -Styled.Footer = styled.div` +const $Footer = styled.div` ${formMixins.footer}; ${layoutMixins.row} gap: 1rem; `; -Styled.Button = styled(Button)` +const $Button = styled(Button)` flex-grow: 1; `; diff --git a/src/views/dialogs/OnboardingDialog/ChooseWallet.tsx b/src/views/dialogs/OnboardingDialog/ChooseWallet.tsx index 4ffb3ff69..0e6ce4964 100644 --- a/src/views/dialogs/OnboardingDialog/ChooseWallet.tsx +++ b/src/views/dialogs/OnboardingDialog/ChooseWallet.tsx @@ -1,84 +1,84 @@ -import { useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; -import { useDispatch } from 'react-redux'; +import { ElementType } from 'react'; + +import styled from 'styled-components'; import { AlertType } from '@/constants/alerts'; +import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; - import { WalletType, wallets } from '@/constants/wallets'; -import { ButtonAction, ButtonSize } from '@/constants/buttons'; - -import { AlertMessage } from '@/components/AlertMessage'; -import { Button } from '@/components/Button'; -import { Icon } from '@/components/Icon'; -import { Link } from '@/components/Link'; -import { useAccounts, useStringGetter, useURLConfigs } from '@/hooks'; +import { useAccounts } from '@/hooks/useAccounts'; import { useDisplayedWallets } from '@/hooks/useDisplayedWallets'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; -export const ChooseWallet = () => { +import { AlertMessage } from '@/components/AlertMessage'; +import { Button } from '@/components/Button'; +import { Icon } from '@/components/Icon'; +import { Link } from '@/components/Link'; + +export const ChooseWallet = ({ + onChooseWallet, +}: { + onChooseWallet: (walletType: WalletType) => void; +}) => { const stringGetter = useStringGetter(); const { walletLearnMore } = useURLConfigs(); const displayedWallets = useDisplayedWallets(); - const { selectWalletType, selectedWalletType, selectedWalletError } = useAccounts(); + const { selectedWalletType, selectedWalletError } = useAccounts(); return ( <> {selectedWalletType && selectedWalletError && ( - - { -

    - {stringGetter({ - key: STRING_KEYS.COULD_NOT_CONNECT, - params: { - WALLET: stringGetter({ - key: wallets[selectedWalletType].stringKey, - }), - }, - })} -

    - } + <$AlertMessage type={AlertType.Error}> +

    + {stringGetter({ + key: STRING_KEYS.COULD_NOT_CONNECT, + params: { + WALLET: stringGetter({ + key: wallets[selectedWalletType].stringKey, + }), + }, + })} +

    {selectedWalletError} -
    + )} - + <$Wallets> {displayedWallets.map((walletType) => ( - selectWalletType(walletType)} - slotLeft={} + onClick={() => onChooseWallet(walletType)} + slotLeft={<$Icon iconComponent={wallets[walletType].icon as ElementType} />} size={ButtonSize.Small} > -
    {stringGetter({ key: wallets[walletType].stringKey })}
    -
    + {stringGetter({ key: wallets[walletType].stringKey })} + ))} -
    + - + <$Footer> {stringGetter({ key: STRING_KEYS.ABOUT_WALLETS })} - + ); }; - -const Styled: Record = {}; - -Styled.AlertMessage = styled(AlertMessage)` +const $AlertMessage = styled(AlertMessage)` h4 { font: var(--font-small-medium); } `; -Styled.Wallets = styled.div` +const $Wallets = styled.div` gap: 0.5rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); @@ -97,7 +97,7 @@ Styled.Wallets = styled.div` } */ `; -Styled.WalletButton = styled(Button)` +const $WalletButton = styled(Button)` justify-content: start; gap: 0.5rem; @@ -110,12 +110,12 @@ Styled.WalletButton = styled(Button)` } `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` width: 1.5em; height: 1.5em; `; -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${layoutMixins.spacedRow} justify-content: center; margin-top: auto; diff --git a/src/views/dialogs/OnboardingDialog/GenerateKeys.tsx b/src/views/dialogs/OnboardingDialog/GenerateKeys.tsx index 23c560e6c..a6be106d6 100644 --- a/src/views/dialogs/OnboardingDialog/GenerateKeys.tsx +++ b/src/views/dialogs/OnboardingDialog/GenerateKeys.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; -import { useSignTypedData } from 'wagmi'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; -import { useSelector } from 'react-redux'; + import { AES } from 'crypto-js'; +import { useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { EvmDerivedAccountStatus } from '@/constants/account'; import { AlertType } from '@/constants/alerts'; @@ -10,10 +10,13 @@ import { AnalyticsEvent } from '@/constants/analytics'; import { ButtonAction } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; -import { DydxAddress, getSignTypedData } from '@/constants/wallets'; +import { DydxAddress } from '@/constants/wallets'; -import { useAccounts, useBreakpoints, useDydxClient, useStringGetter } from '@/hooks'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useDydxClient } from '@/hooks/useDydxClient'; import { useMatchingEvmNetwork } from '@/hooks/useMatchingEvmNetwork'; +import useSignForWalletDerivation from '@/hooks/useSignForWalletDerivation'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -25,7 +28,7 @@ import { Switch } from '@/components/Switch'; import { WithReceipt } from '@/components/WithReceipt'; import { WithTooltip } from '@/components/WithTooltip'; -import { getSelectedNetwork, getSelectedDydxChainId } from '@/state/appSelectors'; +import { getSelectedNetwork } from '@/state/appSelectors'; import { track } from '@/lib/analytics'; import { isTruthy } from '@/lib/isTruthy'; @@ -38,11 +41,7 @@ type ElementProps = { onKeysDerived?: () => void; }; -export const GenerateKeys = ({ - status: status, - setStatus, - onKeysDerived = () => {}, -}: ElementProps) => { +export const GenerateKeys = ({ status, setStatus, onKeysDerived = () => {} }: ElementProps) => { const stringGetter = useStringGetter(); const [shouldRememberMe, setShouldRememberMe] = useState(false); @@ -66,14 +65,14 @@ export const GenerateKeys = ({ try { await matchNetwork?.(); return true; - } catch (error) { + } catch (err) { const { message, walletErrorType, isErrorExpected } = parseWalletError({ - error, + error: err, stringGetter, }); if (!isErrorExpected) { - log('GenerateKeys/switchNetwork', error, { walletErrorType }); + log('GenerateKeys/switchNetwork', err, { walletErrorType }); } if (message) { @@ -98,15 +97,7 @@ export const GenerateKeys = ({ EvmDerivedAccountStatus.Derived, ].includes(status); - const selectedDydxChainId = useSelector(getSelectedDydxChainId); - const signTypedData = getSignTypedData(selectedDydxChainId); - const { signTypedDataAsync } = useSignTypedData({ - ...signTypedData, - domain: { - ...signTypedData.domain, - chainId, - }, - }); + const signTypedDataAsync = useSignForWalletDerivation(); const staticEncryptionKey = import.meta.env.VITE_PK_ENCRYPTION_KEY; @@ -143,9 +134,9 @@ export const GenerateKeys = ({ ); } } - } catch (error) { + } catch (err) { setStatus(EvmDerivedAccountStatus.NotDerived); - const { message } = parseWalletError({ error, stringGetter }); + const { message } = parseWalletError({ error: err, stringGetter }); if (message) { track(AnalyticsEvent.OnboardingWalletIsNonDeterministic); @@ -165,17 +156,17 @@ export const GenerateKeys = ({ // 4. Done setStatus(EvmDerivedAccountStatus.Derived); - } catch (error) { + } catch (err) { setStatus(EvmDerivedAccountStatus.NotDerived); const { message, walletErrorType, isErrorExpected } = parseWalletError({ - error, + error: err, stringGetter, }); if (message) { setError(message); if (!isErrorExpected) { - log('GenerateKeys/deriveKeys', error, { walletErrorType }); + log('GenerateKeys/deriveKeys', err, { walletErrorType }); } } } @@ -183,7 +174,7 @@ export const GenerateKeys = ({ return ( <> - + <$StatusCardsContainer> {[ { status: EvmDerivedAccountStatus.Deriving, @@ -198,24 +189,24 @@ export const GenerateKeys = ({ ] .filter(isTruthy) .map((step) => ( - + <$StatusCard key={step.status} active={status === step.status}> {status < step.status ? ( ) : status === step.status ? ( ) : ( - + <$GreenCheckCircle /> )}

    {step.title}

    {step.description}

    -
    + ))} -
    + - - + <$Footer> + <$RememberMe htmlFor="remember-me"> {stringGetter({ key: STRING_KEYS.REMEMBER_ME })} @@ -226,24 +217,24 @@ export const GenerateKeys = ({ checked={shouldRememberMe} onCheckedChange={setShouldRememberMe} /> - + {error && {error}} - + <$ReceiptArea> {stringGetter({ key: STRING_KEYS.FREE_SIGNING, params: { FREE: ( - + <$Green> {stringGetter({ key: STRING_KEYS.FREE_TRADING_TITLE_ASTERISK_FREE })} - + ), }, })} - + } > {!isMatchingNetwork ? ( @@ -272,23 +263,18 @@ export const GenerateKeys = ({ })} )} - - - {stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })} - - + + <$Disclaimer>{stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })} + ); }; - -const Styled: Record = {}; - -Styled.StatusCardsContainer = styled.div` +const $StatusCardsContainer = styled.div` display: grid; gap: 1rem; `; -Styled.StatusCard = styled.div<{ active?: boolean }>` +const $StatusCard = styled.div<{ active?: boolean }>` ${layoutMixins.row} gap: 1rem; background-color: var(--color-layer-4); @@ -317,7 +303,7 @@ Styled.StatusCard = styled.div<{ active?: boolean }>` } `; -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${layoutMixins.stickyFooter} margin-top: auto; @@ -325,30 +311,30 @@ Styled.Footer = styled.footer` gap: 1rem; `; -Styled.RememberMe = styled.label` +const $RememberMe = styled.label` ${layoutMixins.spacedRow} font: var(--font-base-book); `; -Styled.WithReceipt = styled(WithReceipt)` +const $WithReceipt = styled(WithReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); `; -Styled.ReceiptArea = styled.div` +const $ReceiptArea = styled.div` padding: 1rem; font: var(--font-small-book); color: var(--color-text-0); `; -Styled.Green = styled.span` +const $Green = styled.span` color: var(--color-green); `; -Styled.GreenCheckCircle = styled(GreenCheckCircle)` +const $GreenCheckCircle = styled(GreenCheckCircle)` --icon-size: 2.375rem; `; -Styled.Disclaimer = styled.span` +const $Disclaimer = styled.span` text-align: center; color: var(--color-text-0); font: var(--font-base-book); diff --git a/src/views/dialogs/OnboardingTriggerButton.tsx b/src/views/dialogs/OnboardingTriggerButton.tsx index 42e260b5e..9219054f2 100644 --- a/src/views/dialogs/OnboardingTriggerButton.tsx +++ b/src/views/dialogs/OnboardingTriggerButton.tsx @@ -1,16 +1,16 @@ import { useDispatch, useSelector } from 'react-redux'; -import { useStringGetter } from '@/hooks'; - +import { OnboardingState } from '@/constants/account'; import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { Button } from '@/components/Button'; -import { forceOpenDialog } from '@/state/dialogs'; import { getOnboardingState } from '@/state/accountSelectors'; -import { OnboardingState } from '@/constants/account'; +import { forceOpenDialog } from '@/state/dialogs'; type StyleProps = { size?: ButtonSize; diff --git a/src/views/dialogs/PreferencesDialog.tsx b/src/views/dialogs/PreferencesDialog.tsx index 8562eb966..4d568b590 100644 --- a/src/views/dialogs/PreferencesDialog.tsx +++ b/src/views/dialogs/PreferencesDialog.tsx @@ -1,76 +1,131 @@ import { useEffect, useMemo, useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + +import { SelectedGasDenom } from '@dydxprotocol/v4-client-js'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { NotificationType } from '@/constants/notifications'; +import { NotificationCategoryPreferences } from '@/constants/notifications'; -import { useStringGetter } from '@/hooks'; +import { useDydxClient } from '@/hooks/useDydxClient'; import { useNotifications } from '@/hooks/useNotifications'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu'; import { Switch } from '@/components/Switch'; +import { OtherPreference, setDefaultToAllMarketsInPositionsOrdersFills } from '@/state/configs'; +import { getDefaultToAllMarketsInPositionsOrdersFills } from '@/state/configsSelectors'; + +import { isTruthy } from '@/lib/isTruthy'; + export const usePreferenceMenu = () => { + const dispatch = useDispatch(); const stringGetter = useStringGetter(); // Notifications const { notificationPreferences, setNotificationPreferences } = useNotifications(); const [enabledNotifs, setEnabledNotifs] = useState(notificationPreferences); - const toggleNotifPreference = (type: NotificationType) => + const currentDisplayAllMarketDefault = useSelector(getDefaultToAllMarketsInPositionsOrdersFills); + const [defaultToAllMarkets, setDefaultToAllMarkets] = useState(currentDisplayAllMarketDefault); + + const toggleNotifPreference = (type: NotificationCategoryPreferences) => setEnabledNotifs((prev) => ({ ...prev, [type]: !prev[type] })); useEffect(() => { setNotificationPreferences(enabledNotifs); }, [enabledNotifs]); + useEffect(() => { + setDefaultToAllMarkets(currentDisplayAllMarketDefault); + }, [currentDisplayAllMarketDefault]); + + const getItem = ( + notificationCategory: NotificationCategoryPreferences, + labelStringKey: string + ) => ({ + value: notificationCategory, + label: stringGetter({ key: labelStringKey }), + slotAfter: ( + null} + /> + ), + onSelect: () => toggleNotifPreference(notificationCategory), + }); + const notificationSection = useMemo( () => ({ group: 'Notifications', groupLabel: stringGetter({ key: STRING_KEYS.NOTIFICATIONS }), items: [ { - value: NotificationType.AbacusGenerated, - label: stringGetter({ key: STRING_KEYS.TRADING }), - slotAfter: ( - null} - /> - ), - onSelect: () => toggleNotifPreference(NotificationType.AbacusGenerated), + value: NotificationCategoryPreferences.General, + labelStringKey: STRING_KEYS.GENERAL, + }, + { + value: NotificationCategoryPreferences.Transfers, + labelStringKey: STRING_KEYS.TRANSFERS, }, { - value: NotificationType.SquidTransfer, - label: stringGetter({ key: STRING_KEYS.TRANSFERS }), + value: NotificationCategoryPreferences.Trading, + labelStringKey: STRING_KEYS.TRADING, + }, + ] + .filter(isTruthy) + .map(({ value, labelStringKey }) => getItem(value, labelStringKey)), + }), + [stringGetter, enabledNotifs] + ); + + const { setSelectedGasDenom, selectedGasDenom } = useDydxClient(); + + const otherSection = useMemo( + () => ({ + group: 'Other', + groupLabel: stringGetter({ key: STRING_KEYS.OTHER }), + items: [ + { + value: OtherPreference.DisplayAllMarketsDefault, + label: stringGetter({ key: STRING_KEYS.DEFAULT_TO_ALL_MARKETS_IN_POSITIONS }), slotAfter: ( null} + name={OtherPreference.DisplayAllMarketsDefault} + checked={defaultToAllMarkets} + onCheckedChange={() => null} /> ), - onSelect: () => toggleNotifPreference(NotificationType.SquidTransfer), + onSelect: () => { + dispatch(setDefaultToAllMarketsInPositionsOrdersFills(!defaultToAllMarkets)); + }, }, { - value: NotificationType.ReleaseUpdates, - label: "Release Updates", + value: OtherPreference.GasToken, + label: 'Pay gas with USDC', slotAfter: ( null} + name={OtherPreference.GasToken} + checked={selectedGasDenom === SelectedGasDenom.USDC} + onCheckedChange={() => null} /> ), - onSelect: () => toggleNotifPreference(NotificationType.ReleaseUpdates), - } + onSelect: () => { + setSelectedGasDenom( + selectedGasDenom === SelectedGasDenom.USDC + ? SelectedGasDenom.NATIVE + : SelectedGasDenom.USDC + ); + }, + }, ], }), - [stringGetter, enabledNotifs] + [stringGetter, defaultToAllMarkets, selectedGasDenom, setSelectedGasDenom] ); - return [notificationSection]; + return [notificationSection, otherSection]; }; type ElementProps = { @@ -82,7 +137,7 @@ export const PreferencesDialog = ({ setIsOpen }: ElementProps) => { const preferenceItems = usePreferenceMenu(); return ( - { /> ); }; - -const Styled: Record = {}; - -Styled.ComboboxDialogMenu = styled(ComboboxDialogMenu)` +const $ComboboxDialogMenu = styled(ComboboxDialogMenu)` --dialog-content-paddingBottom: 0.5rem; `; diff --git a/src/views/dialogs/RateLimitDialog.tsx b/src/views/dialogs/RateLimitDialog.tsx index c7dca6bc3..3cfd64daf 100644 --- a/src/views/dialogs/RateLimitDialog.tsx +++ b/src/views/dialogs/RateLimitDialog.tsx @@ -1,7 +1,9 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { Dialog } from '@/components/Dialog'; @@ -21,22 +23,17 @@ export const RateLimitDialog = ({ preventClose, setIsOpen }: ElementProps) => { preventClose={preventClose} setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.RATE_LIMIT_REACHED_ERROR_TITLE })} - slotIcon={} + slotIcon={<$Icon iconName={IconName.Warning} />} > - - {stringGetter({ key: STRING_KEYS.RATE_LIMIT_REACHED_ERROR_MESSAGE })} - + <$Content>{stringGetter({ key: STRING_KEYS.RATE_LIMIT_REACHED_ERROR_MESSAGE })} ); }; - -const Styled: Record = {}; - -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` color: var(--color-warning); `; -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; `; diff --git a/src/views/dialogs/RestrictedGeoDialog.tsx b/src/views/dialogs/RestrictedGeoDialog.tsx index 37cec5f78..5351bd971 100644 --- a/src/views/dialogs/RestrictedGeoDialog.tsx +++ b/src/views/dialogs/RestrictedGeoDialog.tsx @@ -1,8 +1,10 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { isDev } from '@/constants/networks'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { Dialog } from '@/components/Dialog'; @@ -23,23 +25,20 @@ export const RestrictedGeoDialog = ({ preventClose, setIsOpen }: ElementProps) = preventClose={preventClose} setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_TITLE })} - slotIcon={} + slotIcon={<$Icon iconName={IconName.Warning} />} > - + <$Content> {stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })} {isDev && } - + ); }; - -const Styled: Record = {}; - -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` color: var(--color-warning); `; -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; `; diff --git a/src/views/dialogs/RestrictedWalletDialog.tsx b/src/views/dialogs/RestrictedWalletDialog.tsx index e8b6cb316..00fd2720c 100644 --- a/src/views/dialogs/RestrictedWalletDialog.tsx +++ b/src/views/dialogs/RestrictedWalletDialog.tsx @@ -1,8 +1,10 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { isDev } from '@/constants/networks'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { Dialog } from '@/components/Dialog'; @@ -23,23 +25,20 @@ export const RestrictedWalletDialog = ({ preventClose, setIsOpen }: ElementProps preventClose={preventClose} setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.WALLET_RESTRICTED_ERROR_TITLE })} - slotIcon={} + slotIcon={<$Icon iconName={IconName.Warning} />} > - + <$Content> {stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })} {isDev && } - + ); }; - -const Styled: Record = {}; - -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` color: var(--color-warning); `; -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.column} gap: 1rem; `; diff --git a/src/views/dialogs/SelectMarginModeDialog.tsx b/src/views/dialogs/SelectMarginModeDialog.tsx new file mode 100644 index 000000000..a0c831b36 --- /dev/null +++ b/src/views/dialogs/SelectMarginModeDialog.tsx @@ -0,0 +1,20 @@ +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Dialog } from '@/components/Dialog'; + +import { SelectMarginModeForm } from '../forms/SelectMarginModeForm'; + +type ElementProps = { + setIsOpen?: (open: boolean) => void; +}; + +export const SelectMarginModeDialog = ({ setIsOpen }: ElementProps) => { + const stringGetter = useStringGetter(); + return ( + + setIsOpen?.(false)} /> + + ); +}; diff --git a/src/views/dialogs/TradeDialog.tsx b/src/views/dialogs/TradeDialog.tsx index 2eff8c0ad..96f7a2f50 100644 --- a/src/views/dialogs/TradeDialog.tsx +++ b/src/views/dialogs/TradeDialog.tsx @@ -1,26 +1,29 @@ import { useState } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { TradeInputField } from '@/constants/abacus'; -import { STRING_KEYS, StringKey } from '@/constants/localization'; -import { TradeTypes, ORDER_TYPE_STRINGS, MobilePlaceOrderSteps } from '@/constants/trade'; +import { useDispatch } from 'react-redux'; +import styled, { css } from 'styled-components'; + +import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; +import { MobilePlaceOrderSteps } from '@/constants/trade'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { useBreakpoints, useStringGetter } from '@/hooks'; import { layoutMixins } from '@/styles/layoutMixins'; -import { AssetIcon } from '@/components/AssetIcon'; +import { Button } from '@/components/Button'; import { Dialog, DialogPlacement } from '@/components/Dialog'; import { GreenCheckCircle } from '@/components/GreenCheckCircle'; -import { TradeForm } from '@/views/forms/TradeForm'; +import { Icon, IconName } from '@/components/Icon'; import { Ring } from '@/components/Ring'; -import { ToggleGroup } from '@/components/ToggleGroup'; +import { TradeForm } from '@/views/forms/TradeForm'; -import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; -import { getInputTradeData, getInputTradeOptions } from '@/state/inputsSelectors'; +import { openDialog } from '@/state/dialogs'; -import abacusStateManager from '@/lib/abacus'; -import { getSelectedTradeType } from '@/lib/tradeData'; +import { testFlags } from '@/lib/testFlags'; + +import { TradeSideToggle } from '../forms/TradeForm/TradeSideToggle'; type ElementProps = { isOpen?: boolean; @@ -30,37 +33,20 @@ type ElementProps = { export const TradeDialog = ({ isOpen, setIsOpen, slotTrigger }: ElementProps) => { const { isMobile } = useBreakpoints(); + const dispatch = useDispatch(); const stringGetter = useStringGetter(); - const { id } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; - const currentTradeData = useSelector(getInputTradeData, shallowEqual); - const { type } = currentTradeData || {}; - const selectedTradeType = getSelectedTradeType(type); - const { typeOptions } = useSelector(getInputTradeOptions, shallowEqual) ?? {}; - - const allTradeTypeItems = (typeOptions?.toArray() ?? []).map(({ type, stringKey }) => ({ - value: type, - label: stringGetter({ - key: stringKey as StringKey, - }), - slotBefore: , - })); const [currentStep, setCurrentStep] = useState( MobilePlaceOrderSteps.EditOrder ); - const onTradeTypeChange = (tradeType: TradeTypes) => { - abacusStateManager.clearTradeInputValues(); - abacusStateManager.setTradeValue({ value: tradeType, field: TradeInputField.type }); - }; - const onCloseDialog = () => { setCurrentStep(MobilePlaceOrderSteps.EditOrder); setIsOpen?.(false); }; return ( - (open ? setIsOpen?.(true) : onCloseDialog())} placement={isMobile ? DialogPlacement.FullScreen : DialogPlacement.Default} @@ -69,47 +55,66 @@ export const TradeDialog = ({ isOpen, setIsOpen, slotTrigger }: ElementProps) => hasHeaderBorder {...{ [MobilePlaceOrderSteps.EditOrder]: { - title: ( - + title: testFlags.isolatedMargin ? ( + <$EditTradeHeader> + + + + + + + ) : ( + ), }, [MobilePlaceOrderSteps.PreviewOrder]: { title: ( - - {stringGetter({ key: STRING_KEYS.PREVIEW_ORDER_TITLE })} - + <$PreviewTitle>{stringGetter({ key: STRING_KEYS.PREVIEW_ORDER_TITLE })} ), description: stringGetter({ key: STRING_KEYS.PREVIEW_ORDER_DESCRIPTION }), }, [MobilePlaceOrderSteps.PlacingOrder]: { title: stringGetter({ key: STRING_KEYS.PLACING_ORDER_TITLE }), description: stringGetter({ key: STRING_KEYS.PLACING_ORDER_DESCRIPTION }), - slotIcon: , + slotIcon: <$Ring withAnimation value={0.25} />, + }, + [MobilePlaceOrderSteps.PlaceOrderFailed]: { + title: stringGetter({ key: STRING_KEYS.PLACE_ORDER_FAILED }), + description: stringGetter({ key: STRING_KEYS.PLACE_ORDER_FAILED_DESCRIPTION }), + slotIcon: <$WarningIcon iconName={IconName.Warning} />, }, - // TODO(@aforaleka): add error state if trade didn't actually go through [MobilePlaceOrderSteps.Confirmation]: { title: stringGetter({ key: STRING_KEYS.CONFIRMED_TITLE }), description: stringGetter({ key: STRING_KEYS.CONFIRMED_DESCRIPTION }), - slotIcon: , + slotIcon: <$GreenCheckCircle />, }, }[currentStep]} > - - + ); }; - -const Styled: Record = {}; - -Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>` +const $Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>` --dialog-backgroundColor: var(--color-layer-2); --dialog-header-height: 1rem; --dialog-content-paddingTop: 0; @@ -126,34 +131,33 @@ Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>` `} `; -Styled.ToggleGroup = styled(ToggleGroup)` - overflow-x: auto; - - button[data-state='off'] { - gap: 0; - - img { - height: 0; - } - } +const $EditTradeHeader = styled.div` + display: grid; + grid-template-columns: auto auto 1fr; + gap: 0.5rem; `; -Styled.TradeForm = styled(TradeForm)` +const $TradeForm = styled(TradeForm)` --tradeBox-content-paddingTop: 1rem; --tradeBox-content-paddingRight: 1.5rem; --tradeBox-content-paddingBottom: 1.5rem; --tradeBox-content-paddingLeft: 1.5rem; `; -Styled.Ring = styled(Ring)` +const $Ring = styled(Ring)` --ring-color: var(--color-accent); `; -Styled.GreenCheckCircle = styled(GreenCheckCircle)` +const $GreenCheckCircle = styled(GreenCheckCircle)` --icon-size: 2rem; `; -Styled.PreviewTitle = styled.div` +const $WarningIcon = styled(Icon)` + color: var(--color-warning); + font-size: 1.5rem; +`; + +const $PreviewTitle = styled.div` ${layoutMixins.inlineRow} height: var(--dialog-icon-size); `; diff --git a/src/views/dialogs/TransferDialog.tsx b/src/views/dialogs/TransferDialog.tsx index fd0bb7082..e7a2f9c18 100644 --- a/src/views/dialogs/TransferDialog.tsx +++ b/src/views/dialogs/TransferDialog.tsx @@ -1,8 +1,9 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { DydxChainAsset } from '@/constants/wallets'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { Dialog } from '@/components/Dialog'; import { TransferForm } from '@/views/forms/TransferForm'; @@ -16,13 +17,11 @@ export const TransferDialog = ({ selectedAsset, setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); return ( - + <$Dialog isOpen setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.TRANSFER })}> setIsOpen?.(false)} /> - + ); }; -const Styled: Record = {}; - -Styled.Dialog = styled(Dialog)` +const $Dialog = styled(Dialog)` --dialog-content-paddingTop: var(--default-border-width); `; diff --git a/src/views/dialogs/TriggersDialog.tsx b/src/views/dialogs/TriggersDialog.tsx new file mode 100644 index 000000000..fd639db76 --- /dev/null +++ b/src/views/dialogs/TriggersDialog.tsx @@ -0,0 +1,52 @@ +import { useDispatch } from 'react-redux'; + +import { type SubaccountOrder } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Dialog } from '@/components/Dialog'; +import { TriggersForm } from '@/views/forms/TriggersForm/TriggersForm'; + +import { closeDialog } from '@/state/dialogs'; + +type ElementProps = { + marketId: string; + assetId: string; + stopLossOrders: SubaccountOrder[]; + takeProfitOrders: SubaccountOrder[]; + navigateToMarketOrders: (market: string) => void; + setIsOpen: (open: boolean) => void; +}; + +export const TriggersDialog = ({ + marketId, + assetId, + stopLossOrders, + takeProfitOrders, + navigateToMarketOrders, + setIsOpen, +}: ElementProps) => { + const stringGetter = useStringGetter(); + const dispatch = useDispatch(); + + return ( + } + > + { + dispatch(closeDialog()); + navigateToMarketOrders(marketId); + }} + /> + + ); +}; diff --git a/src/views/dialogs/WithdrawDialog.tsx b/src/views/dialogs/WithdrawDialog.tsx index 13ab11091..b27772710 100644 --- a/src/views/dialogs/WithdrawDialog.tsx +++ b/src/views/dialogs/WithdrawDialog.tsx @@ -1,14 +1,15 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useBreakpoints, useStringGetter } from '@/hooks'; -import { Dialog, DialogPlacement } from '@/components/Dialog'; - -import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; +import { Dialog, DialogPlacement } from '@/components/Dialog'; +import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm'; + type ElementProps = { setIsOpen: (open: boolean) => void; }; @@ -24,28 +25,14 @@ export const WithdrawDialog = ({ setIsOpen }: ElementProps) => { title={stringGetter({ key: STRING_KEYS.WITHDRAW })} placement={isTablet ? DialogPlacement.FullScreen : DialogPlacement.Default} > - + <$Content> - + ); }; -const Styled: Record = {}; - -Styled.TextToggle = styled.div` - ${layoutMixins.stickyFooter} - color: var(--color-accent); - cursor: pointer; - - margin-top: auto; - - &:hover { - text-decoration: underline; - } -`; - -Styled.Content = styled.div` +const $Content = styled.div` ${layoutMixins.stickyArea0} --stickyArea0-bottomHeight: 2rem; --stickyArea0-bottomGap: 1rem; diff --git a/src/views/dialogs/WithdrawalGateDialog.tsx b/src/views/dialogs/WithdrawalGateDialog.tsx new file mode 100644 index 000000000..9ce6138ee --- /dev/null +++ b/src/views/dialogs/WithdrawalGateDialog.tsx @@ -0,0 +1,101 @@ +import styled from 'styled-components'; + +import { ButtonAction, ButtonType } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; + +import { LinkOutIcon } from '@/icons'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Button } from '@/components/Button'; +import { Dialog } from '@/components/Dialog'; +import { Icon, IconName } from '@/components/Icon'; + +type ElementProps = { + setIsOpen: (open: boolean) => void; + transferType: 'withdrawal' | 'transfer'; + estimatedUnblockTime?: string | null; +}; + +export const WithdrawalGateDialog = ({ + setIsOpen, + estimatedUnblockTime, + transferType, +}: ElementProps) => { + const stringGetter = useStringGetter(); + const { withdrawalGateLearnMore } = useURLConfigs(); + + return ( + + <$Icon iconName={IconName.Warning} /> + + } + slotFooter={ + <$ButtonRow> + + + + } + > + <$Content> + {stringGetter({ + key: STRING_KEYS.WITHDRAWALS_PAUSED_DESC, + params: { + ESTIMATED_DURATION: estimatedUnblockTime, + }, + })} + + + ); +}; +const $IconContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 4.5rem; + height: 4.5rem; + border-radius: 50%; + min-width: 4.5rem; + min-height: 4.5rem; + background-color: var(--color-gradient-warning); +`; + +const $Icon = styled(Icon)` + color: var(--color-warning); + font-size: 2.5rem; + margin-bottom: 0.125rem; +`; + +const $Content = styled.div` + ${layoutMixins.column} + gap: 1rem; +`; + +const $ButtonRow = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +`; diff --git a/src/views/forms/AccountManagementForms/DepositForm.tsx b/src/views/forms/AccountManagementForms/DepositForm.tsx index 0599d6a7b..d1f2592da 100644 --- a/src/views/forms/AccountManagementForms/DepositForm.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm.tsx @@ -1,26 +1,35 @@ -import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react'; + import { type NumberFormatValues } from 'react-number-format'; import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { Abi, parseUnits } from 'viem'; import erc20 from '@/abi/erc20.json'; import erc20_usdt from '@/abi/erc20_usdt.json'; import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus'; -import { AnalyticsEvent, AnalyticsEventData } from '@/constants/analytics'; import { AlertType } from '@/constants/alerts'; +import { AnalyticsEvent, AnalyticsEventData } from '@/constants/analytics'; import { ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { isMainnet } from '@/constants/networks'; -import { MAX_CCTP_TRANSFER_AMOUNT, MAX_PRICE_IMPACT, NumberSign } from '@/constants/numbers'; -import type { EvmAddress } from '@/constants/wallets'; - -import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks'; -import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance'; +import { TransferNotificationTypes } from '@/constants/notifications'; +import { + MAX_CCTP_TRANSFER_AMOUNT, + MAX_PRICE_IMPACT, + MIN_CCTP_TRANSFER_AMOUNT, + NumberSign, +} from '@/constants/numbers'; +import { WalletType, type EvmAddress } from '@/constants/wallets'; + +import { CHAIN_DEFAULT_TOKEN_ADDRESS, useAccountBalance } from '@/hooks/useAccountBalance'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useDebounce } from '@/hooks/useDebounce'; import { useLocalNotifications } from '@/hooks/useLocalNotifications'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { layoutMixins } from '@/styles/layoutMixins'; import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; import { Button } from '@/components/Button'; @@ -32,6 +41,7 @@ import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { OutputType } from '@/components/Output'; import { Tag } from '@/components/Tag'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; +import { WithTooltip } from '@/components/WithTooltip'; import { getSelectedDydxChainId } from '@/state/appSelectors'; import { getTransferInputs } from '@/state/inputsSelectors'; @@ -42,12 +52,11 @@ import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid'; import { log } from '@/lib/telemetry'; import { parseWalletError } from '@/lib/wallet'; +import { NobleDeposit } from '../NobleDeposit'; +import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt'; import { SourceSelectMenu } from './SourceSelectMenu'; import { TokenSelectMenu } from './TokenSelectMenu'; -import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt'; -import { NobleDeposit } from '../NobleDeposit'; - type DepositFormProps = { onDeposit?: (event?: AnalyticsEventData) => void; onError?: () => void; @@ -133,6 +142,17 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { if (error) onError?.(); }, [error]); + const { walletType } = useAccounts(); + + useEffect(() => { + if (walletType === WalletType.Privy) { + abacusStateManager.setTransferValue({ + field: TransferInputField.exchange, + value: 'coinbase', + }); + } + }, [walletType]); + const onSelectNetwork = useCallback((name: string, type: 'chain' | 'exchange') => { if (name) { abacusStateManager.clearTransferInputValues(); @@ -267,6 +287,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { toAmount: summary?.usdcSize || undefined, triggeredAt: Date.now(), isCctp, + requestId: requestPayload.requestId ?? undefined, + type: TransferNotificationTypes.Deposit, }); abacusStateManager.clearTransferInputValues(); setFromAmount(''); @@ -275,6 +297,13 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { chainId: chainIdStr || undefined, tokenAddress: sourceToken?.address || undefined, tokenSymbol: sourceToken?.symbol || undefined, + slippage: slippage || undefined, + gasFee: summary?.gasFee || undefined, + bridgeFee: summary?.bridgeFee || undefined, + exchangeRate: summary?.exchangeRate || undefined, + estimatedRouteDuration: summary?.estimatedRouteDuration || undefined, + toAmount: summary?.toAmount || undefined, + toAmountMin: summary?.toAmountMin || undefined, }); } } catch (error) { @@ -313,7 +342,24 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { }, ]; + // TODO: abstract as much as possible to a util/hook and share between WithdrawForm const errorMessage = useMemo(() => { + if (isCctp) { + if ( + !debouncedAmountBN.isZero() && + MustBigNumber(debouncedAmountBN).lte(MIN_CCTP_TRANSFER_AMOUNT) + ) { + return 'Amount must be greater than 10 USDC'; + } + if (MustBigNumber(debouncedAmountBN).gte(MAX_CCTP_TRANSFER_AMOUNT)) { + return stringGetter({ + key: STRING_KEYS.MAX_CCTP_TRANSFER_LIMIT_EXCEEDED, + params: { + MAX_CCTP_TRANSFER_AMOUNT: MAX_CCTP_TRANSFER_AMOUNT, + }, + }); + } + } if (error) { return parseWalletError({ error, stringGetter }).message; } @@ -339,17 +385,6 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { return stringGetter({ key: STRING_KEYS.DEPOSIT_MORE_THAN_BALANCE }); } - if (isCctp) { - if (MustBigNumber(debouncedAmountBN).gte(MAX_CCTP_TRANSFER_AMOUNT)) { - return stringGetter({ - key: STRING_KEYS.MAX_CCTP_TRANSFER_LIMIT_EXCEEDED, - params: { - MAX_CCTP_TRANSFER_AMOUNT: MAX_CCTP_TRANSFER_AMOUNT, - }, - }); - } - } - if (isMainnet && MustBigNumber(summary?.aggregatePriceImpact).gte(MAX_PRICE_IMPACT)) { return stringGetter({ key: STRING_KEYS.PRICE_IMPACT_TOO_HIGH }); } @@ -365,6 +400,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { sourceToken, stringGetter, summary, + debouncedAmountBN, ]); const isDisabled = @@ -377,9 +413,22 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { if (!resources) { return ; } - return ( - + <$Form onSubmit={onSubmit}> + <$Subheader> + {stringGetter({ + key: STRING_KEYS.LOWEST_FEE_DEPOSITS, + params: { + LOWEST_FEE_TOKENS_TOOLTIP: ( + + {stringGetter({ + key: STRING_KEYS.SELECT_CHAINS, + })} + + ), + }, + })} + { ) : ( <> - + <$WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}> + <$FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}> {stringGetter({ key: STRING_KEYS.MAX })} - + } /> - + {errorMessage && {errorMessage}} {requireUserActionInWallet && ( {stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })} )} - + <$Footer> { setRequireUserActionInWallet={setRequireUserActionInWallet} setError={setError} /> - + )} - + ); }; - -const Styled: Record = {}; - -Styled.Form = styled.form` +const $Form = styled.form` ${formMixins.transfersForm} `; -Styled.Footer = styled.footer` +const $Subheader = styled.div` + color: var(--color-text-0); +`; + +const $Footer = styled.footer` ${formMixins.footer} --stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom); `; -Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` +const $WithDetailsReceipt = styled(WithDetailsReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); `; -Styled.Link = styled(Link)` +const $Link = styled(Link)` color: var(--color-accent); &:visited { @@ -450,10 +500,10 @@ Styled.Link = styled(Link)` } `; -Styled.TransactionInfo = styled.span` +const $TransactionInfo = styled.span` ${layoutMixins.row} `; -Styled.FormInputButton = styled(Button)` +const $FormInputButton = styled(Button)` ${formMixins.inputInnerButton} `; diff --git a/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx b/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx index c928a3a5b..f34b5137a 100644 --- a/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx @@ -1,28 +1,29 @@ -import { type Dispatch, type SetStateAction, useState, type ReactNode, useEffect } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; -import { shallowEqual, useSelector } from 'react-redux'; -import type { RouteData } from '@0xsquid/sdk'; +import { useState, type Dispatch, type SetStateAction } from 'react'; -import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; +import type { RouteData } from '@0xsquid/sdk'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { TransferInputTokenResource } from '@/constants/abacus'; +import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { NumberSign, TOKEN_DECIMALS } from '@/constants/numbers'; -import { useStringGetter, useTokenConfigs } from '@/hooks'; +import { ConnectionErrorType, useApiState } from '@/hooks/useApiState'; import { useMatchingEvmNetwork } from '@/hooks/useMatchingEvmNetwork'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; import { useWalletConnection } from '@/hooks/useWalletConnection'; import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; -import { Details, DetailsItem } from '@/components/Details'; +import { Details } from '@/components/Details'; import { DiffOutput } from '@/components/DiffOutput'; -import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; import { Tag } from '@/components/Tag'; -import { ToggleButton } from '@/components/ToggleButton'; import { WithReceipt } from '@/components/WithReceipt'; +import { WithTooltip } from '@/components/WithTooltip'; import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; @@ -37,12 +38,10 @@ import { SlippageEditor } from '../SlippageEditor'; type ElementProps = { isDisabled?: boolean; isLoading?: boolean; - chainId?: string | number; setError?: Dispatch>; setRequireUserActionInWallet: (val: boolean) => void; slippage: number; - slotError?: ReactNode; setSlippage: (slippage: number) => void; sourceToken?: TransferInputTokenResource; squidRoute?: RouteData; @@ -54,19 +53,17 @@ export const DepositButtonAndReceipt = ({ slippage, setSlippage, sourceToken, - isDisabled, isLoading, - slotError, setRequireUserActionInWallet, }: ElementProps) => { - const [showFeeBreakdown, setShowFeeBreakdown] = useState(false); const [isEditingSlippage, setIsEditingSlipapge] = useState(false); const stringGetter = useStringGetter(); const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual); const { connectWallet, isConnectedWagmi } = useWalletConnection(); + const { connectionError } = useApiState(); const connectWagmi = async () => { try { @@ -78,12 +75,6 @@ export const DepositButtonAndReceipt = ({ } }; - useEffect(() => { - if (!isConnectedWagmi && canAccountTrade) { - connectWagmi(); - } - }, [isConnectedWagmi, canAccountTrade]); - const { matchNetwork: switchNetwork, isSwitchingNetwork, @@ -99,34 +90,16 @@ export const DepositButtonAndReceipt = ({ const { current: buyingPower, postOrder: newBuyingPower } = useSelector(getSubaccountBuyingPower, shallowEqual) || {}; - const { isCctp, summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {}; + const { + summary, + requestPayload, + depositOptions, + chain: chainIdStr, + } = useSelector(getTransferInputs, shallowEqual) || {}; const { usdcLabel } = useTokenConfigs(); - const feeSubitems: DetailsItem[] = []; - - if (typeof summary?.gasFee === 'number') { - feeSubitems.push({ - key: 'gas-fees', - label: {stringGetter({ key: STRING_KEYS.GAS_FEE })}, - value: , - }); - } - - if (typeof summary?.bridgeFee === 'number') { - feeSubitems.push({ - key: 'bridge-fees', - label: {stringGetter({ key: STRING_KEYS.BRIDGE_FEE })}, - value: , - }); - } - - const hasSubitems = feeSubitems.length > 0; - - const showSubitemsToggle = showFeeBreakdown - ? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS }) - : stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS }); - - const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0); + const sourceChainName = + depositOptions?.chains?.toArray().find((chain) => chain.type === chainIdStr)?.stringKey || ''; const submitButtonReceipt = [ { @@ -139,31 +112,29 @@ export const DepositButtonAndReceipt = ({ value: ( ), - subitems: [ - { - key: 'minimum-deposit-amount', - label: ( - - {stringGetter({ key: STRING_KEYS.MINIMUM_DEPOSIT_AMOUNT })} {usdcLabel} - - ), - value: ( - - ), - tooltip: 'minimum-deposit-amount', - }, - ], + }, + { + key: 'minimum-deposit-amount', + label: ( + + {stringGetter({ key: STRING_KEYS.MINIMUM_DEPOSIT_AMOUNT })} {usdcLabel} + + ), + value: ( + + ), + tooltip: 'minimum-deposit-amount', }, { key: 'exchange-rate', label: {stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}, value: typeof summary?.exchangeRate === 'number' ? ( - + <$ExchangeRate> = - + ) : ( ), }, + typeof summary?.gasFee === 'number' && { + key: 'gas-fees', + label: ( + + {stringGetter({ key: STRING_KEYS.GAS_FEE })} + + ), + value: , + }, + typeof summary?.bridgeFee === 'number' && { + key: 'bridge-fees', + label: ( + + {stringGetter({ key: STRING_KEYS.BRIDGE_FEE })} + + ), + value: , + }, { key: 'equity', label: ( @@ -211,12 +200,6 @@ export const DepositButtonAndReceipt = ({ /> ), }, - !isCctp && { - key: 'total-fees', - label: {stringGetter({ key: STRING_KEYS.TOTAL_FEES })}, - value: , - subitems: feeSubitems, - }, { key: 'slippage', label: {stringGetter({ key: STRING_KEYS.MAX_SLIPPAGE })}, @@ -253,34 +236,17 @@ export const DepositButtonAndReceipt = ({ }, ].filter(isTruthy); - const isFormValid = !isDisabled && !isEditingSlippage; + const isFormValid = + !isDisabled && !isEditingSlippage && connectionError !== ConnectionErrorType.CHAIN_DISRUPTION; return ( - - - - {hasSubitems && ( - } - > - {showSubitemsToggle} - - )} - - - } - slotError={slotError} - > + <$WithReceipt slotReceipt={<$Details items={submitButtonReceipt} />}> {!canAccountTrade ? ( ) : !isConnectedWagmi ? ( - ) : !isMatchingNetwork ? ( )} - - + + ); }; - -const Styled: Record = {}; - -Styled.Form = styled.form` +const $Form = styled.form` ${formMixins.transfersForm} `; -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${formMixins.footer} --stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom); diff --git a/src/views/forms/AccountManagementForms/TokenSelectMenu.tsx b/src/views/forms/AccountManagementForms/TokenSelectMenu.tsx index 96db78f65..f3ff34a08 100644 --- a/src/views/forms/AccountManagementForms/TokenSelectMenu.tsx +++ b/src/views/forms/AccountManagementForms/TokenSelectMenu.tsx @@ -1,42 +1,92 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; -import { STRING_KEYS } from '@/constants/localization'; import { TransferInputTokenResource, TransferType } from '@/constants/abacus'; -import { useStringGetter } from '@/hooks'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useEnvFeatures } from '@/hooks/useEnvFeatures'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { DiffArrow } from '@/components/DiffArrow'; -import { Icon, IconName } from '@/components/Icon'; +import { Icon } from '@/components/Icon'; import { SearchSelectMenu } from '@/components/SearchSelectMenu'; import { Tag } from '@/components/Tag'; -import { layoutMixins } from '@/styles/layoutMixins'; - import { getTransferInputs } from '@/state/inputsSelectors'; +import cctpTokens from '../../../../public/configs/cctp.json'; +import { TokenInfo } from './SourceSelectMenu'; + type ElementProps = { selectedToken?: TransferInputTokenResource; onSelectToken: (token: TransferInputTokenResource) => void; isExchange?: boolean; }; +const CURVE_DAO_TOKEN_ADDRESS = '0xD533a949740bb3306d119CC777fa900bA034cd52'; + +const cctpTokensByAddress = cctpTokens.reduce((acc, token) => { + if (!acc[token.tokenAddress]) { + acc[token.tokenAddress] = []; + } + acc[token.tokenAddress].push(token); + return acc; +}, {} as Record); + export const TokenSelectMenu = ({ selectedToken, onSelectToken, isExchange }: ElementProps) => { const stringGetter = useStringGetter(); const { type, depositOptions, withdrawalOptions, resources } = useSelector(getTransferInputs, shallowEqual) || {}; + const { CCTPWithdrawalOnly, CCTPDepositOnly } = useEnvFeatures(); + const tokens = (type === TransferType.deposit ? depositOptions : withdrawalOptions)?.assets?.toArray() || []; - const tokenItems = Object.values(tokens).map((token) => ({ - value: token.type, - label: token.stringKey, - onSelect: () => { - const selectedToken = resources?.tokenResources?.get(token.type); - selectedToken && onSelectToken(selectedToken); - }, - slotBefore: , - tag: resources?.tokenResources?.get(token.type)?.symbol, - })); + const tokenItems = Object.values(tokens) + .map((token) => ({ + value: token.type, + label: token.stringKey, + onSelect: () => { + const selectedToken = resources?.tokenResources?.get(token.type); + selectedToken && onSelectToken(selectedToken); + }, + slotBefore: ( + // the curve dao token svg causes the web app to lag when rendered + <$Img + src={token.type !== CURVE_DAO_TOKEN_ADDRESS ? token.iconUrl ?? undefined : undefined} + alt="" + /> + ), + slotAfter: !!cctpTokensByAddress[token.type] && ( + <$Text> + {stringGetter({ + key: STRING_KEYS.LOWEST_FEES_WITH_USDC, + params: { + LOWEST_FEES_HIGHLIGHT_TEXT: ( + <$GreenHighlight> + {stringGetter({ key: STRING_KEYS.LOWEST_FEES_HIGHLIGHT_TEXT })} + + ), + }, + })} + + ), + tag: resources?.tokenResources?.get(token.type)?.symbol, + })) + .filter((token) => { + // if deposit and CCTPDepositOnly enabled, only return cctp tokens + if (type === TransferType.deposit && CCTPDepositOnly) { + return !!cctpTokensByAddress[token.value]; + } + // if withdrawal and CCTPWithdrawalOnly enabled, only return cctp tokens + if (type === TransferType.withdrawal && CCTPWithdrawalOnly) { + return !!cctpTokensByAddress[token.value]; + } + return true; + }) + .sort((token) => (!!cctpTokensByAddress[token.value] ? -1 : 1)); return ( - + <$AssetRow> {selectedToken ? ( <> - {selectedToken?.name}{' '} + <$Img src={selectedToken?.iconUrl ?? undefined} alt="" /> {selectedToken?.name}{' '} {selectedToken?.symbol} ) : ( stringGetter({ key: STRING_KEYS.SELECT_ASSET }) )} - + ); }; +const $Text = styled.div` + font: var(--font-small-regular); + color: var(--color-text-0); +`; -const Styled: Record = {}; +const $GreenHighlight = styled.span` + color: var(--color-green); +`; -Styled.AssetRow = styled.div` +const $AssetRow = styled.div` ${layoutMixins.row} gap: 0.5rem; color: var(--color-text-2); font: var(--font-base-book); `; -Styled.Img = styled.img` +const $Img = styled.img` width: 1.25rem; height: 1.25rem; border-radius: 50%; `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` height: 0.5rem; `; diff --git a/src/views/forms/AccountManagementForms/WithdrawForm.tsx b/src/views/forms/AccountManagementForms/WithdrawForm.tsx index 1fa07fc0e..986e59c44 100644 --- a/src/views/forms/AccountManagementForms/WithdrawForm.tsx +++ b/src/views/forms/AccountManagementForms/WithdrawForm.tsx @@ -1,8 +1,9 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ChangeEvent, FormEvent } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + import type { NumberFormatValues } from 'react-number-format'; import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { isAddress } from 'viem'; import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus'; @@ -17,47 +18,48 @@ import { MAX_PRICE_IMPACT, MIN_CCTP_TRANSFER_AMOUNT, NumberSign, + TOKEN_DECIMALS, } from '@/constants/numbers'; +import { WalletType } from '@/constants/wallets'; -import { - useAccounts, - useDebounce, - useDydxClient, - useRestrictions, - useSelectedNetwork, - useStringGetter, - useSubaccount, -} from '@/hooks'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useDebounce } from '@/hooks/useDebounce'; +import { useDydxClient } from '@/hooks/useDydxClient'; import { useLocalNotifications } from '@/hooks/useLocalNotifications'; +import { useRestrictions } from '@/hooks/useRestrictions'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useSubaccount } from '@/hooks/useSubaccount'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; +import { useWithdrawalInfo } from '@/hooks/useWithdrawalInfo'; -import { layoutMixins } from '@/styles/layoutMixins'; import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; import { Button } from '@/components/Button'; import { DiffOutput } from '@/components/DiffOutput'; import { FormInput } from '@/components/FormInput'; +import { Icon, IconName } from '@/components/Icon'; import { InputType } from '@/components/Input'; import { Link } from '@/components/Link'; import { OutputType } from '@/components/Output'; import { Tag } from '@/components/Tag'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; -import { Icon, IconName } from '@/components/Icon'; - +import { WithTooltip } from '@/components/WithTooltip'; import { SourceSelectMenu } from '@/views/forms/AccountManagementForms/SourceSelectMenu'; -import { getSelectedDydxChainId } from '@/state/appSelectors'; import { getSubaccount } from '@/state/accountSelectors'; +import { getSelectedDydxChainId } from '@/state/appSelectors'; import { getTransferInputs } from '@/state/inputsSelectors'; import abacusStateManager from '@/lib/abacus'; +import { validateCosmosAddress } from '@/lib/addressUtils'; +import { track } from '@/lib/analytics'; import { MustBigNumber } from '@/lib/numbers'; import { getNobleChainId } from '@/lib/squid'; import { TokenSelectMenu } from './TokenSelectMenu'; import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt'; -import { validateCosmosAddress } from '@/lib/addressUtils'; -import { track } from '@/lib/analytics'; export const WithdrawForm = () => { const stringGetter = useStringGetter(); @@ -85,6 +87,8 @@ export const WithdrawForm = () => { const [withdrawAmount, setWithdrawAmount] = useState(''); const [slippage, setSlippage] = useState(isCctp ? 0 : 0.01); // 0.1% slippage const debouncedAmount = useDebounce(withdrawAmount, 500); + const { usdcLabel } = useTokenConfigs(); + const { usdcWithdrawalCapacity } = useWithdrawalInfo({ transferType: 'withdrawal' }); const isValidAddress = toAddress && isAddress(toAddress); @@ -195,6 +199,7 @@ export const WithdrawForm = () => { triggeredAt: Date.now(), isCctp, isExchange: Boolean(exchange), + requestId: requestPayload.requestId ?? undefined, }); abacusStateManager.clearTransferInputValues(); setWithdrawAmount(''); @@ -203,6 +208,13 @@ export const WithdrawForm = () => { chainId: toChainId, tokenAddress: toToken?.address || undefined, tokenSymbol: toToken?.symbol || undefined, + slippage: slippage || undefined, + gasFee: summary?.gasFee || undefined, + bridgeFee: summary?.bridgeFee || undefined, + exchangeRate: summary?.exchangeRate || undefined, + estimatedRouteDuration: summary?.estimatedRouteDuration || undefined, + toAmount: summary?.toAmount || undefined, + toAmountMin: summary?.toAmountMin || undefined, }); } } @@ -269,6 +281,17 @@ export const WithdrawForm = () => { setWithdrawAmount(freeCollateralBN.toString()); }, [freeCollateralBN, setWithdrawAmount]); + const { walletType } = useAccounts(); + + useEffect(() => { + if (walletType === WalletType.Privy) { + abacusStateManager.setTransferValue({ + field: TransferInputField.exchange, + value: 'coinbase', + }); + } + }, [walletType]); + const onSelectNetwork = useCallback((name: string, type: 'chain' | 'exchange') => { if (name) { setWithdrawAmount(''); @@ -305,7 +328,7 @@ export const WithdrawForm = () => { ), value: ( - { const { sanctionedAddresses } = useRestrictions(); - const errorMessage = useMemo(() => { + const { alertType, errorMessage } = useMemo(() => { + if (isCctp) { + if (debouncedAmountBN.gte(MAX_CCTP_TRANSFER_AMOUNT)) { + return { + errorMessage: stringGetter({ + key: STRING_KEYS.MAX_CCTP_TRANSFER_LIMIT_EXCEEDED, + params: { + MAX_CCTP_TRANSFER_AMOUNT: MAX_CCTP_TRANSFER_AMOUNT, + }, + }), + }; + } + if ( + !debouncedAmountBN.isZero() && + MustBigNumber(debouncedAmountBN).lte(MIN_CCTP_TRANSFER_AMOUNT) + ) { + return { + errorMessage: 'Amount must be greater than 10 USDC', + }; + } + } if (error) { - return error; + return { + errorMessage: error, + }; } if (routeErrors) { - return routeErrorMessage - ? stringGetter({ - key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, - params: { ERROR_MESSAGE: routeErrorMessage }, - }) - : stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG }); + return { + errorMessage: routeErrorMessage + ? stringGetter({ + key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, + params: { ERROR_MESSAGE: routeErrorMessage }, + }) + : stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG }), + }; } - if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS }); + if (!toAddress) { + return { + alertType: AlertType.Warning, + errorMessage: stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS }), + }; + } if (sanctionedAddresses.has(toAddress)) - return stringGetter({ - key: STRING_KEYS.TRANSFER_INVALID_DYDX_ADDRESS, - }); + return { + errorMessage: stringGetter({ + key: STRING_KEYS.TRANSFER_INVALID_DYDX_ADDRESS, + }), + }; if (debouncedAmountBN) { if (!chainIdStr && !exchange) { - return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_CHAIN }); + return { + errorMessage: stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_CHAIN }), + }; } else if (!toToken) { - return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ASSET }); + return { + errorMessage: stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ASSET }), + }; } } - if (MustBigNumber(debouncedAmountBN).gt(MustBigNumber(freeCollateralBN))) { - return stringGetter({ key: STRING_KEYS.WITHDRAW_MORE_THAN_FREE }); - } - - if (isCctp) { - if (MustBigNumber(debouncedAmountBN).gte(MAX_CCTP_TRANSFER_AMOUNT)) { - return stringGetter({ - key: STRING_KEYS.MAX_CCTP_TRANSFER_LIMIT_EXCEEDED, - params: { - MAX_CCTP_TRANSFER_AMOUNT: MAX_CCTP_TRANSFER_AMOUNT, - }, - }); - } - if ( - !debouncedAmountBN.isZero() && - MustBigNumber(debouncedAmountBN).lte(MIN_CCTP_TRANSFER_AMOUNT) - ) { - return 'Amount must be greater than 10 USDC'; - } + if (debouncedAmountBN.gt(MustBigNumber(freeCollateralBN))) { + return { + errorMessage: stringGetter({ key: STRING_KEYS.WITHDRAW_MORE_THAN_FREE }), + }; } if (isMainnet && MustBigNumber(summary?.aggregatePriceImpact).gte(MAX_PRICE_IMPACT)) { - return stringGetter({ key: STRING_KEYS.PRICE_IMPACT_TOO_HIGH }); + return { errorMessage: stringGetter({ key: STRING_KEYS.PRICE_IMPACT_TOO_HIGH }) }; } - return undefined; + // Withdrawal Safety + if (usdcWithdrawalCapacity.gt(0) && debouncedAmountBN.gt(usdcWithdrawalCapacity)) { + return { + alertType: AlertType.Warning, + errorMessage: stringGetter({ + key: STRING_KEYS.WITHDRAWAL_LIMIT_OVER, + params: { + USDC_LIMIT: ( + + {usdcWithdrawalCapacity.toFormat(TOKEN_DECIMALS)} + <$Tag>{usdcLabel} + + ), + }, + }), + }; + } + return { + errorMessage: undefined, + }; }, [ error, routeErrors, @@ -388,6 +450,7 @@ export const WithdrawForm = () => { sanctionedAddresses, stringGetter, summary, + usdcWithdrawalCapacity, ]); const isInvalidNobleAddress = Boolean( @@ -405,8 +468,22 @@ export const WithdrawForm = () => { isInvalidNobleAddress; return ( - - + <$Form onSubmit={onSubmit}> + <$Subheader> + {stringGetter({ + key: STRING_KEYS.LOWEST_FEE_WITHDRAWALS, + params: { + LOWEST_FEE_TOKENS_TOOLTIP: ( + + {stringGetter({ + key: STRING_KEYS.SELECT_CHAINS, + })} + + ), + }, + })} + + <$DestinationRow> { label={ {stringGetter({ key: STRING_KEYS.DESTINATION })}{' '} - {isValidAddress ? : null} + {isValidAddress ? <$CheckIcon iconName={IconName.Check} /> : null} } /> @@ -424,7 +501,7 @@ export const WithdrawForm = () => { selectedChain={chainIdStr || undefined} onSelect={onSelectNetwork} /> - + {isInvalidNobleAddress && ( {stringGetter({ key: STRING_KEYS.NOBLE_ADDRESS_VALIDATION })} @@ -435,21 +512,23 @@ export const WithdrawForm = () => { onSelectToken={onSelectToken} isExchange={Boolean(exchange)} /> - + <$WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}> + <$FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}> {stringGetter({ key: STRING_KEYS.MAX })} - + } /> - - {errorMessage && {errorMessage}} - + + {errorMessage && ( + <$AlertMessage type={alertType ?? AlertType.Error}>{errorMessage} + )} + <$Footer> { withdrawChain={chainIdStr || undefined} withdrawToken={toToken || undefined} /> - - + + ); }; +const $Subheader = styled.div` + color: var(--color-text-0); +`; -const Styled: Record = {}; +const $Tag = styled(Tag)` + margin-left: 0.5ch; +`; -Styled.DiffOutput = styled(DiffOutput)` +const $DiffOutput = styled(DiffOutput)` --diffOutput-valueWithDiff-fontSize: 1em; `; -Styled.Form = styled.form` +const $Form = styled.form` ${formMixins.transfersForm} `; -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${formMixins.footer} --stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom); `; -Styled.DestinationRow = styled.div` +const $DestinationRow = styled.div` ${layoutMixins.spacedRow} grid-template-columns: 1fr 1fr; gap: 1rem; `; -Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` +const $AlertMessage = styled(AlertMessage)` + display: inline; +`; + +const $WithDetailsReceipt = styled(WithDetailsReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); `; -Styled.Link = styled(Link)` +const $Link = styled(Link)` color: var(--color-accent); &:visited { @@ -496,15 +584,15 @@ Styled.Link = styled(Link)` } `; -Styled.TransactionInfo = styled.span` +const $TransactionInfo = styled.span` ${layoutMixins.row} `; -Styled.FormInputButton = styled(Button)` +const $FormInputButton = styled(Button)` ${formMixins.inputInnerButton} `; -Styled.CheckIcon = styled(Icon)` +const $CheckIcon = styled(Icon)` margin: 0 1ch; color: var(--color-success); diff --git a/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx b/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx index eae0704f4..b7592f6ef 100644 --- a/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx +++ b/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx @@ -1,26 +1,27 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; + import { shallowEqual, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; -import { formatUnits } from 'viem'; +import styled from 'styled-components'; import { TransferInputTokenResource } from '@/constants/abacus'; -import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; +import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { NumberSign, TOKEN_DECIMALS } from '@/constants/numbers'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { ConnectionErrorType, useApiState } from '@/hooks/useApiState'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; -import { useStringGetter, useTokenConfigs } from '@/hooks'; +import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; - import { Details, DetailsItem } from '@/components/Details'; import { DiffOutput } from '@/components/DiffOutput'; -import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; import { Tag } from '@/components/Tag'; import { ToggleButton } from '@/components/ToggleButton'; import { WithReceipt } from '@/components/WithReceipt'; +import { WithTooltip } from '@/components/WithTooltip'; import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; @@ -46,13 +47,11 @@ export const WithdrawButtonAndReceipt = ({ setSlippage, slippage, - withdrawChain, withdrawToken, isDisabled, isLoading, }: ElementProps) => { - const [showFeeBreakdown, setShowFeeBreakdown] = useState(false); const [isEditingSlippage, setIsEditingSlipapge] = useState(false); const stringGetter = useStringGetter(); @@ -60,103 +59,70 @@ export const WithdrawButtonAndReceipt = ({ const { summary, requestPayload, exchange } = useSelector(getTransferInputs, shallowEqual) || {}; const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual); const { usdcLabel } = useTokenConfigs(); + const { connectionError } = useApiState(); - const feeSubitems: DetailsItem[] = []; - - if (typeof summary?.gasFee === 'number') { - feeSubitems.push({ - key: 'gas-fees', - label: {stringGetter({ key: STRING_KEYS.GAS_FEE })}, - value: , - }); - } - - if (typeof summary?.bridgeFee === 'number') { - feeSubitems.push({ - key: 'bridge-fees', - label: {stringGetter({ key: STRING_KEYS.BRIDGE_FEE })}, - value: , - }); - } - - const hasSubitems = feeSubitems.length > 0; - - const showSubitemsToggle = showFeeBreakdown - ? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS }) - : stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS }); - - const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0); - - const submitButtonReceipt = [ + const submitButtonReceipt: DetailsItem[] = [ { - key: 'total-fees', - label: {stringGetter({ key: STRING_KEYS.TOTAL_FEES })}, - value: , - subitems: feeSubitems, - }, - !exchange && { - key: 'exchange-rate', - label: {stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}, - value: withdrawToken && typeof summary?.exchangeRate === 'number' && ( - - - = - - + key: 'expected-amount-received', + + label: ( + <$RowWithGap> + {stringGetter({ key: STRING_KEYS.EXPECTED_AMOUNT_RECEIVED })} + {withdrawToken && {withdrawToken?.symbol}} + ), - }, - { - key: 'estimated-route-duration', - label: {stringGetter({ key: STRING_KEYS.ESTIMATED_TIME })}, - value: typeof summary?.estimatedRouteDuration === 'number' && ( - + value: ( + ), }, { - key: 'expected-amount-received', + key: 'minimum-amount-received', label: ( - - {stringGetter({ key: STRING_KEYS.EXPECTED_AMOUNT_RECEIVED })}{' '} + <$RowWithGap> + {stringGetter({ key: STRING_KEYS.MINIMUM_AMOUNT_RECEIVED })} {withdrawToken && {withdrawToken?.symbol}} - + ), value: ( - + ), - subitems: [ - { - key: 'minimum-amount-received', - label: ( - - {stringGetter({ key: STRING_KEYS.MINIMUM_AMOUNT_RECEIVED })}{' '} - {withdrawToken && {withdrawToken?.symbol}} - - ), - value: ( + tooltip: 'minimum-amount-received', + }, + !exchange && { + key: 'exchange-rate', + label: {stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}, + value: + withdrawToken && typeof summary?.exchangeRate === 'number' ? ( + <$RowWithGap> + + = - ), - tooltip: 'minimum-amount-received', - }, - ], + + ) : undefined, + }, + typeof summary?.gasFee === 'number' && { + key: 'gas-fees', + label: ( + {stringGetter({ key: STRING_KEYS.GAS_FEE })} + ), + value: , + }, + typeof summary?.bridgeFee === 'number' && { + key: 'bridge-fees', + label: ( + + {stringGetter({ key: STRING_KEYS.BRIDGE_FEE })} + + ), + value: , }, !exchange && { key: 'slippage', @@ -170,11 +136,30 @@ export const WithdrawButtonAndReceipt = ({ /> ), }, + { + key: 'estimated-route-duration', + label: {stringGetter({ key: STRING_KEYS.ESTIMATED_TIME })}, + value: + typeof summary?.estimatedRouteDuration === 'number' ? ( + + ) : undefined, + }, { key: 'leverage', label: {stringGetter({ key: STRING_KEYS.ACCOUNT_LEVERAGE })}, value: ( - - - {hasSubitems && ( - - } - > - {showSubitemsToggle} - - - )} - - } - > + <$WithReceipt slotReceipt={<$Details items={submitButtonReceipt} />}> {!canAccountTrade ? ( ) : ( @@ -222,39 +189,32 @@ export const WithdrawButtonAndReceipt = ({ {stringGetter({ key: STRING_KEYS.WITHDRAW })} )} - + ); }; - -const Styled: Record = {}; - -Styled.DiffOutput = styled(DiffOutput)` +const $DiffOutput = styled(DiffOutput)` --diffOutput-valueWithDiff-fontSize: 1em; `; -Styled.ExchangeRate = styled.span` +const $RowWithGap = styled.span` ${layoutMixins.row} gap: 0.5ch; `; -Styled.WithReceipt = styled(WithReceipt)` +const $WithReceipt = styled(WithReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); `; -Styled.CollapsibleDetails = styled.div` - ${layoutMixins.column} +const $Details = styled(Details)` padding: var(--form-input-paddingY) var(--form-input-paddingX); -`; - -Styled.Details = styled(Details)` font-size: 0.8125em; `; -Styled.DetailButtons = styled.div` +const $DetailButtons = styled.div` ${layoutMixins.spacedRow} `; -Styled.ToggleButton = styled(ToggleButton)` +const $ToggleButton = styled(ToggleButton)` --button-toggle-off-backgroundColor: transparent; --button-toggle-on-backgroundColor: transparent; --button-toggle-on-textColor: var(--color-text-0); diff --git a/src/views/forms/AdjustIsolatedMarginForm.tsx b/src/views/forms/AdjustIsolatedMarginForm.tsx new file mode 100644 index 000000000..014ebabee --- /dev/null +++ b/src/views/forms/AdjustIsolatedMarginForm.tsx @@ -0,0 +1,269 @@ +import { FormEvent, useMemo, useState } from 'react'; + +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import type { SubaccountPosition } from '@/constants/abacus'; +import { AlertType } from '@/constants/alerts'; +import { ButtonAction, ButtonShape } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { NumberSign, USD_DECIMALS } from '@/constants/numbers'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { AlertMessage } from '@/components/AlertMessage'; +import { Button } from '@/components/Button'; +import { DiffOutput } from '@/components/DiffOutput'; +import { FormInput } from '@/components/FormInput'; +import { GradientCard } from '@/components/GradientCard'; +import { InputType } from '@/components/Input'; +import { OutputType } from '@/components/Output'; +import { ToggleGroup } from '@/components/ToggleGroup'; +import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; + +import { getOpenPositionFromId, getSubaccount } from '@/state/accountSelectors'; +import { getMarketConfig } from '@/state/perpetualsSelectors'; + +import { calculatePositionMargin } from '@/lib/tradeData'; + +type ElementProps = { + marketId: SubaccountPosition['id']; +}; + +enum MarginAction { + ADD = 'ADD', + REMOVE = 'REMOVE', +} + +const SIZE_PERCENT_OPTIONS = { + '5%': '0.05', + '10%': '0.1', + '25%': '0.25', + '50%': '0.5', + '75%': '0.75', +}; + +export const AdjustIsolatedMarginForm = ({ marketId }: ElementProps) => { + const stringGetter = useStringGetter(); + const [marginAction, setMarginAction] = useState(MarginAction.ADD); + const subaccountPosition = useSelector(getOpenPositionFromId(marketId)); + const { adjustedMmf, leverage, liquidationPrice, notionalTotal } = subaccountPosition ?? {}; + const marketConfig = useSelector(getMarketConfig(marketId)); + const { tickSizeDecimals } = marketConfig ?? {}; + + /** + * @todo: Replace with Abacus functionality + */ + const [percent, setPercent] = useState(''); + const [amount, setAmount] = useState(''); + const onSubmit = () => {}; + + const positionMargin = { + current: calculatePositionMargin({ + adjustedMmf: adjustedMmf?.current, + notionalTotal: notionalTotal?.current, + }).toFixed(tickSizeDecimals ?? USD_DECIMALS), + postOrder: calculatePositionMargin({ + adjustedMmf: adjustedMmf?.postOrder, + notionalTotal: notionalTotal?.postOrder, + }).toFixed(tickSizeDecimals ?? USD_DECIMALS), + }; + + const { freeCollateral, marginUsage } = useSelector(getSubaccount, shallowEqual) ?? {}; + + const renderDiffOutput = ({ + type, + value, + newValue, + withDiff, + }: Pick[0], 'type' | 'value' | 'newValue' | 'withDiff'>) => ( + + ); + + const { + freeCollateralDiffOutput, + marginUsageDiffOutput, + positionMarginDiffOutput, + leverageDiffOutput, + } = useMemo( + () => ({ + freeCollateralDiffOutput: renderDiffOutput({ + withDiff: + !!freeCollateral?.postOrder && freeCollateral?.current !== freeCollateral?.postOrder, + value: freeCollateral?.current, + newValue: freeCollateral?.postOrder, + type: OutputType.Number, + }), + marginUsageDiffOutput: renderDiffOutput({ + withDiff: !!marginUsage?.postOrder && marginUsage?.current !== marginUsage?.postOrder, + value: marginUsage?.current, + newValue: marginUsage?.postOrder, + type: OutputType.Percent, + }), + positionMarginDiffOutput: renderDiffOutput({ + withDiff: !!positionMargin.postOrder && positionMargin.current !== positionMargin.postOrder, + value: positionMargin.current, + newValue: positionMargin.postOrder, + type: OutputType.Fiat, + }), + leverageDiffOutput: renderDiffOutput({ + withDiff: !!leverage?.postOrder && leverage?.current !== leverage?.postOrder, + value: leverage?.current, + newValue: leverage?.postOrder, + type: OutputType.Multiple, + }), + }), + [freeCollateral, marginUsage, positionMargin, leverage] + ); + + const formConfig = + marginAction === MarginAction.ADD + ? { + formLabel: stringGetter({ key: STRING_KEYS.ADDING }), + buttonLabel: stringGetter({ key: STRING_KEYS.ADD_MARGIN }), + inputReceiptItems: [ + { + key: 'cross-free-collateral', + label: stringGetter({ key: STRING_KEYS.CROSS_FREE_COLLATERAL }), + value: freeCollateralDiffOutput, + }, + { + key: 'cross-margin-usage', + label: stringGetter({ key: STRING_KEYS.CROSS_MARGIN_USAGE }), + value: marginUsageDiffOutput, + }, + ], + receiptItems: [ + { + key: 'margin', + label: stringGetter({ key: STRING_KEYS.POSITION_MARGIN }), + value: positionMarginDiffOutput, + }, + { + key: 'leverage', + label: stringGetter({ key: STRING_KEYS.POSITION_LEVERAGE }), + value: leverageDiffOutput, + }, + ], + } + : { + formLabel: stringGetter({ key: STRING_KEYS.REMOVING }), + buttonLabel: stringGetter({ key: STRING_KEYS.REMOVE_MARGIN }), + inputReceiptItems: [ + { + key: 'margin', + label: stringGetter({ key: STRING_KEYS.POSITION_MARGIN }), + value: positionMarginDiffOutput, + }, + { + key: 'leverage', + label: stringGetter({ key: STRING_KEYS.POSITION_LEVERAGE }), + value: leverageDiffOutput, + }, + ], + receiptItems: [ + { + key: 'cross-free-collateral', + label: stringGetter({ key: STRING_KEYS.CROSS_FREE_COLLATERAL }), + value: freeCollateralDiffOutput, + }, + { + key: 'cross-margin-usage', + label: stringGetter({ key: STRING_KEYS.CROSS_MARGIN_USAGE }), + value: marginUsageDiffOutput, + }, + ], + }; + + const CenterElement = false ? ( + Placeholder Error + ) : ( + <$GradientCard fromColor="neutral" toColor="negative"> + <$Column> + <$TertiarySpan>{stringGetter({ key: STRING_KEYS.ESTIMATED })} + {stringGetter({ key: STRING_KEYS.LIQUIDATION_PRICE })} + +
    + +
    + + ); + + return ( + <$Form + onSubmit={(e: FormEvent) => { + e.preventDefault(); + onSubmit(); + }} + > + + + <$ToggleGroup + items={Object.entries(SIZE_PERCENT_OPTIONS).map(([key, value]) => ({ + label: key, + value: value.toString(), + }))} + value={percent} + onValueChange={setPercent} + shape={ButtonShape.Rectangle} + /> + + + + + + {CenterElement} + + + + + + ); +}; + +const $Form = styled.form` + ${formMixins.transfersForm} +`; +const $ToggleGroup = styled(ToggleGroup)` + ${formMixins.inputToggleGroup} +`; +const $GradientCard = styled(GradientCard)` + ${layoutMixins.spacedRow} + height: 4rem; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + align-items: center; +`; +const $Column = styled.div` + ${layoutMixins.column} + font: var(--font-small-medium); +`; +const $TertiarySpan = styled.span` + color: var(--color-text-0); +`; diff --git a/src/views/forms/AdjustTargetLeverageForm.tsx b/src/views/forms/AdjustTargetLeverageForm.tsx new file mode 100644 index 000000000..d42905843 --- /dev/null +++ b/src/views/forms/AdjustTargetLeverageForm.tsx @@ -0,0 +1,180 @@ +import { FormEvent, useState } from 'react'; + +import { NumberFormatValues } from 'react-number-format'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { TradeInputField } from '@/constants/abacus'; +import { ButtonAction, ButtonShape, ButtonType } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { LEVERAGE_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { breakpoints } from '@/styles'; +import { formMixins } from '@/styles/formMixins'; + +import { Button } from '@/components/Button'; +import { DiffOutput } from '@/components/DiffOutput'; +import { Input, InputType } from '@/components/Input'; +import { OutputType } from '@/components/Output'; +import { Slider } from '@/components/Slider'; +import { ToggleGroup } from '@/components/ToggleGroup'; +import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; +import { WithLabel } from '@/components/WithLabel'; + +import { getSubaccount } from '@/state/accountSelectors'; +import { getInputTradeTargetLeverage } from '@/state/inputsSelectors'; + +import abacusStateManager from '@/lib/abacus'; +import { MustBigNumber } from '@/lib/numbers'; + +export const AdjustTargetLeverageForm = ({ + onSetTargetLeverage, +}: { + onSetTargetLeverage: (value: string) => void; +}) => { + const stringGetter = useStringGetter(); + const { buyingPower } = useSelector(getSubaccount, shallowEqual) ?? {}; + + /** + * @todo: Replace with Abacus functionality + */ + const targetLeverage = useSelector(getInputTradeTargetLeverage); + const [leverage, setLeverage] = useState(targetLeverage?.toString() ?? ''); + const leverageBN = MustBigNumber(leverage); + + return ( + <$Form + onSubmit={(e: FormEvent) => { + e.preventDefault(); + + abacusStateManager.setTradeValue({ + value: leverage, + field: TradeInputField.targetLeverage, + }); + + onSetTargetLeverage?.(leverage); + }} + > + <$InputContainer> + <$WithLabel label={stringGetter({ key: STRING_KEYS.TARGET_LEVERAGE })}> + <$LeverageSlider + min={1} + max={10} + value={MustBigNumber(leverage).abs().toNumber()} + onSliderDrag={([value]: number[]) => setLeverage(value.toString())} + onValueCommit={([value]: number[]) => setLeverage(value.toString())} + /> + + <$InnerInputContainer> + + setLeverage(floatValue?.toString() ?? '') + } + /> + + + + <$ToggleGroup + items={[1, 2, 3, 5, 10].map((leverageAmount: number) => ({ + label: `${leverageAmount}×`, + value: MustBigNumber(leverageAmount).toFixed(LEVERAGE_DECIMALS), + }))} + value={leverageBN.abs().toFixed(LEVERAGE_DECIMALS)} // sign agnostic + onValueChange={(value: string) => setLeverage(value)} + shape={ButtonShape.Rectangle} + /> + + + ), + }, + { + key: 'buying-power', + label: stringGetter({ key: STRING_KEYS.BUYING_POWER }), + value: ( + + ), + }, + ]} + > + + + + ); +}; + +const $Form = styled.form` + ${formMixins.transfersForm} +`; +const $InputContainer = styled.div` + ${formMixins.inputContainer} + --input-height: 3.5rem; + + padding: var(--form-input-paddingY) var(--form-input-paddingX); + + @media ${breakpoints.tablet} { + --input-height: 4rem; + } +`; +const $WithLabel = styled(WithLabel)` + ${formMixins.inputLabel} +`; +const $LeverageSlider = styled(Slider)` + margin-top: 0.25rem; + + --slider-track-background: linear-gradient( + 90deg, + var(--color-layer-7) 0%, + var(--color-text-2) 100% + ); +`; +const $InnerInputContainer = styled.div` + ${formMixins.inputContainer} + --input-backgroundColor: var(--color-layer-5); + --input-borderColor: var(--color-layer-7); + --input-height: 2.25rem; + --input-width: 5rem; + + margin-left: 0.25rem; + + input { + text-align: end; + padding: 0 var(--form-input-paddingX); + } + + @media ${breakpoints.tablet} { + --input-height: 2.5rem; + } +`; +const $LeverageSide = styled.div` + cursor: pointer; +`; +const $ToggleGroup = styled(ToggleGroup)` + ${formMixins.inputToggleGroup} +`; diff --git a/src/views/forms/ClosePositionForm.tsx b/src/views/forms/ClosePositionForm.tsx index 82f95656f..4176f2769 100644 --- a/src/views/forms/ClosePositionForm.tsx +++ b/src/views/forms/ClosePositionForm.tsx @@ -1,26 +1,32 @@ -import { type FormEvent, useCallback, useEffect, useState } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import { useCallback, useEffect, useState, type FormEvent } from 'react'; + import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { ClosePositionInputField, + ErrorType, ValidationError, type HumanReadablePlaceOrderPayload, type Nullable, - ErrorType, } from '@/constants/abacus'; import { AlertType } from '@/constants/alerts'; import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; -import { TOKEN_DECIMALS } from '@/constants/numbers'; import { STRING_KEYS } from '@/constants/localization'; +import { NotificationType } from '@/constants/notifications'; +import { TOKEN_DECIMALS } from '@/constants/numbers'; import { MobilePlaceOrderSteps } from '@/constants/trade'; -import { useBreakpoints, useIsFirstRender, useStringGetter, useSubaccount } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useIsFirstRender } from '@/hooks/useIsFirstRender'; +import { useNotifications } from '@/hooks/useNotifications'; import { useOnLastOrderIndexed } from '@/hooks/useOnLastOrderIndexed'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useSubaccount } from '@/hooks/useSubaccount'; import { breakpoints } from '@/styles'; -import { layoutMixins } from '@/styles/layoutMixins'; import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; import { Button } from '@/components/Button'; @@ -28,24 +34,21 @@ import { FormInput } from '@/components/FormInput'; import { InputType } from '@/components/Input'; import { Tag } from '@/components/Tag'; import { ToggleGroup } from '@/components/ToggleGroup'; - -import { PlaceOrderButtonAndReceipt } from './TradeForm/PlaceOrderButtonAndReceipt'; - -import { Orderbook, orderbookMixins, type OrderbookScrollBehavior } from '@/views/tables/Orderbook'; - import { PositionPreview } from '@/views/forms/TradeForm/PositionPreview'; +import { Orderbook, orderbookMixins, type OrderbookScrollBehavior } from '@/views/tables/Orderbook'; import { getCurrentMarketPositionData } from '@/state/accountSelectors'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; +import { closeDialog } from '@/state/dialogs'; import { getClosePositionInputErrors, getInputClosePositionData } from '@/state/inputsSelectors'; import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors'; -import { closeDialog } from '@/state/dialogs'; -import { getCurrentInput } from '@/state/inputsSelectors'; import abacusStateManager from '@/lib/abacus'; import { MustBigNumber } from '@/lib/numbers'; import { getTradeInputAlert } from '@/lib/tradeData'; +import { PlaceOrderButtonAndReceipt } from './TradeForm/PlaceOrderButtonAndReceipt'; + const MAX_KEY = 'MAX'; // Abacus only takes in these percent options @@ -78,7 +81,6 @@ export const ClosePositionForm = ({ const isFirstRender = useIsFirstRender(); const [closePositionError, setClosePositionError] = useState(undefined); - const [isClosingPosition, setIsClosingPosition] = useState(false); const { closePosition } = useSubaccount(); @@ -88,7 +90,6 @@ export const ClosePositionForm = ({ useSelector(getCurrentMarketConfig, shallowEqual) || {}; const { size: sizeData, summary } = useSelector(getInputClosePositionData, shallowEqual) || {}; const { size, percent } = sizeData || {}; - const currentInput = useSelector(getCurrentInput); const closePositionInputErrors = useSelector(getClosePositionInputErrors, shallowEqual); const currentPositionData = useSelector(getCurrentMarketPositionData, shallowEqual); const { size: currentPositionSize } = currentPositionData || {}; @@ -106,31 +107,34 @@ export const ClosePositionForm = ({ tickSizeDecimals, }); + const { getNotificationPreferenceForType } = useNotifications(); + const isErrorShownInOrderStatusToast = getNotificationPreferenceForType( + NotificationType.OrderStatus + ); + let alertContent; let alertType = AlertType.Error; - if (closePositionError) { + if (closePositionError && !isErrorShownInOrderStatusToast) { alertContent = closePositionError; } else if (inputAlert) { - alertContent = inputAlert?.alertString; - alertType = inputAlert?.type; + alertContent = inputAlert.alertString; + alertType = inputAlert.type; } useEffect(() => { if (currentStep && currentStep !== MobilePlaceOrderSteps.EditOrder) return; - if (currentInput !== 'closePosition') { - abacusStateManager.setClosePositionValue({ - value: market, - field: ClosePositionInputField.market, - }); + abacusStateManager.setClosePositionValue({ + value: market, + field: ClosePositionInputField.market, + }); - abacusStateManager.setClosePositionValue({ - value: SIZE_PERCENT_OPTIONS[MAX_KEY], - field: ClosePositionInputField.percent, - }); - } - }, [currentInput, market, currentStep]); + abacusStateManager.setClosePositionValue({ + value: SIZE_PERCENT_OPTIONS[MAX_KEY], + field: ClosePositionInputField.percent, + }); + }, [market, currentStep]); const onLastOrderIndexed = useCallback(() => { if (!isFirstRender) { @@ -140,8 +144,6 @@ export const ClosePositionForm = ({ if (currentStep === MobilePlaceOrderSteps.PlacingOrder) { setCurrentStep?.(MobilePlaceOrderSteps.Confirmation); } - - setIsClosingPosition(false); } }, [currentStep, isFirstRender]); @@ -169,7 +171,7 @@ export const ClosePositionForm = ({ }); }; - const onSubmit = async (e: FormEvent) => { + const onSubmit = (e: FormEvent) => { e.preventDefault(); switch (currentStep) { @@ -178,6 +180,7 @@ export const ClosePositionForm = ({ break; } case MobilePlaceOrderSteps.PlacingOrder: + case MobilePlaceOrderSteps.PlaceOrderFailed: case MobilePlaceOrderSteps.Confirmation: { dispatch(closeDialog()); break; @@ -191,16 +194,15 @@ export const ClosePositionForm = ({ } }; - const onClosePosition = async () => { + const onClosePosition = () => { setClosePositionError(undefined); - setIsClosingPosition(true); - await closePosition({ + closePosition({ onError: (errorParams?: { errorStringKey?: Nullable }) => { setClosePositionError( stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG }) ); - setIsClosingPosition(false); + setCurrentStep?.(MobilePlaceOrderSteps.PlaceOrderFailed); }, onSuccess: (placeOrderPayload: Nullable) => { setUnIndexedClientId(placeOrderPayload?.clientId); @@ -211,8 +213,8 @@ export const ClosePositionForm = ({ const alertMessage = alertContent && {alertContent}; const inputs = ( - - + <$FormInput id="close-position-amount" label={ <> @@ -227,7 +229,7 @@ export const ClosePositionForm = ({ max={currentSize !== null ? currentSizeBN.toNumber() : undefined} /> - ({ label: key === MAX_KEY ? stringGetter({ key: STRING_KEYS.FULL_CLOSE }) : key, value: value.toString(), @@ -238,34 +240,34 @@ export const ClosePositionForm = ({ /> {alertMessage} - + ); return ( - + <$ClosePositionForm onSubmit={onSubmit} className={className}> {!isTablet ? ( inputs ) : currentStep && currentStep !== MobilePlaceOrderSteps.EditOrder ? ( - + <$PreviewAndConfirmContent> {alertMessage} - + ) : ( - - - - + <$MobileLayout> + <$OrderbookScrollArea scrollBehavior="snapToCenterUnlessHovered"> + <$Orderbook hideHeader /> + - + <$Right> {inputs} - - + + )} - + <$Footer> {size != null && ( - + <$ButtonRow> - + )} - - + + ); }; - -const Styled: Record = {}; - -Styled.ClosePositionForm = styled.form` +const $ClosePositionForm = styled.form` --form-rowGap: 1.25rem; ${layoutMixins.expandingColumnWithFooter} @@ -330,12 +328,12 @@ Styled.ClosePositionForm = styled.form` } `; -Styled.PreviewAndConfirmContent = styled.div` +const $PreviewAndConfirmContent = styled.div` ${layoutMixins.flexColumn} gap: var(--form-input-gap); `; -Styled.MobileLayout = styled.div` +const $MobileLayout = styled.div` height: 0; // Apply dialog's top/left/right padding to inner scroll areas min-height: calc(100% + var(--dialog-content-paddingTop) + var(--dialog-content-paddingBottom)); @@ -347,7 +345,7 @@ Styled.MobileLayout = styled.div` gap: var(--form-input-gap); `; -Styled.OrderbookScrollArea = styled.div<{ +const $OrderbookScrollArea = styled.div<{ scrollBehavior: OrderbookScrollBehavior; }>` ${layoutMixins.stickyLeft} @@ -372,12 +370,12 @@ Styled.OrderbookScrollArea = styled.div<{ padding-bottom: var(--form-rowGap); `; -Styled.Orderbook = styled(Orderbook)` +const $Orderbook = styled(Orderbook)` min-height: 100%; --tableCell-padding: 0.5em 1em; `; -Styled.Right = styled.div` +const $Right = styled.div` height: 0; min-height: 100%; ${layoutMixins.scrollArea} @@ -390,11 +388,11 @@ Styled.Right = styled.div` gap: 1rem; `; -Styled.FormInput = styled(FormInput)` +const $FormInput = styled(FormInput)` width: 100%; `; -Styled.ToggleGroup = styled(ToggleGroup)` +const $ToggleGroup = styled(ToggleGroup)` ${formMixins.inputToggleGroup} @media ${breakpoints.mobile} { @@ -404,7 +402,7 @@ Styled.ToggleGroup = styled(ToggleGroup)` } `; -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${layoutMixins.stickyFooter} backdrop-filter: none; @@ -413,12 +411,12 @@ Styled.Footer = styled.footer` margin-top: auto; `; -Styled.ButtonRow = styled.div` +const $ButtonRow = styled.div` ${layoutMixins.row} justify-self: end; padding: 0.5rem 0 0.5rem 0; `; -Styled.InputsColumn = styled.div` +const $InputsColumn = styled.div` ${formMixins.inputsColumn} `; diff --git a/src/views/forms/NewMarketForm/NewMarketPreviewStep.tsx b/src/views/forms/NewMarketForm/NewMarketPreviewStep.tsx index e57b31e6a..ead001ff5 100644 --- a/src/views/forms/NewMarketForm/NewMarketPreviewStep.tsx +++ b/src/views/forms/NewMarketForm/NewMarketPreviewStep.tsx @@ -1,9 +1,11 @@ -import { FormEvent, useCallback, useMemo, useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; -import { useDispatch } from 'react-redux'; -import Long from 'long'; -import { encodeJson } from '@dydxprotocol/v4-client-js'; +import { FormEvent, useMemo, useState } from 'react'; + import type { IndexedTx } from '@cosmjs/stargate'; +import { encodeJson } from '@dydxprotocol/v4-client-js'; +import { PerpetualMarketType } from '@dydxprotocol/v4-client-js/build/node_modules/@dydxprotocol/v4-proto/src/codegen/dydxprotocol/perpetuals/perpetual'; +import Long from 'long'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { AlertType } from '@/constants/alerts'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; @@ -11,17 +13,15 @@ import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { isMainnet } from '@/constants/networks'; import { NumberSign, TOKEN_DECIMALS } from '@/constants/numbers'; -import type { PotentialMarketItem } from '@/constants/potentialMarkets'; - -import { - useAccountBalance, - useGovernanceVariables, - useStringGetter, - useSubaccount, - useTokenConfigs, - useURLConfigs, -} from '@/hooks'; +import type { NewMarketProposal } from '@/constants/potentialMarkets'; + +import { useAccountBalance } from '@/hooks/useAccountBalance'; +import { useGovernanceVariables } from '@/hooks/useGovernanceVariables'; import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useSubaccount } from '@/hooks/useSubaccount'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; import { formMixins } from '@/styles/formMixins'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -41,9 +41,10 @@ import { openDialog } from '@/state/dialogs'; import { MustBigNumber } from '@/lib/numbers'; import { log } from '@/lib/telemetry'; +import { testFlags } from '@/lib/testFlags'; type NewMarketPreviewStepProps = { - assetData: PotentialMarketItem; + assetData: NewMarketProposal; clobPairId: number; liquidityTier: number; onBack: () => void; @@ -64,7 +65,7 @@ export const NewMarketPreviewStep = ({ const stringGetter = useStringGetter(); const { chainTokenDecimals, chainTokenLabel } = useTokenConfigs(); const [errorMessage, setErrorMessage] = useState(); - const { exchangeConfigs, liquidityTiers } = usePotentialMarkets(); + const { liquidityTiers } = usePotentialMarkets(); const { submitNewMarketProposal } = useSubaccount(); const { newMarketProposal } = useGovernanceVariables(); const { newMarketProposalLearnMore } = useURLConfigs(); @@ -74,11 +75,24 @@ export const NewMarketPreviewStep = ({ const initialDepositAmountDecimals = isMainnet ? 0 : chainTokenDecimals; const initialDepositAmount = initialDepositAmountBN.toFixed(initialDepositAmountDecimals); const [hasAcceptedTerms, setHasAcceptedTerms] = useState(false); + const [isLoading, setIsLoading] = useState(false); const { label, initialMarginFraction, maintenanceMarginFraction, impactNotional } = liquidityTiers[liquidityTier as unknown as keyof typeof liquidityTiers]; - const ticker = `${assetData.baseAsset}-USD`; + const { params, meta } = assetData ?? {}; + const { + ticker, + priceExponent, + marketType, + minExchanges, + minPriceChange, + exchangeConfigJson, + atomicResolution, + quantumConversionExponent, + stepBaseQuantums, + subticksPerTick, + } = params ?? {}; const alertMessage = useMemo(() => { if (errorMessage) { @@ -106,7 +120,7 @@ export const NewMarketPreviewStep = ({ const isDisabled = alertMessage !== null; return ( - ) => { e.preventDefault(); @@ -120,23 +134,30 @@ export const NewMarketPreviewStep = ({ }) ); } else { + setIsLoading(true); setErrorMessage(undefined); try { const tx = await submitNewMarketProposal({ id: clobPairId, ticker, - priceExponent: assetData.priceExponent, - minPriceChange: assetData.minPriceChangePpm, - minExchanges: assetData.minExchanges, + priceExponent, + minPriceChange, + minExchanges, exchangeConfigJson: JSON.stringify({ - exchanges: exchangeConfigs?.[assetData.baseAsset], + exchanges: exchangeConfigJson, }), - atomicResolution: assetData.atomicResolution, - liquidityTier: liquidityTier, - quantumConversionExponent: assetData.quantumConversionExponent, - stepBaseQuantums: Long.fromNumber(assetData.stepBaseQuantum), - subticksPerTick: assetData.subticksPerTick, + atomicResolution, + liquidityTier, + quantumConversionExponent, + // @ts-ignore - marketType is not required until v5 + marketType: testFlags.withNewMarketType + ? marketType === 'PERPETUAL_MARKET_TYPE_ISOLATED' + ? PerpetualMarketType.PERPETUAL_MARKET_TYPE_ISOLATED + : PerpetualMarketType.PERPETUAL_MARKET_TYPE_CROSS + : undefined, + stepBaseQuantums: Long.fromNumber(stepBaseQuantums), + subticksPerTick, delayBlocks: newMarketProposal.delayBlocks, }); @@ -156,29 +177,31 @@ export const NewMarketPreviewStep = ({ } catch (error) { log('NewMarketPreviewForm/submitNewMarketProposal', error); setErrorMessage(error.message); + } finally { + setIsLoading(false); } } }} >

    {stringGetter({ key: STRING_KEYS.CONFIRM_NEW_MARKET_PROPOSAL })} - + <$Balance> {stringGetter({ key: STRING_KEYS.BALANCE })}:{' '} {chainTokenLabel}} + slotRight={<$Tag>{chainTokenLabel}} /> - +

    - - - - + - ), @@ -234,7 +257,7 @@ export const NewMarketPreviewStep = ({ key: 'message-details', label: stringGetter({ key: STRING_KEYS.MESSAGE_DETAILS }), value: ( - @@ -247,7 +270,7 @@ export const NewMarketPreviewStep = ({ } > {stringGetter({ key: STRING_KEYS.VIEW_DETAILS })} → - + ), }, { @@ -265,7 +288,7 @@ export const NewMarketPreviewStep = ({ slotRight={ <> {'+ '} -
    - + {alertMessage && ( {alertMessage.message} )} - + <$ButtonRow> - - - - {stringGetter({ - key: STRING_KEYS.PROPOSAL_DISCLAIMER_1, - params: { - NUM_TOKENS_REQUIRED: initialDepositAmount, - NATIVE_TOKEN_DENOM: chainTokenLabel, - HERE: ( - - {stringGetter({ key: STRING_KEYS.HERE })} - - ), - }, - })} - - + + <$DisclaimerContainer> + <$Disclaimer> + {stringGetter({ + key: STRING_KEYS.PROPOSAL_DISCLAIMER_1, + params: { + NUM_TOKENS_REQUIRED: initialDepositAmount, + NATIVE_TOKEN_DENOM: chainTokenLabel, + HERE: ( + <$Link href={newMarketProposalLearnMore}> + {stringGetter({ key: STRING_KEYS.HERE })} + + ), + }, + })} + + + ); }; - -const Styled: Record = {}; - -Styled.Form = styled.form` +const $Form = styled.form` ${formMixins.transfersForm} ${layoutMixins.stickyArea0} --stickyArea0-background: transparent; @@ -346,7 +372,7 @@ Styled.Form = styled.form` } `; -Styled.Balance = styled.span` +const $Balance = styled.span` ${layoutMixins.inlineRow} font: var(--font-small-book); margin-top: 0.125rem; @@ -356,35 +382,40 @@ Styled.Balance = styled.span` } `; -Styled.Tag = styled(Tag)` +const $Tag = styled(Tag)` margin-left: 0.5ch; `; -Styled.FormInput = styled(FormInput)` +const $FormInput = styled(FormInput)` input { font-size: 1rem; } `; -Styled.Icon = styled(Icon)<{ $hasError?: boolean }>` +const $Icon = styled(Icon)<{ $hasError?: boolean }>` margin-left: 0.5ch; ${({ $hasError }) => ($hasError ? 'color: var(--color-error);' : 'color: var(--color-success);')} `; -Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` +const $WithDetailsReceipt = styled(WithDetailsReceipt)` --details-item-fontSize: 1rem; `; -Styled.CheckboxContainer = styled.div` +const $CheckboxContainer = styled.div` display: flex; flex-direction: row; padding: 1rem; align-items: center; `; -Styled.Disclaimer = styled.div<{ textAlign?: string }>` - font: var(--font-small); +const $DisclaimerContainer = styled.div` + min-width: 100%; + width: min-content; +`; + +const $Disclaimer = styled.div<{ textAlign?: string }>` + font: var(--font-small-book); color: var(--color-text-0); text-align: center; margin-left: 0.5ch; @@ -392,18 +423,19 @@ Styled.Disclaimer = styled.div<{ textAlign?: string }>` ${({ textAlign }) => textAlign && `text-align: ${textAlign};`} `; -Styled.ButtonRow = styled.div` +const $ButtonRow = styled.div` display: grid; grid-template-columns: 1fr 2fr; gap: 1rem; width: 100%; `; -Styled.Button = styled(Button)` +const $Button = styled(Button)` --button-padding: 0; + --button-height: auto; `; -Styled.Link = styled(Link)` +const $Link = styled(Link)` --link-color: var(--color-accent); display: inline; `; diff --git a/src/views/forms/NewMarketForm/NewMarketSelectionStep.tsx b/src/views/forms/NewMarketForm/NewMarketSelectionStep.tsx index c3ffda654..95783743b 100644 --- a/src/views/forms/NewMarketForm/NewMarketSelectionStep.tsx +++ b/src/views/forms/NewMarketForm/NewMarketSelectionStep.tsx @@ -1,29 +1,27 @@ import { FormEvent, useEffect, useMemo, useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; -import { Root, Item } from '@radix-ui/react-radio-group'; + +import { Item, Root } from '@radix-ui/react-radio-group'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { OnboardingState } from '@/constants/account'; import { AlertType } from '@/constants/alerts'; -import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; +import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; -import { isMainnet } from '@/constants/networks'; +import { isDev, isMainnet } from '@/constants/networks'; import { TOKEN_DECIMALS } from '@/constants/numbers'; - import { NUM_ORACLES_TO_QUALIFY_AS_SAFE, - type PotentialMarketItem, + type NewMarketProposal, } from '@/constants/potentialMarkets'; -import { - useAccountBalance, - useBreakpoints, - useGovernanceVariables, - useStringGetter, - useTokenConfigs, -} from '@/hooks'; +import { useAccountBalance } from '@/hooks/useAccountBalance'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useGovernanceVariables } from '@/hooks/useGovernanceVariables'; import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; import { breakpoints } from '@/styles'; import { formMixins } from '@/styles/formMixins'; @@ -32,12 +30,10 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; import { Button } from '@/components/Button'; import { Details } from '@/components/Details'; -import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; -import { Tag } from '@/components/Tag'; import { SearchSelectMenu } from '@/components/SearchSelectMenu'; +import { Tag } from '@/components/Tag'; import { WithReceipt } from '@/components/WithReceipt'; - import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { getOnboardingState } from '@/state/accountSelectors'; @@ -48,13 +44,14 @@ import { isTruthy } from '@/lib/isTruthy'; import { MustBigNumber } from '@/lib/numbers'; type NewMarketSelectionStepProps = { - assetToAdd?: PotentialMarketItem; + assetToAdd?: NewMarketProposal; clobPairId?: number; - setAssetToAdd: (assetToAdd?: PotentialMarketItem) => void; + setAssetToAdd: (assetToAdd?: NewMarketProposal) => void; onConfirmMarket: () => void; liquidityTier?: number; setLiquidityTier: (liquidityTier?: number) => void; tickSizeDecimals: number; + tickersFromProposals: Set; }; export const NewMarketSelectionStep = ({ @@ -65,6 +62,7 @@ export const NewMarketSelectionStep = ({ liquidityTier, setLiquidityTier, tickSizeDecimals, + tickersFromProposals, }: NewMarketSelectionStepProps) => { const dispatch = useDispatch(); const { nativeTokenBalance } = useAccountBalance(); @@ -73,7 +71,7 @@ export const NewMarketSelectionStep = ({ const { isMobile } = useBreakpoints(); const marketIds = useSelector(getMarketIds, shallowEqual); const { chainTokenDecimals, chainTokenLabel } = useTokenConfigs(); - const { potentialMarkets, exchangeConfigs, liquidityTiers } = usePotentialMarkets(); + const { potentialMarkets, liquidityTiers } = usePotentialMarkets(); const stringGetter = useStringGetter(); const { newMarketProposal } = useGovernanceVariables(); const initialDepositAmountBN = MustBigNumber(newMarketProposal.initialDepositAmount).div( @@ -82,8 +80,7 @@ export const NewMarketSelectionStep = ({ const initialDepositAmountDecimals = isMainnet ? 0 : chainTokenDecimals; const initialDepositAmount = initialDepositAmountBN.toFixed(initialDepositAmountDecimals); - const [tempLiquidityTier, setTempLiquidityTier] = useState(); - const [canModifyLiqTier, setCanModifyLiqTier] = useState(false); + const [tempLiquidityTier, setTempLiquidityTier] = useState(); const alertMessage = useMemo(() => { if (nativeTokenBalance.lt(initialDepositAmountBN)) { @@ -104,43 +101,50 @@ export const NewMarketSelectionStep = ({ useEffect(() => { if (assetToAdd) { - setTempLiquidityTier(assetToAdd.liquidityTier); - setLiquidityTier(assetToAdd.liquidityTier); + setTempLiquidityTier('' + assetToAdd.params.liquidityTier); + setLiquidityTier(assetToAdd.params.liquidityTier); } }, [assetToAdd]); const filteredPotentialMarkets = useMemo(() => { return potentialMarkets?.filter( - ({ baseAsset, numOracles }) => - exchangeConfigs?.[baseAsset] !== undefined && - Number(numOracles) >= NUM_ORACLES_TO_QUALIFY_AS_SAFE && - !marketIds.includes(`${baseAsset}-USD`) + ({ params: { ticker, exchangeConfigJson, marketType }, meta }) => { + if (marketIds.includes(ticker)) { + return false; + } + + // Disable Isolated markets if the user is not on Staging or Local deployment + if (marketType === 'PERPETUAL_MARKET_TYPE_ISOLATED') { + return isDev && exchangeConfigJson.length > 0; + } + + if (exchangeConfigJson.length >= NUM_ORACLES_TO_QUALIFY_AS_SAFE) { + return true; + } + + return false; + } ); - }, [exchangeConfigs, potentialMarkets, marketIds]); + }, [potentialMarkets, marketIds]); return ( - ) => { e.preventDefault(); - if (canModifyLiqTier) { - setLiquidityTier(tempLiquidityTier); - setCanModifyLiqTier(false); - } else { - onConfirmMarket(); - } + onConfirmMarket(); }} >

    {stringGetter({ key: STRING_KEYS.ADD_A_MARKET })} - + <$Balance> {stringGetter({ key: STRING_KEYS.BALANCE })}:{' '} {chainTokenLabel}} + slotRight={<$Tag>{chainTokenLabel}} /> - +

    ({ + filteredPotentialMarkets?.map((potentialMarket: NewMarketProposal) => ({ value: potentialMarket.baseAsset, - label: potentialMarket?.assetName ?? potentialMarket.baseAsset, - tag: `${potentialMarket.baseAsset}-USD`, + label: potentialMarket.meta.assetName, + tag: potentialMarket.params.ticker, + slotAfter: tickersFromProposals.has(potentialMarket.params.ticker) && ( + {stringGetter({ key: STRING_KEYS.VOTING_LIVE })} + ), onSelect: () => { setAssetToAdd(potentialMarket); }, @@ -161,9 +168,9 @@ export const NewMarketSelectionStep = ({ label={stringGetter({ key: STRING_KEYS.MARKETS })} > {assetToAdd ? ( - - {assetToAdd?.assetName ?? assetToAdd.baseAsset} {assetToAdd?.baseAsset}-USD - + <$SelectedAsset> + {assetToAdd.meta.assetName} {assetToAdd.params.ticker} + ) : ( `${stringGetter({ key: STRING_KEYS.EG })} "BTC-USD"` )} @@ -172,62 +179,28 @@ export const NewMarketSelectionStep = ({ <>
    {stringGetter({ key: STRING_KEYS.POPULATED_DETAILS })}
    - - - {stringGetter({ key: STRING_KEYS.LIQUIDITY_TIER })} - - - {canModifyLiqTier && ( - - )} - - + <$Root value={tempLiquidityTier} onValueChange={setTempLiquidityTier}> + <$Header>{stringGetter({ key: STRING_KEYS.LIQUIDITY_TIER })} {Object.keys(liquidityTiers).map((tier) => { const { maintenanceMarginFraction, impactNotional, label, initialMarginFraction } = liquidityTiers[tier as unknown as keyof typeof liquidityTiers]; return ( - - + <$Header style={{ marginLeft: '1rem' }}> {label} - {Number(tier) === assetToAdd?.liquidityTier && ( + {Number(tier) === assetToAdd?.params.liquidityTier && ( ✨ {stringGetter({ key: STRING_KEYS.RECOMMENDED })} )} - - + <$Details layout={isMobile ? 'grid' : 'rowColumns'} withSeparators={!isMobile} items={[ @@ -264,10 +237,10 @@ export const NewMarketSelectionStep = ({ }, ]} /> - + ); })} - +
    )} @@ -276,7 +249,7 @@ export const NewMarketSelectionStep = ({ )} ), @@ -294,7 +267,7 @@ export const NewMarketSelectionStep = ({ key: 'message-details', label: stringGetter({ key: STRING_KEYS.MESSAGE_DETAILS }), value: ( - @@ -307,7 +280,7 @@ export const NewMarketSelectionStep = ({ } > {stringGetter({ key: STRING_KEYS.VIEW_DETAILS })} → - + ), }, { @@ -319,12 +292,12 @@ export const NewMarketSelectionStep = ({ ), value: ( - + <$Disclaimer> {stringGetter({ key: STRING_KEYS.OR_MORE, params: { NUMBER: ( - + ), }, ].filter(isTruthy)} @@ -348,19 +321,14 @@ export const NewMarketSelectionStep = ({ state={{ isDisabled: !assetToAdd || !liquidityTier === undefined || !clobPairId }} action={ButtonAction.Primary} > - {canModifyLiqTier - ? stringGetter({ key: STRING_KEYS.SAVE }) - : stringGetter({ key: STRING_KEYS.PREVIEW_MARKET_PROPOSAL })} + {stringGetter({ key: STRING_KEYS.PREVIEW_MARKET_PROPOSAL })} )} -
    + ); }; - -const Styled: Record = {}; - -Styled.Form = styled.form` +const $Form = styled.form` ${formMixins.transfersForm} ${layoutMixins.stickyArea0} --stickyArea0-background: transparent; @@ -374,7 +342,7 @@ Styled.Form = styled.form` } `; -Styled.Balance = styled.span` +const $Balance = styled.span` ${layoutMixins.inlineRow} font: var(--font-small-book); margin-top: 0.125rem; @@ -384,20 +352,20 @@ Styled.Balance = styled.span` } `; -Styled.Tag = styled(Tag)` +const $Tag = styled(Tag)` margin-left: 0.5ch; `; -Styled.SelectedAsset = styled.span` +const $SelectedAsset = styled.span` color: var(--color-text-2); `; -Styled.Disclaimer = styled.div` +const $Disclaimer = styled.div` color: var(--color-text-0); margin-left: 0.5ch; `; -Styled.Header = styled.div` +const $Header = styled.div` display: flex; flex: 1; align-items: center; @@ -406,17 +374,7 @@ Styled.Header = styled.div` justify-content: space-between; `; -Styled.ButtonRow = styled.div` - display: flex; - flex-direction: row; - gap: 0.5rem; - - button { - min-width: 80px; - } -`; - -Styled.Root = styled(Root)` +const $Root = styled(Root)` display: flex; flex-direction: column; gap: 1rem; @@ -424,9 +382,9 @@ Styled.Root = styled(Root)` border-radius: 10px; border: 1px solid var(--color-layer-6); background-color: var(--color-layer-4); -`; +` as typeof Root; -Styled.LiquidityTierRadioButton = styled(Item)<{ selected?: boolean }>` +const $LiquidityTierRadioButton = styled(Item)<{ selected?: boolean }>` display: flex; flex-direction: column; border-radius: 0.625rem; @@ -434,10 +392,14 @@ Styled.LiquidityTierRadioButton = styled(Item)<{ selected?: boolean }>` padding: 1rem 0; font: var(--font-mini-book); + &:disabled { + cursor: default; + } + ${({ selected }) => selected && 'background-color: var(--color-layer-2)'} `; -Styled.Details = styled(Details)` +const $Details = styled(Details)` margin-top: 0.5rem; padding: 0; @@ -454,14 +416,15 @@ Styled.Details = styled(Details)` } `; -Styled.ReceiptDetails = styled(Details)` +const $ReceiptDetails = styled(Details)` padding: 0.375rem 0.75rem 0.25rem; `; -Styled.Output = styled(Output)` +const $Output = styled(Output)` display: inline-block; `; -Styled.Button = styled(Button)` +const $Button = styled(Button)` --button-padding: 0; + --button-height: auto; `; diff --git a/src/views/forms/NewMarketForm/NewMarketSuccessStep.tsx b/src/views/forms/NewMarketForm/NewMarketSuccessStep.tsx index 32fda4384..83314f958 100644 --- a/src/views/forms/NewMarketForm/NewMarketSuccessStep.tsx +++ b/src/views/forms/NewMarketForm/NewMarketSuccessStep.tsx @@ -1,8 +1,10 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { ButtonAction, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { LinkOutIcon } from '@/icons'; import { Button } from '@/components/Button'; @@ -16,25 +18,22 @@ export const NewMarketSuccessStep = ({ href }: NewMarketSuccessStepProps) => { const stringGetter = useStringGetter(); return ( - - - + <$ProposalSent> + <$OuterCircle> + <$InnerCircle> - - + +

    {stringGetter({ key: STRING_KEYS.SUBMITTED_PROPOSAL })}

    {stringGetter({ key: STRING_KEYS.PROPOSAL_SUBMISSION_SUCCESSFUL })} -
    + ); }; - -const Styled: Record = {}; - -Styled.ProposalSent = styled.div` +const $ProposalSent = styled.div` text-align: center; display: flex; flex-direction: column; @@ -50,7 +49,7 @@ Styled.ProposalSent = styled.div` } `; -Styled.OuterCircle = styled.div` +const $OuterCircle = styled.div` width: 5.25rem; height: 5.25rem; min-width: 5.25rem; @@ -63,7 +62,7 @@ Styled.OuterCircle = styled.div` justify-content: center; `; -Styled.InnerCircle = styled.div` +const $InnerCircle = styled.div` width: 2rem; height: 2rem; min-width: 2rem; diff --git a/src/views/forms/NewMarketForm/index.tsx b/src/views/forms/NewMarketForm/index.tsx index 85604ce2a..918e43e7f 100644 --- a/src/views/forms/NewMarketForm/index.tsx +++ b/src/views/forms/NewMarketForm/index.tsx @@ -1,15 +1,18 @@ import { useMemo, useState } from 'react'; -import styled, { AnyStyledComponent } from 'styled-components'; + +import styled from 'styled-components'; import { TOKEN_DECIMALS } from '@/constants/numbers'; -import { type PotentialMarketItem } from '@/constants/potentialMarkets'; -import { useNextClobPairId, useURLConfigs } from '@/hooks'; +import { type NewMarketProposal } from '@/constants/potentialMarkets'; + +import { useNextClobPairId } from '@/hooks/useNextClobPairId'; import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; -import { NewMarketSelectionStep } from './NewMarketSelectionStep'; import { NewMarketPreviewStep } from './NewMarketPreviewStep'; +import { NewMarketSelectionStep } from './NewMarketSelectionStep'; import { NewMarketSuccessStep } from './NewMarketSuccessStep'; enum NewMarketFormStep { @@ -20,22 +23,22 @@ enum NewMarketFormStep { export const NewMarketForm = () => { const [step, setStep] = useState(NewMarketFormStep.SELECTION); - const [assetToAdd, setAssetToAdd] = useState(); + const [assetToAdd, setAssetToAdd] = useState(); const [liquidityTier, setLiquidityTier] = useState(); const [proposalTxHash, setProposalTxHash] = useState(); const { mintscan: mintscanTxUrl } = useURLConfigs(); - const { nextAvailableClobPairId } = useNextClobPairId(); + const { nextAvailableClobPairId, tickersFromProposals } = useNextClobPairId(); const { hasPotentialMarketsData } = usePotentialMarkets(); const tickSizeDecimals = useMemo(() => { if (!assetToAdd) return TOKEN_DECIMALS; - const p = Math.floor(Math.log(Number(assetToAdd.referencePrice))); + const p = Math.floor(Math.log(Number(assetToAdd.meta.referencePrice))); return Math.abs(p - 3); }, [assetToAdd]); if (!hasPotentialMarketsData || !nextAvailableClobPairId) { - return ; + return <$LoadingSpace id="new-market-form" />; } if (NewMarketFormStep.SUCCESS === step && proposalTxHash) { @@ -69,12 +72,10 @@ export const NewMarketForm = () => { liquidityTier={liquidityTier} setLiquidityTier={setLiquidityTier} tickSizeDecimals={tickSizeDecimals} + tickersFromProposals={tickersFromProposals} /> ); }; - -const Styled: Record = {}; - -Styled.LoadingSpace = styled(LoadingSpace)` +const $LoadingSpace = styled(LoadingSpace)` min-height: 18.75rem; `; diff --git a/src/views/forms/NobleDeposit.tsx b/src/views/forms/NobleDeposit.tsx index f62e2f7d9..f1de9a3ac 100644 --- a/src/views/forms/NobleDeposit.tsx +++ b/src/views/forms/NobleDeposit.tsx @@ -1,15 +1,18 @@ import { useState } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; -import { OpacityToken } from '@/constants/styles/base'; +import styled, { css } from 'styled-components'; + import { STRING_KEYS } from '@/constants/localization'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { OpacityToken } from '@/constants/styles/base'; + +import { useAccounts } from '@/hooks/useAccounts'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { useAccounts, useStringGetter } from '@/hooks'; +import { layoutMixins } from '@/styles/layoutMixins'; +import { Checkbox } from '@/components/Checkbox'; import { CopyButton } from '@/components/CopyButton'; import { QrCode } from '@/components/QrCode'; -import { Checkbox } from '@/components/Checkbox'; import { TimeoutButton } from '@/components/TimeoutButton'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; import { WithReceipt } from '@/components/WithReceipt'; @@ -34,10 +37,11 @@ export const NobleDeposit = () => { hasAcknowledged && hasTimedout ? nobleAddress : stringGetter({ key: STRING_KEYS.ACKNOWLEDGE_TO_REVEAL }), + allowUserSelection: true, }, ]} > - { /> - + <$CheckboxContainer> { key: STRING_KEYS.NOBLE_ACKNOWLEDGEMENT, })} /> - + } > { } /> - + ); }; - -const Styled: Record = {}; - -Styled.WaitingSpan = styled.span` +const $WaitingSpan = styled.span` ${layoutMixins.row} gap: 1rem; color: var(--color-text-1); `; -Styled.WithReceipt = styled(WithReceipt)` +const $WithReceipt = styled(WithReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); `; -Styled.QrCode = styled(QrCode)<{ blurred: boolean }>` +const $QrCode = styled(QrCode)<{ blurred: boolean }>` border-radius: 0.5em; ${({ blurred }) => @@ -95,12 +96,12 @@ Styled.QrCode = styled(QrCode)<{ blurred: boolean }>` `} `; -Styled.CheckboxContainer = styled.div` +const $CheckboxContainer = styled.div` padding: 1rem; color: var(--color-text-0); `; -Styled.CautionIconContainer = styled.div` +const $CautionIconContainer = styled.div` ${layoutMixins.stack} min-width: 2.5rem; height: 2.5rem; diff --git a/src/views/forms/SelectMarginModeForm.tsx b/src/views/forms/SelectMarginModeForm.tsx new file mode 100644 index 000000000..8149fab5c --- /dev/null +++ b/src/views/forms/SelectMarginModeForm.tsx @@ -0,0 +1,79 @@ +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { AbacusMarginMode, MARGIN_MODE_STRINGS, TradeInputField } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { formMixins } from '@/styles/formMixins'; + +import { Button } from '@/components/Button'; +import { RadioButtonCards } from '@/components/RadioButtonCards'; + +import { getInputTradeMarginMode } from '@/state/inputsSelectors'; + +import abacusStateManager from '@/lib/abacus'; + +export const SelectMarginModeForm = ({ + onChangeMarginMode, +}: { + onChangeMarginMode?: () => void; +}) => { + const marginMode = useSelector(getInputTradeMarginMode, shallowEqual); + const marginModeValue = marginMode?.rawValue; + + const stringGetter = useStringGetter(); + + const setMarginMode = (value: string) => { + abacusStateManager.setTradeValue({ + value, + field: TradeInputField.marginMode, + }); + onChangeMarginMode?.(); + }; + + return ( + <$Form> + <$RadioButtonCards + value={marginModeValue} + onValueChange={setMarginMode} + radioItems={[ + { + value: AbacusMarginMode.cross.rawValue, + label: stringGetter({ key: MARGIN_MODE_STRINGS[AbacusMarginMode.cross.rawValue] }), + body: ( + <$TertiarySpan> + {stringGetter({ key: STRING_KEYS.CROSS_MARGIN_DESCRIPTION })} + + ), + }, + { + value: AbacusMarginMode.isolated.rawValue, + label: stringGetter({ key: MARGIN_MODE_STRINGS[AbacusMarginMode.isolated.rawValue] }), + body: ( + <$TertiarySpan> + {stringGetter({ key: STRING_KEYS.ISOLATED_MARGIN_DESCRIPTION })} + + ), + }, + ]} + /> + + ); +}; + +const $Form = styled.form` + ${formMixins.transfersForm} +`; +const $RadioButtonCards = styled(RadioButtonCards)` + padding: 0; + + --radio-button-cards-item-checked-backgroundColor: var(--color-layer-1); +`; +const $TertiarySpan = styled.span` + color: var(--color-text-0); +`; +const $Button = styled(Button)` + width: 100%; +`; diff --git a/src/views/forms/TradeForm.tsx b/src/views/forms/TradeForm.tsx index 3855f89dd..75c58ef0d 100644 --- a/src/views/forms/TradeForm.tsx +++ b/src/views/forms/TradeForm.tsx @@ -1,65 +1,79 @@ -import { type FormEvent, useState, Ref, useCallback } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import type { NumberFormatValues, SourceInfo } from 'react-number-format'; +import { Ref, useCallback, useState, type FormEvent } from 'react'; import { OrderSide } from '@dydxprotocol/v4-client-js'; - -import { AlertType } from '@/constants/alerts'; +import type { NumberFormatValues, SourceInfo } from 'react-number-format'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { + ComplianceStatus, ErrorType, - type HumanReadablePlaceOrderPayload, - type Nullable, + MARGIN_MODE_STRINGS, TradeInputErrorAction, + TradeInputField, ValidationError, + type HumanReadablePlaceOrderPayload, + type Nullable, } from '@/constants/abacus'; - +import { AlertType } from '@/constants/alerts'; import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; -import { STRING_KEYS } from '@/constants/localization'; +import { DialogTypes, TradeBoxDialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS, StringKey } from '@/constants/localization'; +import { NotificationType } from '@/constants/notifications'; import { USD_DECIMALS } from '@/constants/numbers'; import { InputErrorData, - TradeBoxKeys, MobilePlaceOrderSteps, ORDER_TYPE_STRINGS, + TradeBoxKeys, + TradeTypes, } from '@/constants/trade'; -import { breakpoints } from '@/styles'; -import { useStringGetter, useSubaccount } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useNotifications } from '@/hooks/useNotifications'; import { useOnLastOrderIndexed } from '@/hooks/useOnLastOrderIndexed'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useSubaccount } from '@/hooks/useSubaccount'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { breakpoints } from '@/styles'; import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; +import { AssetIcon } from '@/components/AssetIcon'; import { Button } from '@/components/Button'; import { FormInput } from '@/components/FormInput'; import { Icon, IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; import { InputType } from '@/components/Input'; +import { Output, OutputType } from '@/components/Output'; import { Tag } from '@/components/Tag'; import { ToggleButton } from '@/components/ToggleButton'; +import { ToggleGroup } from '@/components/ToggleGroup'; import { WithTooltip } from '@/components/WithTooltip'; - import { Orderbook } from '@/views/tables/Orderbook'; +import { openDialog, openDialogInTradeBox } from '@/state/dialogs'; import { setTradeFormInputs } from '@/state/inputs'; import { getCurrentInput, getInputTradeData, + getInputTradeOptions, getTradeFormInputs, useTradeFormData, } from '@/state/inputsSelectors'; -import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; +import { getCurrentMarketAssetId, getCurrentMarketConfig } from '@/state/perpetualsSelectors'; import abacusStateManager from '@/lib/abacus'; +import { testFlags } from '@/lib/testFlags'; import { getSelectedOrderSide, getSelectedTradeType, getTradeInputAlert } from '@/lib/tradeData'; import { AdvancedTradeOptions } from './TradeForm/AdvancedTradeOptions'; -import { TradeSizeInputs } from './TradeForm/TradeSizeInputs'; -import { TradeSideToggle } from './TradeForm/TradeSideToggle'; import { PlaceOrderButtonAndReceipt } from './TradeForm/PlaceOrderButtonAndReceipt'; import { PositionPreview } from './TradeForm/PositionPreview'; +import { TradeSideToggle } from './TradeForm/TradeSideToggle'; +import { TradeSizeInputs } from './TradeForm/TradeSizeInputs'; type TradeBoxInputConfig = { key: TradeBoxKeys; @@ -88,13 +102,14 @@ export const TradeForm = ({ onConfirm, className, }: ElementProps & StyleProps) => { - const [isPlacingOrder, setIsPlacingOrder] = useState(false); const [placeOrderError, setPlaceOrderError] = useState(); const [showOrderbook, setShowOrderbook] = useState(false); const dispatch = useDispatch(); const stringGetter = useStringGetter(); const { placeOrder } = useSubaccount(); + const { isTablet } = useBreakpoints(); + const { complianceMessage, complianceStatus } = useComplianceState(); const { price, @@ -114,6 +129,7 @@ export const TradeForm = ({ } = useTradeFormData(); const currentInput = useSelector(getCurrentInput); + const currentAssetId = useSelector(getCurrentMarketAssetId); const { tickSizeDecimals, stepSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {}; @@ -122,11 +138,26 @@ export const TradeForm = ({ const currentTradeData = useSelector(getInputTradeData, shallowEqual); - const { side, type } = currentTradeData || {}; + const { side, type, marginMode, targetLeverage } = currentTradeData || {}; const selectedTradeType = getSelectedTradeType(type); const selectedOrderSide = getSelectedOrderSide(side); + const { typeOptions } = useSelector(getInputTradeOptions, shallowEqual) ?? {}; + + const allTradeTypeItems = (typeOptions?.toArray() ?? []).map(({ type, stringKey }) => ({ + value: type as TradeTypes, + label: stringGetter({ + key: stringKey as StringKey, + }), + slotBefore: , + })); + + const onTradeTypeChange = (tradeType: TradeTypes) => { + abacusStateManager.clearTradeInputValues(); + abacusStateManager.setTradeValue({ value: tradeType, field: TradeInputField.type }); + }; + const needsAdvancedOptions = needsGoodUntil || timeInForceOptions || @@ -157,13 +188,22 @@ export const TradeForm = ({ tickSizeDecimals, }); - if (placeOrderError) { + const { getNotificationPreferenceForType } = useNotifications(); + const isErrorShownInOrderStatusToast = getNotificationPreferenceForType( + NotificationType.OrderStatus + ); + + if (placeOrderError && !isErrorShownInOrderStatusToast) { alertContent = placeOrderError; } else if (inputAlert) { - alertContent = inputAlert?.alertString; - alertType = inputAlert?.type; + alertContent = inputAlert.alertString; + alertType = inputAlert.type; } + const shouldPromptUserToPlaceLimitOrder = ['MARKET_ORDER_ERROR_ORDERBOOK_SLIPPAGE'].some( + (errorCode) => inputAlert?.code === errorCode + ); + const orderSideAction = { [OrderSide.BUY]: ButtonAction.Create, [OrderSide.SELL]: ButtonAction.Destroy, @@ -178,6 +218,7 @@ export const TradeForm = ({ break; } case MobilePlaceOrderSteps.PlacingOrder: + case MobilePlaceOrderSteps.PlaceOrderFailed: case MobilePlaceOrderSteps.Confirmation: { onConfirm?.(); break; @@ -192,9 +233,7 @@ export const TradeForm = ({ }; const onLastOrderIndexed = useCallback(() => { - if (!currentStep || currentStep === MobilePlaceOrderSteps.PlacingOrder) { - setIsPlacingOrder(false); - abacusStateManager.clearTradeInputValues({ shouldResetSize: true }); + if (currentStep === MobilePlaceOrderSteps.PlacingOrder) { setCurrentStep?.(MobilePlaceOrderSteps.Confirmation); } }, [currentStep]); @@ -203,22 +242,22 @@ export const TradeForm = ({ callback: onLastOrderIndexed, }); - const onPlaceOrder = async () => { + const onPlaceOrder = () => { setPlaceOrderError(undefined); - setIsPlacingOrder(true); - await placeOrder({ + placeOrder({ onError: (errorParams?: { errorStringKey?: Nullable }) => { setPlaceOrderError( stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG }) ); - - setIsPlacingOrder(false); + setCurrentStep?.(MobilePlaceOrderSteps.PlaceOrderFailed); }, onSuccess: (placeOrderPayload?: Nullable) => { setUnIndexedClientId(placeOrderPayload?.clientId); }, }); + + abacusStateManager.clearTradeInputValues({ shouldResetSize: true }); }; if (needsTriggerPrice) { @@ -278,7 +317,7 @@ export const TradeForm = ({ } return ( - + <$TradeForm onSubmit={onSubmit} className={className}> {currentStep && currentStep !== MobilePlaceOrderSteps.EditOrder ? ( <> @@ -286,26 +325,67 @@ export const TradeForm = ({ ) : ( <> - - - } - onPressedChange={setShowOrderbook} - isPressed={showOrderbook} - hidePressedStyle - > - {!showOrderbook && stringGetter({ key: STRING_KEYS.ORDERBOOK })} - - {/* TODO[TRCL-1411]: add orderbook scale functionality */} - - - - - - - {showOrderbook && } - - + <$TopActionsRow> + {isTablet && ( + <> + <$OrderbookButtons> + <$OrderbookButton + slotRight={} + onPressedChange={setShowOrderbook} + isPressed={showOrderbook} + > + {!showOrderbook && stringGetter({ key: STRING_KEYS.ORDERBOOK })} + + {/* TODO[TRCL-1411]: add orderbook scale functionality */} + + + <$ToggleGroup + items={allTradeTypeItems} + value={selectedTradeType} + onValueChange={onTradeTypeChange} + /> + + )} + + {!isTablet && ( + <> + {testFlags.isolatedMargin && ( + <$MarginAndLeverageButtons> + + + + + )} + + + )} + + + <$OrderbookAndInputs showOrderbook={showOrderbook}> + {isTablet && showOrderbook && <$Orderbook maxRowsPerSide={5} />} + + <$InputsColumn> {tradeFormInputs.map( ({ key, inputType, label, onChange, validationConfig, value, decimals }) => ( } - {alertContent && {alertContent}} - - + {complianceStatus === ComplianceStatus.CLOSE_ONLY && ( + + <$Message>{complianceMessage} + + )} + + {alertContent && ( + + <$Message> + {alertContent} + {shouldPromptUserToPlaceLimitOrder && ( + <$IconButton + iconName={IconName.Arrow} + shape={ButtonShape.Circle} + action={ButtonAction.Navigation} + size={ButtonSize.XSmall} + onClick={() => onTradeTypeChange(TradeTypes.LIMIT)} + /> + )} + + + )} + + )} - + <$Footer> {isInputFilled && (!currentStep || currentStep === MobilePlaceOrderSteps.EditOrder) && ( - + <$ButtonRow> - + )} - - + + ); }; -const Styled: Record = {}; - -Styled.TradeForm = styled.form` +const $TradeForm = styled.form` /* Params */ --tradeBox-content-paddingTop: ; --tradeBox-content-paddingRight: ; @@ -401,8 +499,16 @@ Styled.TradeForm = styled.form` } } `; +const $MarginAndLeverageButtons = styled.div` + ${layoutMixins.inlineRow} + gap: 0.5rem; + margin-right: 0.5rem; -Styled.TopActionsRow = styled.div` + button { + width: 100%; + } +`; +const $TopActionsRow = styled.div` display: grid; grid-auto-flow: column; @@ -411,8 +517,7 @@ Styled.TopActionsRow = styled.div` gap: var(--form-input-gap); } `; - -Styled.OrderbookButtons = styled.div` +const $OrderbookButtons = styled.div` ${layoutMixins.inlineRow} justify-content: space-between; gap: 0.25rem; @@ -421,8 +526,7 @@ Styled.OrderbookButtons = styled.div` display: none; } `; - -Styled.OrderbookButton = styled(ToggleButton)` +const $OrderbookButton = styled(ToggleButton)` --button-toggle-off-textColor: var(--color-text-1); --button-toggle-off-backgroundColor: transparent; @@ -446,8 +550,7 @@ Styled.OrderbookButton = styled(ToggleButton)` } } `; - -Styled.OrderbookAndInputs = styled.div<{ showOrderbook: boolean }>` +const $OrderbookAndInputs = styled.div<{ showOrderbook: boolean }>` @media ${breakpoints.tablet} { display: grid; align-items: flex-start; @@ -466,8 +569,7 @@ Styled.OrderbookAndInputs = styled.div<{ showOrderbook: boolean }>` `} } `; - -Styled.Orderbook = styled(Orderbook)` +const $Orderbook = styled(Orderbook)` width: 100%; @media ${breakpoints.notTablet} { @@ -484,18 +586,39 @@ Styled.Orderbook = styled(Orderbook)` } } `; +const $ToggleGroup = styled(ToggleGroup)` + overflow-x: auto; -Styled.InputsColumn = styled.div` + button[data-state='off'] { + gap: 0; + + img { + height: 0; + } + } +` as typeof ToggleGroup; +const $InputsColumn = styled.div` ${formMixins.inputsColumn} `; +const $Message = styled.div` + ${layoutMixins.row} + gap: 0.75rem; +`; +const $IconButton = styled(IconButton)` + --button-backgroundColor: var(--color-white-faded); + flex-shrink: 0; -Styled.ButtonRow = styled.div` + svg { + width: 1.25em; + height: 1.25em; + } +`; +const $ButtonRow = styled.div` ${layoutMixins.row} justify-self: end; padding: 0.5rem 0 0.5rem 0; `; - -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${formMixins.footer} --stickyFooterBackdrop-outsetY: var(--tradeBox-content-paddingBottom); backdrop-filter: none; diff --git a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx index 507f20431..46a44d349 100644 --- a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx +++ b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx @@ -1,22 +1,28 @@ -import styled, { AnyStyledComponent } from 'styled-components'; +import { useEffect } from 'react'; + import { type NumberFormatValues } from 'react-number-format'; import { shallowEqual, useSelector } from 'react-redux'; - -import { layoutMixins } from '@/styles/layoutMixins'; -import { formMixins } from '@/styles/formMixins'; +import styled from 'styled-components'; import { TradeInputField } from '@/constants/abacus'; +import { ComplianceStates } from '@/constants/compliance'; import { STRING_KEYS, StringKey } from '@/constants/localization'; import { INTEGER_DECIMALS } from '@/constants/numbers'; import { TimeUnitShort } from '@/constants/time'; import { GOOD_TIL_TIME_TIMESCALE_STRINGS } from '@/constants/trade'; -import { useBreakpoints, useStringGetter } from '@/hooks'; -import { Collapsible } from '@/components/Collapsible'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; + import { Checkbox } from '@/components/Checkbox'; +import { Collapsible } from '@/components/Collapsible'; import { FormInput } from '@/components/FormInput'; import { InputType } from '@/components/Input'; -import { SelectMenu, SelectItem } from '@/components/SelectMenu'; +import { SelectItem, SelectMenu } from '@/components/SelectMenu'; import { WithTooltip } from '@/components/WithTooltip'; import { getInputTradeData, getInputTradeOptions } from '@/state/inputsSelectors'; @@ -26,94 +32,116 @@ import abacusStateManager from '@/lib/abacus'; export const AdvancedTradeOptions = () => { const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); + const { complianceState } = useComplianceState(); + const currentTradeFormConfig = useSelector(getInputTradeOptions, shallowEqual); const inputTradeData = useSelector(getInputTradeData, shallowEqual); const { execution, goodTil, postOnly, reduceOnly, timeInForce, type } = inputTradeData || {}; - const { executionOptions, needsGoodUntil, needsPostOnly, needsReduceOnly, postOnlyTooltip, reduceOnlyTooltip, timeInForceOptions } = - currentTradeFormConfig || {}; + const { + executionOptions, + needsGoodUntil, + needsPostOnly, + needsReduceOnly, + postOnlyTooltip, + reduceOnlyTooltip, + timeInForceOptions, + } = currentTradeFormConfig || {}; const { duration, unit } = goodTil || {}; - const showPostOnly = (needsPostOnly || postOnlyTooltip); - const showReduceOnly = (needsReduceOnly || reduceOnlyTooltip); + const showPostOnly = needsPostOnly || postOnlyTooltip; + const showReduceOnly = needsReduceOnly || reduceOnlyTooltip; const needsExecution = executionOptions || showPostOnly || showReduceOnly; const hasTimeInForce = timeInForceOptions?.toArray()?.length; + useEffect(() => { + if (complianceState === ComplianceStates.CLOSE_ONLY) { + abacusStateManager.setTradeValue({ + value: true, + field: TradeInputField.reduceOnly, + }); + } + }, [complianceState]); + return ( - - - - {hasTimeInForce && ( - - abacusStateManager.setTradeValue({ - value: selectedTimeInForceOption, - field: TradeInputField.timeInForceType, - }) - } - label={stringGetter({ key: STRING_KEYS.TIME_IN_FORCE })} - > - {timeInForceOptions.toArray().map(({ type, stringKey }) => ( - - ))} - - )} - {needsGoodUntil && ( - { - abacusStateManager.setTradeValue({ - value: Number(value), - field: TradeInputField.goodTilDuration, - }); - }} - value={duration ?? ''} - slotRight={ - { - abacusStateManager.setTradeValue({ - value: goodTilTimeTimescale, - field: TradeInputField.goodTilUnit, - }); - }} - > - {Object.values(TimeUnitShort).map((goodTilTimeTimescale: TimeUnitShort) => ( - - ))} - - } - /> - )} - + <$AdvancedInputsContainer> + {(hasTimeInForce || needsGoodUntil) && ( + <$AdvancedInputsRow> + {hasTimeInForce && timeInForce != null && ( + <$SelectMenu + value={timeInForce} + onValueChange={(selectedTimeInForceOption: string) => + abacusStateManager.setTradeValue({ + value: selectedTimeInForceOption, + field: TradeInputField.timeInForceType, + }) + } + label={stringGetter({ key: STRING_KEYS.TIME_IN_FORCE })} + > + {timeInForceOptions.toArray().map(({ type, stringKey }) => ( + <$SelectItem + key={type} + value={type} + label={stringGetter({ key: stringKey as StringKey })} + /> + ))} + + )} + {needsGoodUntil && ( + <$FormInput + id="trade-good-til-time" + type={InputType.Number} + decimals={INTEGER_DECIMALS} + label={stringGetter({ + key: hasTimeInForce ? STRING_KEYS.TIME : STRING_KEYS.GOOD_TIL_TIME, + })} + onChange={({ value }: NumberFormatValues) => { + abacusStateManager.setTradeValue({ + value: Number(value), + field: TradeInputField.goodTilDuration, + }); + }} + value={duration ?? ''} + slotRight={ + unit != null && ( + <$InnerSelectMenu + value={unit} + onValueChange={(goodTilTimeTimescale: string) => { + abacusStateManager.setTradeValue({ + value: goodTilTimeTimescale, + field: TradeInputField.goodTilUnit, + }); + }} + > + {Object.values(TimeUnitShort).map((goodTilTimeTimescale: TimeUnitShort) => ( + <$InnerSelectItem + key={goodTilTimeTimescale} + value={goodTilTimeTimescale} + label={stringGetter({ + key: GOOD_TIL_TIME_TIMESCALE_STRINGS[goodTilTimeTimescale], + })} + /> + ))} + + ) + } + /> + )} + + )} {needsExecution && ( <> - {executionOptions && ( - @@ -124,17 +152,22 @@ export const AdvancedTradeOptions = () => { } > {executionOptions.toArray().map(({ type, stringKey }) => ( - ))} - + )} - {showReduceOnly && abacusStateManager.setTradeValue({ value: checked, @@ -143,12 +176,23 @@ export const AdvancedTradeOptions = () => { } id="reduce-only" label={ - + {stringGetter({ key: STRING_KEYS.REDUCE_ONLY })} } /> - } + )} {showPostOnly && ( { } id="post-only" label={ - + {stringGetter({ key: STRING_KEYS.POST_ONLY })} } @@ -169,14 +216,11 @@ export const AdvancedTradeOptions = () => { )} )} - - + + ); }; - -const Styled: Record = {}; - -Styled.Collapsible = styled(Collapsible)` +const $Collapsible = styled(Collapsible)` --trigger-backgroundColor: transparent; --trigger-open-backgroundColor: transparent; --trigger-textColor: var(--color-text-0); @@ -189,34 +233,34 @@ Styled.Collapsible = styled(Collapsible)` margin: -0.5rem 0; `; -Styled.AdvancedInputsContainer = styled.div` +const $AdvancedInputsContainer = styled.div` display: grid; gap: var(--form-input-gap); `; -Styled.SelectMenu = styled(SelectMenu)` +const $SelectMenu = styled(SelectMenu)` ${formMixins.inputSelectMenu} `; -Styled.InnerSelectMenu = styled(SelectMenu)` +const $InnerSelectMenu = styled(SelectMenu)` ${formMixins.inputInnerSelectMenu} --select-menu-trigger-maxWidth: 4rem; -`; +` as typeof SelectMenu; -Styled.SelectItem = styled(SelectItem)` +const $SelectItem = styled(SelectItem)` ${formMixins.inputSelectMenuItem} -`; +` as typeof SelectItem; -Styled.InnerSelectItem = styled(SelectItem)` +const $InnerSelectItem = styled(SelectItem)` ${formMixins.inputInnerSelectMenuItem} `; -Styled.AdvancedInputsRow = styled.div<{ needsGoodUntil: boolean }>` +const $AdvancedInputsRow = styled.div` ${layoutMixins.gridEqualColumns} gap: var(--form-input-gap); `; -Styled.FormInput = styled(FormInput)` +const $FormInput = styled(FormInput)` input { margin-right: 4rem; } diff --git a/src/views/forms/TradeForm/LeverageSlider.stories.tsx b/src/views/forms/TradeForm/LeverageSlider.stories.tsx index 8290697df..8342ef7fe 100644 --- a/src/views/forms/TradeForm/LeverageSlider.stories.tsx +++ b/src/views/forms/TradeForm/LeverageSlider.stories.tsx @@ -1,33 +1,32 @@ import { useState } from 'react'; -import type { Story } from '@ladle/react'; -import styled, { AnyStyledComponent } from 'styled-components'; + import { OrderSide } from '@dydxprotocol/v4-client-js'; +import type { Story } from '@ladle/react'; +import styled from 'styled-components'; import { PositionSide } from '@/constants/trade'; + import { breakpoints } from '@/styles'; -import { StoryWrapper } from '.ladle/components'; import { LeverageSlider } from './LeverageSlider'; +import { StoryWrapper } from '.ladle/components'; -export const LeverageSliderStory: Story> = (args) => { +export const LeverageSliderStory: Story[0]> = (args) => { const [leverage, setLeverage] = useState(''); return ( - + <$PositionInfoContainer> - + ); }; - -const Styled: Record = {}; - -Styled.PositionInfoContainer = styled.div` +const $PositionInfoContainer = styled.div` height: 4.625rem; margin: auto; position: relative; diff --git a/src/views/forms/TradeForm/LeverageSlider.tsx b/src/views/forms/TradeForm/LeverageSlider.tsx index 57b027702..70ec40209 100644 --- a/src/views/forms/TradeForm/LeverageSlider.tsx +++ b/src/views/forms/TradeForm/LeverageSlider.tsx @@ -1,14 +1,16 @@ import { useCallback, useMemo } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { Root, Thumb, Track } from '@radix-ui/react-slider'; -import _ from 'lodash'; + import { OrderSide } from '@dydxprotocol/v4-client-js'; +import _ from 'lodash'; +import styled, { AnyStyledComponent, css } from 'styled-components'; import { TradeInputField } from '@/constants/abacus'; import { PositionSide } from '@/constants/trade'; -import { MustBigNumber, type BigNumberish } from '@/lib/numbers'; +import { Slider } from '@/components/Slider'; + import abacusStateManager from '@/lib/abacus'; +import { MustBigNumber, type BigNumberish } from '@/lib/numbers'; type ElementProps = { leverage?: BigNumberish | null; @@ -97,117 +99,41 @@ export const LeverageSlider = ({ }; return ( - - + <$Slider + label="MarketLeverage" min={min} max={max} step={0.1} - value={[Math.min(Math.max(leverageInputNumber, min), max)]} - onValueChange={onSliderDrag} + value={Math.min(Math.max(leverageInputNumber, min), max)} + onSliderDrag={onSliderDrag} onValueCommit={onValueCommit} - > - - - - + midpoint={midpoint} + orderSide={orderSide} + /> + ); }; - -const Styled: Record = {}; - -Styled.Root = styled(Root)` - // make thumb covers the start of the track - --radix-slider-thumb-transform: translateX(-65%) !important; - - position: relative; - - display: flex; - align-items: center; - - user-select: none; - - height: 100%; +const $Slider = styled(Slider)<{ midpoint?: number; orderSide: OrderSide }>` + --slider-track-backgroundColor: var(--color-layer-4); + + ${({ midpoint, orderSide }) => css` + --slider-track-background: linear-gradient( + 90deg, + var(--color-negative) 0%, + var(--color-layer-7) + ${midpoint + ? midpoint + : orderSide === OrderSide.BUY + ? 0 + : orderSide === OrderSide.SELL + ? 100 + : 50}%, + var(--color-positive) 100% + ); + `} `; -Styled.Track = styled(Track)` - position: relative; - - display: flex; - flex-grow: 1; - align-items: center; - - height: 0.5rem; - margin-right: 0.25rem; // make thumb covers the end of the track - - cursor: pointer; - - &:before { - content: ''; - width: 100%; - height: 100%; - - background: linear-gradient( - 90deg, - transparent, - transparent 15%, - var(--slider-backgroundColor) 15%, - var(--slider-backgroundColor) 50%, - transparent 50%, - transparent 65%, - var(--slider-backgroundColor) 65% - ) - 0 0 / 0.6rem; - } -`; - -Styled.Thumb = styled(Thumb)` +const $SliderContainer = styled.div` height: 1.375rem; - width: 1.375rem; - - display: flex; - justify-content: center; - align-items: center; - - background-color: var(--color-layer-6); - opacity: 0.8; - - border: 1.5px solid var(--color-layer-7); - border-radius: 50%; - - cursor: grab; -`; - -Styled.SliderContainer = styled.div<{ midpoint?: number; orderSide: OrderSide }>` - --slider-backgroundColor: var(--color-layer-4); - --slider-track-gradient-positive: linear-gradient( - 90deg, - var(--color-layer-7), - var(--color-positive) - ); - --slider-track-gradient-negative: linear-gradient( - 90deg, - var(--color-negative), - var(--color-layer-7) - ); - - height: 1.375rem; - - ${Styled.Track} { - ${({ midpoint, orderSide }) => css` - background: linear-gradient( - 90deg, - var(--color-negative) 0%, - var(--color-layer-7) - ${midpoint - ? midpoint - : orderSide === OrderSide.BUY - ? 0 - : orderSide === OrderSide.SELL - ? 100 - : 50}%, - var(--color-positive) 100% - ); - `} - } `; diff --git a/src/views/forms/TradeForm/MarketLeverageInput.tsx b/src/views/forms/TradeForm/MarketLeverageInput.tsx index 4ca06ee69..bda806de8 100644 --- a/src/views/forms/TradeForm/MarketLeverageInput.tsx +++ b/src/views/forms/TradeForm/MarketLeverageInput.tsx @@ -1,6 +1,6 @@ -import { shallowEqual, useSelector } from 'react-redux'; -import styled, { AnyStyledComponent } from 'styled-components'; import { OrderSide } from '@dydxprotocol/v4-client-js'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { TradeInputField } from '@/constants/abacus'; import { ButtonShape } from '@/constants/buttons'; @@ -8,9 +8,10 @@ import { STRING_KEYS } from '@/constants/localization'; import { LEVERAGE_DECIMALS } from '@/constants/numbers'; import { PositionSide } from '@/constants/trade'; -import { useStringGetter } from '@/hooks'; -import { formMixins } from '@/styles/formMixins'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { breakpoints } from '@/styles'; +import { formMixins } from '@/styles/formMixins'; import { Input, InputType } from '@/components/Input'; import { PositionSideTag } from '@/components/PositionSideTag'; @@ -46,7 +47,7 @@ export const MarketLeverageInput = ({ const { leverage, size: currentPositionSize } = currentPositionData || {}; const { current: currentSize, postOrder: postOrderSize } = currentPositionSize || {}; const { current: currentLeverage, postOrder: postOrderLeverage } = leverage || {}; - const { initialMarginFraction } = currentMarketConfig || {}; + const { initialMarginFraction, effectiveInitialMarginFraction } = currentMarketConfig || {}; const { side } = inputTradeData || {}; const orderSide = getSelectedOrderSide(side); @@ -55,19 +56,24 @@ export const MarketLeverageInput = ({ postOrderSize, }); - const maxLeverage = initialMarginFraction - ? BIG_NUMBERS.ONE.div(initialMarginFraction) - : MustBigNumber(10); + const preferredIMF = effectiveInitialMarginFraction ?? initialMarginFraction; + + const maxLeverage = preferredIMF ? BIG_NUMBERS.ONE.div(preferredIMF) : MustBigNumber(10); + + const leverageOptions = maxLeverage.lt(10) ? [1, 2, 3, 4, 5] : [1, 2, 3, 5, 10]; const leveragePosition = postOrderLeverage ? newPositionSide : currentPositionSide; const getSignedLeverage = (newLeverage: string | number) => { const newLeverageBN = MustBigNumber(newLeverage); + const newLeverageBNCapped = newLeverageBN.isGreaterThan(maxLeverage) + ? maxLeverage + : newLeverageBN; const newLeverageSignedBN = leveragePosition === PositionSide.Short || (leveragePosition === PositionSide.None && orderSide === OrderSide.SELL) - ? newLeverageBN.abs().negated() - : newLeverageBN.abs(); + ? newLeverageBNCapped.abs().negated() + : newLeverageBNCapped.abs(); return newLeverageSignedBN.toFixed(LEVERAGE_DECIMALS); }; @@ -100,7 +106,7 @@ export const MarketLeverageInput = ({ }); }; - const onLeverageSideToggle = (e: Event) => { + const onLeverageSideToggle = (e: React.MouseEvent) => { e.preventDefault(); if (leveragePosition === PositionSide.None) return; @@ -121,8 +127,8 @@ export const MarketLeverageInput = ({ return ( <> - - + <$WithLabel key="leverage" label={ <> @@ -130,13 +136,13 @@ export const MarketLeverageInput = ({ {stringGetter({ key: STRING_KEYS.LEVERAGE })} - + <$LeverageSide onClick={onLeverageSideToggle}> - + } > - - - + + <$InnerInputContainer> - - + + - ({ + <$ToggleGroup + items={leverageOptions.map((leverageAmount: number) => ({ label: `${leverageAmount}×`, value: MustBigNumber(leverageAmount).toFixed(LEVERAGE_DECIMALS), + disabled: maxLeverage.lt(leverageAmount), }))} value={MustBigNumber(formattedLeverageValue).abs().toFixed(LEVERAGE_DECIMALS)} // sign agnostic onValueChange={updateLeverage} @@ -167,10 +174,7 @@ export const MarketLeverageInput = ({ ); }; - -const Styled: Record = {}; - -Styled.InputContainer = styled.div` +const $InputContainer = styled.div` ${formMixins.inputContainer} --input-height: 3.5rem; @@ -181,15 +185,15 @@ Styled.InputContainer = styled.div` } `; -Styled.WithLabel = styled(WithLabel)` +const $WithLabel = styled(WithLabel)` ${formMixins.inputLabel} `; -Styled.LeverageSlider = styled(LeverageSlider)` +const $LeverageSlider = styled(LeverageSlider)` margin-top: 0.25rem; `; -Styled.InnerInputContainer = styled.div` +const $InnerInputContainer = styled.div` ${formMixins.inputContainer} --input-backgroundColor: var(--color-layer-5); --input-borderColor: var(--color-layer-7); @@ -208,10 +212,10 @@ Styled.InnerInputContainer = styled.div` } `; -Styled.LeverageSide = styled.div` +const $LeverageSide = styled.div` cursor: pointer; `; -Styled.ToggleGroup = styled(ToggleGroup)` +const $ToggleGroup = styled(ToggleGroup)` ${formMixins.inputToggleGroup} `; diff --git a/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx b/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx index d55c04680..885a7c618 100644 --- a/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx +++ b/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx @@ -1,13 +1,17 @@ import { useDispatch, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; +import styled from 'styled-components'; import type { TradeInputSummary } from '@/constants/abacus'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { MobilePlaceOrderSteps } from '@/constants/trade'; -import { useStringGetter, useTokenConfigs } from '@/hooks'; +import { ConnectionErrorType, useApiState } from '@/hooks/useApiState'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; import { AssetIcon } from '@/components/AssetIcon'; import { Button } from '@/components/Button'; @@ -15,7 +19,6 @@ import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType, ShowSign } from '@/components/Output'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; import { WithTooltip } from '@/components/WithTooltip'; - import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; @@ -30,7 +33,6 @@ type ConfirmButtonConfig = { }; type ElementProps = { - isLoading: boolean; actionStringKey?: string; summary?: TradeInputSummary; hasValidationErrors?: boolean; @@ -41,7 +43,6 @@ type ElementProps = { }; export const PlaceOrderButtonAndReceipt = ({ - isLoading, actionStringKey, summary, hasValidationErrors, @@ -53,6 +54,8 @@ export const PlaceOrderButtonAndReceipt = ({ const stringGetter = useStringGetter(); const dispatch = useDispatch(); const { chainTokenLabel } = useTokenConfigs(); + const { connectionError } = useApiState(); + const { complianceState } = useComplianceState(); const canAccountTrade = useSelector(calculateCanAccountTrade); const subaccountNumber = useSelector(getSubaccountId); @@ -60,8 +63,16 @@ export const PlaceOrderButtonAndReceipt = ({ const hasMissingData = subaccountNumber === undefined; + const tradingUnavailable = + complianceState === ComplianceStates.READ_ONLY || + connectionError === ConnectionErrorType.CHAIN_DISRUPTION; + const shouldEnableTrade = - canAccountTrade && !hasMissingData && !hasValidationErrors && currentInput !== 'transfer'; + canAccountTrade && + !hasMissingData && + !hasValidationErrors && + currentInput !== 'transfer' && + !tradingUnavailable; const { fee, price: expectedPrice, total, reward } = summary || {}; @@ -109,6 +120,13 @@ export const PlaceOrderButtonAndReceipt = ({ }, ]; + const returnToMarketState = () => ({ + buttonTextStringKey: STRING_KEYS.RETURN_TO_MARKET, + buttonAction: ButtonAction.Secondary, + buttonState: {}, + showValidatorError: false, + }); + const buttonStatesPerStep = { [MobilePlaceOrderSteps.EditOrder]: { buttonTextStringKey: shouldEnableTrade @@ -118,23 +136,18 @@ export const PlaceOrderButtonAndReceipt = ({ : STRING_KEYS.UNAVAILABLE, buttonAction: ButtonAction.Primary, buttonState: { isDisabled: !shouldEnableTrade, isLoading: hasMissingData }, + showValidatorError: true, }, [MobilePlaceOrderSteps.PreviewOrder]: { buttonTextStringKey: STRING_KEYS.CONFIRM_ORDER, buttonAction: confirmButtonConfig.buttonAction, - buttonState: { isLoading }, - }, - [MobilePlaceOrderSteps.PlacingOrder]: { - buttonTextStringKey: STRING_KEYS.RETURN_TO_MARKET, - buttonAction: ButtonAction.Secondary, - buttonState: {}, - }, - [MobilePlaceOrderSteps.Confirmation]: { - buttonTextStringKey: STRING_KEYS.RETURN_TO_MARKET, - buttonAction: ButtonAction.Secondary, buttonState: {}, + showValidatorError: false, }, + [MobilePlaceOrderSteps.PlacingOrder]: returnToMarketState(), + [MobilePlaceOrderSteps.PlaceOrderFailed]: returnToMarketState(), + [MobilePlaceOrderSteps.Confirmation]: returnToMarketState(), }; const buttonAction = currentStep @@ -142,7 +155,9 @@ export const PlaceOrderButtonAndReceipt = ({ : confirmButtonConfig.buttonAction; let buttonTextStringKey = STRING_KEYS.UNAVAILABLE; - if (currentStep) { + if (tradingUnavailable) { + buttonTextStringKey = STRING_KEYS.UNAVAILABLE; + } else if (currentStep) { buttonTextStringKey = buttonStatesPerStep[currentStep].buttonTextStringKey; } else if (shouldEnableTrade) { buttonTextStringKey = confirmButtonConfig.buttonTextStringKey; @@ -153,8 +168,8 @@ export const PlaceOrderButtonAndReceipt = ({ const buttonState = currentStep ? buttonStatesPerStep[currentStep].buttonState : { - isDisabled: !shouldEnableTrade || isLoading, - isLoading: isLoading || hasMissingData, + isDisabled: !shouldEnableTrade, + isLoading: hasMissingData, }; const depositButton = ( @@ -166,14 +181,15 @@ export const PlaceOrderButtonAndReceipt = ({ ); + const showValidatorErrors = + hasValidationErrors && (!currentStep || buttonStatesPerStep[currentStep].showValidatorError); + const submitButton = ( - : undefined - } + slotLeft={showValidatorErrors ? <$WarningIcon iconName={IconName.Warning} /> : undefined} > {stringGetter({ key: buttonTextStringKey, @@ -183,30 +199,27 @@ export const PlaceOrderButtonAndReceipt = ({ }), }, })} - + ); return ( {!canAccountTrade ? ( - ) : showDeposit ? ( + ) : showDeposit && complianceState === ComplianceStates.FULL_ACCESS ? ( depositButton ) : ( - + {submitButton} )} ); }; - -const Styled: Record = {}; - -Styled.Button = styled(Button)` +const $Button = styled(Button)` width: 100%; `; -Styled.WarningIcon = styled(Icon)` +const $WarningIcon = styled(Icon)` color: var(--color-warning); `; diff --git a/src/views/forms/TradeForm/PositionPreview.tsx b/src/views/forms/TradeForm/PositionPreview.tsx index 489fd8da9..0e70c5a27 100644 --- a/src/views/forms/TradeForm/PositionPreview.tsx +++ b/src/views/forms/TradeForm/PositionPreview.tsx @@ -1,18 +1,18 @@ -import styled, { AnyStyledComponent } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; -import { useStringGetter } from '@/hooks'; import { AssetIcon } from '@/components/AssetIcon'; +import { PositionTile } from '@/views/PositionTile'; +import { getCurrentMarketPositionData } from '@/state/accountSelectors'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketData } from '@/state/perpetualsSelectors'; -import { getCurrentMarketPositionData } from '@/state/accountSelectors'; - -import { PositionTile } from '@/views/PositionTile'; type ElementProps = { showNarrowVariation?: boolean; @@ -23,12 +23,13 @@ export const PositionPreview = ({ showNarrowVariation }: ElementProps) => { const { id } = useSelector(getCurrentMarketAssetData, shallowEqual) || {}; const { configs, oraclePrice } = useSelector(getCurrentMarketData, shallowEqual) || {}; - const { size: positionSize } = useSelector(getCurrentMarketPositionData, shallowEqual) || {}; + const { size: positionSize, notionalTotal } = + useSelector(getCurrentMarketPositionData, shallowEqual) || {}; const { stepSizeDecimals, tickSizeDecimals } = configs || {}; return ( - - + <$PositionPreviewContainer> + <$YourPosition> {!showNarrowVariation && } {stringGetter({ @@ -38,23 +39,20 @@ export const PositionPreview = ({ showNarrowVariation }: ElementProps) => { }, })} - + - + ); }; - -const Styled: Record = {}; - -Styled.PositionPreviewContainer = styled.div` +const $PositionPreviewContainer = styled.div` ${layoutMixins.column} align-items: flex-start; width: 100%; @@ -65,7 +63,7 @@ Styled.PositionPreviewContainer = styled.div` } `; -Styled.YourPosition = styled.div` +const $YourPosition = styled.div` ${layoutMixins.inlineRow} color: var(--color-text-0); diff --git a/src/views/forms/TradeForm/TradeSideToggle.tsx b/src/views/forms/TradeForm/TradeSideToggle.tsx index 8af0c6adf..61716e4e7 100644 --- a/src/views/forms/TradeForm/TradeSideToggle.tsx +++ b/src/views/forms/TradeForm/TradeSideToggle.tsx @@ -1,19 +1,21 @@ import { memo } from 'react'; -import styled, { css } from 'styled-components'; -import { shallowEqual, useSelector } from 'react-redux'; + import { OrderSide } from '@dydxprotocol/v4-client-js'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; -import { AbacusOrderSide } from '@/constants/abacus'; +import { AbacusOrderSide, TradeInputField } from '@/constants/abacus'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { TradeInputField } from '@/constants/abacus'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { ToggleGroup } from '@/components/ToggleGroup'; import { getTradeSide } from '@/state/inputsSelectors'; import abacusStateManager from '@/lib/abacus'; +import { getSimpleStyledOutputType } from '@/lib/genericFunctionalComponentUtils'; import { getSelectedOrderSide } from '@/lib/tradeData'; export const TradeSideToggle = memo(() => { @@ -22,7 +24,7 @@ export const TradeSideToggle = memo(() => { const selectedOrderSide = getSelectedOrderSide(side); return ( - { ); }); -const ToggleContainer = styled(ToggleGroup)<{ value: OrderSide }>` +type ToggleContainerStyleProps = { value: OrderSide }; +const toggleContainerType = getSimpleStyledOutputType(ToggleGroup, {} as ToggleContainerStyleProps); +const $ToggleContainer = styled(ToggleGroup)` --toggle-radius: 0.5em; --toggle-color: var(--color-negative); --toggle-background: ${({ theme }) => theme.toggleBackground}; @@ -86,4 +90,4 @@ const ToggleContainer = styled(ToggleGroup)<{ value: OrderSide }>` transform: translateX(100%); `} } -`; +` as typeof toggleContainerType; diff --git a/src/views/forms/TradeForm/TradeSizeInputs.tsx b/src/views/forms/TradeForm/TradeSizeInputs.tsx index f53c4b063..d4bf22222 100644 --- a/src/views/forms/TradeForm/TradeSizeInputs.tsx +++ b/src/views/forms/TradeForm/TradeSizeInputs.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; + import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import styled, { AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { TradeInputField } from '@/constants/abacus'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; @@ -8,26 +9,26 @@ import { STRING_KEYS } from '@/constants/localization'; import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; import { TradeSizeInput } from '@/constants/trade'; -import { useBreakpoints, useStringGetter } from '@/hooks'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + import { formMixins } from '@/styles/formMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { FormInput } from '@/components/FormInput'; +import { Icon, IconName } from '@/components/Icon'; import { InputType } from '@/components/Input'; import { Tag } from '@/components/Tag'; -import { WithTooltip } from '@/components/WithTooltip'; -import { Icon, IconName } from '@/components/Icon'; import { ToggleButton } from '@/components/ToggleButton'; +import { WithTooltip } from '@/components/WithTooltip'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { setTradeFormInputs } from '@/state/inputs'; - import { - getInputTradeSizeData, getInputTradeOptions, + getInputTradeSizeData, getTradeFormInputs, } from '@/state/inputsSelectors'; - import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; import abacusStateManager from '@/lib/abacus'; @@ -103,7 +104,7 @@ export const TradeSizeInputs = () => { }; const inputToggleButton = ( - { > {showUSDCInputOnTablet ? 'USD' : id} - + ); const sizeInput = ( @@ -139,7 +140,7 @@ export const TradeSizeInputs = () => { onInput={onUSDCInput} type={InputType.Currency} value={usdAmountInput || ''} - decimals={tickSizeDecimals || USD_DECIMALS} + decimals={tickSizeDecimals ?? USD_DECIMALS} label={ <> @@ -153,7 +154,7 @@ export const TradeSizeInputs = () => { ); return ( - + <$Column> {isTablet ? ( showUSDCInputOnTablet ? ( usdcInput @@ -161,10 +162,10 @@ export const TradeSizeInputs = () => { sizeInput ) ) : ( - + <$Row> {sizeInput} {usdcInput} - + )} {needsLeverage && ( @@ -175,23 +176,20 @@ export const TradeSizeInputs = () => { } /> )} - + ); }; - -const Styled: Record = {}; - -Styled.Column = styled.div` +const $Column = styled.div` ${layoutMixins.flexColumn} gap: var(--form-input-gap); `; -Styled.Row = styled.div` +const $Row = styled.div` ${layoutMixins.gridEqualColumns} gap: var(--form-input-gap); `; -Styled.ToggleButton = styled(ToggleButton)` +const $ToggleButton = styled(ToggleButton)` ${formMixins.inputInnerToggleButton} --button-font: var(--font-tiny-book); --button-height: 2.25rem; diff --git a/src/views/forms/TransferForm.tsx b/src/views/forms/TransferForm.tsx index fcd83afe4..b7ee0cbe8 100644 --- a/src/views/forms/TransferForm.tsx +++ b/src/views/forms/TransferForm.tsx @@ -1,9 +1,12 @@ -import { type FormEvent, useEffect, useMemo, useState } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import { useEffect, useMemo, useState, type FormEvent } from 'react'; + +import { validation } from '@dydxprotocol/v4-client-js'; +import { noop } from 'lodash'; import { type NumberFormatValues } from 'react-number-format'; -import { shallowEqual, useSelector } from 'react-redux'; import type { SyntheticInputEvent } from 'react-number-format/types/types'; -import { validation } from '@dydxprotocol/v4-client-js'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { Nullable } from 'vitest'; import { TransferInputField, TransferType } from '@/constants/abacus'; import { AlertType } from '@/constants/alerts'; @@ -12,15 +15,14 @@ import { STRING_KEYS } from '@/constants/localization'; import { NumberSign } from '@/constants/numbers'; import { DydxChainAsset } from '@/constants/wallets'; -import { - useAccountBalance, - useAccounts, - useDydxClient, - useRestrictions, - useStringGetter, - useSubaccount, - useTokenConfigs, -} from '@/hooks'; +import { useAccountBalance } from '@/hooks/useAccountBalance'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useDydxClient } from '@/hooks/useDydxClient'; +import { useRestrictions } from '@/hooks/useRestrictions'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useSubaccount } from '@/hooks/useSubaccount'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; +import { useWithdrawalInfo } from '@/hooks/useWithdrawalInfo'; import { formMixins } from '@/styles/formMixins'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -35,8 +37,8 @@ import { OutputType } from '@/components/Output'; import { SelectItem, SelectMenu } from '@/components/SelectMenu'; import { Tag } from '@/components/Tag'; import { ToggleButton } from '@/components/ToggleButton'; -import { TransferButtonAndReceipt } from '@/views/forms/TransferForm/TransferButtonAndReceipt'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; +import { TransferButtonAndReceipt } from '@/views/forms/TransferForm/TransferButtonAndReceipt'; import { getSubaccount } from '@/state/accountSelectors'; import { getSelectedDydxChainId } from '@/state/appSelectors'; @@ -64,6 +66,7 @@ export const TransferForm = ({ const { nativeTokenBalance, usdcBalance } = useAccountBalance(); const selectedDydxChainId = useSelector(getSelectedDydxChainId); const { tokensConfigs, usdcLabel, chainTokenLabel } = useTokenConfigs(); + useWithdrawalInfo({ transferType: 'transfer' }); const { address: recipientAddress, @@ -96,7 +99,7 @@ export const TransferForm = ({ const amountBN = MustBigNumber(amount); const balanceBN = MustBigNumber(balance); - const onChangeAsset = (asset: DydxChainAsset) => { + const onChangeAsset = (asset: Nullable) => { setError(undefined); setCurrentFee(undefined); @@ -220,18 +223,18 @@ export const TransferForm = ({ { value: DydxChainAsset.USDC, label: ( - + <$InlineRow> {usdcLabel} - + ), }, { value: DydxChainAsset.CHAINTOKEN, label: ( - + <$InlineRow> {chainTokenLabel} - + ), }, ]; @@ -240,9 +243,9 @@ export const TransferForm = ({ { chainId: selectedDydxChainId, label: ( - + <$InlineRow> {stringGetter({ key: STRING_KEYS.DYDX_CHAIN })} - + ), }, ]; @@ -279,7 +282,7 @@ export const TransferForm = ({ onClear: () => void; onClick: () => void; }) => ( - (isPressed ? onClick : onClear)()} @@ -287,26 +290,26 @@ export const TransferForm = ({ shape={isInputEmpty ? ButtonShape.Rectangle : ButtonShape.Circle} > {isInputEmpty ? label : } - + ); return ( - { e.preventDefault(); onTransfer(); }} > - + <$Row> onChangeAddress(e.target?.value)} label={ - + <$DestinationInputLabel> {stringGetter({ key: STRING_KEYS.DESTINATION })} - {isAddressValid && } - + {isAddressValid && <$CheckIcon iconName={IconName.Check} />} + } type={InputType.Text} value={recipientAddress ?? ''} @@ -319,40 +322,39 @@ export const TransferForm = ({ })} disabled={isLoading} /> - {networkOptions.map(({ chainId, label }) => ( - + <$SelectItem key={chainId} value={chainId} label={label} /> ))} - - + + {recipientAddress && !isAddressValid && ( - + <$AddressValidationAlertMessage type={AlertType.Error}> {stringGetter({ key: dydxAddress === recipientAddress ? STRING_KEYS.TRANSFER_TO_YOURSELF : STRING_KEYS.TRANSFER_INVALID_DYDX_ADDRESS, })} - + )} - {assetOptions.map(({ value, label }) => ( - + <$SelectItem key={value} value={value} label={label} /> ))} - + - + <$WithDetailsReceipt side="bottom" detailItems={amountDetailItems}> - + {showNotEnoughGasWarning && ( @@ -383,51 +385,48 @@ export const TransferForm = ({ {error && {error}} - + <$Footer> - - + + ); }; - -const Styled: Record = {}; - -Styled.Form = styled.form` +const $Form = styled.form` ${formMixins.transfersForm} `; -Styled.Footer = styled.footer` +const $Footer = styled.footer` ${formMixins.footer} --stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom); `; -Styled.Row = styled.div` +const $Row = styled.div` ${layoutMixins.gridEqualColumns} gap: var(--form-input-gap); `; -Styled.SelectMenu = styled(SelectMenu)` +const $SelectMenu = styled(SelectMenu)` ${formMixins.inputSelectMenu} -`; +` as typeof SelectMenu; -Styled.SelectItem = styled(SelectItem)` +const $SelectItem = styled(SelectItem)` ${formMixins.inputSelectMenuItem} -`; +` as typeof SelectItem; -Styled.NetworkSelectMenu = styled(Styled.SelectMenu)` +const $NetworkSelectMenu = styled($SelectMenu)` pointer-events: none; -`; +` as typeof SelectMenu; -Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` +const $WithDetailsReceipt = styled(WithDetailsReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); `; -Styled.InlineRow = styled.span` +const $InlineRow = styled.span` ${layoutMixins.inlineRow} height: 100%; @@ -436,19 +435,19 @@ Styled.InlineRow = styled.span` } `; -Styled.DestinationInputLabel = styled.span` +const $DestinationInputLabel = styled.span` ${layoutMixins.inlineRow} `; -Styled.CheckIcon = styled(Icon)` +const $CheckIcon = styled(Icon)` color: var(--color-success); `; -Styled.AddressValidationAlertMessage = styled(AlertMessage)` +const $AddressValidationAlertMessage = styled(AlertMessage)` margin-top: -0.75rem; `; -Styled.FormInputToggleButton = styled(ToggleButton)` +const $FormInputToggleButton = styled(ToggleButton)` ${formMixins.inputInnerToggleButton} svg { diff --git a/src/views/forms/TransferForm/TransferButtonAndReceipt.tsx b/src/views/forms/TransferForm/TransferButtonAndReceipt.tsx index ecf524d0c..30e468f48 100644 --- a/src/views/forms/TransferForm/TransferButtonAndReceipt.tsx +++ b/src/views/forms/TransferForm/TransferButtonAndReceipt.tsx @@ -1,12 +1,14 @@ import { shallowEqual, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { NumberSign } from '@/constants/numbers'; import { DydxChainAsset } from '@/constants/wallets'; -import { useAccountBalance, useTokenConfigs, useStringGetter } from '@/hooks'; +import { useAccountBalance } from '@/hooks/useAccountBalance'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; import { Button } from '@/components/Button'; import { DiffOutput } from '@/components/DiffOutput'; @@ -103,7 +105,7 @@ export const TransferButtonAndReceipt = ({ ].filter(isTruthy); return ( - + <$WithDetailsReceipt detailItems={transferDetailItems}> {!canAccountTrade ? ( ) : ( @@ -115,13 +117,10 @@ export const TransferButtonAndReceipt = ({ {stringGetter({ key: STRING_KEYS.CONFIRM_TRANSFER })} )} - + ); }; - -const Styled: Record = {}; - -Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` +const $WithDetailsReceipt = styled(WithDetailsReceipt)` --withReceipt-backgroundColor: var(--color-layer-2); dl { diff --git a/src/views/forms/TriggersForm/AdvancedTriggersOptions.tsx b/src/views/forms/TriggersForm/AdvancedTriggersOptions.tsx new file mode 100644 index 000000000..761fd5534 --- /dev/null +++ b/src/views/forms/TriggersForm/AdvancedTriggersOptions.tsx @@ -0,0 +1,89 @@ +import styled from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; + +import { useEnvFeatures } from '@/hooks/useEnvFeatures'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { HorizontalSeparatorFiller } from '@/components/Separator'; + +import { LimitPriceInputs } from './LimitPriceInputs'; +import { OrderSizeInput } from './OrderSizeInput'; + +type ElementProps = { + symbol: string; + existsLimitOrder: boolean; + size: number | null; + positionSize: number | null; + differingOrderSizes: boolean; + multipleTakeProfitOrders: boolean; + multipleStopLossOrders: boolean; + stepSizeDecimals?: number; + tickSizeDecimals?: number; +}; + +type StyleProps = { + className?: string; +}; + +export const AdvancedTriggersOptions = ({ + symbol, + existsLimitOrder, + size, + positionSize, + differingOrderSizes, + multipleTakeProfitOrders, + multipleStopLossOrders, + stepSizeDecimals, + tickSizeDecimals, + className, +}: ElementProps & StyleProps) => { + const stringGetter = useStringGetter(); + const { isSlTpLimitOrdersEnabled } = useEnvFeatures(); + + return ( + <$Container> + <$Header> + {stringGetter({ key: STRING_KEYS.ADVANCED })} + + + <$Content> + + {isSlTpLimitOrdersEnabled && ( + + )} + + + ); +}; +const $Container = styled.div` + ${layoutMixins.column} +`; + +const $Header = styled.h3` + ${layoutMixins.inlineRow} + font: var(--font-small-medium); + color: var(--color-text-0); + + margin-bottom: 0.5rem; +`; + +const $Content = styled.div` + display: grid; + gap: 0.5em; +`; diff --git a/src/views/forms/TriggersForm/LimitPriceInputs.tsx b/src/views/forms/TriggersForm/LimitPriceInputs.tsx new file mode 100644 index 000000000..8967c099e --- /dev/null +++ b/src/views/forms/TriggersForm/LimitPriceInputs.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; + +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { + AbacusOrderType, + TriggerOrdersInputField, + TriggerOrdersInputFields, +} from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; +import { USD_DECIMALS } from '@/constants/numbers'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Checkbox } from '@/components/Checkbox'; +import { Collapsible } from '@/components/Collapsible'; +import { FormInput } from '@/components/FormInput'; +import { Tag } from '@/components/Tag'; +import { WithTooltip } from '@/components/WithTooltip'; + +import { getTriggerOrdersInputs } from '@/state/inputsSelectors'; + +import abacusStateManager from '@/lib/abacus'; +import { MustBigNumber } from '@/lib/numbers'; + +type ElementProps = { + existsLimitOrder: boolean; + multipleTakeProfitOrders: boolean; + multipleStopLossOrders: boolean; + tickSizeDecimals?: number; +}; + +type StyleProps = { + className?: string; +}; + +export const LimitPriceInputs = ({ + existsLimitOrder, + multipleTakeProfitOrders, + multipleStopLossOrders, + tickSizeDecimals, + className, +}: ElementProps & StyleProps) => { + const stringGetter = useStringGetter(); + + const { stopLossOrder, takeProfitOrder } = + useSelector(getTriggerOrdersInputs, shallowEqual) || {}; + + const [shouldShowLimitPrice, setShouldShowLimitPrice] = useState(existsLimitOrder); + + const decimals = tickSizeDecimals ?? USD_DECIMALS; + + const onToggleLimit = (isLimitChecked: boolean) => { + if (!isLimitChecked) { + abacusStateManager.setTriggerOrdersValue({ + value: AbacusOrderType.takeProfitMarket.rawValue, + field: TriggerOrdersInputField.takeProfitOrderType, + }); + abacusStateManager.setTriggerOrdersValue({ + value: null, + field: TriggerOrdersInputField.takeProfitLimitPrice, + }); + abacusStateManager.setTriggerOrdersValue({ + value: AbacusOrderType.stopMarket.rawValue, + field: TriggerOrdersInputField.stopLossOrderType, + }); + abacusStateManager.setTriggerOrdersValue({ + value: null, + field: TriggerOrdersInputField.stopLossLimitPrice, + }); + } + setShouldShowLimitPrice(isLimitChecked); + }; + + const onLimitInput = + (field: TriggerOrdersInputFields) => + ({ floatValue, formattedValue }: { floatValue?: number; formattedValue: string }) => { + const newLimitPrice = MustBigNumber(floatValue).toFixed(decimals); + + abacusStateManager.setTriggerOrdersValue({ + value: formattedValue === '' || newLimitPrice === 'NaN' ? null : newLimitPrice, + field, + }); + }; + + return ( + <> + + } + open={shouldShowLimitPrice} + label={ + + {stringGetter({ key: STRING_KEYS.LIMIT_PRICE })} + + } + > + { + <$InputsRow> + {!multipleTakeProfitOrders && ( + + {stringGetter({ key: STRING_KEYS.TP_LIMIT })} + USD + + } + onInput={onLimitInput(TriggerOrdersInputField.takeProfitLimitPrice)} + /> + )} + {!multipleStopLossOrders && ( + + {stringGetter({ key: STRING_KEYS.SL_LIMIT })} + USD + + } + onInput={onLimitInput(TriggerOrdersInputField.stopLossLimitPrice)} + /> + )} + + } + + + ); +}; +const $InputsRow = styled.span` + ${layoutMixins.flexEqualColumns} + gap: 1ch; +`; diff --git a/src/views/forms/TriggersForm/OrderSizeInput.tsx b/src/views/forms/TriggersForm/OrderSizeInput.tsx new file mode 100644 index 000000000..2004a4deb --- /dev/null +++ b/src/views/forms/TriggersForm/OrderSizeInput.tsx @@ -0,0 +1,131 @@ +import { useEffect, useState } from 'react'; + +import styled from 'styled-components'; + +import { TriggerOrdersInputField } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; +import { TOKEN_DECIMALS } from '@/constants/numbers'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Checkbox } from '@/components/Checkbox'; +import { Collapsible } from '@/components/Collapsible'; +import { FormInput } from '@/components/FormInput'; +import { InputType } from '@/components/Input'; +import { Tag } from '@/components/Tag'; +import { WithTooltip } from '@/components/WithTooltip'; + +import abacusStateManager from '@/lib/abacus'; +import { MustBigNumber } from '@/lib/numbers'; + +import { OrderSizeSlider } from './OrderSizeSlider'; + +type ElementProps = { + symbol: string; + differingOrderSizes: boolean; + size: number | null; + positionSize: number | null; + stepSizeDecimals?: number; +}; + +type StyleProps = { + className?: string; +}; + +export const OrderSizeInput = ({ + symbol, + differingOrderSizes, + size, + positionSize, + stepSizeDecimals, + className, +}: ElementProps & StyleProps) => { + const stringGetter = useStringGetter(); + + const [shouldShowCustomAmount, setShouldShowCustomAmount] = useState(false); + const [orderSize, setOrderSize] = useState(size); + + useEffect(() => { + setOrderSize(size); + setShouldShowCustomAmount(!!(size && size !== positionSize)); + }, [size]); + + const onCustomAmountToggle = (isToggled: boolean) => { + if (!isToggled) { + // Default to full position size + abacusStateManager.setTriggerOrdersValue({ + value: MustBigNumber(positionSize).toString(), + field: TriggerOrdersInputField.size, + }); + } + setShouldShowCustomAmount(isToggled); + }; + + const setAbacusSize = (newSize: number | null) => { + const newSizeString = MustBigNumber( + newSize && positionSize ? Math.min(positionSize, newSize) : newSize + ).toString(); + + abacusStateManager.setTriggerOrdersValue({ + value: newSize !== null ? newSizeString : null, + field: TriggerOrdersInputField.size, + }); + }; + + const onSizeInput = ({ floatValue }: { floatValue?: number }) => { + if (floatValue) { + setOrderSize(Math.min(floatValue, positionSize ?? 0)); + setAbacusSize(floatValue); + } else { + setOrderSize(null); + setAbacusSize(null); + } + }; + + return ( + + + + } + label={ + + {stringGetter({ key: STRING_KEYS.CUSTOM_AMOUNT })} + + } + open={shouldShowCustomAmount} + > + <$SizeInputRow> + <$OrderSizeSlider + setAbacusSize={(sizeString: string) => setAbacusSize(parseFloat(sizeString))} + setOrderSizeInput={(sizeString: string) => setOrderSize(parseFloat(sizeString))} + size={orderSize} + positionSize={positionSize ?? undefined} + stepSizeDecimals={stepSizeDecimals ?? TOKEN_DECIMALS} + className={className} + /> + {symbol}} + onInput={onSizeInput} + /> + + + ); +}; +const $OrderSizeSlider = styled(OrderSizeSlider)` + width: 100%; +`; + +const $SizeInputRow = styled.div` + display: flex; + align-items: center; + gap: 0.25rem; +`; diff --git a/src/views/forms/TriggersForm/OrderSizeSlider.stories.tsx b/src/views/forms/TriggersForm/OrderSizeSlider.stories.tsx new file mode 100644 index 000000000..01201c368 --- /dev/null +++ b/src/views/forms/TriggersForm/OrderSizeSlider.stories.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; + +import type { Story } from '@ladle/react'; +import styled from 'styled-components'; + +import { TOKEN_DECIMALS } from '@/constants/numbers'; + +import { breakpoints } from '@/styles'; + +import { OrderSizeSlider } from './OrderSizeSlider'; +import { StoryWrapper } from '.ladle/components'; + +export const OrderSizeSliderStory: Story[0]> = (args) => { + const [size, setSize] = useState(20); + + return ( + + <$Container> + null} + setOrderSizeInput={(sizeString: string) => setSize(parseFloat(sizeString))} + size={size} + stepSizeDecimals={TOKEN_DECIMALS} + positionSize={100} + /> + + + ); +}; +const $Container = styled.div` + height: 4.625rem; + margin: auto; + position: relative; + + display: grid; + grid-template-columns: minmax(0, 23.75rem); + justify-content: center; + padding: 2rem 2rem 0; + + @media ${breakpoints.desktopLarge} { + padding: 3rem 2rem 0; + } +`; diff --git a/src/views/forms/TriggersForm/OrderSizeSlider.tsx b/src/views/forms/TriggersForm/OrderSizeSlider.tsx new file mode 100644 index 000000000..b01e2adce --- /dev/null +++ b/src/views/forms/TriggersForm/OrderSizeSlider.tsx @@ -0,0 +1,80 @@ +import { useCallback } from 'react'; + +import _ from 'lodash'; +import styled from 'styled-components'; + +import { Slider } from '@/components/Slider'; + +import { MustBigNumber } from '@/lib/numbers'; + +type ElementProps = { + setAbacusSize: (value: string) => void; + setOrderSizeInput: (value: string) => void; + size: number | null; + positionSize?: number; + stepSizeDecimals: number; +}; + +type StyleProps = { + className?: string; +}; + +export const OrderSizeSlider = ({ + setOrderSizeInput, + setAbacusSize, + size, + positionSize, + stepSizeDecimals, + className, +}: ElementProps & StyleProps) => { + const step = positionSize ? Math.pow(10, Math.floor(Math.log10(positionSize) - 1)) : 0.1; + const maxSize = positionSize ?? 0; + const currSize = size ?? 0; + + // Debounced slightly to avoid excessive updates to Abacus while still providing a smooth slide + const debouncedSetAbacusSize = useCallback( + _.debounce((newSize: string) => { + setAbacusSize(newSize); + }, 50), + [] + ); + + const onSliderDrag = ([newSize]: number[]) => { + const roundedSize = MustBigNumber(newSize).toFixed(stepSizeDecimals); + setOrderSizeInput(roundedSize); + debouncedSetAbacusSize(roundedSize); + }; + + const onValueCommit = ([newSize]: number[]) => { + const roundedSize = MustBigNumber(newSize).toFixed(stepSizeDecimals); + setOrderSizeInput(roundedSize); + // Ensure Abacus is updated with the latest, committed value + debouncedSetAbacusSize.cancel(); + setAbacusSize(roundedSize); + }; + + return ( + <$SliderContainer className={className}> + <$Slider + label="PositionSize" + min={0} + max={maxSize} + step={step} + onSliderDrag={onSliderDrag} + onValueCommit={onValueCommit} + value={Math.min(currSize, maxSize)} + /> + + ); +}; +const $SliderContainer = styled.div` + height: 1.375rem; +`; +const $Slider = styled(Slider)` + --slider-track-backgroundColor: var(--color-layer-4); + --slider-track-background: linear-gradient( + 90deg, + var(--color-layer-6) 0%, + var(--color-text-0) 100% + ); +`; diff --git a/src/views/forms/TriggersForm/TriggerOrderInputs.tsx b/src/views/forms/TriggersForm/TriggerOrderInputs.tsx new file mode 100644 index 000000000..a45738550 --- /dev/null +++ b/src/views/forms/TriggersForm/TriggerOrderInputs.tsx @@ -0,0 +1,324 @@ +import { useState } from 'react'; + +import styled from 'styled-components'; + +import { + Nullable, + TriggerOrdersInputPrice, + type TriggerOrdersInputFields, +} from '@/constants/abacus'; +import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { NumberSign, PERCENT_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Button } from '@/components/Button'; +import { DropdownSelectMenu } from '@/components/DropdownSelectMenu'; +import { FormInput } from '@/components/FormInput'; +import { Icon, IconName } from '@/components/Icon'; +import { InputType } from '@/components/Input'; +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { VerticalSeparator } from '@/components/Separator'; +import { Tag } from '@/components/Tag'; +import { WithTooltip } from '@/components/WithTooltip'; + +import abacusStateManager from '@/lib/abacus'; +import { MustBigNumber, getNumberSign } from '@/lib/numbers'; + +type InputChangeType = InputType.Currency | InputType.Percent; + +type InputOrderFields = { + triggerPriceField: TriggerOrdersInputFields; + percentDiffField: TriggerOrdersInputFields; + usdcDiffField: TriggerOrdersInputFields; +}; + +type ElementProps = { + symbol: string; + tooltipId: string; + stringKeys: { + header: string; + headerDiff: string; + price: string; + output: string; + }; + inputOrderFields: InputOrderFields; + isMultiple: boolean; + isNegativeDiff: boolean; + price: Nullable; + tickSizeDecimals?: number; + onViewOrdersClick: () => void; +}; + +export const TriggerOrderInputs = ({ + symbol, + tooltipId, + stringKeys, + inputOrderFields, + isMultiple, + isNegativeDiff, + price, + tickSizeDecimals, + onViewOrdersClick, +}: ElementProps) => { + const stringGetter = useStringGetter(); + + const [inputType, setInputType] = useState(InputType.Percent); + + const { triggerPrice, percentDiff, usdcDiff } = price ?? {}; + + const clearPriceInputFields = () => { + abacusStateManager.setTriggerOrdersValue({ + value: null, + field: inputOrderFields.triggerPriceField, + }); + abacusStateManager.setTriggerOrdersValue({ + value: null, + field: inputOrderFields.percentDiffField, + }); + abacusStateManager.setTriggerOrdersValue({ + value: null, + field: inputOrderFields.usdcDiffField, + }); + }; + + const onTriggerPriceInput = ({ + floatValue, + formattedValue, + }: { + floatValue?: number; + formattedValue: string; + }) => { + const newPrice = MustBigNumber(floatValue).toFixed(tickSizeDecimals ?? USD_DECIMALS); + abacusStateManager.setTriggerOrdersValue({ + value: formattedValue === '' || newPrice === 'NaN' ? null : newPrice, + field: inputOrderFields.triggerPriceField, + }); + }; + + const onPercentageDiffInput = ({ + floatValue, + formattedValue, + }: { + floatValue?: number; + formattedValue: string; + }) => { + const newPercentage = MustBigNumber(floatValue).toFixed(PERCENT_DECIMALS); + abacusStateManager.setTriggerOrdersValue({ + value: formattedValue === '' || newPercentage === 'NaN' ? null : newPercentage, + field: inputOrderFields.percentDiffField, + }); + }; + + const onUsdcDiffInput = ({ + floatValue, + formattedValue, + }: { + floatValue?: number; + formattedValue: string; + }) => { + const newAmount = MustBigNumber(floatValue).toFixed(tickSizeDecimals ?? USD_DECIMALS); + abacusStateManager.setTriggerOrdersValue({ + value: formattedValue === '' || newAmount === 'NaN' ? null : newAmount, + field: inputOrderFields.usdcDiffField, + }); + }; + + const getDecimalsForInputType = (inputType: InputChangeType) => { + switch (inputType) { + case InputType.Currency: + return USD_DECIMALS; + case InputType.Percent: + return PERCENT_DECIMALS; + } + }; + + const getOutputDiffData = () => { + const formattedPercentDiff = percentDiff + ? MustBigNumber(percentDiff).div(100).toNumber() + : null; + + const outputType = inputType === InputType.Percent ? OutputType.Fiat : OutputType.Percent; + const value = outputType === OutputType.Fiat ? usdcDiff : formattedPercentDiff; + + return { + outputType, + outputValue: value && isNegativeDiff ? value * -1 : value, + }; + }; + + const signedOutput = () => { + const { outputType, outputValue } = getOutputDiffData(); + return ( + <$SignedOutput + sign={getNumberSign(outputValue)} + showSign={ShowSign.Both} + type={outputType} + value={outputValue} + /> + ); + }; + + const priceDiffSelector = ({ + value, + onValueChange, + }: { + value: InputChangeType; + onValueChange: (value: InputChangeType) => void; + }) => ( + + ); + + const multipleOrdersButton = () => ( + <$MultipleOrdersContainer> + {stringGetter({ key: STRING_KEYS.MULTIPLE_ORDERS_FOUND })} + <$ViewAllButton action={ButtonAction.Navigation} onClick={onViewOrdersClick}> + {stringGetter({ key: STRING_KEYS.VIEW_ORDERS })} + {<$ArrowIcon iconName={IconName.Arrow} />} + + + ); + + const headerTooltip = () => ( + {stringGetter({ key: stringKeys.header })} + ); + + return isMultiple ? ( + <$TriggerRow key={tooltipId}> + <$Heading>{headerTooltip()} + <$InlineRow>{multipleOrdersButton()} + + ) : ( + <$TriggerRow key={tooltipId}> + <$Heading> + {headerTooltip()} + <$HeadingInfo> + {stringGetter({ key: stringKeys.headerDiff })} + {signedOutput()} + <$VerticalSeparator /> + <$ClearButton + action={ButtonAction.Destroy} + size={ButtonSize.Base} + type={ButtonType.Button} + onClick={clearPriceInputFields} + > + {stringGetter({ key: STRING_KEYS.CLEAR })} + + + + <$InlineRow> + + {stringGetter({ key: stringKeys.price })} {symbol} + + } + type={InputType.Currency} + decimals={tickSizeDecimals} + value={triggerPrice} + onInput={onTriggerPriceInput} + allowNegative={true} + /> + setInputType(value), + })} + value={ + inputType === InputType.Percent + ? percentDiff + ? MustBigNumber(percentDiff).toFixed(PERCENT_DECIMALS) + : null + : usdcDiff + } + onInput={inputType === InputType.Percent ? onPercentageDiffInput : onUsdcDiffInput} + allowNegative={true} + /> + + + ); +}; +const $Heading = styled.div` + ${layoutMixins.spacedRow} +`; + +const $HeadingInfo = styled.div` + ${layoutMixins.row} + font: var(--font-base-book); + gap: 0.5em; + color: var(--color-text-0); +`; + +const $SignedOutput = styled(Output)<{ sign: NumberSign }>` + color: ${({ sign }) => + ({ + [NumberSign.Positive]: `var(--color-positive)`, + [NumberSign.Negative]: `var(--color-negative)`, + [NumberSign.Neutral]: `var(--color-text-2)`, + }[sign])}; +`; + +const $VerticalSeparator = styled(VerticalSeparator)` + && { + height: 1.5rem; + } +`; + +const $ClearButton = styled(Button)` + --button-backgroundColor: transparent; + --button-border: none; + --button-height: 1.5rem; + --button-textColor: var(--color-red); + --button-padding: 0; +`; + +const $TriggerRow = styled.div` + ${layoutMixins.column} + gap: 1ch; +`; + +const $InlineRow = styled.span` + ${layoutMixins.flexEqualColumns} + gap: 1ch; +`; + +const $MultipleOrdersContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25em 0.625em; + + border: var(--default-border-width) solid var(--color-border); + border-radius: 0.5em; + + color: var(--color-text-2); +`; + +const $ViewAllButton = styled(Button)` + color: var(--color-accent); +`; + +const $ArrowIcon = styled(Icon)` + stroke-width: 2; +`; diff --git a/src/views/forms/TriggersForm/TriggerOrdersInputs.tsx b/src/views/forms/TriggersForm/TriggerOrdersInputs.tsx new file mode 100644 index 000000000..fa8012c5c --- /dev/null +++ b/src/views/forms/TriggersForm/TriggerOrdersInputs.tsx @@ -0,0 +1,72 @@ +import { shallowEqual, useSelector } from 'react-redux'; + +import { TriggerOrdersInputField } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; + +import { getTriggerOrdersInputs } from '@/state/inputsSelectors'; + +import { TriggerOrderInputs } from './TriggerOrderInputs'; + +type ElementProps = { + symbol: string; + multipleTakeProfitOrders: boolean; + multipleStopLossOrders: boolean; + tickSizeDecimals?: number; + onViewOrdersClick: () => void; +}; + +export const TriggerOrdersInputs = ({ + symbol, + multipleTakeProfitOrders, + multipleStopLossOrders, + tickSizeDecimals, + onViewOrdersClick, +}: ElementProps) => { + const { stopLossOrder, takeProfitOrder } = + useSelector(getTriggerOrdersInputs, shallowEqual) || {}; + + return ( + <> + + + + ); +}; diff --git a/src/views/forms/TriggersForm/TriggersForm.tsx b/src/views/forms/TriggersForm/TriggersForm.tsx new file mode 100644 index 000000000..73bb24888 --- /dev/null +++ b/src/views/forms/TriggersForm/TriggersForm.tsx @@ -0,0 +1,188 @@ +import { FormEvent } from 'react'; + +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { ErrorType, ValidationError, type SubaccountOrder } from '@/constants/abacus'; +import { ButtonAction, ButtonType } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useSubaccount } from '@/hooks/useSubaccount'; +import { useTriggerOrdersFormInputs } from '@/hooks/useTriggerOrdersFormInputs'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType } from '@/components/Output'; +import { WithTooltip } from '@/components/WithTooltip'; + +import { calculateIsAccountViewOnly } from '@/state/accountCalculators'; +import { getPositionDetails } from '@/state/accountSelectors'; +import { closeDialog } from '@/state/dialogs'; + +import { getTradeInputAlert } from '@/lib/tradeData'; + +import { AdvancedTriggersOptions } from './AdvancedTriggersOptions'; +import { TriggerOrdersInputs } from './TriggerOrdersInputs'; + +type ElementProps = { + marketId: string; + stopLossOrders: SubaccountOrder[]; + takeProfitOrders: SubaccountOrder[]; + onViewOrdersClick: () => void; +}; + +export const TriggersForm = ({ + marketId, + stopLossOrders, + takeProfitOrders, + onViewOrdersClick, +}: ElementProps) => { + const stringGetter = useStringGetter(); + const dispatch = useDispatch(); + + const { placeTriggerOrders } = useSubaccount(); + const isAccountViewOnly = useSelector(calculateIsAccountViewOnly); + + const { asset, entryPrice, size, stepSizeDecimals, tickSizeDecimals, oraclePrice } = + useSelector(getPositionDetails(marketId)) || {}; + + const { + differingOrderSizes, + inputErrors, + inputSize, + existingStopLossOrder, + existingTakeProfitOrder, + existsLimitOrder, + } = useTriggerOrdersFormInputs({ + marketId, + positionSize: size?.current ?? null, + stopLossOrder: stopLossOrders.length === 1 ? stopLossOrders[0] : undefined, + takeProfitOrder: takeProfitOrders.length === 1 ? takeProfitOrders[0] : undefined, + }); + + const symbol = asset?.id ?? ''; + const multipleTakeProfitOrders = takeProfitOrders.length > 1; + const multipleStopLossOrders = stopLossOrders.length > 1; + + const hasInputErrors = inputErrors?.some( + (error: ValidationError) => error.type !== ErrorType.warning + ); + const inputAlert = getTradeInputAlert({ + abacusInputErrors: inputErrors ?? [], + stringGetter, + stepSizeDecimals, + tickSizeDecimals, + }); + + // The triggers form does not support editing multiple stop loss or take profit orders - so if both have + // multiple, we hide the triggers button CTA + const existsEditableOrCreatableOrders = !(multipleTakeProfitOrders && multipleStopLossOrders); + + const priceInfo = ( + <$PriceBox> + <$PriceRow> + <$PriceLabel>{stringGetter({ key: STRING_KEYS.AVG_ENTRY_PRICE })} + <$Price + type={OutputType.Fiat} + value={entryPrice?.current} + fractionDigits={tickSizeDecimals} + /> + + <$PriceRow> + <$PriceLabel>{stringGetter({ key: STRING_KEYS.ORACLE_PRICE })} + <$Price type={OutputType.Fiat} value={oraclePrice} fractionDigits={tickSizeDecimals} /> + + + ); + + const onSubmitOrders = async () => { + placeTriggerOrders(); + dispatch(closeDialog()); + }; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + onSubmitOrders(); + }; + + return ( + <$Form onSubmit={onSubmit}> + {priceInfo} + + {existsEditableOrCreatableOrders && ( + <> + + + <$Button + action={ButtonAction.Primary} + type={ButtonType.Submit} + state={{ isDisabled: hasInputErrors || isAccountViewOnly }} + slotLeft={hasInputErrors ? <$WarningIcon iconName={IconName.Warning} /> : undefined} + > + {hasInputErrors + ? stringGetter({ + key: inputAlert?.actionStringKey ?? STRING_KEYS.UNAVAILABLE, + }) + : !!(existingStopLossOrder || existingTakeProfitOrder) + ? stringGetter({ key: STRING_KEYS.ENTER_TRIGGERS }) + : stringGetter({ key: STRING_KEYS.ADD_TRIGGERS })} + + + + )} + + ); +}; +const $Form = styled.form` + ${layoutMixins.column} + gap: 1.25ch; +`; + +const $PriceBox = styled.div` + background-color: var(--color-layer-2); + border-radius: 0.5em; + font: var(--font-base-medium); + + display: grid; + gap: 0.625em; + padding: 0.625em 0.75em; +`; + +const $PriceRow = styled.div` + ${layoutMixins.spacedRow}; +`; + +const $PriceLabel = styled.h3` + color: var(--color-text-0); +`; + +const $Price = styled(Output)` + color: var(--color-text-2); +`; + +const $Button = styled(Button)` + width: 100%; +`; + +const $WarningIcon = styled(Icon)` + color: var(--color-warning); +`; diff --git a/src/views/menus/AccountMenu.tsx b/src/views/menus/AccountMenu.tsx index 5dd57324a..cfd9f9878 100644 --- a/src/views/menus/AccountMenu.tsx +++ b/src/views/menus/AccountMenu.tsx @@ -1,76 +1,109 @@ import { ElementType, memo } from 'react'; -import styled, { AnyStyledComponent, css } from 'styled-components'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; + +import { usePrivy } from '@privy-io/react-auth'; import type { Dispatch } from '@reduxjs/toolkit'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { OnboardingState } from '@/constants/account'; import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; import { DialogTypes } from '@/constants/dialogs'; -import { STRING_KEYS, StringGetterFunction, TOOLTIP_STRING_KEYS } from '@/constants/localization'; -import { DydxChainAsset, wallets } from '@/constants/wallets'; - -import { layoutMixins } from '@/styles/layoutMixins'; -import { headerMixins } from '@/styles/headerMixins'; - import { - useAccounts, - useBreakpoints, - useTokenConfigs, - useStringGetter, - useAccountBalance, - useURLConfigs, -} from '@/hooks'; - -import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; + STRING_KEYS, + TOOLTIP_STRING_KEYS, + type StringGetterFunction, +} from '@/constants/localization'; +import { isDev } from '@/constants/networks'; +import { DydxChainAsset, WalletType, wallets } from '@/constants/wallets'; + +import { useAccountBalance } from '@/hooks/useAccountBalance'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; + +import { DiscordIcon, GoogleIcon, TwitterIcon } from '@/icons'; +import { headerMixins } from '@/styles/headerMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { CopyButton } from '@/components/CopyButton'; import { DropdownMenu } from '@/components/DropdownMenu'; -import { Output, OutputType } from '@/components/Output'; import { Icon, IconName } from '@/components/Icon'; import { IconButton } from '@/components/IconButton'; +import { Output, OutputType } from '@/components/Output'; import { WithTooltip } from '@/components/WithTooltip'; - -import { AppTheme } from '@/state/configs'; -import { openDialog } from '@/state/dialogs'; +import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { getOnboardingState, getSubaccount } from '@/state/accountSelectors'; +import { AppTheme } from '@/state/configs'; import { getAppTheme } from '@/state/configsSelectors'; +import { openDialog } from '@/state/dialogs'; import { isTruthy } from '@/lib/isTruthy'; -import { truncateAddress } from '@/lib/wallet'; import { MustBigNumber } from '@/lib/numbers'; +import { truncateAddress } from '@/lib/wallet'; + import { getMobileAppUrl } from '../dialogs/MobileDownloadDialog'; export const AccountMenu = () => { const stringGetter = useStringGetter(); const { mintscanBase } = useURLConfigs(); const { isTablet } = useBreakpoints(); + const { complianceState } = useComplianceState(); + const dispatch = useDispatch(); const onboardingState = useSelector(getOnboardingState); - const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; + const { freeCollateral } = useSelector(getSubaccount, shallowEqual) ?? {}; + const { nativeTokenBalance } = useAccountBalance(); const { usdcLabel, chainTokenLabel } = useTokenConfigs(); const theme = useSelector(getAppTheme); const { evmAddress, walletType, dydxAddress, hdKey } = useAccounts(); - const usdcBalance = freeCollateral?.current || 0; + const privy = usePrivy(); + const { google, discord, twitter } = privy?.user ?? {}; + + const usdcBalance = freeCollateral?.current ?? 0; const onRecoverKeys = () => { dispatch(openDialog({ type: DialogTypes.Onboarding })); }; + let walletIcon; + if (onboardingState === OnboardingState.WalletConnected) { + walletIcon = <$WarningIcon iconName={IconName.Warning} />; + } else if ( + onboardingState === OnboardingState.AccountConnected && + walletType === WalletType.Privy + ) { + if (google) { + walletIcon = ; + } else if (discord) { + walletIcon = ; + } else if (twitter) { + walletIcon = ; + } else { + walletIcon = ; + } + } else if (walletType) { + walletIcon = ; + } + return onboardingState === OnboardingState.Disconnected ? ( ) : ( - - - - + <$AccountInfo> + <$AddressRow> + <$AssetIcon symbol="DYDX" /> + <$Column> @@ -86,15 +119,13 @@ export const AccountMenu = () => { } > - - {stringGetter({ key: STRING_KEYS.DYDX_CHAIN_ADDRESS })} - + <$label>{stringGetter({ key: STRING_KEYS.DYDX_CHAIN_ADDRESS })} - {truncateAddress(dydxAddress)} - - + <$Address>{truncateAddress(dydxAddress)} + + <$CopyButton buttonType="icon" value={dydxAddress} shape={ButtonShape.Square} /> - { type={ButtonType.Link} /> - - - {walletType && ( - - + + {walletType && walletType !== WalletType.Privy && ( + <$AddressRow> + <$SourceIcon> + <$ConnectorIcon iconName={IconName.AddressConnector} /> - - )} - - {stringGetter({ key: STRING_KEYS.SOURCE_ADDRESS })} - {truncateAddress(evmAddress, '0x')} - - - + + <$Column> + <$label>{stringGetter({ key: STRING_KEYS.SOURCE_ADDRESS })} + <$Address>{truncateAddress(evmAddress, '0x')} + + + )} + <$Balances>
    - + <$label> {stringGetter({ key: STRING_KEYS.ASSET_BALANCE, params: { ASSET: chainTokenLabel }, })} - - + + <$BalanceOutput type={OutputType.Asset} value={nativeTokenBalance} />
    {
    - + <$label> {stringGetter({ key: STRING_KEYS.ASSET_BALANCE, params: { ASSET: usdcLabel }, })} - - + + <$BalanceOutput type={OutputType.Asset} value={usdcBalance} fractionDigits={2} />
    -
    - + + ) } items={[ onboardingState === OnboardingState.WalletConnected && { value: 'ConnectToChain', label: ( - + <$ConnectToChain>

    {stringGetter({ key: STRING_KEYS.MISSING_KEYS_DESCRIPTION })}

    -
    + ), onSelect: onRecoverKeys, separator: true, @@ -190,6 +219,18 @@ export const AccountMenu = () => { label: stringGetter({ key: STRING_KEYS.DISPLAY_SETTINGS }), onSelect: () => dispatch(openDialog({ type: DialogTypes.DisplaySettings })), }, + ...(isDev + ? [ + { + value: 'ComplianceConfig', + icon: , + label: 'Compliance Config', + onSelect: () => { + dispatch(openDialog({ type: DialogTypes.ComplianceConfig })); + }, + }, + ] + : []), ...(getMobileAppUrl() ? [ { @@ -214,7 +255,7 @@ export const AccountMenu = () => { value: 'MnemonicExport', icon: , label: {stringGetter({ key: STRING_KEYS.EXPORT_SECRET_PHRASE })}, - highlightColor: 'destroy', + highlightColor: 'destroy' as const, onSelect: () => dispatch(openDialog({ type: DialogTypes.MnemonicExport })), }, ] @@ -223,20 +264,16 @@ export const AccountMenu = () => { value: 'Disconnect', icon: , label: stringGetter({ key: STRING_KEYS.DISCONNECT }), - highlightColor: 'destroy', + highlightColor: 'destroy' as const, onSelect: () => dispatch(openDialog({ type: DialogTypes.DisconnectWallet })), }, ].filter(isTruthy)} align="end" sideOffset={16} > - {onboardingState === OnboardingState.WalletConnected ? ( - - ) : onboardingState === OnboardingState.AccountConnected ? ( - walletType && - ) : null} - {!isTablet && {truncateAddress(dydxAddress)}} -
    + {walletIcon} + {!isTablet && <$Address>{truncateAddress(dydxAddress)}} + ); }; @@ -244,89 +281,90 @@ const AssetActions = memo( ({ asset, dispatch, + complianceState, withOnboarding, hasBalance, stringGetter, }: { asset: DydxChainAsset; dispatch: Dispatch; + complianceState: ComplianceStates; withOnboarding?: boolean; hasBalance?: boolean; stringGetter: StringGetterFunction; }) => ( - + <$InlineRow> {[ - withOnboarding && { - dialogType: DialogTypes.Deposit, - iconName: IconName.Deposit, - tooltipStringKey: STRING_KEYS.DEPOSIT, - }, + withOnboarding && + complianceState === ComplianceStates.FULL_ACCESS && { + dialogType: DialogTypes.Deposit, + iconName: IconName.Deposit, + tooltipStringKey: STRING_KEYS.DEPOSIT, + }, withOnboarding && hasBalance && { dialogType: DialogTypes.Withdraw, iconName: IconName.Withdraw, tooltipStringKey: STRING_KEYS.WITHDRAW, }, - hasBalance && { - dialogType: DialogTypes.Transfer, - dialogProps: { selectedAsset: asset }, - iconName: IconName.Send, - tooltipStringKey: STRING_KEYS.TRANSFER, - }, + hasBalance && + complianceState === ComplianceStates.FULL_ACCESS && { + dialogType: DialogTypes.Transfer, + dialogProps: { selectedAsset: asset }, + iconName: IconName.Send, + tooltipStringKey: STRING_KEYS.TRANSFER, + }, ] .filter(isTruthy) .map(({ iconName, tooltipStringKey, dialogType, dialogProps }) => ( - - dispatch(openDialog({ type: dialogType, dialogProps }))} /> - + ))} - + ) ); - -const Styled: Record = {}; - -Styled.AccountInfo = styled.div` +const $AccountInfo = styled.div` ${layoutMixins.flexColumn} gap: 1rem; padding: 1rem 1rem 0.5rem 1rem; `; -Styled.Column = styled.div` +const $Column = styled.div` ${layoutMixins.column} `; -Styled.InlineRow = styled.div` +const $InlineRow = styled.div` ${layoutMixins.inlineRow} `; -Styled.AddressRow = styled.div` +const $AddressRow = styled.div` ${layoutMixins.row} gap: 0.5rem; - ${Styled.Column} { + ${$Column} { margin-right: 0.5rem; } `; -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` z-index: 2; font-size: 1.75rem; `; -Styled.SourceIcon = styled.div` +const $SourceIcon = styled.div` padding: 0.375rem; position: relative; z-index: 1; @@ -338,13 +376,13 @@ Styled.SourceIcon = styled.div` background-color: #303045; `; -Styled.ConnectorIcon = styled(Icon)` +const $ConnectorIcon = styled(Icon)` position: absolute; top: -1.625rem; height: 1.75rem; `; -Styled.label = styled.div` +const $label = styled.div` ${layoutMixins.row} gap: 0.25rem; @@ -356,7 +394,7 @@ Styled.label = styled.div` } `; -Styled.Balances = styled.div` +const $Balances = styled.div` ${layoutMixins.flexColumn} > div { @@ -377,28 +415,28 @@ Styled.Balances = styled.div` } `; -Styled.BalanceOutput = styled(Output)` +const $BalanceOutput = styled(Output)` font-size: var(--fontSize-medium); `; -Styled.DropdownMenu = styled(DropdownMenu)` +const $DropdownMenu = styled(DropdownMenu)` ${headerMixins.dropdownTrigger} --dropdownMenu-item-font-size: 0.875rem; --popover-padding: 0 0 0.5rem 0; -`; +` as typeof DropdownMenu; -Styled.WarningIcon = styled(Icon)` +const $WarningIcon = styled(Icon)` font-size: 1.25rem; color: var(--color-warning); `; -Styled.Address = styled.span` +const $Address = styled.span` font: var(--font-base-book); font-feature-settings: var(--fontFeature-monoNumbers); `; -Styled.ConnectToChain = styled(Styled.Column)` +const $ConnectToChain = styled($Column)` max-width: 12em; gap: 0.5rem; text-align: center; @@ -409,22 +447,23 @@ Styled.ConnectToChain = styled(Styled.Column)` } `; -Styled.IconButton = styled(IconButton)<{ iconName: IconName }>` +const $IconButton = styled(IconButton)` --button-padding: 0 0.25rem; --button-border: solid var(--border-width) var(--color-layer-6); ${({ iconName }) => + iconName != null && [IconName.Withdraw, IconName.Deposit].includes(iconName) && css` --button-icon-size: 1.375em; `} `; -Styled.CopyButton = styled(CopyButton)` +const $CopyButton = styled(CopyButton)` --button-padding: 0 0.25rem; --button-border: solid var(--border-width) var(--color-layer-6); `; -Styled.WithTooltip = styled(WithTooltip)` +const $WithTooltip = styled(WithTooltip)` --tooltip-backgroundColor: var(--color-layer-5); `; diff --git a/src/views/menus/LanguageSelector.tsx b/src/views/menus/LanguageSelector.tsx index a845d739d..d2561fe41 100644 --- a/src/views/menus/LanguageSelector.tsx +++ b/src/views/menus/LanguageSelector.tsx @@ -1,13 +1,13 @@ import { useDispatch, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; + +import { SUPPORTED_LOCALE_STRING_LABELS, SupportedLocales } from '@/constants/localization'; -import { SupportedLocales, SUPPORTED_LOCALE_STRING_LABELS } from '@/constants/localization'; import { headerMixins } from '@/styles/headerMixins'; import { DropdownSelectMenu } from '@/components/DropdownSelectMenu'; import { setSelectedLocale } from '@/state/localization'; - import { getSelectedLocale } from '@/state/localizationSelectors'; type StyleProps = { @@ -25,7 +25,7 @@ export const LanguageSelector = ({ align, sideOffset }: StyleProps) => { const selectedLocale = useSelector(getSelectedLocale); return ( - dispatch(setSelectedLocale({ locale }))} @@ -34,9 +34,6 @@ export const LanguageSelector = ({ align, sideOffset }: StyleProps) => { /> ); }; - -const Styled: Record = {}; - -Styled.DropdownSelectMenu = styled(DropdownSelectMenu)` +const $DropdownSelectMenu = styled(DropdownSelectMenu)` ${headerMixins.dropdownTrigger} -`; +` as typeof DropdownSelectMenu; diff --git a/src/views/menus/NetworkSelectMenu.tsx b/src/views/menus/NetworkSelectMenu.tsx index 7a23a1cc7..105b249cb 100644 --- a/src/views/menus/NetworkSelectMenu.tsx +++ b/src/views/menus/NetworkSelectMenu.tsx @@ -1,11 +1,11 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; + +import { useSelectedNetwork } from '@/hooks/useSelectedNetwork'; -import { useSelectedNetwork } from '@/hooks'; -import { layoutMixins } from '@/styles/layoutMixins'; import { headerMixins } from '@/styles/headerMixins'; +import { layoutMixins } from '@/styles/layoutMixins'; import { DropdownSelectMenu } from '@/components/DropdownSelectMenu'; - import { useNetworks } from '@/views/menus/useNetworks'; type StyleProps = { @@ -18,7 +18,7 @@ export const NetworkSelectMenu = ({ align, sideOffset }: StyleProps) => { const { switchNetwork, selectedNetwork } = useSelectedNetwork(); return ( - { /> ); }; - -const Styled: Record = {}; - -Styled.DropdownSelectMenu = styled(DropdownSelectMenu)` +const $DropdownSelectMenu = styled(DropdownSelectMenu)` ${headerMixins.dropdownTrigger} width: max-content; @@ -41,4 +38,4 @@ Styled.DropdownSelectMenu = styled(DropdownSelectMenu)` min-width: 0; white-space: nowrap; } -`; +` as typeof DropdownSelectMenu; diff --git a/src/views/menus/NotificationsMenu.tsx b/src/views/menus/NotificationsMenu.tsx index e07a9c104..aa27420f0 100644 --- a/src/views/menus/NotificationsMenu.tsx +++ b/src/views/menus/NotificationsMenu.tsx @@ -1,14 +1,17 @@ import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import { layoutMixins } from '@/styles/layoutMixins'; + import { groupBy } from 'lodash'; +import styled from 'styled-components'; import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; -import { type Notification, NotificationStatus } from '@/constants/notifications'; +import { NotificationStatus, type Notification } from '@/constants/notifications'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useNotifications } from '@/hooks/useNotifications'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu'; @@ -66,14 +69,14 @@ export const NotificationsMenu = ({ () => (Object.entries(notificationsByStatus) as unknown as [NotificationStatus, Notification[]][]) .filter(([status]) => status < NotificationStatus.Cleared) - .map(([status, notifications]) => ({ + .map(([status, innerNotifications]) => ({ group: status, groupLabel: { [NotificationStatus.Triggered]: stringGetter({ key: STRING_KEYS.NEW }), [NotificationStatus.Seen]: 'Seen', }[status as number], - items: notifications + items: innerNotifications .sort( (n1, n2) => n2.timestamps[NotificationStatus.Triggered]! - @@ -95,6 +98,7 @@ export const NotificationsMenu = ({ slotTitle={displayData.title} slotDescription={displayData.body} notification={notification} + withClose={displayData.withClose} /> ), disabled: notification.status === NotificationStatus.Cleared, @@ -104,7 +108,7 @@ export const NotificationsMenu = ({ }, })), })) - .filter(({ items }) => items.length), + .filter(({ items: allItems }) => allItems.length), [notificationsByStatus, getDisplayData, onNotificationAction, markSeen, stringGetter] ); diff --git a/src/views/menus/useGlobalCommands.tsx b/src/views/menus/useGlobalCommands.tsx index 504c052d9..c04f281d6 100644 --- a/src/views/menus/useGlobalCommands.tsx +++ b/src/views/menus/useGlobalCommands.tsx @@ -1,23 +1,25 @@ -import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; -import { type MenuConfig } from '@/constants/menus'; +import { Asset, PerpetualMarket } from '@/constants/abacus'; import { TradeLayouts } from '@/constants/layout'; +import { type MenuConfig } from '@/constants/menus'; +import { AppRoute } from '@/constants/routes'; import { AssetIcon } from '@/components/AssetIcon'; +import { getAssets } from '@/state/assetsSelectors'; import { + AppColorMode, AppTheme, AppThemeSystemSetting, - AppColorMode, - setAppThemeSetting, setAppColorMode, + setAppThemeSetting, } from '@/state/configs'; import { setSelectedTradeLayout } from '@/state/layout'; - -import { getAssets } from '@/state/assetsSelectors'; import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; -import { Asset, PerpetualMarket } from '@/constants/abacus'; + +import { orEmptyObj } from '@/lib/typeUtils'; enum LayoutItems { setDefaultLayout = 'SetDefaultLayout', @@ -25,12 +27,6 @@ enum LayoutItems { setAlternativeLayout = 'SetAlternativeLayout', } -enum TradeItems { - PlaceMarketOrder = 'PlaceMarketOrder', - PlaceLimitOrder = 'PlaceLimitOrder', - PlaceStopLimitOrder = 'PlaceStopLimitOrder', -} - enum NavItems { NavigateToMarket = 'NavigateToMarket', } @@ -39,12 +35,12 @@ export const useGlobalCommands = (): MenuConfig => { const dispatch = useDispatch(); const navigate = useNavigate(); - const allPerpetualMarkets = useSelector(getPerpetualMarkets, shallowEqual) || {}; - const allAssets = useSelector(getAssets, shallowEqual) || {}; + const allPerpetualMarkets = orEmptyObj(useSelector(getPerpetualMarkets, shallowEqual)); + const allAssets = orEmptyObj(useSelector(getAssets, shallowEqual)); const joinedPerpetualMarketsAndAssets = Object.values(allPerpetualMarkets).map((market) => ({ ...market, - ...allAssets[market?.assetId], + ...(market != null ? allAssets[market.assetId] : {}), })) as Array; return [ @@ -162,7 +158,7 @@ export const useGlobalCommands = (): MenuConfig => { slotBefore: , label: name ?? '', tag: id, - onSelect: () => navigate(`/trade/${market}`), + onSelect: () => navigate(`${AppRoute.Trade}/${market}`), })), }, ], diff --git a/src/views/menus/useNetworks.tsx b/src/views/menus/useNetworks.tsx index a15a2497d..d52483921 100644 --- a/src/views/menus/useNetworks.tsx +++ b/src/views/menus/useNetworks.tsx @@ -1,8 +1,8 @@ import { type MenuItem } from '@/constants/menus'; import { AVAILABLE_ENVIRONMENTS, - type DydxNetwork, ENVIRONMENT_CONFIG_MAP, + type DydxNetwork, } from '@/constants/networks'; export const useNetworks = (): MenuItem[] => diff --git a/src/views/notifications/BlockRewardNotification/index.tsx b/src/views/notifications/BlockRewardNotification/index.tsx index fce36eedd..f4acf5e11 100644 --- a/src/views/notifications/BlockRewardNotification/index.tsx +++ b/src/views/notifications/BlockRewardNotification/index.tsx @@ -1,12 +1,14 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { Details } from '@/components/Details'; +import { Icon, IconName } from '@/components/Icon'; +// eslint-disable-next-line import/no-cycle import { Notification, NotificationProps } from '@/components/Notification'; import { Output, OutputType } from '@/components/Output'; -import { Icon, IconName } from '@/components/Icon'; type ElementProps = { data: { @@ -28,23 +30,19 @@ export const BlockRewardNotification = ({ const { BLOCK_REWARD_AMOUNT, TOKEN_NAME } = data; return ( - } slotTitle={stringGetter({ key: STRING_KEYS.TRADING_REWARD_RECEIVED })} slotCustomContent={ - + <$Output type={OutputType.Asset} value={BLOCK_REWARD_AMOUNT} tag={TOKEN_NAME} /> ), }, ]} @@ -53,10 +51,7 @@ export const BlockRewardNotification = ({ /> ); }; - -const Styled: Record = {}; - -Styled.Details = styled(Details)` +const $Details = styled(Details)` --details-item-height: 1.5rem; dd { @@ -64,12 +59,12 @@ Styled.Details = styled(Details)` } `; -Styled.Notification = styled(Notification)` +const $Notification = styled(Notification)` background-image: url('/dots-background-2.svg'); background-size: cover; `; -Styled.Output = styled(Output)` +const $Output = styled(Output)` &:before { content: '+'; color: var(--color-success); diff --git a/src/views/notifications/IncentiveSeasonDistributionNotification.tsx b/src/views/notifications/IncentiveSeasonDistributionNotification.tsx new file mode 100644 index 000000000..aa759fcce --- /dev/null +++ b/src/views/notifications/IncentiveSeasonDistributionNotification.tsx @@ -0,0 +1,64 @@ +import styled from 'styled-components'; + +import { Details } from '@/components/Details'; +import { Icon, IconName } from '@/components/Icon'; +// eslint-disable-next-line import/no-cycle +import { Notification, NotificationProps } from '@/components/Notification'; +import { Output, OutputType } from '@/components/Output'; + +type ElementProps = { + data: { + points: number; + chainTokenLabel: string; + }; +}; + +export type IncentiveSeasonDistributionNotificationProps = NotificationProps & ElementProps; + +export const IncentiveSeasonDistributionNotification = ({ + isToast, + data, + notification, +}: IncentiveSeasonDistributionNotificationProps) => { + const { chainTokenLabel, points } = data; + + return ( + <$Notification + isToast={isToast} + notification={notification} + slotIcon={} + slotTitle="Season 3 launch rewards have been distributed!" + slotCustomContent={ + <$Details + items={[ + { + key: 'season_distribution', + label: 'Season 3 rewards', + value: <$Output type={OutputType.Asset} value={points} tag={chainTokenLabel} />, + }, + ]} + /> + } + /> + ); +}; +const $Details = styled(Details)` + --details-item-height: 1.5rem; + + dd { + color: var(--color-text-2); + } +`; + +const $Notification = styled(Notification)` + background-image: url('/dots-background-2.svg'); + background-size: cover; +`; + +const $Output = styled(Output)` + &:before { + content: '+'; + color: var(--color-success); + margin-right: 0.5ch; + } +`; diff --git a/src/views/notifications/OrderCancelNotification.tsx b/src/views/notifications/OrderCancelNotification.tsx new file mode 100644 index 000000000..e6b4c1c40 --- /dev/null +++ b/src/views/notifications/OrderCancelNotification.tsx @@ -0,0 +1,98 @@ +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { AbacusOrderStatus } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; +import { CancelOrderStatuses, LocalCancelOrderData, ORDER_TYPE_STRINGS } from '@/constants/trade'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Icon, IconName } from '@/components/Icon'; +import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; +// eslint-disable-next-line import/no-cycle +import { Notification, NotificationProps } from '@/components/Notification'; + +import { getOrderById } from '@/state/accountSelectors'; +import { getMarketData } from '@/state/perpetualsSelectors'; + +import { getTradeType } from '@/lib/orders'; + +import { OrderStatusIcon } from '../OrderStatusIcon'; + +type ElementProps = { + localCancel: LocalCancelOrderData; +}; + +export const OrderCancelNotification = ({ + isToast, + localCancel, + notification, +}: NotificationProps & ElementProps) => { + const stringGetter = useStringGetter(); + const order = useSelector(getOrderById(localCancel.orderId), shallowEqual)!!; + const marketData = useSelector(getMarketData(order.marketId), shallowEqual); + const { assetId } = marketData ?? {}; + const tradeType = getTradeType(order.type.rawValue) ?? undefined; + const orderTypeKey = tradeType && ORDER_TYPE_STRINGS[tradeType]?.orderTypeKey; + const indexedOrderStatus = order.status.rawValue; + const cancelStatus = localCancel.submissionStatus; + + let orderStatusStringKey = STRING_KEYS.CANCELING; + let orderStatusIcon = <$LoadingSpinner />; + let customContent = null; + + // whichever canceled confirmation happens first (node / indexer) + const canceledStatusValue = AbacusOrderStatus.cancelled.rawValue; + if (cancelStatus === CancelOrderStatuses.Canceled || indexedOrderStatus === canceledStatusValue) { + orderStatusStringKey = STRING_KEYS.CANCELED; + orderStatusIcon = <$OrderStatusIcon status={canceledStatusValue} />; + } + + if (localCancel.errorStringKey) { + orderStatusStringKey = STRING_KEYS.ERROR; + orderStatusIcon = <$WarningIcon iconName={IconName.Warning} />; + customContent = {stringGetter({ key: localCancel.errorStringKey })}; + } + + return ( + } + slotTitle={orderTypeKey && stringGetter({ key: orderTypeKey })} + slotTitleRight={ + <$OrderStatus> + {stringGetter({ key: orderStatusStringKey })} + {orderStatusIcon} + + } + slotCustomContent={customContent} + /> + ); +}; +const $Label = styled.span` + ${layoutMixins.row} + gap: 0.5ch; +`; + +const $OrderStatus = styled($Label)` + color: var(--color-text-0); + font: var(--font-small-book); +`; + +const $LoadingSpinner = styled(LoadingSpinner)` + --spinner-width: 0.9375rem; + color: var(--color-accent); +`; + +const $WarningIcon = styled(Icon)` + color: var(--color-warning); +`; + +const $OrderStatusIcon = styled(OrderStatusIcon)` + width: 0.9375rem; + height: 0.9375rem; +`; diff --git a/src/views/notifications/OrderStatusNotification.tsx b/src/views/notifications/OrderStatusNotification.tsx new file mode 100644 index 000000000..d58955ed5 --- /dev/null +++ b/src/views/notifications/OrderStatusNotification.tsx @@ -0,0 +1,134 @@ +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { + AbacusOrderStatus, + KotlinIrEnumValues, + ORDER_SIDES, + ORDER_STATUS_STRINGS, +} from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; +import { USD_DECIMALS } from '@/constants/numbers'; +import { + ORDER_TYPE_STRINGS, + PlaceOrderStatuses, + type LocalPlaceOrderData, +} from '@/constants/trade'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Icon, IconName } from '@/components/Icon'; +import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; +// eslint-disable-next-line import/no-cycle +import { Notification, NotificationProps } from '@/components/Notification'; + +import { getFillByClientId, getOrderByClientId } from '@/state/accountSelectors'; +import { getMarketData } from '@/state/perpetualsSelectors'; + +import { assertNever } from '@/lib/assertNever'; +import { getTradeType } from '@/lib/orders'; + +import { OrderStatusIcon } from '../OrderStatusIcon'; +import { FillDetails } from './TradeNotification/FillDetails'; + +type ElementProps = { + localOrder: LocalPlaceOrderData; +}; + +export const OrderStatusNotification = ({ + isToast, + localOrder, + notification, +}: NotificationProps & ElementProps) => { + const stringGetter = useStringGetter(); + const order = useSelector(getOrderByClientId(localOrder.clientId), shallowEqual); + const fill = useSelector(getFillByClientId(localOrder.clientId), shallowEqual); + const marketData = useSelector(getMarketData(localOrder.marketId), shallowEqual); + const { assetId } = marketData ?? {}; + const titleKey = ORDER_TYPE_STRINGS[localOrder.orderType]?.orderTypeKey; + const indexedOrderStatus = order?.status?.rawValue as KotlinIrEnumValues< + typeof AbacusOrderStatus + >; + const submissionStatus = localOrder.submissionStatus; + + let orderStatusStringKey = STRING_KEYS.SUBMITTING; + let orderStatusIcon = <$LoadingSpinner />; + let customContent = null; + + switch (submissionStatus) { + case PlaceOrderStatuses.Placed: + case PlaceOrderStatuses.Filled: + if (indexedOrderStatus) { + // skip pending / best effort open state -> still show as submitted (loading) + if (indexedOrderStatus === AbacusOrderStatus.pending.rawValue) break; + + orderStatusStringKey = ORDER_STATUS_STRINGS[indexedOrderStatus]; + orderStatusIcon = <$OrderStatusIcon status={indexedOrderStatus} />; + } + if (order && fill) { + customContent = ( + + ); + } + break; + case PlaceOrderStatuses.Submitted: + if (localOrder.errorStringKey) { + orderStatusStringKey = STRING_KEYS.ERROR; + orderStatusIcon = <$WarningIcon iconName={IconName.Warning} />; + customContent = {stringGetter({ key: localOrder.errorStringKey })}; + } + break; + default: + assertNever(submissionStatus); + break; + } + + return ( + } + slotTitle={titleKey && stringGetter({ key: titleKey })} + slotTitleRight={ + <$OrderStatus> + {stringGetter({ key: orderStatusStringKey })} + {orderStatusIcon} + + } + slotCustomContent={customContent} + /> + ); +}; +const $Label = styled.span` + ${layoutMixins.row} + gap: 0.5ch; +`; + +const $OrderStatus = styled($Label)` + color: var(--color-text-0); + font: var(--font-small-book); +`; + +const $LoadingSpinner = styled(LoadingSpinner)` + --spinner-width: 0.9375rem; + color: var(--color-accent); +`; + +const $WarningIcon = styled(Icon)` + color: var(--color-warning); +`; + +const $OrderStatusIcon = styled(OrderStatusIcon)` + width: 0.9375rem; + height: 0.9375rem; +`; diff --git a/src/views/notifications/TradeNotification/FillDetails.tsx b/src/views/notifications/TradeNotification/FillDetails.tsx new file mode 100644 index 000000000..387425a11 --- /dev/null +++ b/src/views/notifications/TradeNotification/FillDetails.tsx @@ -0,0 +1,81 @@ +import { OrderSide } from '@dydxprotocol/v4-client-js'; +import styled from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; +import { TradeTypes } from '@/constants/trade'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Details } from '@/components/Details'; +import { OrderSideTag } from '@/components/OrderSideTag'; +import { Output, OutputType } from '@/components/Output'; + +export const FillDetails = ({ + orderSide, + tradeType, + filledAmount, + assetId, + averagePrice, + tickSizeDecimals, +}: { + orderSide: OrderSide; + tradeType?: TradeTypes; + filledAmount: any; + assetId?: string; + averagePrice?: any; + tickSizeDecimals?: number; +}) => { + const stringGetter = useStringGetter(); + return ( + <$Details + items={[ + { + key: 'size', + label: ( + <$Label> + {stringGetter({ key: STRING_KEYS.SIZE })} + + + ), + value: , + }, + { + key: 'price', + label: stringGetter({ key: STRING_KEYS.PRICE }), + value: + tradeType === TradeTypes.MARKET ? ( + {stringGetter({ key: STRING_KEYS.MARKET_ORDER_SHORT })} + ) : ( + + ), + }, + ]} + /> + ); +}; +const $Label = styled.span` + ${layoutMixins.row} + gap: 0.5ch; +`; + +const $Details = styled(Details)` + --details-item-height: 1rem; + + dd { + color: var(--color-text-2); + } + + div { + padding: 0.25rem 0; + } + + div:last-of-type { + padding-bottom: 0; + } +`; diff --git a/src/views/notifications/TradeNotification/index.tsx b/src/views/notifications/TradeNotification/index.tsx index b723f13e6..526e9faa3 100644 --- a/src/views/notifications/TradeNotification/index.tsx +++ b/src/views/notifications/TradeNotification/index.tsx @@ -1,6 +1,6 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { AbacusOrderStatus, @@ -9,21 +9,23 @@ import { ORDER_STATUS_STRINGS, TRADE_TYPES, } from '@/constants/abacus'; - import { STRING_KEYS } from '@/constants/localization'; -import { ORDER_TYPE_STRINGS, TradeTypes } from '@/constants/trade'; -import { useStringGetter } from '@/hooks'; +import { USD_DECIMALS } from '@/constants/numbers'; +import { ORDER_TYPE_STRINGS } from '@/constants/trade'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; -import { Details } from '@/components/Details'; +// eslint-disable-next-line import/no-cycle import { Notification, NotificationProps } from '@/components/Notification'; -import { OrderSideTag } from '@/components/OrderSideTag'; import { OrderStatusIcon } from '@/views/OrderStatusIcon'; -import { Output, OutputType } from '@/components/Output'; import { getMarketData } from '@/state/perpetualsSelectors'; +import { FillDetails } from './FillDetails'; + type ElementProps = { data: { AMOUNT: string; @@ -47,7 +49,7 @@ export const TradeNotification = ({ isToast, data, notification }: TradeNotifica const marketData = useSelector(getMarketData(MARKET), shallowEqual); const { assetId } = marketData ?? {}; const orderType = ORDER_TYPE as KotlinIrEnumValues; - const tradeType = TRADE_TYPES[orderType]; + const tradeType = TRADE_TYPES[orderType] ?? undefined; const titleKey = tradeType && ORDER_TYPE_STRINGS[tradeType]?.orderTypeKey; const orderStatus = ORDER_STATUS as KotlinIrEnumValues; @@ -58,70 +60,35 @@ export const TradeNotification = ({ isToast, data, notification }: TradeNotifica slotIcon={} slotTitle={titleKey && stringGetter({ key: titleKey })} slotTitleRight={ - + <$OrderStatus> {stringGetter({ key: ORDER_STATUS_STRINGS[orderStatus] })} - - + <$OrderStatusIcon status={orderStatus} /> + } slotCustomContent={ - - {stringGetter({ key: STRING_KEYS.SIZE })} - - - ), - value: ( - - ), - }, - { - key: 'price', - label: stringGetter({ key: STRING_KEYS.PRICE }), - value: - ORDER_TYPE === TradeTypes.MARKET ? ( - {stringGetter({ key: STRING_KEYS.MARKET_ORDER_SHORT })} - ) : ( - - ), - }, - ]} + } /> ); }; - -const Styled: Record = {}; - -Styled.Label = styled.span` +const $Label = styled.span` ${layoutMixins.row} gap: 0.5ch; `; -Styled.OrderStatus = styled(Styled.Label)` +const $OrderStatus = styled($Label)` color: var(--color-text-0); font: var(--font-small-book); `; -Styled.OrderStatusIcon = styled(OrderStatusIcon)` +const $OrderStatusIcon = styled(OrderStatusIcon)` width: 0.9375rem; height: 0.9375rem; `; - -Styled.Details = styled(Details)` - --details-item-height: 1.5rem; - - dd { - color: var(--color-text-2); - } -`; diff --git a/src/views/notifications/TransferStatusNotification/TransferStatusSteps.tsx b/src/views/notifications/TransferStatusNotification/TransferStatusSteps.tsx index f522a062d..16775ab27 100644 --- a/src/views/notifications/TransferStatusNotification/TransferStatusSteps.tsx +++ b/src/views/notifications/TransferStatusNotification/TransferStatusSteps.tsx @@ -1,17 +1,19 @@ import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import styled, { css, type AnyStyledComponent } from 'styled-components'; + import { StatusResponse } from '@0xsquid/sdk'; +import { useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; import { TransferNotificationTypes } from '@/constants/notifications'; -import { useStringGetter, useURLConfigs } from '@/hooks'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; import { layoutMixins } from '@/styles/layoutMixins'; -import { Link } from '@/components/Link'; import { Icon, IconName } from '@/components/Icon'; +import { Link } from '@/components/Link'; import { LoadingDots } from '@/components/Loading/LoadingDots'; import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; @@ -43,9 +45,12 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps & const fromChain = status?.fromChain?.chainData?.chainId; const toChain = status?.toChain?.chainData?.chainId; - const currentStatus = routeStatus?.[routeStatus?.length - 1]; + const currentStatus = + routeStatus != null && routeStatus.length != null + ? routeStatus[routeStatus.length - 1] + : undefined; - const steps = [ + const newSteps = [ { label: stringGetter({ key: @@ -89,26 +94,26 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps & }, ]; - let currentStep = TransferStatusStep.Bridge; + let newCurrentStep = TransferStatusStep.Bridge; if (!routeStatus?.length) { - currentStep = TransferStatusStep.FromChain; + newCurrentStep = TransferStatusStep.FromChain; } else if (currentStatus.chainId === toChain) { - currentStep = + newCurrentStep = currentStatus.status !== 'success' ? TransferStatusStep.ToChain : TransferStatusStep.Complete; } else if (currentStatus.chainId === fromChain && currentStatus.status !== 'success') { - currentStep = TransferStatusStep.FromChain; + newCurrentStep = TransferStatusStep.FromChain; } if (status?.squidTransactionStatus === 'success') { - currentStep = TransferStatusStep.Complete; + newCurrentStep = TransferStatusStep.Complete; } return { - currentStep, - steps, + currentStep: newCurrentStep, + steps: newSteps, type, }; }, [status, stringGetter]); @@ -116,57 +121,54 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps & if (!status) return ; return ( - + <$BridgingStatus className={className}> {steps.map((step) => ( - - + <$Step key={step.step}> + <$row> {step.step === currentStep ? ( - - - + <$Icon> + <$Spinner /> + ) : step.step < currentStep ? ( - + <$Icon state="complete"> - + ) : ( - {step.step + 1} + <$Icon state="default">{step.step + 1} )} {step.link && currentStep >= step.step ? ( - = step.step}> + <$Label highlighted={currentStep >= step.step}> {step.label} - + ) : ( - = step.step}>{step.label} + <$Label highlighted={currentStep >= step.step}>{step.label} )} - - + + ))} - + ); }; - -const Styled: Record = {}; - -Styled.BridgingStatus = styled.div` +const $BridgingStatus = styled.div` ${layoutMixins.flexColumn}; gap: 1rem; padding: 1rem 0; `; -Styled.Step = styled.div` +const $Step = styled.div` ${layoutMixins.spacedRow}; `; -Styled.row = styled.div` +const $row = styled.div` ${layoutMixins.inlineRow}; gap: 0.5rem; `; -Styled.Icon = styled.div<{ state: 'complete' | 'default' }>` +const $Icon = styled.div<{ state?: 'complete' | 'default' }>` display: flex; align-items: center; justify-content: center; @@ -179,23 +181,25 @@ Styled.Icon = styled.div<{ state: 'complete' | 'default' }>` background-color: var(--color-layer-3); ${({ state }) => - ({ - ['complete']: css` - color: var(--color-success); - `, - ['default']: css` - color: var(--color-text-0); - `, - }[state])} + state == null + ? undefined + : { + complete: css` + color: var(--color-success); + `, + default: css` + color: var(--color-text-0); + `, + }[state]} `; -Styled.Spinner = styled(LoadingSpinner)` +const $Spinner = styled(LoadingSpinner)` --spinner-width: 1.25rem; color: var(--color-accent); `; -Styled.Label = styled(Styled.row)<{ highlighted?: boolean }>` +const $Label = styled($row)<{ highlighted?: boolean }>` ${({ highlighted }) => highlighted ? css` diff --git a/src/views/notifications/TransferStatusNotification/index.tsx b/src/views/notifications/TransferStatusNotification/index.tsx index eb8c5abef..9bedeafbc 100644 --- a/src/views/notifications/TransferStatusNotification/index.tsx +++ b/src/views/notifications/TransferStatusNotification/index.tsx @@ -1,23 +1,26 @@ -import { useCallback, useState, useMemo, MouseEvent } from 'react'; -import styled, { css, type AnyStyledComponent } from 'styled-components'; +import { MouseEvent, useCallback, useState } from 'react'; -import { useInterval, useStringGetter } from '@/hooks'; +import styled, { css } from 'styled-components'; import { AlertType } from '@/constants/alerts'; import { STRING_KEYS } from '@/constants/localization'; import { TransferNotifcation, TransferNotificationTypes } from '@/constants/notifications'; -import { formatSeconds } from '@/lib/timeUtils'; +import { useInterval } from '@/hooks/useInterval'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; import { Collapsible } from '@/components/Collapsible'; import { Icon, IconName } from '@/components/Icon'; import { LoadingDots } from '@/components/Loading/LoadingDots'; +// eslint-disable-next-line import/no-cycle import { Notification, NotificationProps } from '@/components/Notification'; import { Output, OutputType } from '@/components/Output'; import { WithReceipt } from '@/components/WithReceipt'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { formatSeconds } from '@/lib/timeUtils'; import { TransferStatusSteps } from './TransferStatusSteps'; @@ -32,7 +35,6 @@ export const TransferStatusNotification = ({ notification, slotIcon, slotTitle, - slotDescription, transfer, type, triggeredAt = Date.now(), @@ -40,15 +42,15 @@ export const TransferStatusNotification = ({ const stringGetter = useStringGetter(); const [open, setOpen] = useState(false); const [secondsLeft, setSecondsLeft] = useState(0); - const { fromChainId, status, txHash, toAmount, isExchange } = transfer; + const { status, toAmount, isExchange } = transfer; // @ts-ignore status.errors is not in the type definition but can be returned const error = status?.errors?.length ? status?.errors[0] : status?.error; const hasError = error && Object.keys(error).length !== 0; const updateSecondsLeft = useCallback(() => { - const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000; - const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000; + const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration ?? 0) * 1000; + const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration ?? 0) * 1000; setSecondsLeft(Math.floor((triggeredAt + fromChainEta + toChainEta - Date.now()) / 1000)); }, [status]); @@ -76,20 +78,20 @@ export const TransferStatusNotification = ({ const content = ( <> - + <$Status> {stringGetter({ key: statusString, params: { - AMOUNT_USD: , + AMOUNT_USD: <$InlineOutput type={OutputType.Fiat} value={toAmount} />, ESTIMATED_DURATION: ( - ), }, })} - + {hasError && ( {stringGetter({ @@ -113,18 +115,18 @@ export const TransferStatusNotification = ({ !status && !isExchange ? ( ) : ( - + <$BridgingStatus> {content} {!isToast && !isComplete && !hasError && ( - + <$TransferStatusSteps status={status} type={type} /> )} - + ) } slotAction={ isToast && status && ( - { e.stopPropagation(); @@ -135,7 +137,7 @@ export const TransferStatusNotification = ({ {stringGetter({ key: open ? STRING_KEYS.HIDE_DETAILS : STRING_KEYS.VIEW_DETAILS, })} - + ) } withClose={false} @@ -148,9 +150,9 @@ export const TransferStatusNotification = ({ side="bottom" slotReceipt={ - + <$Receipt> - + } > @@ -160,15 +162,12 @@ export const TransferStatusNotification = ({ transferNotif ); }; - -const Styled: Record = {}; - -Styled.BridgingStatus = styled.div` +const $BridgingStatus = styled.div` ${layoutMixins.flexColumn}; gap: 0.5rem; `; -Styled.Status = styled.div<{ withMarginBottom?: boolean }>` +const $Status = styled.div<{ withMarginBottom?: boolean }>` color: var(--color-text-0); font-size: 0.875rem; @@ -179,23 +178,17 @@ Styled.Status = styled.div<{ withMarginBottom?: boolean }>` `} `; -Styled.InlineOutput = styled(Output)` +const $InlineOutput = styled(Output)` display: inline-block; color: var(--color-text-1); `; -Styled.Step = styled.div` - ${layoutMixins.row}; - - gap: 0.5rem; -`; - -Styled.TransferStatusSteps = styled(TransferStatusSteps)` +const $TransferStatusSteps = styled(TransferStatusSteps)` padding: 0.5rem 0 0; `; -Styled.Trigger = styled.button<{ isOpen?: boolean }>` +const $Trigger = styled.button<{ isOpen?: boolean }>` display: flex; align-items: center; gap: 0.5em; @@ -222,6 +215,6 @@ Styled.Trigger = styled.button<{ isOpen?: boolean }>` `} `; -Styled.Receipt = styled.div` +const $Receipt = styled.div` padding: 0 1rem; `; diff --git a/src/views/tables/FillsTable.tsx b/src/views/tables/FillsTable.tsx index 4a32c1c16..927842839 100644 --- a/src/views/tables/FillsTable.tsx +++ b/src/views/tables/FillsTable.tsx @@ -1,41 +1,44 @@ -import { useEffect } from 'react'; -import styled, { type AnyStyledComponent, css } from 'styled-components'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { Key, useEffect, useMemo } from 'react'; + +import { Nullable } from '@dydxprotocol/v4-abacus'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import type { ColumnSize } from '@react-types/table'; -import { Nullable } from '@dydxprotocol/v4-abacus'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { type Asset, type SubaccountFill } from '@/constants/abacus'; import { DialogTypes } from '@/constants/dialogs'; -import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; +import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; +import { EMPTY_ARR } from '@/constants/objects'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; import { tradeViewMixins } from '@/styles/tradeViewMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; -import { MarketTableCell } from '@/components/Table/MarketTableCell'; import { OrderSideTag } from '@/components/OrderSideTag'; import { Output, OutputType } from '@/components/Output'; import { Table, TableCell, TableColumnHeader, type ColumnDef } from '@/components/Table'; +import { MarketTableCell } from '@/components/Table/MarketTableCell'; +import { PageSize } from '@/components/Table/TablePaginationRow'; import { TagSize } from '@/components/Tag'; +import { viewedFills } from '@/state/account'; import { getCurrentMarketFills, getHasUnseenFillUpdates, getSubaccountFills, } from '@/state/accountSelectors'; - import { getAssets } from '@/state/assetsSelectors'; -import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; -import { viewedFills } from '@/state/account'; - import { openDialog } from '@/state/dialogs'; +import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; import { getHydratedTradingData } from '@/lib/orders'; +import { orEmptyObj } from '@/lib/typeUtils'; const MOBILE_FILLS_PER_PAGE = 50; @@ -88,7 +91,7 @@ const getFillsTableColumnDef = ({ key: STRING_KEYS.AMOUNT, })}`, renderCell: ({ resources, size, stepSizeDecimals, asset: { id } }) => ( - }> + }> {resources.typeStringKey ? stringGetter({ key: resources.typeStringKey }) : null} @@ -109,21 +112,21 @@ const getFillsTableColumnDef = ({ })}`, renderCell: ({ fee, orderSide, price, resources, tickSizeDecimals }) => ( - - + <$InlineRow> + <$Side side={orderSide}> {resources.sideStringKey ? stringGetter({ key: resources.sideStringKey }) : null} - - @ + + <$SecondaryColor>@ - - - + + <$InlineRow> + <$BaseColor> {resources.liquidityStringKey ? stringGetter({ key: resources.liquidityStringKey }) : null} - + - + ), }, @@ -132,7 +135,7 @@ const getFillsTableColumnDef = ({ getCellValue: (row) => row.createdAtMilliseconds, label: stringGetter({ key: STRING_KEYS.TIME }), renderCell: ({ createdAtMilliseconds }) => ( - row.marketId, label: stringGetter({ key: STRING_KEYS.ACTION }), renderCell: ({ asset, orderSide }) => ( - - + <$TableCell> + <$Side side={orderSide}> {stringGetter({ key: { [OrderSide.BUY]: STRING_KEYS.BUY, [OrderSide.SELL]: STRING_KEYS.SELL, }[orderSide], })} - + - + ), }, [FillsTableColumnKey.Liquidity]: { @@ -290,6 +293,7 @@ type ElementProps = { columnKeys: FillsTableColumnKey[]; columnWidths?: Partial>; currentMarket?: string; + initialPageSize?: PageSize; }; type StyleProps = { @@ -302,6 +306,7 @@ export const FillsTable = ({ columnKeys, columnWidths, currentMarket, + initialPageSize, withGradientCardRows, withOuterBorder, withInnerBorders = true, @@ -310,12 +315,12 @@ export const FillsTable = ({ const dispatch = useDispatch(); const { isMobile, isTablet } = useBreakpoints(); - const marketFills = useSelector(getCurrentMarketFills, shallowEqual) || []; - const allFills = useSelector(getSubaccountFills, shallowEqual) || []; + const marketFills = useSelector(getCurrentMarketFills, shallowEqual) ?? EMPTY_ARR; + const allFills = useSelector(getSubaccountFills, shallowEqual) ?? EMPTY_ARR; const fills = currentMarket ? marketFills : allFills; - const allPerpetualMarkets = useSelector(getPerpetualMarkets, shallowEqual) || {}; - const allAssets = useSelector(getAssets, shallowEqual) || {}; + const allPerpetualMarkets = orEmptyObj(useSelector(getPerpetualMarkets, shallowEqual)); + const allAssets = orEmptyObj(useSelector(getAssets, shallowEqual)); const hasUnseenFillUpdates = useSelector(getHasUnseenFillUpdates); @@ -325,23 +330,27 @@ export const FillsTable = ({ const symbol = currentMarket ? allAssets[allPerpetualMarkets[currentMarket]?.assetId]?.id : null; - const fillsData = fills.map((fill: SubaccountFill) => - getHydratedTradingData({ - data: fill, - assets: allAssets, - perpetualMarkets: allPerpetualMarkets, - }) - ) as FillTableRow[]; + const fillsData = useMemo( + () => + fills.map((fill: SubaccountFill) => + getHydratedTradingData({ + data: fill, + assets: allAssets, + perpetualMarkets: allPerpetualMarkets, + }) + ) as FillTableRow[], + [fills, allPerpetualMarkets, allAssets] + ); return ( - row.id} - onRowAction={(key: string) => + onRowAction={(key: Key) => dispatch( openDialog({ type: DialogTypes.FillDetails, @@ -349,7 +358,7 @@ export const FillsTable = ({ }) ) } - columns={columnKeys.map((key: FillsTableColumnKey, index: number) => + columns={columnKeys.map((key: FillsTableColumnKey) => getFillsTableColumnDef({ key, isTablet, @@ -360,10 +369,11 @@ export const FillsTable = ({ )} slotEmpty={ <> - + <$Icon iconName={IconName.History} />

    {stringGetter({ key: STRING_KEYS.TRADES_EMPTY_STATE })}

    } + initialPageSize={initialPageSize} withOuterBorder={withOuterBorder} withInnerBorders={withInnerBorders} withScrollSnapColumns @@ -372,42 +382,39 @@ export const FillsTable = ({ /> ); }; - -const Styled: Record = {}; - -Styled.Table = styled(Table)` +const $Table = styled(Table)` ${tradeViewMixins.horizontalTable} -`; +` as typeof Table; -Styled.TableCell = styled(TableCell)` +const $TableCell = styled(TableCell)` gap: 0.25rem; `; -Styled.InlineRow = styled.div` +const $InlineRow = styled.div` ${layoutMixins.inlineRow} `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` font-size: 3em; `; -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` font-size: 2.25rem; `; -Styled.SecondaryColor = styled.span` +const $SecondaryColor = styled.span` color: var(--color-text-0); `; -Styled.BaseColor = styled.span` +const $BaseColor = styled.span` color: var(--color-text-1); `; -Styled.TimeOutput = styled(Output)` +const $TimeOutput = styled(Output)` color: var(--color-text-0); `; -Styled.Side = styled.span<{ side: OrderSide }>` +const $Side = styled.span<{ side: OrderSide }>` ${({ side }) => ({ [OrderSide.BUY]: css` diff --git a/src/views/tables/FundingPaymentsTable.tsx b/src/views/tables/FundingPaymentsTable.tsx index 5b5c37c38..225238098 100644 --- a/src/views/tables/FundingPaymentsTable.tsx +++ b/src/views/tables/FundingPaymentsTable.tsx @@ -1,16 +1,19 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; -import { shallowEqual, useSelector } from 'react-redux'; import { DateTime } from 'luxon'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; import type { Asset, SubaccountFundingPayment } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; -import { useStringGetter } from '@/hooks'; +import { EMPTY_ARR } from '@/constants/objects'; + +import { useStringGetter } from '@/hooks/useStringGetter'; import { tradeViewMixins } from '@/styles/tradeViewMixins'; -import { MarketTableCell } from '@/components/Table/MarketTableCell'; import { Output, OutputType } from '@/components/Output'; import { Table, TableCell, type ColumnDef } from '@/components/Table'; +import { MarketTableCell } from '@/components/Table/MarketTableCell'; +import { PageSize } from '@/components/Table/TablePaginationRow'; import { getCurrentMarketFundingPayments, @@ -22,9 +25,11 @@ import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; import { getHydratedTradingData } from '@/lib/orders'; import { getStringsForDateTimeDiff } from '@/lib/timeUtils'; +import { orEmptyObj } from '@/lib/typeUtils'; type ElementProps = { currentMarket?: string; + initialPageSize?: PageSize; }; type StyleProps = { @@ -39,16 +44,18 @@ export type FundingPaymentTableRow = { export const FundingPaymentsTable = ({ currentMarket, + initialPageSize, withOuterBorder, }: ElementProps & StyleProps) => { const stringGetter = useStringGetter(); - const marketFundingPayments = useSelector(getCurrentMarketFundingPayments, shallowEqual) || []; - const allFundingPayments = useSelector(getSubaccountFundingPayments, shallowEqual) || []; + const marketFundingPayments = + useSelector(getCurrentMarketFundingPayments, shallowEqual) ?? EMPTY_ARR; + const allFundingPayments = useSelector(getSubaccountFundingPayments, shallowEqual) ?? EMPTY_ARR; const fundingPayments = currentMarket ? marketFundingPayments : allFundingPayments; - const allPerpetualMarkets = useSelector(getPerpetualMarkets, shallowEqual) || {}; - const allAssets = useSelector(getAssets, shallowEqual) || {}; + const allPerpetualMarkets = orEmptyObj(useSelector(getPerpetualMarkets, shallowEqual)); + const allAssets = orEmptyObj(useSelector(getAssets, shallowEqual)); const fundingPaymentsData = fundingPayments.map((fundingPayment: SubaccountFundingPayment) => getHydratedTradingData({ @@ -59,7 +66,7 @@ export const FundingPaymentsTable = ({ ) as FundingPaymentTableRow[]; return ( - row.rate, label: stringGetter({ key: STRING_KEYS.FUNDING_RATE }), renderCell: (row) => ( - - [] ).filter(Boolean)} - slotEmpty={ - <> -

    {stringGetter({ key: STRING_KEYS.FUNDING_PAYMENTS_EMPTY_STATE })}

    - - } + slotEmpty={

    {stringGetter({ key: STRING_KEYS.FUNDING_PAYMENTS_EMPTY_STATE })}

    } + initialPageSize={initialPageSize} withOuterBorder={withOuterBorder} withInnerBorders withScrollSnapColumns @@ -151,14 +155,11 @@ export const FundingPaymentsTable = ({ /> ); }; - -const Styled: Record = {}; - -Styled.Table = styled(Table)` +const $Table = styled(Table)` ${tradeViewMixins.horizontalTable} -`; +` as typeof Table; -Styled.Output = styled(Output)<{ isNegative?: boolean }>` +const $Output = styled(Output)<{ isNegative?: boolean }>` color: ${({ isNegative }) => isNegative ? `var(--color-negative)` : `var(--color-positive)`} !important; `; diff --git a/src/views/tables/LiveTrades.tsx b/src/views/tables/LiveTrades.tsx index 7dd3022f6..3381f75de 100644 --- a/src/views/tables/LiveTrades.tsx +++ b/src/views/tables/LiveTrades.tsx @@ -1,11 +1,15 @@ import { useMemo } from 'react'; -import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; -import { shallowEqual, useSelector } from 'react-redux'; + import { OrderSide } from '@dydxprotocol/v4-client-js'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled, { css, keyframes } from 'styled-components'; import { MarketTrade } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { EMPTY_ARR } from '@/constants/objects'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; @@ -15,9 +19,11 @@ import { Output, OutputType } from '@/components/Output'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketConfig, getCurrentMarketLiveTrades } from '@/state/perpetualsSelectors'; -import { OrderbookTradesOutput, OrderbookTradesTable } from './OrderbookTradesTable'; -import { getSelectedOrderSide } from '@/lib/tradeData'; +import { getSimpleStyledOutputType } from '@/lib/genericFunctionalComponentUtils'; import { isTruthy } from '@/lib/isTruthy'; +import { getSelectedOrderSide } from '@/lib/tradeData'; + +import { OrderbookTradesOutput, OrderbookTradesTable } from './OrderbookTradesTable'; const MAX_ORDERBOOK_BAR_SIZE = 0.4; const LARGE_TRADE_USD_VALUE = 10000; @@ -41,10 +47,11 @@ export const LiveTrades = ({ className, histogramSide = 'left' }: StyleProps) => const { isTablet } = useBreakpoints(); const currentMarketAssetData = useSelector(getCurrentMarketAssetData, shallowEqual); const currentMarketConfig = useSelector(getCurrentMarketConfig, shallowEqual); - const currentMarketLiveTrades = useSelector(getCurrentMarketLiveTrades, shallowEqual) || []; + const currentMarketLiveTrades = + useSelector(getCurrentMarketLiveTrades, shallowEqual) ?? EMPTY_ARR; const { id = '' } = currentMarketAssetData ?? {}; - const { stepSizeDecimals, tickSizeDecimals } = currentMarketConfig || {}; + const { stepSizeDecimals, tickSizeDecimals } = currentMarketConfig ?? {}; const rows = currentMarketLiveTrades.map( ({ createdAtMilliseconds, price, size, side }: MarketTrade, idx) => ({ @@ -62,7 +69,7 @@ export const LiveTrades = ({ className, histogramSide = 'left' }: StyleProps) => getCellValue: (row: RowData) => row.createdAtMilliseconds, label: stringGetter({ key: STRING_KEYS.TIME }), renderCell: (row: RowData) => ( - + <$TimeOutput type={OutputType.Time} value={row.createdAtMilliseconds} /> ), }; return [ @@ -72,7 +79,7 @@ export const LiveTrades = ({ className, histogramSide = 'left' }: StyleProps) => getCellValue: (row: RowData) => row.size, label: stringGetter({ key: STRING_KEYS.SIDE }), renderCell: (row: RowData) => ( - label: stringGetter({ key: STRING_KEYS.SIZE }), tag: id, renderCell: (row: RowData) => ( - }, [stepSizeDecimals, tickSizeDecimals, id, histogramSide, stringGetter]); return ( - /> ); }; - -const Styled: Record = {}; - -Styled.TimeOutput = styled(OrderbookTradesOutput)` +const $TimeOutput = styled(OrderbookTradesOutput)` color: var(--color-text-0); font-feature-settings: var(--fontFeature-monoNumbers); `; -Styled.SideOutput = styled(Output)` +const $SideOutput = styled(Output)` color: var(--accent-color); `; -Styled.SizeOutput = styled(Output)` +const $SizeOutput = styled(Output)` color: var(--accent-color); @media ${breakpoints.tablet} { @@ -160,7 +164,8 @@ Styled.SizeOutput = styled(Output)` } `; -Styled.LiveTradesTable = styled(OrderbookTradesTable)` +const liveTradesTableType = getSimpleStyledOutputType(OrderbookTradesTable, {} as StyleProps); +const $LiveTradesTable = styled(OrderbookTradesTable)` tr { --histogram-bucket-size: 1; background-color: var(--color-layer-2); @@ -228,4 +233,4 @@ Styled.LiveTradesTable = styled(OrderbookTradesTable)` font-size: 0.875em; } -`; +` as typeof liveTradesTableType; diff --git a/src/views/tables/MarketsCompactTable.tsx b/src/views/tables/MarketsCompactTable.tsx new file mode 100644 index 000000000..620db306a --- /dev/null +++ b/src/views/tables/MarketsCompactTable.tsx @@ -0,0 +1,329 @@ +import { Key, PropsWithChildren, useMemo } from 'react'; + +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; +import { MarketFilters, MarketSorting, type MarketData } from '@/constants/markets'; +import { AppRoute } from '@/constants/routes'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useMarketsData } from '@/hooks/useMarketsData'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { breakpoints } from '@/styles'; +import { layoutMixins } from '@/styles/layoutMixins'; +import { tradeViewMixins } from '@/styles/tradeViewMixins'; + +import { Icon, IconName } from '@/components/Icon'; +import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; +import { Output, OutputType } from '@/components/Output'; +import { AssetTableCell, Table, TableCell, type ColumnDef } from '@/components/Table'; +import { TriangleIndicator } from '@/components/TriangleIndicator'; + +import { MustBigNumber } from '@/lib/numbers'; + +interface MarketsCompactTableProps { + className?: string; + filters?: MarketFilters; + sorting?: MarketSorting; +} + +export const MarketsCompactTable = ({ + className, + filters, + sorting, +}: PropsWithChildren) => { + const stringGetter = useStringGetter(); + const { isTablet } = useBreakpoints(); + const navigate = useNavigate(); + + const { filteredMarkets } = useMarketsData(filters); + + const columns = useMemo[]>( + () => + [ + { + columnKey: 'market', + getCellValue: (row) => row.market, + allowsSorting: false, + label: stringGetter({ key: STRING_KEYS.MARKET }), + renderCell: ({ asset }) => , + }, + { + columnKey: 'oraclePrice', + getCellValue: (row) => row.oraclePrice, + allowsSorting: false, + label: stringGetter({ key: STRING_KEYS.ORACLE_PRICE }), + renderCell: ({ + oraclePrice, + priceChange24H, + priceChange24HPercent, + tickSizeDecimals, + }) => ( + + <$TabletOutput + withBaseFont + withSubscript + type={OutputType.Fiat} + value={oraclePrice} + fractionDigits={tickSizeDecimals} + /> + <$TabletPriceChange> + {!priceChange24H ? ( + + ) : ( + <> + {priceChange24H > 0 && ( + + )} + <$Output + type={OutputType.Percent} + value={MustBigNumber(priceChange24HPercent).abs()} + isPositive={MustBigNumber(priceChange24HPercent).gt(0)} + isNegative={MustBigNumber(priceChange24HPercent).isNegative()} + /> + + )} + + + ), + }, + filters === MarketFilters.NEW + ? { + columnKey: 'listing', + getCellValue: (row) => row.isNew, + allowsSorting: false, + renderCell: ({ listingDate }) => ( + <$DetailsCell> + <$RecentlyListed> + Listed + {listingDate && ( + <$RelativeTimeOutput + type={OutputType.RelativeTime} + relativeTimeFormatOptions={{ + format: 'singleCharacter', + }} + value={listingDate.getTime()} + /> + )} + + + + ), + } + : { + columnKey: 'openInterest', + allowsSorting: false, + getCellValue: (row) => row.openInterestUSDC, + label: stringGetter({ key: STRING_KEYS.OPEN_INTEREST }), + renderCell: ({ asset, openInterestUSDC, openInterest }) => ( + <$DetailsCell> + <$RecentlyListed> + + <$InterestOutput + type={OutputType.CompactNumber} + value={openInterest} + slotRight={` ${asset.id}`} + /> + + + + ), + }, + ] as ColumnDef[], + [stringGetter, isTablet] + ); + + const sortedMarkets = useMemo(() => { + const sortingFunction = (marketA: MarketData, marketB: MarketData) => { + if (marketA.priceChange24HPercent == null && marketB.priceChange24HPercent == null) { + return 0; + } + + if (marketA.priceChange24HPercent == null) { + return 1; + } + + if (marketB.priceChange24HPercent == null) { + return -1; + } + + return sorting === MarketSorting.GAINERS + ? marketB.priceChange24HPercent - marketA.priceChange24HPercent + : marketA.priceChange24HPercent - marketB.priceChange24HPercent; + }; + + if (sorting === MarketSorting.LOSERS) { + return filteredMarkets + .filter((market) => !!market.priceChange24HPercent && market.priceChange24HPercent < 0) + .sort(sortingFunction); + } + + return filteredMarkets.sort(sortingFunction); + }, [sorting, filteredMarkets]); + + return ( + <$Table + withInnerBorders + data={sortedMarkets.slice(0, 5)} + getRowKey={(row) => row.market ?? ''} + label="Markets" + onRowAction={(market: Key) => + navigate(`${AppRoute.Trade}/${market}`, { state: { from: AppRoute.Markets } }) + } + columns={columns} + className={className} + slotEmpty={ + <$MarketNotFound> + {filters === MarketFilters.NEW ? ( +

    {stringGetter({ key: STRING_KEYS.NO_RECENTLY_LISTED_MARKETS })}

    + ) : ( + + )} + + } + initialPageSize={5} + /> + ); +}; + +const $Table = styled(Table)` + ${tradeViewMixins.horizontalTable} + --tableCell-padding: 0.625rem 1.5rem; + --tableRow-backgroundColor: var(--color-layer-3); + --tableHeader-backgroundColor: var(--color-layer-3); + border-bottom-right-radius: 0.625rem; + border-bottom-left-radius: 0.625rem; + + & table { + --stickyArea1-background: var(--color-layer-5); + } + + & tr:last-child { + box-shadow: none; + } + + & tbody:after { + content: none; + } + + & > div { + padding: 1rem; + } + + @media ${breakpoints.desktopSmall} { + --tableCell-padding: 0.5rem 0.5rem; + + & tr > td:nth-child(1) { + --tableCell-padding: 0.5rem 0.5rem; + } + + & tr > td:nth-child(2) { + --tableCell-padding: 0.5rem 0; + } + + & tr > td:nth-child(3) { + --tableCell-padding: 0.5rem 0.5rem; + } + } + + @media ${breakpoints.tablet} { + table { + max-width: 100vw; + } + + & tr > td:nth-child(1) { + --tableCell-padding: 0.5rem 0.625rem 0.5rem 1rem; + } + + & tr > td:nth-child(2) { + --tableCell-padding: 0.5rem 0; + } + + & tr > td:nth-child(3) { + --tableCell-padding: 0.5rem 1rem 0.5rem 0.625rem; + } + } +` as typeof Table; + +const $TabletOutput = styled(Output)` + font: var(--font-small-medium); + color: var(--color-text-1); +`; + +const $TabletPriceChange = styled.div` + ${layoutMixins.inlineRow} + + & output { + font: var(--font-mini-medium); + } +`; + +const $Output = styled(Output)<{ isNegative?: boolean; isPositive?: boolean }>` + color: ${({ isNegative, isPositive }) => + isNegative + ? `var(--color-negative)` + : isPositive + ? `var(--color-positive)` + : `var(--color-text-1)`}; + font: var(--font-base-medium); +`; + +const $MarketNotFound = styled.div` + ${layoutMixins.column} + justify-content: center; + align-items: center; + text-align: center; + padding: 0; + + & p { + color: var(--color-text-0); + font: var(--font-base-medium); + } + + & button { + color: var(--color-accent); + } +`; + +const $DetailsCell = styled(TableCell)` + ${layoutMixins.row} + gap: 0.75rem; + + & > svg { + opacity: 0.4; + } +`; + +const $RecentlyListed = styled.div` + ${layoutMixins.column} + gap: 0.125rem; + + & > span, + & > output { + text-align: right; + justify-content: flex-end; + } + + & > span:first-child { + color: var(--color-text-0); + font: var(--font-mini-medium); + } + + & > span:last-child, + & > output:first-child { + color: var(--color-text-1); + font: var(--font-small-medium); + } +`; + +const $InterestOutput = styled(Output)` + color: var(--color-text-0); + font: var(--font-mini-medium); +`; + +const $RelativeTimeOutput = styled(Output)` + color: var(--color-text-1); + font: var(--font-small-medium); +`; diff --git a/src/views/tables/MarketsTable.tsx b/src/views/tables/MarketsTable.tsx index b66ed66e4..1e9b6d752 100644 --- a/src/views/tables/MarketsTable.tsx +++ b/src/views/tables/MarketsTable.tsx @@ -1,33 +1,47 @@ -import { useMemo, useState } from 'react'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import { Key, useMemo, useState } from 'react'; + +import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import { ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { MarketFilters, type MarketData } from '@/constants/markets'; -import { FUNDING_DECIMALS, LARGE_TOKEN_DECIMALS } from '@/constants/numbers'; -import { AppRoute } from '@/constants/routes'; +import { FUNDING_DECIMALS } from '@/constants/numbers'; +import { AppRoute, MarketsRoute } from '@/constants/routes'; -import { useBreakpoints, useStringGetter } from '@/hooks'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useMarketsData } from '@/hooks/useMarketsData'; +import { usePotentialMarkets } from '@/hooks/usePotentialMarkets'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { tradeViewMixins } from '@/styles/tradeViewMixins'; +import { Button } from '@/components/Button'; import { Output, OutputType } from '@/components/Output'; -import { type ColumnDef, MarketTableCell, Table, TableCell } from '@/components/Table'; +import { AssetTableCell, Table, TableCell, type ColumnDef } from '@/components/Table'; +import { Toolbar } from '@/components/Toolbar'; import { TriangleIndicator } from '@/components/TriangleIndicator'; +import { SparklineChart } from '@/components/visx/SparklineChart'; import { MarketFilter } from '@/views/MarketFilter'; +import { setMarketFilter } from '@/state/perpetuals'; +import { getMarketFilter } from '@/state/perpetualsSelectors'; + import { MustBigNumber } from '@/lib/numbers'; export const MarketsTable = ({ className }: { className?: string }) => { const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); - const [filter, setFilter] = useState(MarketFilters.ALL); + const dispatch = useDispatch(); + const filter: MarketFilters = useSelector(getMarketFilter); + const [searchFilter, setSearchFilter] = useState(); const navigate = useNavigate(); - const { filteredMarkets, marketFilters } = useMarketsData(filter); + const { filteredMarkets, marketFilters } = useMarketsData(filter, searchFilter); + const { hasPotentialMarketsData } = usePotentialMarkets(); const columns = useMemo[]>( () => @@ -37,7 +51,7 @@ export const MarketsTable = ({ className }: { className?: string }) => { columnKey: 'market', getCellValue: (row) => row.market, label: stringGetter({ key: STRING_KEYS.MARKET }), - renderCell: ({ asset, id }) => , + renderCell: ({ asset }) => , }, { columnKey: 'price', @@ -50,13 +64,13 @@ export const MarketsTable = ({ className }: { className?: string }) => { tickSizeDecimals, }) => ( - - + <$TabletPriceChange> {!priceChange24H ? ( ) : ( @@ -64,7 +78,7 @@ export const MarketsTable = ({ className }: { className?: string }) => { {priceChange24H > 0 && ( )} - { /> )} - + ), }, @@ -82,58 +96,74 @@ export const MarketsTable = ({ className }: { className?: string }) => { columnKey: 'market', getCellValue: (row) => row.market, label: stringGetter({ key: STRING_KEYS.MARKET }), - renderCell: ({ asset, id }) => , + renderCell: ({ asset }) => , }, { columnKey: 'oraclePrice', getCellValue: (row) => row.oraclePrice, label: stringGetter({ key: STRING_KEYS.ORACLE_PRICE }), renderCell: ({ oraclePrice, tickSizeDecimals }) => ( - ), }, + { + columnKey: 'priceChange24HChart', + getCellValue: (row) => row.priceChange24HPercent, + label: stringGetter({ key: STRING_KEYS.LAST_24H }), + renderCell: ({ line, priceChange24HPercent }) => ( +
    + ({ + x: index + 1, + y: parseFloat(datum.toString()), + }))} + xAccessor={(datum) => datum.x} + yAccessor={(datum) => datum.y} + positive={MustBigNumber(priceChange24HPercent).gt(0)} + /> +
    + ), + allowsSorting: false, + }, { columnKey: 'priceChange24HPercent', getCellValue: (row) => row.priceChange24HPercent, label: stringGetter({ key: STRING_KEYS.CHANGE_24H }), - renderCell: ({ priceChange24H, priceChange24HPercent, tickSizeDecimals }) => ( + renderCell: ({ priceChange24HPercent }) => ( - + <$InlineRow> {!priceChange24HPercent ? ( ) : ( - )} - - + ), }, { - columnKey: 'nextFundingRate', - getCellValue: (row) => row.nextFundingRate, - label: stringGetter({ key: STRING_KEYS.FUNDING_RATE_1H_SHORT }), + columnKey: 'volume24H', + getCellValue: (row) => row.volume24H, + label: stringGetter({ key: STRING_KEYS.VOLUME_24H }), renderCell: (row) => ( - + <$NumberOutput type={OutputType.CompactFiat} value={row.volume24H} /> + ), + }, + { + columnKey: 'trades24H', + getCellValue: (row) => row.trades24H, + label: stringGetter({ key: STRING_KEYS.TRADES }), + renderCell: (row) => ( + <$NumberOutput type={OutputType.CompactNumber} value={row.trades24H} /> ), }, { @@ -141,54 +171,52 @@ export const MarketsTable = ({ className }: { className?: string }) => { getCellValue: (row) => row.openInterestUSDC, label: stringGetter({ key: STRING_KEYS.OPEN_INTEREST }), renderCell: (row) => ( - - - - - + <$NumberOutput type={OutputType.CompactFiat} value={row.openInterestUSDC} /> ), }, { - columnKey: 'volume24H', - getCellValue: (row) => row.volume24H, - label: stringGetter({ key: STRING_KEYS.VOLUME_24H }), - renderCell: (row) => , - }, - { - columnKey: 'trades24H', - getCellValue: (row) => row.trades24H, - label: stringGetter({ key: STRING_KEYS.TRADES_24H }), - renderCell: (row) => , + columnKey: 'nextFundingRate', + getCellValue: (row) => row.nextFundingRate, + label: stringGetter({ key: STRING_KEYS.FUNDING_RATE_1H_SHORT }), + renderCell: (row) => ( + <$Output + type={OutputType.Percent} + fractionDigits={FUNDING_DECIMALS} + value={row.nextFundingRate} + isPositive={MustBigNumber(row.nextFundingRate).gt(0)} + isNegative={MustBigNumber(row.nextFundingRate).isNegative()} + /> + ), }, ] as ColumnDef[]), [stringGetter, isTablet] ); + const setFilter = (newFilter: MarketFilters) => { + dispatch(setMarketFilter(newFilter)); + }; + return ( <> - {isTablet && ( - - - - )} - + + + + <$Table withInnerBorders - withOuterBorder={!isTablet} data={filteredMarkets} - getRowKey={(row: MarketData) => row.market} + getRowKey={(row: MarketData) => row.market ?? ''} label="Markets" - onRowAction={(market: string) => - navigate(`/trade/${market}`, { state: { from: AppRoute.Markets } }) + onRowAction={(market: Key) => + navigate(`${AppRoute.Trade}/${market}`, { state: { from: AppRoute.Markets } }) } defaultSortDescriptor={{ column: 'volume24H', @@ -196,18 +224,66 @@ export const MarketsTable = ({ className }: { className?: string }) => { }} columns={columns} className={className} + slotEmpty={ + <$MarketNotFound> + {filter === MarketFilters.NEW && !searchFilter ? ( + <> +

    + {stringGetter({ + key: STRING_KEYS.QUERY_NOT_FOUND, + params: { QUERY: stringGetter({ key: STRING_KEYS.NEW }) }, + })} +

    + {hasPotentialMarketsData && ( +

    {stringGetter({ key: STRING_KEYS.ADD_DETAILS_TO_LAUNCH_MARKET })}

    + )} + + ) : ( + <> +

    + {stringGetter({ + key: STRING_KEYS.QUERY_NOT_FOUND, + params: { QUERY: searchFilter ?? '' }, + })} +

    +

    {stringGetter({ key: STRING_KEYS.MARKET_SEARCH_DOES_NOT_EXIST_YET })}

    + + )} + + {hasPotentialMarketsData && ( +
    + +
    + )} + + } /> ); }; +const $Toolbar = styled(Toolbar)` + max-width: 100vw; + overflow: hidden; + margin-bottom: 0.625rem; + padding-left: 0.375rem; + padding-right: 0; -const Styled: Record = {}; + @media ${breakpoints.desktopSmall} { + padding-right: 0.375rem; + } -Styled.FilterWrapper = styled.div` - padding: 0 1rem; + @media ${breakpoints.tablet} { + padding-left: 1rem; + padding-right: 1rem; + } `; -Styled.Table = styled(Table)` +const $Table = styled(Table)` ${tradeViewMixins.horizontalTable} @media ${breakpoints.tablet} { @@ -215,39 +291,46 @@ Styled.Table = styled(Table)` max-width: 100vw; } } -`; +` as typeof Table; -Styled.MarketTableCell = styled(MarketTableCell)` - @media ${breakpoints.tablet} { - span:first-child { - font: var(--font-medium-book); - } - span:last-child { - font: var(--font-mini-regular); - } - } -`; - -Styled.TabletOutput = styled(Output)` - @media ${breakpoints.tablet} { - font: var(--font-medium-book); - color: var(--color-text-2); - } +const $TabletOutput = styled(Output)` + font: var(--font-medium-book); + color: var(--color-text-2); `; -Styled.InlineRow = styled.div` +const $InlineRow = styled.div` ${layoutMixins.inlineRow} `; -Styled.TabletPriceChange = styled(Styled.InlineRow)` +const $TabletPriceChange = styled($InlineRow)` font: var(--font-small-book); `; -Styled.Output = styled(Output)<{ isNegative?: boolean; isPositive?: boolean }>` +const $NumberOutput = styled(Output)` + font: var(--font-base-medium); + color: var(--color-text-2); +`; + +const $Output = styled(Output)<{ isNegative?: boolean; isPositive?: boolean }>` color: ${({ isNegative, isPositive }) => isNegative ? `var(--color-negative)` : isPositive ? `var(--color-positive)` : `var(--color-text-1)`}; + font: var(--font-base-medium); +`; + +const $MarketNotFound = styled.div` + ${layoutMixins.column} + justify-content: center; + align-items: center; + text-align: center; + gap: 1rem; + padding: 2rem 1.5rem; + + h2 { + font: var(--font-medium-book); + font-weight: 500; + } `; diff --git a/src/views/tables/Orderbook.tsx b/src/views/tables/Orderbook.tsx index 86f5829ea..ec0df8339 100644 --- a/src/views/tables/Orderbook.tsx +++ b/src/views/tables/Orderbook.tsx @@ -1,34 +1,36 @@ -import { useCallback, useMemo } from 'react'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; +import { Key, useCallback, useMemo } from 'react'; import { OrderSide } from '@dydxprotocol/v4-client-js'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css, keyframes } from 'styled-components'; + import { type OrderbookLine } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; import { ORDERBOOK_MAX_ROWS_PER_SIDE } from '@/constants/orderbook'; -import { useBreakpoints, useStringGetter } from '@/hooks'; - -import { calculateCanViewAccount } from '@/state/accountCalculators'; -import { setTradeFormInputs } from '@/state/inputs'; - -import { getSubaccountOpenOrdersBySideAndPrice } from '@/state/accountSelectors'; -import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; -import { getCurrentMarketConfig, getCurrentMarketOrderbook } from '@/state/perpetualsSelectors'; -import { getCurrentInput } from '@/state/inputsSelectors'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { MustBigNumber } from '@/lib/numbers'; +import { breakpoints } from '@/styles'; +import { layoutMixins } from '@/styles/layoutMixins'; import { Details } from '@/components/Details'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { Output, OutputType } from '@/components/Output'; -import { type CustomRowConfig, TableRow } from '@/components/Table'; +import { ColumnDef, TableRow, type CustomRowConfig } from '@/components/Table'; import { WithTooltip } from '@/components/WithTooltip'; -import { OrderbookTradesOutput, OrderbookTradesTable } from './OrderbookTradesTable'; +import { calculateCanViewAccount } from '@/state/accountCalculators'; +import { getSubaccountOrderSizeBySideAndPrice } from '@/state/accountSelectors'; +import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; +import { setTradeFormInputs } from '@/state/inputs'; +import { getCurrentInput } from '@/state/inputsSelectors'; +import { getCurrentMarketConfig, getCurrentMarketOrderbook } from '@/state/perpetualsSelectors'; -import { breakpoints } from '@/styles'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { getSimpleStyledOutputType } from '@/lib/genericFunctionalComponentUtils'; +import { MustBigNumber } from '@/lib/numbers'; + +import { OrderbookTradesOutput, OrderbookTradesTable } from './OrderbookTradesTable'; type ElementProps = { maxRowsPerSide?: number; @@ -50,8 +52,8 @@ type RowData = Pick & { const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => { const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual); - const openOrdersBySideAndPrice = - useSelector(getSubaccountOpenOrdersBySideAndPrice, shallowEqual) || {}; + const subaccountOrderSizeBySideAndPrice = + useSelector(getSubaccountOrderSizeBySideAndPrice, shallowEqual) || {}; return useMemo(() => { const asks = (orderbook?.asks?.toArray() ?? []) @@ -60,8 +62,7 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number ({ key: `ask-${idx}`, side: 'ask', - mine: openOrdersBySideAndPrice[OrderSide.SELL]?.[row.price]?.size, - ...row, + mine: subaccountOrderSizeBySideAndPrice[OrderSide.SELL]?.[row.price], } as RowData) ) .slice(0, maxRowsPerSide); @@ -72,7 +73,7 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number ({ key: `bid-${idx}`, side: 'bid', - mine: openOrdersBySideAndPrice[OrderSide.BUY]?.[row.price]?.size, + mine: subaccountOrderSizeBySideAndPrice[OrderSide.BUY]?.[row.price], ...row, } as RowData) ) @@ -107,8 +108,8 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number const spreadPercent = orderbook?.spreadPercent; const histogramRange = Math.max( - isNaN(Number(bids[bids.length - 1]?.depth)) ? 0 : Number(bids[bids.length - 1]?.depth), - isNaN(Number(asks[asks.length - 1]?.depth)) ? 0 : Number(asks[asks.length - 1]?.depth) + Number.isNaN(Number(bids[bids.length - 1]?.depth)) ? 0 : Number(bids[bids.length - 1]?.depth), + Number.isNaN(Number(asks[asks.length - 1]?.depth)) ? 0 : Number(asks[asks.length - 1]?.depth) ); // Ensure asks and bids are of length maxRowsPerSide by adding empty rows. @@ -141,7 +142,7 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number } return { asks, bids, spread, spreadPercent, histogramRange, hasOrderbook: !!orderbook }; - }, [orderbook, openOrdersBySideAndPrice]); + }, [orderbook, subaccountOrderSizeBySideAndPrice]); }; const OrderbookTable = ({ @@ -163,13 +164,13 @@ const OrderbookTable = ({ stepSizeDecimals: number | undefined | null; tickSizeDecimals: number | undefined | null; histogramRange: number; - onRowAction: (key: string, row: RowData) => void; + onRowAction: (key: Key, row: RowData) => void; className?: string; hideHeader?: boolean; }) => { const stringGetter = useStringGetter(); - const columns = useMemo(() => { + const columns = useMemo((): ColumnDef[] => { return [ { columnKey: 'size', @@ -178,12 +179,12 @@ const OrderbookTable = ({ tag: symbol, renderCell: (row: RowData) => row.size > 0 && ( - ), @@ -206,16 +207,16 @@ const OrderbookTable = ({ }, { columnKey: 'subaccount-orders', - getCellValue: (row: RowData) => row.mine, + getCellValue: (row: RowData) => row.mine ?? '', label: showMineColumn && stringGetter({ key: STRING_KEYS.ORDERBOOK_MY_ORDER_SIZE }), renderCell: (row: RowData) => ( - @@ -225,7 +226,7 @@ const OrderbookTable = ({ }, [showMineColumn, symbol, stepSizeDecimals, tickSizeDecimals, histogramSide, stringGetter]); return ( - ( - + <$SpreadTableRow key="spread" {...props}>
    - + ), } as CustomRowConfig, ...asks, @@ -301,7 +304,7 @@ export const Orderbook = ({ ); const onRowAction = useCallback( - (key: string, row: RowData) => { + (key: Key, row: RowData) => { if (currentInput === 'trade' && key !== 'spread' && row?.price) { dispatch(setTradeFormInputs({ limitPriceInput: row?.price?.toString() })); } @@ -327,9 +330,9 @@ export const Orderbook = ({ return layout === 'vertical' ? ( ) : ( - - - + <$Header> + <$SpreadDetails items={[ { key: 'spread', @@ -350,12 +353,12 @@ export const Orderbook = ({ layout="row" /> {/* TODO: TRCL-1411 implement zoom here */} - - + + <$SplitOrderbook> - - + + ); }; @@ -420,16 +423,13 @@ export const orderbookMixins = { scroll-snap-stop: always; `, } as const; - -const Styled: Record = {}; - const fadeAnimation = keyframes` 20% { opacity: 0.6; } `; -Styled.HorizontalLayout = styled.div` +const $HorizontalLayout = styled.div` ${layoutMixins.expandingColumnWithHeader} ${layoutMixins.withInnerHorizontalBorders} @@ -445,7 +445,7 @@ Styled.HorizontalLayout = styled.div` } `; -Styled.HistogramOutput = styled(OrderbookTradesOutput)` +const $HistogramOutput = styled(OrderbookTradesOutput)` ${({ histogramSide }) => histogramSide ? css` @@ -511,7 +511,8 @@ Styled.HistogramOutput = styled(OrderbookTradesOutput)` : ''} `; -Styled.OrderbookTable = styled(OrderbookTradesTable)` +const orderbookTableType = getSimpleStyledOutputType(OrderbookTradesTable, {} as StyleProps); +const $OrderbookTable = styled(OrderbookTradesTable)` /* Params */ --orderbook-spreadRowHeight: 2rem; @@ -554,12 +555,12 @@ Styled.OrderbookTable = styled(OrderbookTradesTable)` `} } - ${Styled.HorizontalLayout} & { + ${$HorizontalLayout} & { --tableCell-padding: 0.25rem 1rem; } -`; +` as typeof orderbookTableType; -Styled.SpreadTableRow = styled(TableRow)` +const $SpreadTableRow = styled(TableRow)` ${layoutMixins.sticky} position: sticky !important; @@ -575,7 +576,8 @@ Styled.SpreadTableRow = styled(TableRow)` bottom: 50%; } - ${Styled.OrderbookTable}:not(:focus-within) & { + // have to override since we destroyed the string typings with the inline cast above + ${$OrderbookTable as any}:not(:focus-within) & { ${orderbookMixins.scrollSnapItem} } @@ -594,7 +596,7 @@ Styled.SpreadTableRow = styled(TableRow)` } `; -Styled.SpreadDetails = styled(Details)<{ asTableCells?: boolean }>` +const $SpreadDetails = styled(Details)<{ asTableCells?: boolean }>` /* Overrides */ --details-item-backgroundColor: var(--color-layer-2); --details-value-font: var(--font-mini-book); @@ -606,12 +608,12 @@ Styled.SpreadDetails = styled(Details)<{ asTableCells?: boolean }>` } `; -Styled.SplitOrderbook = styled.div` +const $SplitOrderbook = styled.div` ${layoutMixins.gridEqualColumns} gap: var(--border-width); `; -Styled.Header = styled.header` +const $Header = styled.header` ${layoutMixins.stickyHeader} ${layoutMixins.spacedRow} `; diff --git a/src/views/tables/OrderbookTradesTable.tsx b/src/views/tables/OrderbookTradesTable.tsx index f02aae314..c81dea345 100644 --- a/src/views/tables/OrderbookTradesTable.tsx +++ b/src/views/tables/OrderbookTradesTable.tsx @@ -3,9 +3,21 @@ import styled, { css, keyframes } from 'styled-components'; import { breakpoints } from '@/styles'; import { Output } from '@/components/Output'; -import { Table } from '@/components/Table'; +import { AllTableProps, BaseTableRowData, Table } from '@/components/Table'; -export const OrderbookTradesTable = styled(Table)<{ histogramSide: 'left' | 'right' }>` +import { getSimpleStyledOutputType } from '@/lib/genericFunctionalComponentUtils'; + +type OrderbookTradesTableStyleProps = { histogramSide?: 'left' | 'right' }; +const orderbookTradesTableType = getSimpleStyledOutputType( + Table, + {} as OrderbookTradesTableStyleProps +); + +export const OrderbookTradesTable = ( + props: AllTableProps +) => <$OrderbookTradesTable {...props} paginationBehavior="showAll" />; + +const $OrderbookTradesTable = styled(Table)` // Params --histogram-width: 100%; @@ -91,7 +103,7 @@ export const OrderbookTradesTable = styled(Table)<{ histogramSide: 'left' | 'rig --histogram-width: calc(100% / var(--approximate-column-width)); } } -`; +` as typeof orderbookTradesTableType; const colorAnimation = keyframes` 20% { diff --git a/src/views/tables/OrdersTable.tsx b/src/views/tables/OrdersTable.tsx index 96299691a..e6d85b1a8 100644 --- a/src/views/tables/OrdersTable.tsx +++ b/src/views/tables/OrdersTable.tsx @@ -1,17 +1,21 @@ -import { useEffect } from 'react'; -import styled, { css, type AnyStyledComponent } from 'styled-components'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import { DateTime } from 'luxon'; +import { Key, useEffect, useMemo } from 'react'; + import { OrderSide } from '@dydxprotocol/v4-client-js'; import { ColumnSize } from '@react-types/table'; import type { Dispatch } from '@reduxjs/toolkit'; +import { DateTime } from 'luxon'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; import { Asset, Nullable, SubaccountOrder } from '@/constants/abacus'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; import { TOKEN_DECIMALS } from '@/constants/numbers'; +import { EMPTY_ARR } from '@/constants/objects'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; -import { useBreakpoints, useStringGetter } from '@/hooks'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { tradeViewMixins } from '@/styles/tradeViewMixins'; @@ -20,42 +24,39 @@ import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; import { OrderSideTag } from '@/components/OrderSideTag'; import { Output, OutputType } from '@/components/Output'; - import { + MarketTableCell, Table, TableCell, TableColumnHeader, - MarketTableCell, type ColumnDef, } from '@/components/Table'; - +import { PageSize } from '@/components/Table/TablePaginationRow'; import { TagSize } from '@/components/Tag'; import { WithTooltip } from '@/components/WithTooltip'; +import { viewedOrders } from '@/state/account'; import { calculateIsAccountViewOnly } from '@/state/accountCalculators'; - import { getCurrentMarketOrders, getHasUnseenOrderUpdates, getSubaccountUnclearedOrders, } from '@/state/accountSelectors'; - import { getAssets } from '@/state/assetsSelectors'; -import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; - -import { viewedOrders } from '@/state/account'; import { openDialog } from '@/state/dialogs'; +import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; -import { getStringsForDateTimeDiff } from '@/lib/timeUtils'; - import { getHydratedTradingData, - getStatusIconInfo, + getOrderStatusInfo, isMarketOrderType, isOrderStatusClearable, } from '@/lib/orders'; +import { getStringsForDateTimeDiff } from '@/lib/timeUtils'; +import { orEmptyObj } from '@/lib/typeUtils'; +import { OrderStatusIcon } from '../OrderStatusIcon'; import { OrderActionsCell } from './OrdersTable/OrderActionsCell'; export enum OrdersTableColumnKey { @@ -82,7 +83,6 @@ export type OrderTableRow = { const getOrdersTableColumnDef = ({ key, - dispatch, stringGetter, symbol = '', isAccountViewOnly, @@ -110,26 +110,19 @@ const getOrdersTableColumnDef = ({ columnKey: 'status', getCellValue: (row) => row.status.name, label: stringGetter({ key: STRING_KEYS.STATUS }), - renderCell: ({ status, totalFilled, resources }) => { - const { statusIcon, statusIconColor, statusStringKey } = getStatusIconInfo({ - status, - totalFilled, - }); - + renderCell: ({ status, resources }) => { return ( - - - + + {resources.typeStringKey && stringGetter({ key: resources.typeStringKey })} ); @@ -181,7 +174,7 @@ const getOrdersTableColumnDef = ({ columnKey: 'triggerPrice', getCellValue: (row) => row.triggerPrice ?? -1, label: stringGetter({ key: STRING_KEYS.TRIGGER_PRICE_SHORT }), - renderCell: ({ type, triggerPrice, trailingPercent, tickSizeDecimals }) => ( + renderCell: ({ triggerPrice, trailingPercent, tickSizeDecimals }) => ( {trailingPercent && ( @@ -231,34 +224,29 @@ const getOrdersTableColumnDef = ({ key: STRING_KEYS.FILL, })}`, renderCell: ({ asset, createdAtMilliseconds, size, status, totalFilled, resources }) => { - const { statusIconColor, statusStringKey } = getStatusIconInfo({ - status, - totalFilled, - }); + const { statusIconColor } = getOrderStatusInfo({ status: status.rawValue }); return ( - - - - - + <$AssetIconWithStatus> + <$AssetIcon symbol={asset?.id} /> + <$StatusDot color={statusIconColor} /> + } > - {statusStringKey - ? stringGetter({ key: statusStringKey }) - : resources.statusStringKey && stringGetter({ key: resources.statusStringKey })} + {resources.statusStringKey && stringGetter({ key: resources.statusStringKey })} - + <$InlineRow> - + ); }, @@ -287,13 +275,13 @@ const getOrdersTableColumnDef = ({ getCellValue: (row) => row.price, renderCell: ({ price, orderSide, tickSizeDecimals, resources }) => ( - - + <$InlineRow> + <$Side side={orderSide}> {resources.sideStringKey ? stringGetter({ key: resources.sideStringKey }) : null} - - @ + + <$SecondaryColor>@ - + {resources.typeStringKey ? stringGetter({ key: resources.typeStringKey }) : null} @@ -308,6 +296,7 @@ type ElementProps = { columnKeys: OrdersTableColumnKey[]; columnWidths?: Partial>; currentMarket?: string; + initialPageSize?: PageSize; }; type StyleProps = { @@ -318,6 +307,7 @@ export const OrdersTable = ({ columnKeys = [], columnWidths, currentMarket, + initialPageSize, withOuterBorder, }: ElementProps & StyleProps) => { const stringGetter = useStringGetter(); @@ -325,12 +315,12 @@ export const OrdersTable = ({ const { isTablet } = useBreakpoints(); const isAccountViewOnly = useSelector(calculateIsAccountViewOnly); - const marketOrders = useSelector(getCurrentMarketOrders, shallowEqual) || []; - const allOrders = useSelector(getSubaccountUnclearedOrders, shallowEqual) || []; + const marketOrders = useSelector(getCurrentMarketOrders, shallowEqual) ?? EMPTY_ARR; + const allOrders = useSelector(getSubaccountUnclearedOrders, shallowEqual) ?? EMPTY_ARR; const orders = currentMarket ? marketOrders : allOrders; - const allPerpetualMarkets = useSelector(getPerpetualMarkets, shallowEqual) || {}; - const allAssets = useSelector(getAssets, shallowEqual) || {}; + const allPerpetualMarkets = orEmptyObj(useSelector(getPerpetualMarkets, shallowEqual)); + const allAssets = orEmptyObj(useSelector(getAssets, shallowEqual)); const hasUnseenOrderUpdates = useSelector(getHasUnseenOrderUpdates); @@ -340,16 +330,20 @@ export const OrdersTable = ({ const symbol = currentMarket ? allAssets[allPerpetualMarkets[currentMarket]?.assetId]?.id : null; - const ordersData = orders.map((order: SubaccountOrder) => - getHydratedTradingData({ - data: order, - assets: allAssets, - perpetualMarkets: allPerpetualMarkets, - }) - ) as OrderTableRow[]; + const ordersData = useMemo( + () => + orders.map((order: SubaccountOrder) => + getHydratedTradingData({ + data: order, + assets: allAssets, + perpetualMarkets: allPerpetualMarkets, + }) + ) as OrderTableRow[], + [orders, allPerpetualMarkets, allAssets] + ); return ( - ({ 'data-clearable': isOrderStatusClearable(row.status), })} - onRowAction={(key: string) => + onRowAction={(key: Key) => dispatch( openDialog({ type: DialogTypes.OrderDetails, @@ -365,7 +359,7 @@ export const OrdersTable = ({ }) ) } - columns={columnKeys.map((key: OrdersTableColumnKey, index: number) => + columns={columnKeys.map((key: OrdersTableColumnKey) => getOrdersTableColumnDef({ key, dispatch, @@ -378,10 +372,11 @@ export const OrdersTable = ({ )} slotEmpty={ <> - + <$EmptyIcon iconName={IconName.OrderPending} />

    {stringGetter({ key: STRING_KEYS.ORDERS_EMPTY_STATE })}

    } + initialPageSize={initialPageSize} withOuterBorder={withOuterBorder} withInnerBorders withScrollSnapColumns @@ -390,10 +385,7 @@ export const OrdersTable = ({ /> ); }; - -const Styled: Record = {}; - -Styled.Table = styled(Table)` +const $Table = styled(Table)` ${tradeViewMixins.horizontalTable} tbody tr { @@ -401,13 +393,13 @@ Styled.Table = styled(Table)` opacity: 0.5; } } -`; +` as typeof Table; -Styled.InlineRow = styled.div` +const $InlineRow = styled.div` ${layoutMixins.inlineRow} `; -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` font-size: 2rem; @media ${breakpoints.tablet} { @@ -415,15 +407,15 @@ Styled.AssetIcon = styled(AssetIcon)` } `; -Styled.TimeOutput = styled(Output)` +const $TimeOutput = styled(Output)` color: var(--color-text-0); `; -Styled.SecondaryColor = styled.span` +const $SecondaryColor = styled.span` color: var(--color-text-0); `; -Styled.Side = styled.span<{ side: OrderSide }>` +const $Side = styled.span<{ side: OrderSide }>` ${({ side }) => ({ [OrderSide.BUY]: css` @@ -435,19 +427,19 @@ Styled.Side = styled.span<{ side: OrderSide }>` }[side])}; `; -Styled.EmptyIcon = styled(Icon)` +const $EmptyIcon = styled(Icon)` font-size: 3em; `; -Styled.AssetIconWithStatus = styled.div` +const $AssetIconWithStatus = styled.div` ${layoutMixins.stack} - ${Styled.AssetIcon} { + ${$AssetIcon} { margin: 0.125rem; } `; -Styled.StatusDot = styled.div<{ color: string }>` +const $StatusDot = styled.div<{ color: string }>` place-self: start end; width: 0.875rem; height: 0.875rem; @@ -457,10 +449,6 @@ Styled.StatusDot = styled.div<{ color: string }>` background-color: ${({ color }) => color}; `; -Styled.StatusIcon = styled(Icon)<{ color: string }>` - color: ${({ color }) => color}; -`; - -Styled.WithTooltip = styled(WithTooltip)` +const $WithTooltip = styled(WithTooltip)` --tooltip-backgroundColor: var(--color-layer-5); `; diff --git a/src/views/tables/OrdersTable/OrderActionsCell.tsx b/src/views/tables/OrdersTable/OrderActionsCell.tsx index e9743846f..060cbb087 100644 --- a/src/views/tables/OrdersTable/OrderActionsCell.tsx +++ b/src/views/tables/OrdersTable/OrderActionsCell.tsx @@ -1,15 +1,16 @@ -import { useCallback, useEffect, useState } from 'react'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import styled, { AnyStyledComponent } from 'styled-components'; +import { useCallback, useState } from 'react'; + +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { AbacusOrderStatus, type OrderStatus } from '@/constants/abacus'; +import { ButtonShape } from '@/constants/buttons'; -import { useSubaccount } from '@/hooks'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { useSubaccount } from '@/hooks/useSubaccount'; import { IconName } from '@/components/Icon'; import { IconButton } from '@/components/IconButton'; -import { Toolbar } from '@/components/Toolbar'; +import { ActionsTableCell } from '@/components/Table'; import { clearOrder } from '@/state/account'; @@ -29,59 +30,33 @@ export const OrderActionsCell = ({ orderId, status, isDisabled }: ElementProps) const onCancel = useCallback(async () => { setIsCanceling(true); - await cancelOrder({ orderId, onError: () => setIsCanceling(false) }); + cancelOrder({ orderId, onError: () => setIsCanceling(false) }); }, []); return ( - - - {isOrderStatusClearable(status) ? ( - dispatch(clearOrder(orderId))} - /> - ) : ( - - )} - - + + <$CancelButton + key="cancelorder" + iconName={IconName.Close} + shape={ButtonShape.Square} + {...(isOrderStatusClearable(status) + ? { onClick: () => dispatch(clearOrder(orderId)) } + : { + onClick: onCancel, + state: { + isLoading: isCanceling || status === AbacusOrderStatus.canceling, + isDisabled: isCanceling || !!isDisabled || status === AbacusOrderStatus.canceling, + }, + })} + /> + ); }; - -const Styled: Record = {}; - -Styled.OrderActions = styled.div` - ${layoutMixins.row}; - justify-content: var(--table-cell-currentAlign); -`; - -Styled.Toolbar = styled(Toolbar)` - width: 3rem; - - padding: 0; - display: flex; - justify-content: center; -`; - -Styled.ActionButton = styled(IconButton)` - --button-backgroundColor: transparent; - --button-border: none; +const $CancelButton = styled(IconButton)` + --button-hover-textColor: var(--color-red); svg { width: 0.875em; height: 0.875em; } `; - -Styled.CancelButton = styled(Styled.ActionButton)` - &:not(:disabled) { - --button-textColor: var(--color-red); - } -`; diff --git a/src/views/tables/PositionsTable.tsx b/src/views/tables/PositionsTable.tsx index 3e8f80f15..41536ae7d 100644 --- a/src/views/tables/PositionsTable.tsx +++ b/src/views/tables/PositionsTable.tsx @@ -1,38 +1,52 @@ -import { useSelector, shallowEqual } from 'react-redux'; -import styled, { type AnyStyledComponent } from 'styled-components'; -import { useNavigate } from 'react-router-dom'; +import { Key, useMemo } from 'react'; + import type { ColumnSize } from '@react-types/table'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; import { type Asset, type Nullable, + type SubaccountOrder, type SubaccountPosition, - POSITION_SIDES, } from '@/constants/abacus'; - -import { StringGetterFunction, STRING_KEYS } from '@/constants/localization'; -import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; +import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; +import { NumberSign, TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; +import { EMPTY_ARR } from '@/constants/objects'; +import { AppRoute } from '@/constants/routes'; import { PositionSide } from '@/constants/trade'; -import { useStringGetter } from '@/hooks'; + import { MediaQueryKeys } from '@/hooks/useBreakpoints'; +import { useEnvFeatures } from '@/hooks/useEnvFeatures'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; import { tradeViewMixins } from '@/styles/tradeViewMixins'; -import { getExistingOpenPositions } from '@/state/accountSelectors'; -import { getAssets } from '@/state/assetsSelectors'; -import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; - import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; -import { MarketTableCell } from '@/components/Table/MarketTableCell'; import { Output, OutputType, ShowSign } from '@/components/Output'; -import { PositionSideTag } from '@/components/PositionSideTag'; -import { type ColumnDef, Table, TableColumnHeader } from '@/components/Table'; +import { Table, TableColumnHeader, type ColumnDef } from '@/components/Table'; +import { MarketTableCell } from '@/components/Table/MarketTableCell'; import { TableCell } from '@/components/Table/TableCell'; -import { TagSize } from '@/components/Tag'; +import { PageSize } from '@/components/Table/TablePaginationRow'; + +import { + calculateIsAccountViewOnly, + calculateShouldRenderTriggersInPositionsTable, +} from '@/state/accountCalculators'; +import { getExistingOpenPositions, getSubaccountConditionalOrders } from '@/state/accountSelectors'; +import { getAssets } from '@/state/assetsSelectors'; +import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; -import { MustBigNumber } from '@/lib/numbers'; +import { MustBigNumber, getNumberSign } from '@/lib/numbers'; +import { testFlags } from '@/lib/testFlags'; +import { orEmptyObj } from '@/lib/typeUtils'; + +import { PositionsActionsCell } from './PositionsTable/PositionsActionsCell'; +import { PositionsMarginCell } from './PositionsTable/PositionsMarginCell'; +import { PositionsTriggersCell } from './PositionsTable/PositionsTriggersCell'; export enum PositionsTableColumnKey { Details = 'Details', @@ -40,29 +54,42 @@ export enum PositionsTableColumnKey { PnL = 'PnL', Market = 'Market', - Side = 'Side', Size = 'Size', - Leverage = 'Leverage', LiquidationAndOraclePrice = 'LiquidationAndOraclePrice', + Margin = 'Margin', UnrealizedPnl = 'UnrealizedPnl', RealizedPnl = 'RealizedPnl', AverageOpenAndClose = 'AverageOpenAndClose', + NetFunding = 'NetFunding', + Triggers = 'Triggers', + Actions = 'Actions', } type PositionTableRow = { asset: Asset; oraclePrice: Nullable; tickSizeDecimals: number; + fundingRate: Nullable; + stopLossOrders: SubaccountOrder[]; + takeProfitOrders: SubaccountOrder[]; } & SubaccountPosition; const getPositionsTableColumnDef = ({ key, stringGetter, width, + isAccountViewOnly, + showClosePositionAction, + shouldRenderTriggers, + navigateToOrders, }: { key: PositionsTableColumnKey; stringGetter: StringGetterFunction; width?: ColumnSize; + isAccountViewOnly: boolean; + showClosePositionAction: boolean; + shouldRenderTriggers: boolean; + navigateToOrders: (market: string) => void; }) => ({ width, ...( @@ -71,27 +98,27 @@ const getPositionsTableColumnDef = ({ columnKey: 'details', getCellValue: (row) => row.id, label: stringGetter({ key: STRING_KEYS.DETAILS }), - renderCell: ({ id, asset, leverage, resources, size }) => ( - }> - ( + }> + <$HighlightOutput type={OutputType.Asset} value={size?.current} fractionDigits={TOKEN_DECIMALS} showSign={ShowSign.None} tag={asset?.id} /> - - + <$InlineRow> + <$PositionSide> {resources.sideStringKey?.current && stringGetter({ key: resources.sideStringKey?.current })} - - @ - + <$SecondaryColor>@ + <$HighlightOutput type={OutputType.Multiple} value={leverage?.current} showSign={ShowSign.None} /> - + ), }, @@ -120,13 +147,13 @@ const getPositionsTableColumnDef = ({ hideOnBreakpoint: MediaQueryKeys.isNotTablet, renderCell: ({ unrealizedPnl, unrealizedPnlPercent }) => ( - - row.id, label: stringGetter({ key: STRING_KEYS.MARKET }), hideOnBreakpoint: MediaQueryKeys.isMobile, - renderCell: ({ id, asset }) => , - }, - [PositionsTableColumnKey.Side]: { - columnKey: 'market-side', - getCellValue: (row) => row.side?.current && POSITION_SIDES[row.side.current.name], - label: stringGetter({ key: STRING_KEYS.SIDE }), - renderCell: ({ side }) => - side?.current && ( - - ), + renderCell: ({ id, asset, leverage }) => ( + + ), }, [PositionsTableColumnKey.Size]: { columnKey: 'size', @@ -161,11 +183,12 @@ const getPositionsTableColumnDef = ({ hideOnBreakpoint: MediaQueryKeys.isMobile, renderCell: ({ assetId, size, notionalTotal, tickSizeDecimals }) => ( - ), }, - [PositionsTableColumnKey.Leverage]: { - columnKey: 'leverage', + [PositionsTableColumnKey.Margin]: { + columnKey: 'margin', getCellValue: (row) => row.leverage?.current, - label: stringGetter({ key: STRING_KEYS.LEVERAGE }), + label: stringGetter({ key: STRING_KEYS.MARGIN }), hideOnBreakpoint: MediaQueryKeys.isMobile, - renderCell: ({ leverage }) => ( - + isActionable: true, + renderCell: ({ id, adjustedMmf, notionalTotal }) => ( + + ), + }, + [PositionsTableColumnKey.NetFunding]: { + columnKey: 'netFunding', + getCellValue: (row) => row.netFunding, + label: ( + + {stringGetter({ key: STRING_KEYS.FUNDING_PAYMENTS_SHORT })} + {stringGetter({ key: STRING_KEYS.RATE })} + + ), + hideOnBreakpoint: MediaQueryKeys.isTablet, + renderCell: ({ netFunding, fundingRate }) => ( + + <$OutputSigned + sign={getNumberSign(netFunding)} + type={OutputType.Fiat} + value={netFunding} + /> + + ), }, [PositionsTableColumnKey.LiquidationAndOraclePrice]: { @@ -211,8 +256,8 @@ const getPositionsTableColumnDef = ({ hideOnBreakpoint: MediaQueryKeys.isTablet, renderCell: ({ unrealizedPnl, unrealizedPnlPercent }) => ( - @@ -227,8 +272,8 @@ const getPositionsTableColumnDef = ({ hideOnBreakpoint: MediaQueryKeys.isTablet, renderCell: ({ realizedPnl, realizedPnlPercent }) => ( - row.entryPrice?.current, - label: stringGetter({ key: STRING_KEYS.AVERAGE_OPEN_CLOSE }), - hideOnBreakpoint: MediaQueryKeys.isDesktopSmall, + label: ( + + {stringGetter({ key: STRING_KEYS.AVERAGE_OPEN_SHORT })} + {stringGetter({ key: STRING_KEYS.AVERAGE_CLOSE_SHORT })} + + ), + hideOnBreakpoint: MediaQueryKeys.isTablet, renderCell: ({ entryPrice, exitPrice, tickSizeDecimals }) => ( ), }, + [PositionsTableColumnKey.Triggers]: { + columnKey: 'triggers', + label: stringGetter({ key: STRING_KEYS.TRIGGERS }), + isActionable: true, + allowsSorting: false, + hideOnBreakpoint: MediaQueryKeys.isTablet, + renderCell: ({ + id, + assetId, + tickSizeDecimals, + liquidationPrice, + side, + size, + stopLossOrders, + takeProfitOrders, + }) => ( + + ), + }, + [PositionsTableColumnKey.Actions]: { + columnKey: 'actions', + label: stringGetter({ + key: + shouldRenderTriggers && showClosePositionAction && !testFlags.isolatedMargin + ? STRING_KEYS.ACTIONS + : showClosePositionAction + ? STRING_KEYS.CLOSE + : STRING_KEYS.ACTION, + }), + isActionable: true, + allowsSorting: false, + hideOnBreakpoint: MediaQueryKeys.isTablet, + renderCell: ({ id, assetId, stopLossOrders, takeProfitOrders }) => ( + + ), + }, } as Record> )[key], }); @@ -261,7 +366,11 @@ type ElementProps = { columnKeys: PositionsTableColumnKey[]; columnWidths?: Partial>; currentRoute?: string; + currentMarket?: string; + showClosePositionAction: boolean; + initialPageSize?: PageSize; onNavigate?: () => void; + navigateToOrders: (market: string) => void; }; type StyleProps = { @@ -273,27 +382,70 @@ export const PositionsTable = ({ columnKeys, columnWidths, currentRoute, + currentMarket, + showClosePositionAction, + initialPageSize, onNavigate, + navigateToOrders, withGradientCardRows, withOuterBorder, }: ElementProps & StyleProps) => { const stringGetter = useStringGetter(); const navigate = useNavigate(); + const { isSlTpLimitOrdersEnabled } = useEnvFeatures(); + + const isAccountViewOnly = useSelector(calculateIsAccountViewOnly); + const perpetualMarkets = orEmptyObj(useSelector(getPerpetualMarkets, shallowEqual)); + const assets = orEmptyObj(useSelector(getAssets, shallowEqual)); + const shouldRenderTriggers = useSelector(calculateShouldRenderTriggersInPositionsTable); + + const openPositions = useSelector(getExistingOpenPositions, shallowEqual) ?? EMPTY_ARR; + const positions = useMemo(() => { + const marketPosition = openPositions.find((position) => position.id === currentMarket); + return currentMarket ? (marketPosition ? [marketPosition] : []) : openPositions; + }, [currentMarket, openPositions]); - const perpetualMarkets = useSelector(getPerpetualMarkets, shallowEqual) || {}; - const assets = useSelector(getAssets, shallowEqual) || {}; - const openPositions = useSelector(getExistingOpenPositions, shallowEqual) || []; + const { stopLossOrders: allStopLossOrders, takeProfitOrders: allTakeProfitOrders } = useSelector( + getSubaccountConditionalOrders(isSlTpLimitOrdersEnabled), + { + equalityFn: (oldVal, newVal) => { + return ( + shallowEqual(oldVal.stopLossOrders, newVal.stopLossOrders) && + shallowEqual(oldVal.takeProfitOrders, newVal.takeProfitOrders) + ); + }, + } + ); - const positionsData = openPositions.map((position: SubaccountPosition) => ({ - tickSizeDecimals: perpetualMarkets?.[position.id]?.configs?.tickSizeDecimals || USD_DECIMALS, - asset: assets?.[position.assetId], - oraclePrice: perpetualMarkets?.[position.id]?.oraclePrice, - ...position, - })) as PositionTableRow[]; + const positionsData = useMemo( + () => + positions.map((position: SubaccountPosition): PositionTableRow => { + // object splat ... doesn't copy getter defined properties + // eslint-disable-next-line prefer-object-spread + return Object.assign( + {}, + { + tickSizeDecimals: + perpetualMarkets?.[position.id]?.configs?.tickSizeDecimals ?? USD_DECIMALS, + asset: assets?.[position.assetId], + oraclePrice: perpetualMarkets?.[position.id]?.oraclePrice, + fundingRate: perpetualMarkets?.[position.id]?.perpetual?.nextFundingRate, + stopLossOrders: allStopLossOrders.filter( + (order: SubaccountOrder) => order.marketId === position.id + ), + takeProfitOrders: allTakeProfitOrders.filter( + (order: SubaccountOrder) => order.marketId === position.id + ), + }, + position + ); + }), + [positions, perpetualMarkets, assets, allStopLossOrders, allTakeProfitOrders] + ); return ( - row.id} - onRowAction={(market: string) => { - navigate(`/trade/${market}`, { - state: { from: currentRoute }, - }); - onNavigate?.(); - }} + onRowAction={ + currentMarket + ? undefined + : (market: Key) => { + navigate(`${AppRoute.Trade}/${market}`, { + state: { from: currentRoute }, + }); + onNavigate?.(); + } + } getRowAttributes={(row: PositionTableRow) => ({ 'data-side': row.side.current, })} slotEmpty={ <> - + <$Icon iconName={IconName.Positions} />

    {stringGetter({ key: STRING_KEYS.POSITIONS_EMPTY_STATE })}

    } + initialPageSize={initialPageSize} withGradientCardRows={withGradientCardRows} withOuterBorder={withOuterBorder} withInnerBorders @@ -332,10 +493,7 @@ export const PositionsTable = ({ /> ); }; - -const Styled: Record = {}; - -Styled.Table = styled(Table)` +const $Table = styled(Table)` ${tradeViewMixins.horizontalTable} tr { @@ -358,27 +516,32 @@ Styled.Table = styled(Table)` --table-row-gradient-to-color: var(--color-gradient-negative); } } -`; +` as typeof Table; -Styled.InlineRow = styled.div` +const $InlineRow = styled.div` ${layoutMixins.inlineRow} `; -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` ${layoutMixins.inlineRow} min-width: unset; font-size: 2.25rem; `; -Styled.SecondaryColor = styled.span` +const $SecondaryColor = styled.span` color: var(--color-text-0); `; -Styled.OutputSigned = styled(Output)<{ isNegative?: boolean }>` - color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)}; +const $OutputSigned = styled(Output)<{ sign: NumberSign }>` + color: ${({ sign }) => + ({ + [NumberSign.Positive]: `var(--color-positive)`, + [NumberSign.Negative]: `var(--color-negative)`, + [NumberSign.Neutral]: `var(--color-text-2)`, + }[sign])}; `; -Styled.HighlightOutput = styled(Output)<{ isNegative?: boolean }>` +const $HighlightOutput = styled(Output)<{ isNegative?: boolean }>` color: var(--color-text-1); --secondary-item-color: currentColor; --output-sign-color: ${({ isNegative }) => @@ -389,12 +552,12 @@ Styled.HighlightOutput = styled(Output)<{ isNegative?: boolean }>` : `currentColor`}; `; -Styled.PositionSide = styled.span` +const $PositionSide = styled.span` && { color: var(--side-color); } `; -Styled.Icon = styled(Icon)` +const $Icon = styled(Icon)` font-size: 3em; `; diff --git a/src/views/tables/PositionsTable/PositionsActionsCell.tsx b/src/views/tables/PositionsTable/PositionsActionsCell.tsx new file mode 100644 index 000000000..631754a21 --- /dev/null +++ b/src/views/tables/PositionsTable/PositionsActionsCell.tsx @@ -0,0 +1,120 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +import { type SubaccountOrder } from '@/constants/abacus'; +import { ButtonShape } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; +import { DialogTypes, TradeBoxDialogTypes } from '@/constants/dialogs'; +import { AppRoute } from '@/constants/routes'; + +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useEnvFeatures } from '@/hooks/useEnvFeatures'; + +import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { ActionsTableCell } from '@/components/Table'; + +import { closeDialogInTradeBox, openDialog, openDialogInTradeBox } from '@/state/dialogs'; +import { getActiveTradeBoxDialog } from '@/state/dialogsSelectors'; +import { getCurrentMarketId } from '@/state/perpetualsSelectors'; + +import abacusStateManager from '@/lib/abacus'; +import { testFlags } from '@/lib/testFlags'; + +type ElementProps = { + marketId: string; + assetId: string; + stopLossOrders: SubaccountOrder[]; + takeProfitOrders: SubaccountOrder[]; + isDisabled?: boolean; + showClosePositionAction: boolean; + navigateToMarketOrders: (market: string) => void; +}; + +export const PositionsActionsCell = ({ + marketId, + assetId, + stopLossOrders, + takeProfitOrders, + isDisabled, + showClosePositionAction, + navigateToMarketOrders, +}: ElementProps) => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { complianceState } = useComplianceState(); + const { isSlTpEnabled } = useEnvFeatures(); + + const currentMarketId = useSelector(getCurrentMarketId); + const activeTradeBoxDialog = useSelector(getActiveTradeBoxDialog); + const { type: tradeBoxDialogType } = activeTradeBoxDialog ?? {}; + + const onCloseButtonToggle = (isPressed: boolean) => { + navigate(`${AppRoute.Trade}/${marketId}`); + dispatch( + isPressed + ? openDialogInTradeBox({ + type: TradeBoxDialogTypes.ClosePosition, + }) + : closeDialogInTradeBox() + ); + + if (!isPressed) { + abacusStateManager.clearClosePositionInputValues({ shouldFocusOnTradeInput: true }); + } + }; + + return ( + + {isSlTpEnabled && + !testFlags.isolatedMargin && + complianceState === ComplianceStates.FULL_ACCESS && ( + <$TriggersButton + key="edittriggers" + onClick={() => + dispatch( + openDialog({ + type: DialogTypes.Triggers, + dialogProps: { + marketId, + assetId, + stopLossOrders, + takeProfitOrders, + navigateToMarketOrders, + }, + }) + ) + } + iconName={IconName.Pencil} + shape={ButtonShape.Square} + disabled={isDisabled} + /> + )} + {showClosePositionAction && ( + <$CloseButtonToggle + key="closepositions" + isToggle + isPressed={ + tradeBoxDialogType === TradeBoxDialogTypes.ClosePosition && currentMarketId === marketId + } + onPressedChange={onCloseButtonToggle} + iconName={IconName.Close} + shape={ButtonShape.Square} + disabled={isDisabled} + /> + )} + + ); +}; +const $TriggersButton = styled(IconButton)` + --button-icon-size: 1.33em; + --button-textColor: var(--color-text-0); + --button-hover-textColor: var(--color-text-1); +`; + +const $CloseButtonToggle = styled(IconButton)` + --button-icon-size: 1em; + --button-hover-textColor: var(--color-red); + --button-toggle-on-textColor: var(--color-red); +`; diff --git a/src/views/tables/PositionsTable/PositionsMarginCell.tsx b/src/views/tables/PositionsTable/PositionsMarginCell.tsx new file mode 100644 index 000000000..e3aeb9649 --- /dev/null +++ b/src/views/tables/PositionsTable/PositionsMarginCell.tsx @@ -0,0 +1,76 @@ +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; + +import { type SubaccountPosition } from '@/constants/abacus'; +import { ButtonShape } from '@/constants/buttons'; +import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { TableCell } from '@/components/Table/TableCell'; + +import { openDialog } from '@/state/dialogs'; + +import { calculatePositionMargin } from '@/lib/tradeData'; + +type PositionsMarginCellProps = { + id: SubaccountPosition['id']; + notionalTotal: SubaccountPosition['notionalTotal']; + adjustedMmf: SubaccountPosition['adjustedMmf']; +}; + +export const PositionsMarginCell = ({ + id, + adjustedMmf, + notionalTotal, +}: PositionsMarginCellProps) => { + const stringGetter = useStringGetter(); + const dispatch = useDispatch(); + const margin = calculatePositionMargin({ + notionalTotal: notionalTotal?.current, + adjustedMmf: adjustedMmf?.current, + }); + const perpetualMarketType = 'CROSS'; // Todo: Replace with perpetualMarketType when available + + const marginModeLabel = + perpetualMarketType === 'CROSS' + ? stringGetter({ key: STRING_KEYS.CROSS }) + : stringGetter({ key: STRING_KEYS.ISOLATED }); + + return ( + + dispatch( + openDialog({ + type: DialogTypes.AdjustIsolatedMargin, + dialogProps: { positionId: id }, + }) + ) + } + /> + } + > + + {marginModeLabel} + + ); +}; +const $EditButton = styled(IconButton)` + --button-icon-size: 1.5em; + --button-padding: 0; + --button-textColor: var(--color-text-0); + --button-hover-textColor: var(--color-text-1); + + margin-left: 0.5rem; +`; diff --git a/src/views/tables/PositionsTable/PositionsTriggersCell.tsx b/src/views/tables/PositionsTable/PositionsTriggersCell.tsx new file mode 100644 index 000000000..e4e1844b1 --- /dev/null +++ b/src/views/tables/PositionsTable/PositionsTriggersCell.tsx @@ -0,0 +1,300 @@ +import { useDispatch } from 'react-redux'; +import styled, { css } from 'styled-components'; + +import { + AbacusPositionSide, + Nullable, + type AbacusPositionSides, + type SubaccountOrder, +} from '@/constants/abacus'; +import { ButtonAction, ButtonShape, ButtonSize } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; +import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useEnvFeatures } from '@/hooks/useEnvFeatures'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { Output, OutputType } from '@/components/Output'; +import { TableCell } from '@/components/Table'; +import { WithHovercard } from '@/components/WithHovercard'; + +import { openDialog } from '@/state/dialogs'; + +import { isStopLossOrder } from '@/lib/orders'; +import { testFlags } from '@/lib/testFlags'; + +type ElementProps = { + marketId: string; + assetId: string; + tickSizeDecimals: number; + liquidationPrice: Nullable; + stopLossOrders: SubaccountOrder[]; + takeProfitOrders: SubaccountOrder[]; + onViewOrdersClick: (marketId: string) => void; + positionSide: Nullable; + positionSize: Nullable; + isDisabled?: boolean; +}; + +export const PositionsTriggersCell = ({ + marketId, + assetId, + tickSizeDecimals, + liquidationPrice, + stopLossOrders, + takeProfitOrders, + onViewOrdersClick, + positionSide, + positionSize, + isDisabled, +}: ElementProps) => { + const stringGetter = useStringGetter(); + const dispatch = useDispatch(); + const { isSlTpLimitOrdersEnabled } = useEnvFeatures(); + const { complianceState } = useComplianceState(); + + const onViewOrders = isDisabled ? null : () => onViewOrdersClick(marketId); + + const showLiquidationWarning = (order: SubaccountOrder) => { + if (!isStopLossOrder(order, isSlTpLimitOrdersEnabled) || !liquidationPrice) { + return false; + } + return ( + (positionSide === AbacusPositionSide.SHORT && + (order.triggerPrice ?? order.price) > liquidationPrice) || + (positionSide === AbacusPositionSide.LONG && + (order.triggerPrice ?? order.price) < liquidationPrice) + ); + }; + + const openTriggersDialog = () => { + dispatch( + openDialog({ + type: DialogTypes.Triggers, + dialogProps: { + marketId, + assetId, + stopLossOrders, + takeProfitOrders, + navigateToMarketOrders: onViewOrders, + }, + }) + ); + }; + + const viewOrdersButton = ( + <$Button + action={ButtonAction.Navigation} + size={ButtonSize.XSmall} + onClick={onViewOrders ?? undefined} + > + {stringGetter({ key: STRING_KEYS.VIEW_ORDERS })} + <$ArrowIcon iconName={IconName.Arrow} /> + + ); + + const renderOutput = ({ label, orders }: { label: string; orders: SubaccountOrder[] }) => { + const triggerLabel = ({ + liquidationWarningSide, + }: { + liquidationWarningSide?: Nullable; + } = {}) => { + const styledLabel = ( + <$Label warning={liquidationWarningSide != null} hasOrders={orders.length > 0}> + {label} + + ); + return liquidationWarningSide ? ( + + {stringGetter({ key: STRING_KEYS.EDIT_STOP_LOSS })} + + } + slotTrigger={styledLabel} + /> + ) : ( + styledLabel + ); + }; + + if (orders.length === 0) { + return ( + <> + {triggerLabel()} <$Output type={OutputType.Fiat} value={null} /> + + ); + } + + if (orders.length === 1) { + const order = orders[0]; + const { size, triggerPrice } = order; + + const isPartialPosition = !!(positionSize && Math.abs(size) < Math.abs(positionSize)); + const liquidationWarningSide = showLiquidationWarning(order) ? positionSide : undefined; + + return ( + <> + {triggerLabel({ liquidationWarningSide })} + <$Output + type={OutputType.Fiat} + value={triggerPrice ?? null} + fractionDigits={tickSizeDecimals} + /> + {isPartialPosition && ( + + {stringGetter({ + key: isStopLossOrder(order, isSlTpLimitOrdersEnabled) + ? STRING_KEYS.EDIT_STOP_LOSS + : STRING_KEYS.EDIT_TAKE_PROFIT, + })} + + } + slotTrigger={ + <$PartialFillIcon> + + + } + /> + )} + + ); + } + + return ( + <> + {triggerLabel()} + {viewOrdersButton} + + ); + }; + + return ( + <$TableCell + stacked + stackedWithSecondaryStyling={false} + slotRight={ + !isDisabled && + testFlags.isolatedMargin && + complianceState === ComplianceStates.FULL_ACCESS && ( + <$EditButton + key="edit-margin" + iconName={IconName.Pencil} + shape={ButtonShape.Square} + onClick={openTriggersDialog} + /> + ) + } + > + <$Row>{renderOutput({ label: 'TP', orders: takeProfitOrders })} + <$Row>{renderOutput({ label: 'SL', orders: stopLossOrders })} + + ); +}; +const $Row = styled.span` + ${layoutMixins.inlineRow} + + --item-height: 1.25rem; +`; + +const $Label = styled.div<{ warning?: boolean; hasOrders: boolean }>` + align-items: center; + border: solid var(--border-width) var(--color-border); + border-radius: 0.5em; + display: flex; + font: var(--font-tiny-book); + height: var(--item-height); + padding: 0 0.25rem; + + ${({ warning }) => + warning && + css` + background-color: var(--color-warning); + color: var(--color-black); + `} + + ${({ hasOrders }) => + hasOrders + ? css` + color: var(--color-text-1); + background-color: var(--color-layer-4); + ` + : css` + color: var(--color-text-0); + `} +`; + +const $Output = styled(Output)<{ value: number | null }>` + font: var(--font-mini-medium); + ${({ value }) => + value + ? css` + color: var(--color-text-1); + ` + : css` + color: var(--color-text-0); + `} +`; + +const $Button = styled(Button)` + --button-height: var(--item-height); + --button-padding: 0; + --button-textColor: var(--color-text-1); +`; + +const $ArrowIcon = styled(Icon)` + stroke-width: 2; +`; + +const $PartialFillIcon = styled.span` + svg { + display: block; + + width: 0.875em; + height: 0.875em; + } +`; + +const $EditButton = styled(IconButton)` + --button-icon-size: 1.5em; + --button-padding: 0; + --button-textColor: var(--color-text-0); + --button-hover-textColor: var(--color-text-1); + + margin-left: 0.5rem; +`; + +const $TableCell = styled(TableCell)` + justify-content: space-between; +`; diff --git a/src/views/tables/TradingRewardHistoryTable.tsx b/src/views/tables/TradingRewardHistoryTable.tsx index f856ed7da..a0cc46b61 100644 --- a/src/views/tables/TradingRewardHistoryTable.tsx +++ b/src/views/tables/TradingRewardHistoryTable.tsx @@ -1,19 +1,21 @@ -import styled, { type AnyStyledComponent } from 'styled-components'; +import { useMemo } from 'react'; + import { shallowEqual, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { HistoricalTradingReward, HistoricalTradingRewardsPeriods } from '@/constants/abacus'; +import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; -import { HistoricalTradingRewardsPeriods, HistoricalTradingReward } from '@/constants/abacus'; -import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; -import { useStringGetter, useTokenConfigs } from '@/hooks'; import { layoutMixins } from '@/styles/layoutMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { Output, OutputType } from '@/components/Output'; import { Table, TableCell, type ColumnDef } from '@/components/Table'; -import { - getHistoricalTradingRewards, - getHistoricalTradingRewardsForPeriod, -} from '@/state/accountSelectors'; +import { getHistoricalTradingRewardsForPeriod } from '@/state/accountSelectors'; export enum TradingRewardHistoryTableColumnKey { Event = 'Event', @@ -37,8 +39,8 @@ const getTradingRewardHistoryTableColumnDef = ({ label: stringGetter({ key: STRING_KEYS.EVENT }), renderCell: ({ startedAtInMilliseconds, endedAtInMilliseconds }) => ( - {stringGetter({ key: STRING_KEYS.REWARDED })} - + <$Rewarded>{stringGetter({ key: STRING_KEYS.REWARDED })} + <$TimePeriod> {stringGetter({ key: STRING_KEYS.FOR_TRADING, params: { @@ -59,7 +61,7 @@ const getTradingRewardHistoryTableColumnDef = ({ ), }, })} - + ), }, @@ -71,7 +73,7 @@ const getTradingRewardHistoryTableColumnDef = ({ } + slotRight={<$AssetIcon symbol={chainTokenLabel} />} /> ), }, @@ -103,10 +105,12 @@ export const TradingRewardHistoryTable = ({ shallowEqual ); + const rewardsData = useMemo(() => periodTradingRewards?.toArray() ?? [], [periodTradingRewards]); + return ( - row.startedAtInMilliseconds} columns={columnKeys.map((key: TradingRewardHistoryTableColumnKey) => getTradingRewardHistoryTableColumnDef({ @@ -121,31 +125,28 @@ export const TradingRewardHistoryTable = ({ selectionBehavior="replace" withOuterBorder={withOuterBorder} withInnerBorders={withInnerBorders} - viewMoreConfig={{ initialNumRowsToShow: 5, numRowsPerPage: 10 }} + initialPageSize={15} withScrollSnapColumns withScrollSnapRows /> ); }; -const Styled: Record = {}; - -Styled.Table = styled(Table)` +const $Table = styled(Table)` --tableCell-padding: 0.5rem 0; - --tableHeader-backgroundColor: var(--color-layer-3); + --tableStickyRow-backgroundColor: var(--color-layer-3); --tableRow-backgroundColor: var(--color-layer-3); - --tableViewMore-borderColor: var(--color-layer-3); tbody { font: var(--font-medium-book); } -`; +` as typeof Table; -Styled.Rewarded = styled.span` +const $Rewarded = styled.span` color: var(--color-text-2); `; -Styled.TimePeriod = styled.div` +const $TimePeriod = styled.div` ${layoutMixins.inlineRow} && { @@ -159,6 +160,6 @@ Styled.TimePeriod = styled.div` } `; -Styled.AssetIcon = styled(AssetIcon)` +const $AssetIcon = styled(AssetIcon)` margin-left: 0.5ch; `; diff --git a/src/views/tables/TransferHistoryTable.tsx b/src/views/tables/TransferHistoryTable.tsx index 8b495d856..31862ae48 100644 --- a/src/views/tables/TransferHistoryTable.tsx +++ b/src/views/tables/TransferHistoryTable.tsx @@ -1,33 +1,31 @@ -import styled, { type AnyStyledComponent, css } from 'styled-components'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import type { ColumnSize } from '@react-types/table'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { type SubaccountTransfer } from '@/constants/abacus'; import { ButtonAction } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; -import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; +import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; -import { useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useURLConfigs } from '@/hooks/useURLConfigs'; -import { layoutMixins } from '@/styles/layoutMixins'; import { tradeViewMixins } from '@/styles/tradeViewMixins'; import { Button } from '@/components/Button'; import { CopyButton } from '@/components/CopyButton'; -import { Icon } from '@/components/Icon'; import { Link } from '@/components/Link'; import { Output, OutputType } from '@/components/Output'; import { Table, TableCell, TableColumnHeader, type ColumnDef } from '@/components/Table'; +import { PageSize } from '@/components/Table/TablePaginationRow'; import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; -import { getSubaccountTransfers } from '@/state/accountSelectors'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; +import { getSubaccountTransfers } from '@/state/accountSelectors'; import { openDialog } from '@/state/dialogs'; import { truncateAddress } from '@/lib/wallet'; -const MOBILE_TRANSFERS_PER_PAGE = 50; - export enum TransferHistoryTableColumnKey { Time = 'Time', Action = 'Action', @@ -43,7 +41,6 @@ const getTransferHistoryTableColumnDef = ({ mintscanTxUrl, }: { key: TransferHistoryTableColumnKey; - isTablet?: boolean; stringGetter: StringGetterFunction; width?: ColumnSize; mintscanTxUrl?: string; @@ -56,7 +53,7 @@ const getTransferHistoryTableColumnDef = ({ getCellValue: (row) => row.updatedAtMilliseconds, label: stringGetter({ key: STRING_KEYS.TIME }), renderCell: ({ updatedAtMilliseconds }) => ( - transactionHash ? ( - + <$TxHash withIcon href={`${mintscanTxUrl?.replace('{tx_hash}', transactionHash)}`}> {truncateAddress(transactionHash, '')} - + ) : ( '-' ), @@ -119,6 +113,7 @@ const getTransferHistoryTableColumnDef = ({ type ElementProps = { columnKeys?: TransferHistoryTableColumnKey[]; columnWidths?: Partial>; + initialPageSize?: PageSize; }; type StyleProps = { @@ -129,12 +124,12 @@ type StyleProps = { export const TransferHistoryTable = ({ columnKeys = Object.values(TransferHistoryTableColumnKey), columnWidths, + initialPageSize, withOuterBorder, withInnerBorders = true, }: ElementProps & StyleProps) => { const stringGetter = useStringGetter(); const dispatch = useDispatch(); - const { isMobile, isTablet } = useBreakpoints(); const { mintscan: mintscanTxUrl } = useURLConfigs(); const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual); @@ -142,14 +137,13 @@ export const TransferHistoryTable = ({ const transfers = useSelector(getSubaccountTransfers, shallowEqual) ?? []; return ( - row.id} columns={columnKeys.map((key: TransferHistoryTableColumnKey) => getTransferHistoryTableColumnDef({ key, - isTablet, stringGetter, width: columnWidths?.[key], mintscanTxUrl, @@ -170,6 +164,7 @@ export const TransferHistoryTable = ({ )} } + initialPageSize={initialPageSize} selectionBehavior="replace" withOuterBorder={withOuterBorder} withInnerBorders={withInnerBorders} @@ -178,25 +173,14 @@ export const TransferHistoryTable = ({ /> ); }; - -const Styled: Record = {}; - -Styled.Table = styled(Table)` +const $Table = styled(Table)` ${tradeViewMixins.horizontalTable} -`; - -Styled.InlineRow = styled.div` - ${layoutMixins.inlineRow} -`; - -Styled.Icon = styled(Icon)` - font-size: 3em; -`; +` as typeof Table; -Styled.TimeOutput = styled(Output)` +const $TimeOutput = styled(Output)` color: var(--color-text-0); `; -Styled.TxHash = styled(Link)` +const $TxHash = styled(Link)` justify-content: flex-end; `; diff --git a/index.html b/template.html similarity index 67% rename from index.html rename to template.html index bcb21eb9a..332b1673b 100644 --- a/index.html +++ b/template.html @@ -11,13 +11,26 @@ - + +
    diff --git a/tsconfig.json b/tsconfig.json index e8cf44e72..a1f0cfa6d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,8 +20,9 @@ "useUnknownInCatchVariables": false, "paths": { "@/*": ["src/*"] - } + }, + "types": ["node", "@wdio/globals", "mocha"] }, - "include": ["src", "scripts"], + "include": ["src", "scripts", "styled.d.ts", "__tests__", "wdio.conf.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/vercel.json b/vercel.json index 0f32683a9..daa7ecd39 100644 --- a/vercel.json +++ b/vercel.json @@ -1,3 +1,8 @@ { - "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] -} + "rewrites": [ + { + "source": "/(.*)", + "destination": "/entry-points/index.html" + } + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 1fd758dfb..47356feed 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,28 @@ -import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import fs from 'fs'; import path from 'path'; +import sourcemaps from 'rollup-plugin-sourcemaps'; +import { defineConfig } from 'vite'; +import ViteRestart from 'vite-plugin-restart'; import svgr from 'vite-plugin-svgr'; +const entryPointsDir = path.join(__dirname, 'entry-points'); +const entryPointsExist = fs.existsSync(entryPointsDir); + +const entryPoints = entryPointsExist + ? fs.readdirSync(entryPointsDir).map((file) => `/entry-points/${file}`) + : []; + // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ define: { 'process.env': {}, }, + rollupOptions: { + // Needed for Abacus sourcemaps since Rollup doesn't load external sourcemaps by default. + // https://github.com/vitejs/vite/issues/11743 + plugins: mode === 'development' ? [sourcemaps()] : [], + }, resolve: { alias: [ { find: '@', replacement: path.resolve(__dirname, 'src') }, @@ -50,6 +65,27 @@ export default defineConfig(({ mode }) => ({ svgr({ exportAsDefault: true, }), + // Currently, the Vite file watcher is unable to watch folders within node_modules. + // Workaround is to use ViteRestart plugin + a generated file to trigger the restart. + // See https://github.com/vitejs/vite/issues/8619 + ViteRestart({ + restart: ['local-abacus-hash'], + }), ], publicDir: 'public', + test: { + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + '**/e2e/**', + ], + }, + build: { + rollupOptions: { + input: entryPoints, + }, + }, })); diff --git a/wdio.conf.ts b/wdio.conf.ts new file mode 100644 index 000000000..63e008fab --- /dev/null +++ b/wdio.conf.ts @@ -0,0 +1,372 @@ +import type { Options } from '@wdio/types'; + +export const config: Options.Testrunner = { + // + // ==================== + // Runner Configuration + // ==================== + // WebdriverIO supports running e2e tests as well as unit and component tests. + runner: 'local', + autoCompileOpts: { + autoCompile: true, + tsNodeOpts: { + project: './__tests__/tsconfig.json', + transpileOnly: true, + }, + }, + + // + // ================= + // Service Providers + // ================= + // WebdriverIO supports Sauce Labs, Browserstack, Testing Bot and LambdaTest (other cloud providers + // should work too though). These services define specific user and key (or access key) + // values you need to put in here in order to connect to these services. + // + user: process.env.BROWSERSTACK_USERNAME, + key: process.env.BROWSERSTACK_ACCESS_KEY, + // + // If you run your tests on Sauce Labs you can specify the region you want to run your tests + // in via the `region` property. Available short handles for regions are `us` (default), `eu` and `apac`. + // These regions are used for the Sauce Labs VM cloud and the Sauce Labs Real Device Cloud. + // If you don't provide the region it will default for the `us` + + // + // ================== + // Specify Test Files + // ================== + // Define which test specs should run. The pattern is relative to the directory + // of the configuration file being run. + // + // The specs are defined as an array of spec files (optionally using wildcards + // that will be expanded). The test for each spec file will be run in a separate + // worker process. In order to have a group of spec files run in the same worker + // process simply enclose them in an array within the specs array. + // + // The path of the spec files will be resolved relative from the directory of + // of the config file unless it's absolute. + // + specs: ['./__tests__/e2e/**/*.ts'], + // Patterns to exclude. + exclude: [ + // 'path/to/excluded/files' + ], + // + // ============ + // Capabilities + // ============ + // Define your capabilities here. WebdriverIO can run multiple capabilities at the same + // time. Depending on the number of capabilities, WebdriverIO launches several test + // sessions. Within your capabilities you can overwrite the spec and exclude options in + // order to group specific specs to a specific capability. + // + // First, you can define how many instances should be started at the same time. Let's + // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have + // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec + // files and you set maxInstances to 10, all spec files will get tested at the same time + // and 30 processes will get spawned. The property handles how many capabilities + // from the same test should run tests. + // + maxInstances: 10, + // + // If you have trouble getting all important capabilities together, check out the + // Sauce Labs platform configurator - a great tool to configure your capabilities: + // https://saucelabs.com/platform/platform-configurator + // + capabilities: [ + { + browserName: 'chrome', + 'bstack:options': { + os: 'Windows', + osVersion: '10', + }, + }, + { + browserName: 'firefox', + 'bstack:options': { + os: 'Windows', + osVersion: '10', + }, + }, + { + browserName: 'safari', + 'bstack:options': { + os: 'OS X', + osVersion: 'Monterey', + }, + }, + { + browserName: 'chrome', + 'bstack:options': { + os: 'OS X', + osVersion: 'Monterey', + }, + }, + { + browserName: 'edge', + 'bstack:options': { + os: 'Windows', + osVersion: '10', + }, + }, + { + browserName: 'safari', + 'bstack:options': { + deviceName: 'iPhone 14 Pro Max', + osVersion: '16', + }, + }, + { + browserName: 'chrome', + 'bstack:options': { + deviceName: 'Samsung Galaxy S23 Ultra', + osVersion: '13.0', + }, + }, + { + browserName: 'safari', + 'bstack:options': { + deviceName: 'iPad Pro 12.9 2022', + osVersion: '16', + }, + }, + ], + + // + // =================== + // Test Configurations + // =================== + // Define all options that are relevant for the WebdriverIO instance here + // + // Level of logging verbosity: trace | debug | info | warn | error | silent + logLevel: 'info', + // + // Set specific log levels per logger + // loggers: + // - webdriver, webdriverio + // - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service + // - @wdio/mocha-framework, @wdio/jasmine-framework + // - @wdio/local-runner + // - @wdio/sumologic-reporter + // - @wdio/cli, @wdio/config, @wdio/utils + // Level of logging verbosity: trace | debug | info | warn | error | silent + // logLevels: { + // webdriver: 'info', + // '@wdio/appium-service': 'info' + // }, + // + // If you only want to run your tests until a specific amount of tests have failed use + // bail (default is 0 - don't bail, run all tests). + bail: 0, + // + // Set a base URL in order to shorten url command calls. If your `url` parameter starts + // with `/`, the base url gets prepended, not including the path portion of your baseUrl. + // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url + // gets prepended directly. + // baseUrl: 'http://localhost:8080', + // + // Default timeout for all waitFor* commands. + waitforTimeout: 10000, + // + // Default timeout in milliseconds for request + // if browser driver or grid doesn't send response + connectionRetryTimeout: 120000, + // + // Default request retries count + connectionRetryCount: 3, + // + // Test runner services + // Services take over a specific job you don't want to take care of. They enhance + // your test setup with almost no effort. Unlike plugins, they don't add new + // commands. Instead, they hook themselves up into the test process. + services: ['browserstack'], + + // Framework you want to run your specs with. + // The following are supported: Mocha, Jasmine, and Cucumber + // see also: https://webdriver.io/docs/frameworks + // + // Make sure you have the wdio adapter package for the specific framework installed + // before running any tests. + framework: 'mocha', + + // + // The number of times to retry the entire specfile when it fails as a whole + // specFileRetries: 1, + // + // Delay in seconds between the spec file retry attempts + // specFileRetriesDelay: 0, + // + // Whether or not retried spec files should be retried immediately or deferred to the end of the queue + // specFileRetriesDeferred: false, + // + // Test reporter for stdout. + // The only one supported by default is 'dot' + // see also: https://webdriver.io/docs/dot-reporter + reporters: ['spec'], + + // Options to be passed to Mocha. + // See the full list at http://mochajs.org/ + mochaOpts: { + ui: 'bdd', + timeout: 60000, + }, + + // + // ===== + // Hooks + // ===== + // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance + // it and to build services around it. You can either apply a single function or an array of + // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got + // resolved to continue. + /** + * Gets executed once before all workers get launched. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + */ + // onPrepare: function (config, capabilities) { + // }, + /** + * Gets executed before a worker process is spawned and can be used to initialize specific service + * for that worker as well as modify runtime environments in an async fashion. + * @param {string} cid capability id (e.g 0-0) + * @param {object} caps object containing capabilities for session that will be spawn in the worker + * @param {object} specs specs to be run in the worker process + * @param {object} args object that will be merged with the main configuration once worker is initialized + * @param {object} execArgv list of string arguments passed to the worker process + */ + // onWorkerStart: function (cid, caps, specs, args, execArgv) { + // }, + /** + * Gets executed just after a worker process has exited. + * @param {string} cid capability id (e.g 0-0) + * @param {number} exitCode 0 - success, 1 - fail + * @param {object} specs specs to be run in the worker process + * @param {number} retries number of retries used + */ + // onWorkerEnd: function (cid, exitCode, specs, retries) { + // }, + /** + * Gets executed just before initialising the webdriver session and test framework. It allows you + * to manipulate configurations depending on the capability or spec. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that are to be run + * @param {string} cid worker id (e.g. 0-0) + */ + // beforeSession: function (config, capabilities, specs, cid) { + // }, + /** + * Gets executed before test execution begins. At this point you can access to all global + * variables like `browser`. It is the perfect place to define custom commands. + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that are to be run + * @param {object} browser instance of created browser/device session + */ + // before: function (capabilities, specs) { + // }, + /** + * Runs before a WebdriverIO command gets executed. + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + */ + // beforeCommand: function (commandName, args) { + // }, + /** + * Hook that gets executed before the suite starts + * @param {object} suite suite details + */ + // beforeSuite: function (suite) { + // }, + /** + * Function to be executed before a test (in Mocha/Jasmine) starts. + */ + // beforeTest: function (test, context) { + // }, + /** + * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling + * beforeEach in Mocha) + */ + // beforeHook: function (test, context, hookName) { + // }, + /** + * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling + * afterEach in Mocha) + */ + // afterHook: function (test, context, { error, result, duration, passed, retries }, hookName) { + // }, + /** + * Function to be executed after a test (in Mocha/Jasmine only) + * @param {object} test test object + * @param {object} context scope object the test was executed with + * @param {Error} result.error error object in case the test fails, otherwise `undefined` + * @param {*} result.result return object of test function + * @param {number} result.duration duration of test + * @param {boolean} result.passed true if test has passed, otherwise false + * @param {object} result.retries information about spec related retries, e.g. `{ attempts: 0, limit: 0 }` + */ + // afterTest: function(test, context, { error, result, duration, passed, retries }) { + // }, + + /** + * Hook that gets executed after the suite has ended + * @param {object} suite suite details + */ + // afterSuite: function (suite) { + // }, + /** + * Runs after a WebdriverIO command gets executed + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + * @param {number} result 0 - command success, 1 - command error + * @param {object} error error object if any + */ + // afterCommand: function (commandName, args, result, error) { + // }, + /** + * Gets executed after all tests are done. You still have access to all global variables from + * the test. + * @param {number} result 0 - test pass, 1 - test fail + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that ran + */ + // after: function (result, capabilities, specs) { + // }, + /** + * Gets executed right after terminating the webdriver session. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that ran + */ + // afterSession: function (config, capabilities, specs) { + // }, + /** + * Gets executed after all workers got shut down and the process is about to exit. An error + * thrown in the onComplete hook will result in the test run failing. + * @param {object} exitCode 0 - success, 1 - fail + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {} results object containing test results + */ + // onComplete: function(exitCode, config, capabilities, results) { + // }, + /** + * Gets executed when a refresh happens. + * @param {string} oldSessionId session ID of the old session + * @param {string} newSessionId session ID of the new session + */ + // onReload: function(oldSessionId, newSessionId) { + // } + /** + * Hook that gets executed before a WebdriverIO assertion happens. + * @param {object} params information about the assertion to be executed + */ + // beforeAssertion: function(params) { + // } + /** + * Hook that gets executed after a WebdriverIO assertion happened. + * @param {object} params information about the assertion that was executed, including its results + */ + // afterAssertion: function(params) { + // } +};
    */ headerCellContent: css` ${() => tableMixins.cellContent} + gap: 0.25em; - color: var(--tableHeader-textColor, var(--color-text-0)); + color: var(--tableStickyRow-textColor, var(--color-text-0)); span:nth-child(2) { :before { - content: ' / '; + content: '| '; } } `, diff --git a/src/styles/text.css b/src/styles/text.css index 0710da26f..6a061bc60 100644 --- a/src/styles/text.css +++ b/src/styles/text.css @@ -1,7 +1,7 @@ /* Constants */ :root { - --fontFamily-base: "Satoshi", system-ui, -apple-system, Helvetica, Arial, sans-serif; + --fontFamily-base: 'Satoshi', system-ui, -apple-system, Helvetica, Arial, sans-serif; --fontFamily-monospace: Courier, monospace, var(--fontFamily-base); --fontWeight-base-0: 450; @@ -22,18 +22,20 @@ --fontWeight-regular: 400; --fontWeight-book: 450; --fontWeight-medium: 500; + --fontWeight-bold: 700; --font-tiny-regular: var(--fontWeight-regular) var(--fontSize-tiny) var(--fontFamily-base); --font-tiny-book: var(--fontWeight-book) var(--fontSize-tiny) var(--fontFamily-base); --font-tiny-medium: var(--fontWeight-medium) var(--fontSize-tiny) var(--fontFamily-base); - + --font-mini-regular: var(--fontWeight-regular) var(--fontSize-mini) var(--fontFamily-base); --font-mini-book: var(--fontWeight-book) var(--fontSize-mini) var(--fontFamily-base); --font-mini-medium: var(--fontWeight-medium) var(--fontSize-mini) var(--fontFamily-base); - + --font-small-regular: var(--fontWeight-regular) var(--fontSize-small) var(--fontFamily-base); --font-small-book: var(--fontWeight-book) var(--fontSize-small) var(--fontFamily-base); --font-small-medium: var(--fontWeight-medium) var(--fontSize-small) var(--fontFamily-base); + --font-small-bold: var(--fontWeight-bold) var(--fontSize-small) var(--fontFamily-base); --font-base-regular: var(--fontWeight-regular) var(--fontSize-base) var(--fontFamily-base); --font-base-book: var(--fontWeight-book) var(--fontSize-base) var(--fontFamily-base); @@ -50,7 +52,7 @@ --font-extra-regular: var(--fontWeight-regular) var(--fontSize-extra) var(--fontFamily-base); --font-extra-book: var(--fontWeight-book) var(--fontSize-extra) var(--fontFamily-base); --font-extra-medium: var(--fontWeight-medium) var(--fontSize-extra) var(--fontFamily-base); - + --fontFeature-monoNumbers: 'tnum' on, 'lnum' on, 'zero' 1; } diff --git a/src/styles/text.stories.tsx b/src/styles/text.stories.tsx index f78211b7d..a8b5c6730 100644 --- a/src/styles/text.stories.tsx +++ b/src/styles/text.stories.tsx @@ -1,5 +1,5 @@ import type { Story } from '@ladle/react'; -import styled, { type AnyStyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -9,7 +9,7 @@ const FONT_SIZES = ['tiny', 'mini', 'small', 'base', 'medium', 'large', 'extra'] export const TextStory: Story = () => ( - + <$Table>
    Regular(-)
    {stringGetter({ key: STRING_KEYS.ORDERBOOK_SPREAD })} @@ -292,7 +295,7 @@ export const Orderbook = ({ {!isTablet && }