From 11df3aed2d2505ed5caa77b0273ef10a9ace13e4 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Tue, 10 Sep 2024 21:53:34 +0400 Subject: [PATCH] New Layout + Page structure (#47) * Update navbar * Add trade page w/ new layout * Add recent swaps to explorer page * Add swaps component * Add PairSelector component * Remove neon style from loader * Fix depth chart * Cleanup code * Remove .vscode from git * Add .vscode to gitignore * Add redirect from / to /trade --- .gitignore | 1 + package.json | 3 + pnpm-lock.yaml | 130 +++- src/components/blockTimestamp.tsx | 54 +- src/components/blocks/index.tsx | 276 ++++++++ src/components/charts/buySellChart.tsx | 4 - src/components/charts/depthChart.tsx | 16 +- src/components/charts/ohlcChart.tsx | 8 +- .../executionHistory/blockDetails.tsx | 2 +- .../executionHistory/blockSummary.tsx | 52 +- .../liquidityPositions/executionEvent.tsx | 2 +- .../liquidityPositions/timelinePosition.tsx | 2 +- src/components/lpSearchBar.tsx | 12 +- src/components/navbar.tsx | 12 +- src/components/pairSelector.tsx | 90 +++ src/components/swaps/index.tsx | 190 +++++ src/components/util/loadingSpinner.tsx | 3 +- src/pages/block/[block_height].tsx | 13 +- src/pages/explorer/index.tsx | 34 + src/pages/index.tsx | 229 +----- src/pages/lp/[lp_nft_id].tsx | 4 +- src/pages/pair/[...params].tsx | 2 +- src/pages/trade/[[...params]].tsx | 670 ++++++++++++++++++ src/pages/trades.tsx | 2 +- styles/Home.module.css | 2 +- styles/global.css | 16 +- 26 files changed, 1473 insertions(+), 356 deletions(-) create mode 100644 src/components/blocks/index.tsx create mode 100644 src/components/pairSelector.tsx create mode 100644 src/components/swaps/index.tsx create mode 100644 src/pages/explorer/index.tsx create mode 100644 src/pages/trade/[[...params]].tsx diff --git a/.gitignore b/.gitignore index 64748db4..7aafb9ba 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ # misc .DS_Store +.vscode # debug npm-debug.log* diff --git a/package.json b/package.json index 199232c0..1394b3ce 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@penumbra-zone/ui": "^9.0.0", "@penumbra-zone/wasm": "^26.2.0", "@radix-ui/react-icons": "^1.3.0", + "@styled-icons/octicons": "^10.47.0", "@vercel/analytics": "^1.3.1", "@vercel/speed-insights": "^1.0.12", "bech32": "^2.0.0", @@ -48,6 +49,7 @@ "react": "^18.3.1", "react-chartjs-2": "^5.2.0", "react-dom": "^18.3.1", + "react-outside-click-handler": "^1.3.0", "sharp": "^0.33.5", "webpack": "^5.94.0", "zod": "^3.23.8" @@ -61,6 +63,7 @@ "@types/pg": "^8.11.8", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "@types/react-outside-click-handler": "^1.3.3", "babel-loader": "^9.1.3", "eslint": "8.57.0", "eslint-config-next": "14.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8871e6a2..59bc569f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.1) + '@styled-icons/octicons': + specifier: ^10.47.0 + version: 10.47.0(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@vercel/analytics': specifier: ^1.3.1 version: 1.3.1(next@14.2.7(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -110,6 +113,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-outside-click-handler: + specifier: ^1.3.0 + version: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) sharp: specifier: ^0.33.5 version: 0.33.5 @@ -144,6 +150,9 @@ importers: '@types/react-dom': specifier: ^18.3.0 version: 18.3.0 + '@types/react-outside-click-handler': + specifier: ^1.3.3 + version: 1.3.3 babel-loader: specifier: ^9.1.3 version: 9.1.3(@babel/core@7.25.2)(webpack@5.94.0) @@ -2506,6 +2515,18 @@ packages: '@rushstack/eslint-patch@1.10.4': resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + '@styled-icons/octicons@10.47.0': + resolution: {integrity: sha512-JOqEbwbh23u/AdaINod8ZeVN5MEcOvkSeMTwGIPaPgz5PNuWRj/+1LpixmBeZjAr/WhB0pxZBBwA+e91BelQCA==} + peerDependencies: + react: '*' + styled-components: '*' + + '@styled-icons/styled-icon@10.7.1': + resolution: {integrity: sha512-WLYaeMTMhMkSxE+v+of+r2ovIk0tceDGfv8iqWHRMxvbm+6zxngVcQ4ELx6Zt/LFxqckmmoAdvo6ehiiYj6I6A==} + peerDependencies: + react: '*' + styled-components: '>=4.1.0' + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -2587,6 +2608,9 @@ packages: '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-outside-click-handler@1.3.3': + resolution: {integrity: sha512-fF7x4dHf/IPIne8kkt3rlCGuWFrWkFJmzQm4JkxSBzXJIM9WDLob++VnmGpE3ToVWrW3Xw9D5TxcUWrwqe04Gg==} + '@types/react-transition-group@4.4.11': resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} @@ -2815,6 +2839,12 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + airbnb-prop-types@2.16.0: + resolution: {integrity: sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==} + deprecated: This package has been renamed to 'prop-types-tools' + peerDependencies: + react: ^0.14 || ^15.0.0 || ^16.0.0-alpha + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -2899,6 +2929,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + array.prototype.find@2.2.3: + resolution: {integrity: sha512-fO/ORdOELvjbbeIfZfzrXFMhYHGofRGqd+am9zm3tZ4GlJINj/pA2eITyfd65Vg6+ZbHd/Cys7stpoRSWtQFdA==} + engines: {node: '>= 0.4'} + array.prototype.findlast@1.2.5: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} @@ -3140,6 +3174,9 @@ packages: console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + consolidated-events@2.0.2: + resolution: {integrity: sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==} + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -3304,6 +3341,9 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + document.contains@1.0.2: + resolution: {integrity: sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -4416,6 +4456,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prop-types-exact@1.2.5: + resolution: {integrity: sha512-wHDhA5TSSvU07gdzsdeT/FZg6zay94K4Y7swSK4YsRG3moWB0Qsp9g1Y5BBausP1HF8K4UeVe2Xt7ZFJByKp6A==} + engines: {node: '>= 0.8'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4474,6 +4518,12 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-outside-click-handler@1.3.0: + resolution: {integrity: sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ==} + peerDependencies: + react: ^0.14 || >=15 + react-dom: ^0.14 || >=15 + react-remove-scroll-bar@2.3.6: resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} @@ -4584,6 +4634,9 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} + reflect.ownkeys@1.1.4: + resolution: {integrity: sha512-iUNmtLgzudssL+qnTUosCmnq3eczlrVd1wXrgx/GhiI/8FvwrTYWtCJ9PNvWIRX+4ftupj2WUfB5mu5s9t6LnA==} + regenerate-unicode-properties@10.1.1: resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} engines: {node: '>=4'} @@ -8041,6 +8094,19 @@ snapshots: '@rushstack/eslint-patch@1.10.4': {} + '@styled-icons/octicons@10.47.0(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + '@babel/runtime': 7.25.6 + '@styled-icons/styled-icon': 10.7.1(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + react: 18.3.1 + styled-components: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@styled-icons/styled-icon@10.7.1(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + '@babel/runtime': 7.25.6 + react: 18.3.1 + styled-components: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.5': @@ -8124,6 +8190,10 @@ snapshots: dependencies: '@types/react': 18.3.5 + '@types/react-outside-click-handler@1.3.3': + dependencies: + '@types/react': 18.3.5 + '@types/react-transition-group@4.4.11': dependencies: '@types/react': 18.3.5 @@ -8437,6 +8507,19 @@ snapshots: transitivePeerDependencies: - supports-color + airbnb-prop-types@2.16.0(react@18.3.1): + dependencies: + array.prototype.find: 2.2.3 + function.prototype.name: 1.1.6 + is-regex: 1.1.4 + object-is: 1.1.6 + object.assign: 4.1.5 + object.entries: 1.1.8 + prop-types: 15.8.1 + prop-types-exact: 1.2.5 + react: 18.3.1 + react-is: 16.13.1 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -8520,6 +8603,14 @@ snapshots: array-union@2.1.0: {} + array.prototype.find@2.2.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + array.prototype.findlast@1.2.5: dependencies: call-bind: 1.0.7 @@ -8788,6 +8879,8 @@ snapshots: console-control-strings@1.1.0: {} + consolidated-events@2.0.2: {} + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -8965,6 +9058,10 @@ snapshots: dependencies: esutils: 2.0.3 + document.contains@1.0.2: + dependencies: + define-properties: 1.2.1 + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.25.6 @@ -9119,7 +9216,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -9150,7 +9247,7 @@ snapshots: is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -9168,7 +9265,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -10188,6 +10285,15 @@ snapshots: prelude-ls@1.2.1: {} + prop-types-exact@1.2.5: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + hasown: 2.0.2 + isarray: 2.0.5 + object.assign: 4.1.5 + reflect.ownkeys: 1.1.4 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -10258,6 +10364,16 @@ snapshots: react-is: 18.3.1 styled-components: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-outside-click-handler@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + airbnb-prop-types: 2.16.0(react@18.3.1) + consolidated-events: 2.0.2 + document.contains: 1.0.2 + object.values: 1.2.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll-bar@2.3.6(@types/react@18.3.5)(react@18.3.1): dependencies: react: 18.3.1 @@ -10384,6 +10500,14 @@ snapshots: globalthis: 1.0.4 which-builtin-type: 1.1.4 + reflect.ownkeys@1.1.4: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-set-tostringtag: 2.0.3 + globalthis: 1.0.4 + regenerate-unicode-properties@10.1.1: dependencies: regenerate: 1.4.2 diff --git a/src/components/blockTimestamp.tsx b/src/components/blockTimestamp.tsx index 317b9eb5..dab45f1b 100644 --- a/src/components/blockTimestamp.tsx +++ b/src/components/blockTimestamp.tsx @@ -30,48 +30,18 @@ const BlockTimestampView: FC = ({ timestamp, }) => { return ( - <> - - - Block{" "} - - - - {blockHeight} - - - - - {" "} - {formatTimestampOrDefault(timestamp)} - - + + {" "} + {formatTimestampOrDefault(timestamp)} + ); }; diff --git a/src/components/blocks/index.tsx b/src/components/blocks/index.tsx new file mode 100644 index 00000000..20547de8 --- /dev/null +++ b/src/components/blocks/index.tsx @@ -0,0 +1,276 @@ +// copied from pages/trades.tsx + +import { + Text, + Box, + FormLabel, + NumberInput, + FormControl, + NumberInputField, +} from "@chakra-ui/react"; +import { LoadingSpinner } from "@/components/util/loadingSpinner"; +import { useEffect, useRef, useState } from "react"; +import { BlockSummary } from "@/components/executionHistory/blockSummary"; +import { BlockInfo, LiquidityPositionEvent } from "@/utils/indexer/types/lps"; +import { SwapExecutionWithBlockHeight } from "@/utils/protos/types/DexQueryServiceClientInterface"; +import { BlockInfoMap, BlockSummaryMap } from "@/utils/types/block"; + +export default function Blocks() { + // Go back hardcoded N blocks + const NUMBER_BLOCKS_IN_TIMELINE = 50; + + const [isBlockRangeLoading, setIsBlockRangeLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true); + const [isLineLoading, setIsLineLoading] = useState(true); + + const [startingBlockHeight, setStartingBlockHeight] = useState(-1); // negative number meaning not set yet + const [endingBlockHeight, setEndingBlockHeight] = useState(-1); // negative number meaning not set yet + const [blockInfo, setBlockInfo] = useState({}); + const [blockData, setBlockData] = useState({}); + const [userRequestedBlockEndHeight, setUserRequestedBlockEndHeight] = + useState(-1); + + const currentStatusRef = useRef(null); + const originalStatusRef = useRef(null); + const [lineHeight, setLineHeight] = useState(0); + const [lineTop, setLineTop] = useState(0); + const [error, setError] = useState(undefined); + + // Get starting block number + useEffect(() => { + setIsBlockRangeLoading(true); + if (endingBlockHeight <= 0 || startingBlockHeight <= 0) { + var blockInfoPromise: Promise; + if (userRequestedBlockEndHeight >= 1) { + const startHeight = Math.max( + userRequestedBlockEndHeight - NUMBER_BLOCKS_IN_TIMELINE + 1, + 1 + ); // Lowest block_height is 1 + blockInfoPromise = fetch( + `/api/blocks/${startHeight}/${userRequestedBlockEndHeight + 1}` + ).then((res) => res.json()); + } else { + blockInfoPromise = fetch( + `/api/blocks/${NUMBER_BLOCKS_IN_TIMELINE}` + ).then((res) => res.json()); + } + Promise.all([blockInfoPromise]) + .then(([blockInfoResponse]) => { + const blockInfoList: BlockInfo[] = blockInfoResponse as BlockInfo[]; + const blockInfoMap: BlockInfoMap = {}; + blockInfoList.forEach((blockInfo: BlockInfo, i: number) => { + //console.log(blockInfo) + blockInfoMap[blockInfo["height"]] = blockInfo; + }); + + if (blockInfoList.length === 0) { + setIsLoading(false); + setError("No blocks found before: " + userRequestedBlockEndHeight); + console.log("No blocks found"); + return; + } else { + setEndingBlockHeight(blockInfoList[0]["height"]); + setStartingBlockHeight( + blockInfoList[NUMBER_BLOCKS_IN_TIMELINE - 1]["height"] + ); + setBlockInfo(blockInfoMap); + setError(undefined); + } + }) + .catch((error) => { + console.error("Error fetching most recent blocks:", error); + }) + .finally(() => { + setIsBlockRangeLoading(false); + }); + } + }, [userRequestedBlockEndHeight, endingBlockHeight, startingBlockHeight]); + + // Load block dex data from grpc/indexer + useEffect(() => { + if (error !== undefined) { + return; + } + console.log("Loading block data"); + setIsLoading(true); + if (blockInfo && endingBlockHeight >= 1 && startingBlockHeight >= 1) { + const liquidityPositionOpenClosePromise = fetch( + `/api/lp/block/${startingBlockHeight}/${endingBlockHeight + 1}` + ).then((res) => res.json()); + const arbsPromise = fetch( + `/api/arbs/${startingBlockHeight}/${endingBlockHeight + 1}` + ).then((res) => res.json()); + const swapsPromise = fetch( + `/api/swaps/${startingBlockHeight}/${endingBlockHeight + 1}` + ).then((res) => res.json()); + + Promise.all([ + liquidityPositionOpenClosePromise, + arbsPromise, + swapsPromise, + ]) + .then( + ([ + liquidityPositionOpenCloseResponse, + arbsResponse, + swapsResponse, + ]) => { + const positionData: LiquidityPositionEvent[] = + liquidityPositionOpenCloseResponse as LiquidityPositionEvent[]; + const arbData: SwapExecutionWithBlockHeight[] = + arbsResponse as SwapExecutionWithBlockHeight[]; + const swapData: SwapExecutionWithBlockHeight[] = + swapsResponse as SwapExecutionWithBlockHeight[]; + + // Initialize blocks + const blockSummaryMap: BlockSummaryMap = {}; + var i: number; + for (i = startingBlockHeight; i <= endingBlockHeight; i++) { + blockSummaryMap[i] = { + openPositionEvents: [], + closePositionEvents: [], + withdrawPositionEvents: [], + swapExecutions: [], + arbExecutions: [], + createdAt: blockInfo[i]["created_at"], + }; + } + + positionData.forEach( + (positionOpenCloseEvent: LiquidityPositionEvent) => { + if (positionOpenCloseEvent["type"].includes("PositionOpen")) { + blockSummaryMap[positionOpenCloseEvent["block_height"]][ + "openPositionEvents" + ].push(positionOpenCloseEvent); + } else if ( + positionOpenCloseEvent["type"].includes("PositionClose") + ) { + blockSummaryMap[positionOpenCloseEvent["block_height"]][ + "closePositionEvents" + ].push(positionOpenCloseEvent); + } else if ( + positionOpenCloseEvent["type"].includes("PositionWithdraw") + ) { + blockSummaryMap[positionOpenCloseEvent["block_height"]][ + "withdrawPositionEvents" + ].push(positionOpenCloseEvent); + } + } + ); + + arbData.forEach((arb: SwapExecutionWithBlockHeight) => { + blockSummaryMap[arb.blockHeight]["arbExecutions"].push( + arb.swapExecution + ); + }); + + swapData.forEach((swap: SwapExecutionWithBlockHeight) => { + blockSummaryMap[swap.blockHeight]["swapExecutions"].push( + swap.swapExecution + ); + }); + + setBlockData(blockSummaryMap); + } + ) + .catch((error) => { + console.error("Error fetching block summary data:", error); + }) + .finally(() => { + setIsLoading(false); + }); + } + }, [ + isBlockRangeLoading, + blockInfo, + startingBlockHeight, + endingBlockHeight, + error, + ]); + + // Draw vertical line + useEffect(() => { + setIsLineLoading(true); + if (currentStatusRef.current && originalStatusRef.current) { + const firstBoxRect = currentStatusRef.current.getBoundingClientRect(); + const lastBoxRect = originalStatusRef.current.getBoundingClientRect(); + + // Calculate the new height of the line to stretch from the bottom of the first box to the top of the last box. + const newLineHeight = lastBoxRect.top - firstBoxRect.top; + setLineHeight(newLineHeight - 50); + setLineTop(firstBoxRect.bottom); + } + setIsLineLoading(false); + }); + + const onSearch = (event: React.FormEvent) => { + event.preventDefault(); + if ( + userRequestedBlockEndHeight >= 1 && + endingBlockHeight !== userRequestedBlockEndHeight + ) { + setEndingBlockHeight(userRequestedBlockEndHeight); + setStartingBlockHeight(-1); // trigger update + setIsBlockRangeLoading(true); + setIsLoading(true); + } + }; + + return isLoading ? ( + + ) : error ? ( + + {error} + + ) : ( + <> + + + +
+ + + setUserRequestedBlockEndHeight(parseInt(e.target.value)) + } + width="100%" + p={5} + border="none" + /> + +
+
+ + {Array.from(Array(endingBlockHeight - startingBlockHeight + 1)).map( + (_, index: number) => ( + + ) + )} +
+ + ); +} diff --git a/src/components/charts/buySellChart.tsx b/src/components/charts/buySellChart.tsx index 38215e07..2cf7f949 100644 --- a/src/components/charts/buySellChart.tsx +++ b/src/components/charts/buySellChart.tsx @@ -347,10 +347,6 @@ export default dynamic( return ( prevIndex - 1); // Decreasing index zooms in } }; - const [pageLoad, setPageLoad] = useState(false); + const didInitRef = useRef(); const maxLiquidity = Math.max( ...sellSideData.map((p) => p.y), @@ -216,7 +216,11 @@ const DepthChart = ({ setDisableMinusButton(true); return; } - if (zoomIndex === lastZoomIndex && pageLoad) return; + if (zoomIndex === lastZoomIndex && didInitRef.current) { + return; + } + didInitRef.current = true; + // Change zoom to only show points within how close they are to the midpoint price, // If zoom index is 1, show all points, if z oom index is 2, show middle 50% of points, if zoom index is 3, show middle 25% of points, etc. const localMin = 0; @@ -321,16 +325,16 @@ const DepthChart = ({ // Update the last zoom level setLastZoomIndex(zoomIndex); console.log("running"); - }, [zoomIndex, midMarketPrice, buySideData, sellSideData, pageLoad]); + }, [zoomIndex, midMarketPrice, buySideData, sellSideData]); // set initial zoom at 0 to load the chart appropriately useEffect(() => { setZoomIndex(0); - setPageLoad(true); }, []); console.log("multi", buySideData, sellSideData, midMarketPrice); console.log("single", buySideSingleHopData, sellSideSingleHopData); + const data: any = { datasets: [ { @@ -635,9 +639,9 @@ const DepthChart = ({ return ( <> - +
diff --git a/src/components/charts/ohlcChart.tsx b/src/components/charts/ohlcChart.tsx index 0788359b..5d5f698a 100644 --- a/src/components/charts/ohlcChart.tsx +++ b/src/components/charts/ohlcChart.tsx @@ -599,13 +599,13 @@ const OHLCChart = ({ asset1Token, asset2Token }: OHLCChartProps) => { // ! Width should be the same as that of the DepthChart return ( - + {isLoading && error === undefined ? ( ) : error !== undefined ? ( @@ -672,7 +672,7 @@ const OHLCChart = ({ asset1Token, asset2Token }: OHLCChartProps) => { diff --git a/src/components/executionHistory/blockDetails.tsx b/src/components/executionHistory/blockDetails.tsx index d56692ed..d6c99e89 100644 --- a/src/components/executionHistory/blockDetails.tsx +++ b/src/components/executionHistory/blockDetails.tsx @@ -8,7 +8,7 @@ export interface BlockDetailsProps { export const BlockDetails = ({ blockSummary }: BlockDetailsProps) => { return ( <> - + {"Positions Opened: "} {blockSummary.openPositionEvents.length} diff --git a/src/components/executionHistory/blockSummary.tsx b/src/components/executionHistory/blockSummary.tsx index d32c21e5..8217c84b 100644 --- a/src/components/executionHistory/blockSummary.tsx +++ b/src/components/executionHistory/blockSummary.tsx @@ -1,4 +1,4 @@ -import { Box, HStack, VStack, Text } from "@chakra-ui/react"; +import { Box, HStack, Flex, Text } from "@chakra-ui/react"; import BlockTimestampView from "../blockTimestamp"; import { BlockDetails } from "./blockDetails"; import { BlockSummaryData } from "@/utils/types/block"; @@ -10,36 +10,24 @@ export interface BlockSummaryProps { export const BlockSummary = ({ blockHeight, blockSummary }: BlockSummaryProps) => { return ( - <> - - - - - - - - Block: {blockHeight} - - - - - - - - + + + + Block: {blockHeight} + + + + + ); }; \ No newline at end of file diff --git a/src/components/liquidityPositions/executionEvent.tsx b/src/components/liquidityPositions/executionEvent.tsx index 8f1222a9..40d2c582 100644 --- a/src/components/liquidityPositions/executionEvent.tsx +++ b/src/components/liquidityPositions/executionEvent.tsx @@ -44,7 +44,7 @@ const ExecutionEvent = ({ nftId, lp_event }: ExecutionEventProps) => { /> { flexDirection={{ base: "column", md: "row" }} > { @@ -14,17 +14,17 @@ export const LPSearchBar = ({ isMobile = false }) => { } return ( -
+ setLpId(e.target.value)} - pr={isMobile ? "4.5rem" : "1rem"} + p={6} spellCheck={false} + border="none" /> {isMobile && ( @@ -38,7 +38,7 @@ export const LPSearchBar = ({ isMobile = false }) => { )} - +
); } diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 44601d31..bc3af84e 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -11,7 +11,7 @@ interface NavLinkProps { link: string } -const Links = [{ 'name': 'Trades', 'link': '/trades' }, { 'name': 'Pairs', 'link': '/pair' }] +const Links = [{ 'name': 'Trade', 'link': '/trade' }, { 'name': 'Explorer', 'link': '/explorer' }] const NavLink = (props: NavLinkProps) => { return ( @@ -26,7 +26,7 @@ export const Navbar = () => { return ( - + { display={{ md: 'none' }} onClick={isOpen ? onClose : onOpen} /> - + Penumbra Logo Dex Explorer @@ -43,9 +43,6 @@ export const Navbar = () => { {Links.map((x) => ({x.name}))} - - - {/* Placeholder for symmetry */} @@ -55,9 +52,6 @@ export const Navbar = () => { {Links.map((x) => ({x.name}))} - - - )}
diff --git a/src/components/pairSelector.tsx b/src/components/pairSelector.tsx new file mode 100644 index 00000000..3f6b4abe --- /dev/null +++ b/src/components/pairSelector.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; +import { Box, Text, Flex } from "@chakra-ui/react"; +import { fetchAllTokenAssets } from "@/utils/token/tokenFetch"; +import OutsideClickHandler from "react-outside-click-handler"; +import { Token } from "@/utils/types/token"; + +const orderedAssets = ["UM", "USDC"]; + +export default function PairSelector({ + show, + setShow, + onSelect, +}: { + show: Boolean; + setShow: (show: boolean) => void; + onSelect: (assets: [Token, Token]) => void; +}) { + const [tokenAssets, setTokenAssets] = useState>({}); + const [selectedAssets, setSelectedAssets] = useState([]); + + useEffect(() => { + const tokenAssets = fetchAllTokenAssets(); + setTokenAssets( + Object.fromEntries(tokenAssets.map((asset) => [asset.symbol, asset])) + ); + }, []); + + useEffect(() => { + setSelectedAssets([]); + }, [show]); + + useEffect(() => { + if (selectedAssets.length === 2) { + onSelect(selectedAssets as [Token, Token]); + setShow(false); + } + }, [selectedAssets]); + + return ( + setShow(false)}> + + + {!selectedAssets.length ? "Select Base Asset" : "Select Quote Asset"} + + {[ + ...orderedAssets.map((symbol) => tokenAssets[symbol]), + ...Object.values(tokenAssets) + .filter((asset) => !orderedAssets.includes(asset.symbol)) + .sort((a, b) => { + return a.symbol.localeCompare(b.symbol); + }), + ] + .filter((asset) => asset && !selectedAssets.includes(asset)) + .map((asset) => ( + setSelectedAssets([...selectedAssets, asset])} + > + + {asset.symbol} + + + {asset.display} + + + ))} + + + ); +} diff --git a/src/components/swaps/index.tsx b/src/components/swaps/index.tsx new file mode 100644 index 00000000..2b3bced9 --- /dev/null +++ b/src/components/swaps/index.tsx @@ -0,0 +1,190 @@ +// copied from pages/index + +import { useEffect, useState } from "react"; +import { Price, Trace, TraceType } from "../../pages/block/[block_height]"; +import { Box, Heading, HStack, Link, Stack, VStack } from "@chakra-ui/react"; +import { + SwapExecution, + SwapExecution_Trace, +} from "@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb"; +import { fetchAllTokenAssets } from "@/utils/token/tokenFetch"; +import { Token } from "@/utils/types/token"; +import { LoadingSpinner } from "@/components/util/loadingSpinner"; + +export const routes = [ + { path: "/lp/utils" }, + { path: "/lp/" }, + { + path: "/trade/:", + }, +]; + +export default function Swaps() { + const [swapExecutions, setSwapExecutions] = useState([]); + const [metadataByAssetId, setMetadataByAssetId] = useState< + Record + >({}); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let fetchData = async () => { + const blockHeight = await fetch("/api/blocks/1") + .then((res) => res.json()) + .then((data) => { + return data[0]["height"]; + }) + .catch((err) => { + console.error(err); + return null; + }); + + console.log("Current block height: ", blockHeight); + + let swaps = []; + let blockRange = 10; + let maxBlocks = 100000; + + while (blockRange <= maxBlocks && swaps.length == 0) { + console.log( + "route: ", + `/api/swaps/${blockHeight - blockRange}/${blockHeight}` + ); + swaps = await fetch( + `/api/swaps/${blockHeight - blockRange}/${blockHeight}` + ) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + throw new Error(data.error); + } + return data; + }) + .catch((err) => { + console.error(err); + return []; + }); + + if (swaps.length != 0) { + swaps = swaps.sort((a: any, b: any) => { + return b.blockHeight - a.blockHeight; + }); + + setSwapExecutions(swaps as SwapExecution[]); + + const tokenAssets = fetchAllTokenAssets(); + const metadataByAssetId: Record = {}; + tokenAssets.forEach((asset) => { + metadataByAssetId[asset.inner] = { + symbol: asset.symbol, + display: asset.display, + decimals: asset.decimals, + inner: asset.inner, + imagePath: asset.imagePath, + }; + }); + setMetadataByAssetId(metadataByAssetId); + } + + blockRange *= 10; + console.log("Block range: ", blockRange); + console.log(swaps); + } + console.log("Latest swap executions: ", swaps); + setIsLoading(false); // Set loading to false after fetching is complete + }; + fetchData(); + }, []); + + return isLoading ? ( + + + + ) : swapExecutions.length === 0 ? ( + + No recent swaps found. + + ) : ( + + {swapExecutions.map((swapExecution: any, execIndex: number) => { + const firstTrace = swapExecution.swapExecution.traces[0]; + const lastTrace = + swapExecution.swapExecution.traces[ + swapExecution.swapExecution.traces.length - 1 + ]; + const startAssetId = firstTrace.value[0].assetId?.inner; + const endAssetId = + lastTrace.value[lastTrace.value.length - 1].assetId?.inner; + const startAssetDisplay = metadataByAssetId[startAssetId]?.display; + const endAssetDisplay = metadataByAssetId[endAssetId]?.display; + const poolLink = `/trade/${startAssetDisplay}:${endAssetDisplay}`; + + return ( + + + + + Block #{swapExecution.blockHeight} + + + View {startAssetDisplay}:{endAssetDisplay} Pool + + + + {swapExecution.swapExecution.traces.map( + (trace: SwapExecution_Trace, index: number) => ( + + + + + + + + + ) + )} + + + ); + })} + + ); +} diff --git a/src/components/util/loadingSpinner.tsx b/src/components/util/loadingSpinner.tsx index 8bd9fb50..e009d5e9 100644 --- a/src/components/util/loadingSpinner.tsx +++ b/src/components/util/loadingSpinner.tsx @@ -5,10 +5,9 @@ export const LoadingSpinner = () => {
diff --git a/src/pages/block/[block_height].tsx b/src/pages/block/[block_height].tsx index 7198f25a..9cb873b8 100644 --- a/src/pages/block/[block_height].tsx +++ b/src/pages/block/[block_height].tsx @@ -104,20 +104,21 @@ export const Trace = ({ const isMobile = window.innerWidth < 768; return ( - + {/* Background line for trace connections */} + + + + + Penumbra Explorer + + + + + + + Recents Blocks + + + + + + Recents Swaps + + + + + + + ); +} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 010798c3..29eb4d99 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,224 +1,11 @@ -import styles from "@/Home.module.css"; -import Layout from "../components/layout"; -import { useEffect, useState } from "react"; -import { Price, Trace, TraceType } from "./block/[block_height]"; -import { Box, Heading, HStack, Link, Stack, VStack } from "@chakra-ui/react"; -import { - SwapExecution, - SwapExecution_Trace, -} from "@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb"; -import { fetchAllTokenAssets } from "@/utils/token/tokenFetch"; -import { Token } from "@/utils/types/token"; -import { LoadingSpinner } from "@/components/util/loadingSpinner"; - -export const routes = [ - { path: "/lp/utils" }, - { path: "/lp/" }, - { - path: "/pair/:", - }, -]; - export default function Home() { - const [swapExecutions, setSwapExecutions] = useState([]); - const [metadataByAssetId, setMetadataByAssetId] = useState< - Record - >({}); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - let fetchData = async () => { - const blockHeight = await fetch("/api/blocks/1") - .then((res) => res.json()) - .then((data) => { - return data[0]["height"]; - }) - .catch((err) => { - console.error(err); - return null; - }); - - console.log("Current block height: ", blockHeight); - - let swaps = []; - let blockRange = 10; - let maxBlocks = 100000; - - while (blockRange <= maxBlocks && swaps.length == 0) { - console.log( - "route: ", - `/api/swaps/${blockHeight - blockRange}/${blockHeight}` - ); - swaps = await fetch( - `/api/swaps/${blockHeight - blockRange}/${blockHeight}` - ) - .then((res) => res.json()) - .then((data) => { - return data; - }) - .catch((err) => { - console.error(err); - return []; - }); - - if (swaps.length != 0) { - swaps = swaps.sort((a: any, b: any) => { - return b.blockHeight - a.blockHeight; - }); - - setSwapExecutions(swaps as SwapExecution[]); - - const tokenAssets = fetchAllTokenAssets(); - const metadataByAssetId: Record = {}; - tokenAssets.forEach((asset) => { - metadataByAssetId[asset.inner] = { - symbol: asset.symbol, - display: asset.display, - decimals: asset.decimals, - inner: asset.inner, - imagePath: asset.imagePath, - }; - }); - setMetadataByAssetId(metadataByAssetId); - } - - blockRange *= 10; - console.log("Block range: ", blockRange); - console.log(swaps); - } - console.log("Latest swap executions: ", swaps); - setIsLoading(false); // Set loading to false after fetching is complete - }; - fetchData(); - }, []); - - return ( - -
-
-
- - Recent Swaps - - {isLoading ? ( - - - - ) : swapExecutions.length === 0 ? ( - - No recent swaps found. - - ) : ( - - {swapExecutions.map((swapExecution: any, execIndex: number) => { - const firstTrace = swapExecution.swapExecution.traces[0]; - const lastTrace = - swapExecution.swapExecution.traces[ - swapExecution.swapExecution.traces.length - 1 - ]; - const startAssetId = firstTrace.value[0].assetId?.inner; - const endAssetId = - lastTrace.value[lastTrace.value.length - 1].assetId?.inner; - const startAssetDisplay = - metadataByAssetId[startAssetId]?.display; - const endAssetDisplay = - metadataByAssetId[endAssetId]?.display; - const poolLink = `/pair/${startAssetDisplay}:${endAssetDisplay}`; - - return ( - - - - - Block #{swapExecution.blockHeight} - - - View {startAssetDisplay}:{endAssetDisplay} Pool - - + return null; +} - {swapExecution.swapExecution.traces.map( - (trace: SwapExecution_Trace, index: number) => ( - - - - - - - - - ) - )} - - - ); - })} - - )} -
-
-
-
- ); +export async function getServerSideProps() { + return { + redirect: { + destination: "/trade", + }, + }; } diff --git a/src/pages/lp/[lp_nft_id].tsx b/src/pages/lp/[lp_nft_id].tsx index 10636772..6662e41b 100644 --- a/src/pages/lp/[lp_nft_id].tsx +++ b/src/pages/lp/[lp_nft_id].tsx @@ -238,7 +238,7 @@ export default function LP() { Position Status diff --git a/src/pages/pair/[...params].tsx b/src/pages/pair/[...params].tsx index 05aa77be..d6a804a3 100644 --- a/src/pages/pair/[...params].tsx +++ b/src/pages/pair/[...params].tsx @@ -608,7 +608,7 @@ export default function TradingPairs() { width={["95vw", "95vw", "95vw", "95vw", "95vw", "85vw"]} spacing={4} > - + (); + + // Pairs are in the form of baseToken:quoteToken + const router = useRouter(); + const [token1Symbol, setToken1Symbol] = useState("unknown"); + const [token2Symbol, setToken2Symbol] = useState("unknown"); + + const [showPairSelector, setShowPairSelector] = useState(false); + + useEffect(() => { + // Check if there are no query params + if (!router.query.params) { + // Redirect to /trade/penumbra:usdc + router.push('/trade/penumbra:usdc'); + } + }, [router.query]) + + useEffect(() => { + const params = router.query as { params: string[] | string | undefined }; + if (!params.params) { + return; + } + + // Concat the whole array into a string, it will split on '/' so rejoin + let pair = ""; + if (Array.isArray(params.params)) { + pair = params.params.join("/"); + } else { + pair = params.params; + } + + const [token1, token2] = pair.split(":"); + setToken1Symbol(token1); + setToken2Symbol(token2); + }, [router.query]); + + const [asset1Token, setAsset1Token] = useState(); + const [asset2Token, setAsset2Token] = useState(); + + // Sell Side + const [ + simulatedSingleHopAsset1SellData, + setSimulatedSingleHopAsset1SellData, + ] = useState(undefined); + const [simulatedMultiHopAsset1SellData, setSimulatedMultiHopAsset1SellData] = + useState(undefined); + + // Buy Side + const [simulatedSingleHopAsset1BuyData, setSimulatedSingleHopAsset1BuyData] = + useState(undefined); + const [simulatedMultiHopAsset1BuyData, setSimulatedMultiHopAsset1BuyData] = + useState(undefined); + + // Depth chart data points, x is price, y is liquidity + + // Sell Side + const [ + depthChartMultiHopAsset1SellPoints, + setDepthChartMultiHopAsset1SellPoints, + ] = useState< + { + x: number; + y: number; + }[] + >([]); + const [ + depthChartSingleHopAsset1SellPoints, + setDepthChartSingleHopAsset1SellPoints, + ] = useState<{ x: number; y: number }[]>([]); + + // Buy Side + const [ + depthChartMultiHopAsset1BuyPoints, + setDepthChartMultiHopAsset1BuyPoints, + ] = useState< + { + x: number; + y: number; + }[] + >([]); + const [ + depthChartSingleHopAsset1BuyPoints, + setDepthChartSingleHopAsset1BuyPoints, + ] = useState<{ x: number; y: number }[]>([]); + + // ! Note this needs to be kind of extreme for now due to limited 'real' liquidity + const bestPriceDeviationPercent = 100; // 100%, + // Override to 100% to show all liquidity + + // Sell Side + const [bestAsset1SellPriceMultiHop, setBestAsset1SellPriceMultiHop] = + useState(undefined); + const [bestAsset1SellPriceSingleHop, setBestAsset1SellPriceSingleHop] = + useState(undefined); + + // Buy Side + const [bestAsset1BuyPriceMultiHop, setBestAsset1BuyPriceMultiHop] = useState< + number | undefined + >(undefined); + const [bestAsset1BuyPriceSingleHop, setBestAsset1BuyPriceSingleHop] = + useState(undefined); + + // TODO: Decide how to set this more intelligently/dynamically + const unitsToSimulateSelling = 1000000; // 1M units + const unitsToSimulateBuying = 1000000; // 1M units + + useEffect(() => { + setIsLoading(true); + + // Get token 1 & 2 + const tokenAssets = fetchAllTokenAssets(); + const asset1Token = tokenAssets.find( + (x) => x.display.toLocaleLowerCase() === token1Symbol.toLocaleLowerCase() + ); + const asset2Token = tokenAssets.find( + (x) => x.display.toLocaleLowerCase() === token2Symbol.toLocaleLowerCase() + ); + + if (!asset1Token || !asset2Token) { + setIsLoading(false); + setIsChartLoading(false); + setIsLPsLoading(false); + setError( + `Token not found: ${!asset1Token ? token1Symbol : token2Symbol}` + ); + return; + } + setError(undefined); + setAsset1Token(asset1Token); + setAsset2Token(asset2Token); + + const simSellMultiHopPromise = fetch( + `/api/simulations/${asset1Token.display}/${asset2Token.display}/${unitsToSimulateSelling}` + ).then((res) => res.json()); + const simSellSingleHopPromise = fetch( + `/api/simulations/${asset1Token.display}/${asset2Token.display}/${unitsToSimulateSelling}/singleHop` + ).then((res) => res.json()); + const simBuyMultiHopPromise = fetch( + `/api/simulations/${asset2Token.display}/${asset1Token.display}/${unitsToSimulateBuying}` + ).then((res) => res.json()); + const simBuySingleHopPromise = fetch( + `/api/simulations/${asset2Token.display}/${asset1Token.display}/${unitsToSimulateBuying}/singleHop` + ).then((res) => res.json()); + + Promise.all([ + simSellMultiHopPromise, + simSellSingleHopPromise, + simBuyMultiHopPromise, + simBuySingleHopPromise, + ]) + .then( + ([ + simQueryMultiHopAsset1SellResponse, + simQuerySingleHopAsset1SellResponse, + simQueryMultiHopAsset1BuyResponse, + simQuerySingleHopAsset1BuyResponse, + ]) => { + if ( + !simQueryMultiHopAsset1SellResponse || + simQueryMultiHopAsset1SellResponse.error || + !simQuerySingleHopAsset1SellResponse || + simQuerySingleHopAsset1SellResponse.error || + !simQueryMultiHopAsset1BuyResponse || + simQueryMultiHopAsset1BuyResponse.error || + !simQuerySingleHopAsset1BuyResponse || + simQuerySingleHopAsset1BuyResponse.error + ) { + console.error("Error querying simulated trades"); + setError("Error querying simulated trades"); + } + setError(undefined); + + setSimulatedMultiHopAsset1SellData( + simQueryMultiHopAsset1SellResponse as SwapExecution + ); + setSimulatedSingleHopAsset1SellData( + simQuerySingleHopAsset1SellResponse as SwapExecution + ); + setSimulatedMultiHopAsset1BuyData( + simQueryMultiHopAsset1BuyResponse as SwapExecution + ); + setSimulatedSingleHopAsset1BuyData( + simQuerySingleHopAsset1BuyResponse as SwapExecution + ); + console.log( + "simQueryMultiHopAsset1SellResponse", + simQueryMultiHopAsset1SellResponse + ); + console.log( + "simQuerySingleHopAsset1SellResponse", + simQuerySingleHopAsset1SellResponse + ); + console.log( + "simQueryMultiHopAsset1BuyResponse", + simQueryMultiHopAsset1BuyResponse + ); + console.log( + "simQuerySingleHopAsset1BuyResponse", + simQuerySingleHopAsset1BuyResponse + ); + } + ) + .catch((error) => { + console.error("Error querying simulated trades", error); + }) + .finally(() => { + setIsLoading(false); + }); + }, [token1Symbol, token2Symbol]); + + const [lpsBuySide, setLPsBuySide] = useState([]); + const [lpsSellSide, setLPsSellSide] = useState([]); + + useEffect(() => { + setIsLPsLoading(true); + + try { + // Get token 1 & 2 + const tokenAssets = fetchAllTokenAssets(); + const asset1Token = tokenAssets.find( + (x) => + x.display.toLocaleLowerCase() === token1Symbol.toLocaleLowerCase() + ); + const asset2Token = tokenAssets.find( + (x) => + x.display.toLocaleLowerCase() === token2Symbol.toLocaleLowerCase() + ); + if (!asset1Token || !asset2Token) { + setIsLoading(false); + setIsChartLoading(false); + setIsLPsLoading(false); + setError( + `Token not found: ${!asset1Token ? token1Symbol : token2Symbol}` + ); + return; + } + setError(undefined); + + const lpsBuySidePromise = fetch( + `/api/lp/positionsByPrice/${asset2Token.display}/${asset1Token.display}/${LPS_TO_RENDER}` + ).then((res) => res.json()); + const lpsSellSidePromise = fetch( + `/api/lp/positionsByPrice/${asset1Token.display}/${asset2Token.display}/${LPS_TO_RENDER}` + ).then((res) => res.json()); + + Promise.all([lpsBuySidePromise, lpsSellSidePromise]) + .then(([lpsBuySideResponse, lpsSellSideResponse]) => { + if ( + !lpsBuySideResponse || + lpsBuySideResponse.error || + !lpsSellSideResponse || + lpsSellSideResponse.error + ) { + console.error("Error querying liquidity positions"); + setError("Error querying liquidity positions"); + } + setError(undefined); + + console.log("lpsBuySideResponse", lpsBuySideResponse); + console.log("lpsSellSideResponse", lpsSellSideResponse); + + setLPsBuySide(lpsBuySideResponse as Position[]); + setLPsSellSide(lpsSellSideResponse as Position[]); + }) + .catch((error) => { + console.error("Error querying lps", error); + }) + .finally(() => { + setIsLPsLoading(false); + }); + setError(undefined); + } catch (error) { + console.error("Error querying liquidity positions", error); + setError("Error querying liquidity positions"); + setIsLPsLoading(false); + } + }, [token1Symbol, token2Symbol]); + + useEffect(() => { + setIsChartLoading(true); + + if ( + !simulatedMultiHopAsset1SellData || + !simulatedSingleHopAsset1SellData || + !asset1Token || + !asset2Token || + !simulatedMultiHopAsset1BuyData || + !simulatedSingleHopAsset1BuyData + ) { + return; + } + let bestAsset1SellPriceMultiHop: number | undefined; + let bestAsset1SellPriceSingleHop: number | undefined; + + // Clear depth chart data + setDepthChartMultiHopAsset1SellPoints([]); + setDepthChartSingleHopAsset1SellPoints([]); + + console.log(asset1Token); + console.log(asset2Token); + + // Set single and multi hop depth chart data + simulatedMultiHopAsset1SellData!.traces.forEach((trace) => { + // First item is the input, last item is the output + const input = trace.value.at(0); + const output = trace.value.at(trace.value.length - 1); + + const inputValue = + Number( + joinLoHi(BigInt(input!.amount!.lo!), BigInt(input!.amount!.hi)) + ) / Number(10 ** asset1Token.decimals); + const outputValue = + Number( + joinLoHi(BigInt(output!.amount!.lo), BigInt(output!.amount!.hi)) + ) / Number(BigInt(10 ** asset2Token.decimals)); + + const price: number = outputValue / inputValue; + + // First trace will have best price, so set only on first iteration + if (trace === simulatedMultiHopAsset1SellData!.traces[0]) { + console.log("Best Asset1 Sell Price for multi hop", price); + setBestAsset1SellPriceMultiHop(Number(price)); + bestAsset1SellPriceMultiHop = price; + } + + // If price is within % of best price, add to depth chart, else ignore + if ( + price >= + bestAsset1SellPriceMultiHop! * (1 - bestPriceDeviationPercent / 100) + ) { + depthChartMultiHopAsset1SellPoints.push({ + x: Number(price), + y: Number(inputValue), + }); + } else { + /* + console.log( + `Price not within ${bestPriceDeviationPercent}% of best price, ignoring`, + price + ); + */ + + // break the loop + return; + } + }); + + // Similar logic for single hop + simulatedSingleHopAsset1SellData!.traces.forEach((trace) => { + // First item is the input, last item is the output + const input = trace.value.at(0); + const output = trace.value.at(1); // If this isnt 1 then something is wrong + + const inputValue = + Number( + joinLoHi(BigInt(input!.amount!.lo!), BigInt(input!.amount!.hi)) + ) / Number(10 ** asset1Token.decimals); + const outputValue = + Number( + joinLoHi(BigInt(output!.amount!.lo), BigInt(output!.amount!.hi)) + ) / Number(BigInt(10 ** asset2Token.decimals)); + + const price: number = outputValue / inputValue; + + // First trace will have best price, so set only on first iteration + if (trace === simulatedSingleHopAsset1SellData!.traces[0]) { + console.log("Best Asset1 Sell Price for single hop", price); + setBestAsset1SellPriceSingleHop(Number(price)); + bestAsset1SellPriceSingleHop = price; + } + + // If price is within % of best price, add to depth chart, else ignore + if ( + price >= + bestAsset1SellPriceSingleHop! * (1 - bestPriceDeviationPercent / 100) + ) { + depthChartSingleHopAsset1SellPoints.push({ + x: Number(price), + y: Number(inputValue), + }); + } else { + /* + console.log( + `Price not within ${bestPriceDeviationPercent}% of best price, ignoring`, + price + );*/ + + // break the loop + return; + } + }); + + // Do it all again for the buy side :) + //! Maybe theres a way to refactor this to be more concise + let bestAsset1BuyPriceMultiHop: number | undefined; + let bestAsset1BuyPriceSingleHop: number | undefined; + + // Clear depth chart data + setDepthChartMultiHopAsset1BuyPoints([]); + setDepthChartSingleHopAsset1BuyPoints([]); + + // Set single and multi hop depth chart data + simulatedMultiHopAsset1BuyData!.traces.forEach((trace) => { + // First item is the input, last item is the output + const input = trace.value.at(0); + const output = trace.value.at(trace.value.length - 1); + + const inputValue = + Number( + joinLoHi(BigInt(input!.amount!.lo!), BigInt(input!.amount!.hi)) + ) / Number(10 ** asset2Token.decimals); + const outputValue = + Number( + joinLoHi(BigInt(output!.amount!.lo), BigInt(output!.amount!.hi)) + ) / Number(BigInt(10 ** asset1Token.decimals)); + + // ! Important to note that the price is inverted here, so we do input/output instead of output/input + const price: number = inputValue / outputValue; + + // First trace will have best price, so set only on first iteration + if (trace === simulatedMultiHopAsset1BuyData!.traces[0]) { + console.log("Best Asset1 Buy Price for multi hop", price); + setBestAsset1BuyPriceMultiHop(Number(price)); + bestAsset1BuyPriceMultiHop = price; + } + + // If price is within % of best price, add to depth chart, else ignore + if ( + price <= + bestAsset1BuyPriceMultiHop! * (1 + bestPriceDeviationPercent / 100) + ) { + depthChartMultiHopAsset1BuyPoints.push({ + x: Number(price), + y: Number(outputValue), + }); + } else { + /* + console.log( + `Price not within ${bestPriceDeviationPercent}% of best price, ignoring`, + price + ); + */ + + // break the loop + return; + } + }); + + // Similar logic for single hop + simulatedSingleHopAsset1BuyData!.traces.forEach((trace) => { + // First item is the input, last item is the output + const input = trace.value.at(0); + const output = trace.value.at(1); // If this isnt 1 then something is wrong + + const inputValue = + Number( + joinLoHi(BigInt(input!.amount!.lo!), BigInt(input!.amount!.hi)) + ) / Number(10 ** asset2Token.decimals); + const outputValue = + Number( + joinLoHi(BigInt(output!.amount!.lo), BigInt(output!.amount!.hi)) + ) / Number(BigInt(10 ** asset1Token.decimals)); + + // ! Important to note that the price is inverted here, so we do input/output instead of output/input + const price: number = inputValue / outputValue; + + // First trace will have best price, so set only on first iteration + if (trace === simulatedSingleHopAsset1BuyData!.traces[0]) { + console.log("Best Asset1 Buy Price for single hop", price); + setBestAsset1BuyPriceSingleHop(Number(price)); + bestAsset1BuyPriceSingleHop = price; + } + + // If price is within % of best price, add to depth chart, else ignore + if ( + price <= + bestAsset1BuyPriceSingleHop! * (1 + bestPriceDeviationPercent / 100) + ) { + depthChartSingleHopAsset1BuyPoints.push({ + x: Number(price), + y: Number(outputValue), + }); + } else { + /* + console.log( + `Price not within ${bestPriceDeviationPercent}% of best price, ignoring`, + price + );*/ + + // break the loop + return; + } + }); + + // Set all of the stateful data + // ! First update depth and sell charts to show CUMULTAIVE liquidity (y) of all points before them + for (let i = 1; i < depthChartMultiHopAsset1SellPoints.length; i++) { + depthChartMultiHopAsset1SellPoints[i].y += + depthChartMultiHopAsset1SellPoints[i - 1].y; + } + for (let i = 1; i < depthChartSingleHopAsset1SellPoints.length; i++) { + depthChartSingleHopAsset1SellPoints[i].y += + depthChartSingleHopAsset1SellPoints[i - 1].y; + } + for (let i = 1; i < depthChartMultiHopAsset1BuyPoints.length; i++) { + depthChartMultiHopAsset1BuyPoints[i].y += + depthChartMultiHopAsset1BuyPoints[i - 1].y; + } + for (let i = 1; i < depthChartSingleHopAsset1BuyPoints.length; i++) { + depthChartSingleHopAsset1BuyPoints[i].y += + depthChartSingleHopAsset1BuyPoints[i - 1].y; + } + + setDepthChartMultiHopAsset1SellPoints(depthChartMultiHopAsset1SellPoints); + setDepthChartSingleHopAsset1SellPoints(depthChartSingleHopAsset1SellPoints); + setDepthChartMultiHopAsset1BuyPoints(depthChartMultiHopAsset1BuyPoints); + setDepthChartSingleHopAsset1BuyPoints(depthChartSingleHopAsset1BuyPoints); + + // print to debug + console.log( + "depthChartMultiHopAsset1SellPoints", + depthChartMultiHopAsset1SellPoints + ); + console.log( + "depthChartSingleHopAsset1SellPoints", + depthChartSingleHopAsset1SellPoints + ); + + console.log( + "depthChartMultiHopAsset1BuyPoints", + depthChartMultiHopAsset1BuyPoints + ); + console.log( + "depthChartSingleHopAsset1BuyPoints", + depthChartSingleHopAsset1BuyPoints + ); + + // TODO: these should really be &&s but theres no real market so sometimes they can be 0 (we should also handle this edgecase gracefully) + + // ! If this point is reached with no data, it means there is no liquidity + // TODO: Consider rendering something extra to denote theres no error, just no liquidity + + setIsChartLoading(false); + }, [ + simulatedMultiHopAsset1SellData, + simulatedSingleHopAsset1SellData, + simulatedMultiHopAsset1BuyData, + simulatedSingleHopAsset1BuyData, + asset1Token, + asset2Token, + ]); + + useEffect(() => { + console.log("isLoading", isLoading); + console.log("isChartLoading", isChartLoading); + console.log("isLPsLoading", isLPsLoading); + console.log("error", error); + console.log("asset1Token", asset1Token); + console.log("asset2Token", asset2Token); + }, [ + isLoading, + isChartLoading, + isLPsLoading, + error, + asset1Token, + asset2Token, + ]); + + return ( + + + + + + + setShowPairSelector(true)}> + + + {asset1Token?.symbol}/{asset2Token?.symbol} + + + {asset1Token?.display}/{asset2Token?.display} + + + + + { + router.push(`/trade/${pair[0].display}:${pair[1].display}`); + }} + /> + + {asset1Token && asset2Token && ( + + )} + + + + Depth Chart + + {asset1Token && asset2Token && ( + + )} + + + + + Order Book + + {asset1Token && asset2Token && ( + + )} + + + + + ); +} diff --git a/src/pages/trades.tsx b/src/pages/trades.tsx index 67756d9f..c58edd81 100644 --- a/src/pages/trades.tsx +++ b/src/pages/trades.tsx @@ -303,7 +303,7 @@ export default function Trades() { top={`${lineTop}`} height={`${lineHeight}`} width={".1em"} - className="neon-box" + className="box-card" backgroundColor="var(--complimentary-background)" id="vertical-line" /> diff --git a/styles/Home.module.css b/styles/Home.module.css index ccf3c00d..db071f8e 100644 --- a/styles/Home.module.css +++ b/styles/Home.module.css @@ -93,7 +93,7 @@ .header { width: 100%; - background: linear-gradient(to bottom, var(--purple-accent-700), transparent); + background: var(--charcoal); /* position: fixed; /* Optional: makes the header stay at the top on scroll */ top: 0; z-index: 10; diff --git a/styles/global.css b/styles/global.css index 9fd78afc..10d667f8 100644 --- a/styles/global.css +++ b/styles/global.css @@ -1,4 +1,5 @@ :root { + --body-background: #272629; --background: 0, 0%, 0%; --foreground: 0, 4%, 73%; --muted: 0, 0%, 100%; @@ -62,6 +63,7 @@ html, body { padding: 0; margin: 0; + background: var(--body-background) !important; } a { @@ -78,20 +80,8 @@ img { height: auto; } -.neon-box { - outline: 0.15em solid var(--complimentary-background); +.box-card { border-radius: 0.5em; width: fit-content; background: var(--charcoal); - box-shadow: 0 0 5px var(--complimentary-background), - /* Smaller glow */ 0 0 10px var(--complimentary-background), - /* Smaller glow */ 0 0 15px var(--complimentary-background), - /* Subtle bright glow */ 0 0 20px var(--complimentary-background), - /* Subtle bright glow */ 0 0 25px var(--complimentary-background), - /* Subtle bright glow */ 0 0 30px var(--complimentary-background); -} - -.neon-spinner { - filter: drop-shadow(0 0 8px white) - drop-shadow(0 0 12px white); }