From f1a00bfd9a7d68083d996ab86d6dabab660427e8 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Wed, 21 Feb 2024 10:47:06 -0600 Subject: [PATCH] feat(mempool-header): added mempool header --- package.json | 1 + pnpm-lock.yaml | 219 ++++++++++ .../BlockListWithControls.test.tsx.snap | 8 +- src/app/transactions/MempoolFeePieChart.tsx | 133 ++++++ src/app/transactions/MempoolFeeStats.tsx | 394 ++++++++++++++---- .../TransactionTypeFilterMenu.tsx | 70 ++++ src/app/txid/[txId]/TxDetails/Fees.tsx | 4 +- .../TxDetails/__tests__/Broadcast.test.tsx | 1 - src/common/components/FilterMenu.tsx | 10 +- .../components/modals/unlocking-schedule.tsx | 2 +- .../{usMempoolFee.ts => useMempoolFee.ts} | 10 +- src/common/queries/useMempoolTxStats.ts | 30 ++ .../txs-list/tabs/CSVDownloadButton.tsx | 2 +- src/ui/Grid.tsx | 7 +- src/ui/MenuButton.tsx | 6 +- src/ui/theme/colors.ts | 5 + src/ui/theme/theme.ts | 4 +- 17 files changed, 785 insertions(+), 121 deletions(-) create mode 100644 src/app/transactions/MempoolFeePieChart.tsx create mode 100644 src/app/transactions/TransactionTypeFilterMenu.tsx rename src/common/queries/{usMempoolFee.ts => useMempoolFee.ts} (71%) create mode 100644 src/common/queries/useMempoolTxStats.ts diff --git a/package.json b/package.json index 4d8320c7c..ae36f245e 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "react-redux": "8.1.2", "react-simple-code-editor": "0.13.1", "react-ssr-prepass": "npm:preact-ssr-prepass", + "recharts": "2.11.0", "schema-inspector": "2.0.3", "server-only": "0.0.1", "sharp": "0.33.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20a83d22b..f88c2d379 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,6 +231,9 @@ dependencies: react-ssr-prepass: specifier: npm:preact-ssr-prepass version: /preact-ssr-prepass@1.2.1(preact@10.19.3) + recharts: + specifier: 2.11.0 + version: 2.11.0(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) schema-inspector: specifier: 2.0.0 version: 2.0.0 @@ -5856,6 +5859,48 @@ packages: resolution: {integrity: sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==} dev: false + /@types/d3-array@3.2.1: + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + dev: false + + /@types/d3-color@3.1.3: + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + dev: false + + /@types/d3-ease@3.0.2: + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + dev: false + + /@types/d3-interpolate@3.0.4: + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + dependencies: + '@types/d3-color': 3.1.3 + dev: false + + /@types/d3-path@3.0.2: + resolution: {integrity: sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==} + dev: false + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: false + + /@types/d3-shape@3.1.6: + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + dependencies: + '@types/d3-path': 3.0.2 + dev: false + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: false + + /@types/d3-timer@3.0.2: + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + dev: false + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: false @@ -6895,6 +6940,11 @@ packages: engines: {node: '>=6'} dev: false + /clsx@2.1.0: + resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + engines: {node: '>=6'} + dev: false + /cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -7130,6 +7180,77 @@ packages: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} dev: false + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true @@ -7197,6 +7318,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: true @@ -7350,6 +7475,12 @@ packages: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dev: true + /dom-helpers@3.4.0: + resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} + dependencies: + '@babel/runtime': 7.23.4 + dev: false + /domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -8124,6 +8255,11 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true + /fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + dev: false + /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -8694,6 +8830,11 @@ packages: hasown: 2.0.0 side-channel: 1.0.4 + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /intl-messageformat@10.5.8: resolution: {integrity: sha512-NRf0jpBWV0vd671G5b06wNofAN8tp7WWDogMZyaU8GUAsmbouyvgwmFJI7zLjfAMpm3zK+vSwRP3jzaoIcMbaA==} dependencies: @@ -10784,6 +10925,10 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + /react-loading-skeleton@3.3.1(react@18.2.0): resolution: {integrity: sha512-NilqqwMh2v9omN7LteiDloEVpFyMIa0VGqF+ukqp0ncVlYu1sKYbYGX9JEl+GtOT9TKsh04zCHAbavnQ2USldA==} peerDependencies: @@ -10881,6 +11026,20 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-smooth@2.0.5(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==} + peerDependencies: + prop-types: ^15.6.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-style-singleton@2.2.1(@types/react@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -10909,6 +11068,20 @@ packages: scheduler: 0.23.0 dev: true + /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} + peerDependencies: + react: '>=15.0.0' + react-dom: '>=15.0.0' + dependencies: + dom-helpers: 3.4.0 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -10964,6 +11137,33 @@ packages: util-deprecate: 1.0.2 dev: true + /recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + dependencies: + decimal.js-light: 2.5.1 + dev: false + + /recharts@2.11.0(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-5s+u1m5Hwxb2nh0LABkE3TS/lFqFHyWl7FnPbQhHobbQQia4ih1t3o3+ikPYr31Ns+kYe4FASIthKeKi/YYvMg==} + engines: {node: '>=14'} + peerDependencies: + prop-types: ^15.6.0 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + clsx: 2.1.0 + eventemitter3: 4.0.7 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 16.13.1 + react-smooth: 2.0.5(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.1 + victory-vendor: 36.8.6 + dev: false + /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -12089,6 +12289,25 @@ packages: engines: {node: '>= 0.8'} dev: false + /victory-vendor@36.8.6: + resolution: {integrity: sha512-PH8Wj9b0xIZ4AVfyn0c1SJrOhtxDJ5PNxj1ZDABPg1Gw1vJr1mJVqESPhvsFj7mXLQohVdiKqp4kWZkXlPcRcA==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} diff --git a/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap b/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap index 94a93c614..7702882d1 100644 --- a/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap +++ b/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap @@ -209,7 +209,7 @@ exports[`BlockListWithControls renders correctly 1`] = ` - in 54071 years + in 54087 years @@ -256,7 +256,7 @@ exports[`BlockListWithControls renders correctly 1`] = ` - in 54071 years + in 54087 years @@ -303,7 +303,7 @@ exports[`BlockListWithControls renders correctly 1`] = ` - in 54071 years + in 54087 years @@ -377,7 +377,7 @@ exports[`BlockListWithControls renders correctly 1`] = ` - in 54071 years + in 54087 years diff --git a/src/app/transactions/MempoolFeePieChart.tsx b/src/app/transactions/MempoolFeePieChart.tsx new file mode 100644 index 000000000..20ea2c675 --- /dev/null +++ b/src/app/transactions/MempoolFeePieChart.tsx @@ -0,0 +1,133 @@ +import { useColorModeValue, useTheme } from '@chakra-ui/react'; +import { Cell, Pie, PieChart } from 'recharts'; + +const pieChartWidth = 215; +const pieChartHeight = 200; + +const renderCustomizedLabel = ({ + percent, + cx, + cy, + midAngle, + outerRadius, + color, +}: { + percent: number; + x: number; + y: number; + cx: number; + cy: number; + midAngle: number; + outerRadius: number; + color: string; +}) => { + if (!percent) return null; // Don't show zeros + const RADIAN = Math.PI / 180; + const radius = outerRadius + 25; // Increase radius to move label further out + const newX = cx + radius * Math.cos(-midAngle * RADIAN); + const newY = cy + radius * Math.sin(-midAngle * RADIAN); + return ( + + {percent * 100 < 1 ? '<1%' : `${(percent * 100).toFixed(0)}%`} + + ); +}; + +const renderCenterCustomizedLabel = ({ + label, + cx, + cy, + color, +}: { + label: string; + cx: number; + cy: number; + color: string; +}) => { + return ( + + {label} + + ); +}; + +export function getTxTypePieChartColor(txType: string) { + switch (txType) { + case 'token_transfer': + return 'var(--stacks-colors-others-lilac)'; + case 'contract_call': + return 'var(--stacks-colors-purple-850)'; + case 'smart_contract': + return 'var(--stacks-colors-others-limeGreen)'; + default: + return 'purple'; + } +} + +export function MempoolFeePieChart({ + filteredTxTypeCounts, + totalTxCount, +}: { + filteredTxTypeCounts: { + token_transfer: number; + smart_contract: number; + contract_call: number; + }; + totalTxCount: number; +}) { + const pieData = Object.entries(filteredTxTypeCounts) + .filter(([_, count]) => count !== 0) + .map(([txType, count]) => { + return { + name: txType, + value: (count / totalTxCount) * 100, + }; + }); + const textColor = useColorModeValue('black', 'white'); + const theme = useTheme(); + const secondaryTextColor = useColorModeValue(theme.colors.slate[700], theme.colors.slate[500]); + return ( + + renderCustomizedLabel({ ...props, color: secondaryTextColor })} + innerRadius={60} + outerRadius={67} + > + {pieData.map((entry, index) => ( + + ))} + + {renderCenterCustomizedLabel({ + label: `${totalTxCount} tx`, + cx: pieChartWidth / 2, + cy: pieChartHeight / 2, + color: textColor, + })} + + ); +} diff --git a/src/app/transactions/MempoolFeeStats.tsx b/src/app/transactions/MempoolFeeStats.tsx index 74fb1b423..0541815bc 100644 --- a/src/app/transactions/MempoolFeeStats.tsx +++ b/src/app/transactions/MempoolFeeStats.tsx @@ -1,127 +1,339 @@ -import React from 'react'; +import { StackDivider, VStack, useColorModeValue } from '@chakra-ui/react'; +import { useMemo, useState } from 'react'; +import { + TbCircleChevronDown, + TbCircleChevronUp, + TbCircleChevronsUp, + TbCircleMinus, +} from 'react-icons/tb'; import { MempoolFeePriorities } from '@stacks/blockchain-api-client/src/generated/models'; import { MempoolFeePrioritiesAll } from '@stacks/blockchain-api-client/src/generated/models/MempoolFeePrioritiesAll'; +import { Card } from '../../common/components/Card'; import { getTxTypeIcon } from '../../common/components/TxIcon'; -import { useSuspenseMempoolFee } from '../../common/queries/usMempoolFee'; +import { useSuspenseMempoolFee } from '../../common/queries/useMempoolFee'; +import { useSuspenseMempoolTransactionStats } from '../../common/queries/useMempoolTxStats'; import { TokenPrice } from '../../common/types/tokenPrice'; import { MICROSTACKS_IN_STACKS, capitalize, getUsdValue } from '../../common/utils/utils'; import { Box } from '../../ui/Box'; import { Flex, FlexProps } from '../../ui/Flex'; import { HStack } from '../../ui/HStack'; import { Icon } from '../../ui/Icon'; +import { Text } from '../../ui/Text'; import { Tooltip } from '../../ui/Tooltip'; -import { Caption } from '../../ui/typography'; import { ExplorerErrorBoundary } from '../_components/ErrorBoundary'; -import { StatSection } from '../_components/Stats/StatSection'; -import { Wrapper } from '../_components/Stats/Wrapper'; +import { MempoolFeePieChart, getTxTypePieChartColor } from './MempoolFeePieChart'; +import { + TransactionTypeFilterMenu, + TransactionTypeFilterTypes, + mapTransactionTypeToFilterValue, +} from './TransactionTypeFilterMenu'; -function MempoolFeeByTxType({ - mempoolFeeTokenTransfer, - mempoolFeeContractCall, - mempoolFeeSmartContract, -}: { - mempoolFeeTokenTransfer: number; - mempoolFeeContractCall: number; - mempoolFeeSmartContract: number; -}) { - return ( - |} gap={1}> - - - - - {`${Number((mempoolFeeTokenTransfer / MICROSTACKS_IN_STACKS).toFixed(3))}`} STX - - - - - - - - {`${Number((mempoolFeeContractCall / MICROSTACKS_IN_STACKS).toFixed(3))}`} STX - - - - - - - - {`${Number((mempoolFeeSmartContract / MICROSTACKS_IN_STACKS).toFixed(3))}`} STX - - - - - ); -} +export const getFeePriorityIcon = (priority: keyof MempoolFeePrioritiesAll) => { + switch (priority) { + case 'no_priority': + return ; + case 'low_priority': + return ; + case 'medium_priority': + return ; + case 'high_priority': + return ; + default: + throw new Error('Invalid priority'); + } +}; -function MempoolFeeSection({ +function MempoolFeePriorityCard({ mempoolFeeResponse, priority, stxPrice, - ...rest + txTypeFilter, }: { mempoolFeeResponse: MempoolFeePriorities; priority: keyof MempoolFeePrioritiesAll; stxPrice: number; + txTypeFilter: TransactionTypeFilterTypes; } & FlexProps) { - const mempoolFeeAll = mempoolFeeResponse?.all?.[priority] || 0; + const borderColor = useColorModeValue('slate.200', 'slate.800'); + const isTxTypeFiltered = txTypeFilter !== TransactionTypeFilterTypes.AverageForAllTransactions; + const mempoolFeeAll = isTxTypeFiltered + ? mempoolFeeResponse?.[mapTransactionTypeToFilterValue(txTypeFilter)]?.[priority] || 0 + : mempoolFeeResponse?.all?.[priority] || 0; const mempoolFeeTokenTransfer = mempoolFeeResponse?.token_transfer?.[priority] || 0; const mempoolFeeContractCall = mempoolFeeResponse?.contract_call?.[priority] || 0; const mempoolFeeSmartContract = mempoolFeeResponse?.smart_contract?.[priority] || 0; + + const title = capitalize(priority.replaceAll('_', ' ')); + return ( - - } - borderColor={'border'} - {...rest} - /> + + + {getFeePriorityIcon(priority)} + + {title} + + + } + spacing={4} + alignItems="flex-start" + > + + + {mempoolFeeAll / MICROSTACKS_IN_STACKS} STX + + + {getUsdValue(mempoolFeeAll, stxPrice, true)} + + + {isTxTypeFiltered ? null : ( + + + + + + {`${Number((mempoolFeeTokenTransfer / MICROSTACKS_IN_STACKS).toFixed(3))}`} STX + + + + + + + + {`${Number((mempoolFeeContractCall / MICROSTACKS_IN_STACKS).toFixed(3))}`} STX + + + + + + + + {`${Number((mempoolFeeSmartContract / MICROSTACKS_IN_STACKS).toFixed(3))}`} STX + + + + + )} + + ); } export function MempoolFeeStats({ tokenPrice }: { tokenPrice: TokenPrice }) { const mempoolFeeResponse = useSuspenseMempoolFee().data as MempoolFeePriorities; + const mempoolTransactionStats = useSuspenseMempoolTransactionStats().data; + + const [transactionType, setTransactionType] = useState( + TransactionTypeFilterTypes.AverageForAllTransactions + ); + + const txTypeCounts = mempoolTransactionStats?.tx_type_counts; + + const mappedTxType = mapTransactionTypeToFilterValue(transactionType); + + const filteredTxTypeCounts = useMemo(() => { + const { poison_microblock, ...filteredTxTypeCounts } = txTypeCounts || {}; + Object.keys(filteredTxTypeCounts).forEach(key => { + if (mappedTxType !== 'all' && key !== mappedTxType) { + delete filteredTxTypeCounts[key as keyof typeof filteredTxTypeCounts]; + } + }); + return filteredTxTypeCounts; + }, [txTypeCounts, mappedTxType]); + + const filteredMempoolFeeResponse = useMemo(() => { + const filteredMempoolFeeResponse = { ...mempoolFeeResponse }; + Object.keys(filteredMempoolFeeResponse).forEach(key => { + if (mappedTxType !== 'all' && key !== mappedTxType) { + delete filteredMempoolFeeResponse[key as keyof typeof filteredMempoolFeeResponse]; + } + }); + return filteredMempoolFeeResponse; + }, [mappedTxType, mempoolFeeResponse]); + + const totalTxCount = Object.entries(filteredTxTypeCounts).reduce((acc, [key, val]) => { + return acc + val; + }, 0); + + const textColor = useColorModeValue('slate.700', 'slate.500'); + return ( - - - - - - + + + + + IN MEMPOOL + + + + + + {Object.entries(filteredTxTypeCounts).map(([key, value]) => { + const icon = getTxTypeIcon(key as keyof typeof filteredTxTypeCounts); + const text = + key === 'token_transfer' + ? 'Token transfer' + : key === 'contract_call' + ? 'Function call' + : key === 'smart_contract' + ? 'Contract deploy' + : null; + const bg = getTxTypePieChartColor(key); + + return ( + + + + + {text ? `${value} ${text}` : null} + + + ); + })} + + + + + + + CURRENT FEE RATES + + + + + + + + + + + + + + + + + ); } diff --git a/src/app/transactions/TransactionTypeFilterMenu.tsx b/src/app/transactions/TransactionTypeFilterMenu.tsx new file mode 100644 index 000000000..6ff93f2ab --- /dev/null +++ b/src/app/transactions/TransactionTypeFilterMenu.tsx @@ -0,0 +1,70 @@ +import { useCallback, useMemo } from 'react'; + +import { MempoolFeePriorities } from '@stacks/blockchain-api-client'; + +import { FilterMenu } from '../../common/components/FilterMenu'; + +export enum TransactionTypeFilterTypes { + AverageForAllTransactions = 'AverageForAllTransactions', + AverageForTokenTransfers = 'AverageForTokenTransfers', + AverageForFunctionCalls = 'AverageForFunctionCalls', + AverageForContractDeploys = 'AverageForContractDeploys', +} + +function getTransactionTypeFilterLabel(transactionType: TransactionTypeFilterTypes) { + if (transactionType === TransactionTypeFilterTypes.AverageForAllTransactions) { + return 'Average for all transactions'; + } + if (transactionType === TransactionTypeFilterTypes.AverageForTokenTransfers) { + return 'Average for token transfers'; + } + if (transactionType === TransactionTypeFilterTypes.AverageForFunctionCalls) { + return 'Average for function calls'; + } + if (transactionType === TransactionTypeFilterTypes.AverageForContractDeploys) { + return 'Average for contract deploys'; + } + throw new Error('Invalid transactionType'); +} + +export function mapTransactionTypeToFilterValue( + txType: TransactionTypeFilterTypes +): keyof MempoolFeePriorities { + if (txType === TransactionTypeFilterTypes.AverageForAllTransactions) { + return 'all'; + } + if (txType === TransactionTypeFilterTypes.AverageForTokenTransfers) { + return 'token_transfer'; + } + if (txType === TransactionTypeFilterTypes.AverageForFunctionCalls) { + return 'contract_call'; + } + if (txType === TransactionTypeFilterTypes.AverageForContractDeploys) { + return 'smart_contract'; + } + throw new Error('txType'); +} + +export function TransactionTypeFilterMenu({ + transactionType, + setTransactionType, +}: { + transactionType: TransactionTypeFilterTypes; + setTransactionType: (transactionType: TransactionTypeFilterTypes) => void; +}) { + const menuItems = useMemo( + () => + Object.keys(TransactionTypeFilterTypes).map(filterType => ({ + onClick: () => setTransactionType(filterType as TransactionTypeFilterTypes), + label: getTransactionTypeFilterLabel(filterType as TransactionTypeFilterTypes), + })), + [setTransactionType] + ); + + const filterLabel = useCallback( + () => getTransactionTypeFilterLabel(transactionType), + [transactionType] + ); + + return ; +} diff --git a/src/app/txid/[txId]/TxDetails/Fees.tsx b/src/app/txid/[txId]/TxDetails/Fees.tsx index 34d8b10f4..172f21925 100644 --- a/src/app/txid/[txId]/TxDetails/Fees.tsx +++ b/src/app/txid/[txId]/TxDetails/Fees.tsx @@ -1,7 +1,6 @@ 'use client'; import { FC } from 'react'; -import * as React from 'react'; import { MempoolTransaction, Transaction } from '@stacks/stacks-blockchain-api-types'; @@ -9,8 +8,7 @@ import { Badge } from '../../../../common/components/Badge'; import { KeyValueHorizontal } from '../../../../common/components/KeyValueHorizontal'; import { StxPriceButton } from '../../../../common/components/StxPriceButton'; import { Value } from '../../../../common/components/Value'; -import { StyledBadge } from '../../../../common/components/status'; -import { microToStacks, microToStacksFormatted } from '../../../../common/utils/utils'; +import { microToStacksFormatted } from '../../../../common/utils/utils'; import { Flex } from '../../../../ui/Flex'; import { useColorMode } from '../../../../ui/hooks/useColorMode'; diff --git a/src/app/txid/[txId]/TxDetails/__tests__/Broadcast.test.tsx b/src/app/txid/[txId]/TxDetails/__tests__/Broadcast.test.tsx index 653842689..d666364fd 100644 --- a/src/app/txid/[txId]/TxDetails/__tests__/Broadcast.test.tsx +++ b/src/app/txid/[txId]/TxDetails/__tests__/Broadcast.test.tsx @@ -1,6 +1,5 @@ import '@testing-library/jest-dom/extend-expect'; import { render } from '@testing-library/react'; -import React from 'react'; import { Transaction } from '@stacks/stacks-blockchain-api-types'; diff --git a/src/common/components/FilterMenu.tsx b/src/common/components/FilterMenu.tsx index 7e0caf4d2..1ec4132ae 100644 --- a/src/common/components/FilterMenu.tsx +++ b/src/common/components/FilterMenu.tsx @@ -19,7 +19,7 @@ interface MenuItem { interface FilterMenuProps { filterLabel: string | (() => string); menuItems: MenuItem[] | ReactNode[]; - leftIcon: IconType; + leftIcon?: IconType; } function isMenuItemArray(items: any[]): items is { label: string; onClick: () => void }[] { @@ -41,9 +41,13 @@ export function FilterMenu({ filterLabel, menuItems, leftIcon }: FilterMenuProps } - leftIcon={} + leftIcon={ + leftIcon ? ( + + ) : null + } fontSize={'sm'} - bg={'bg'} + bg="bg" fontWeight={'semibold'} border={'1px'} borderColor={borderColor} diff --git a/src/common/components/modals/unlocking-schedule.tsx b/src/common/components/modals/unlocking-schedule.tsx index 22195c7ab..f157ff028 100644 --- a/src/common/components/modals/unlocking-schedule.tsx +++ b/src/common/components/modals/unlocking-schedule.tsx @@ -12,7 +12,7 @@ import { Grid } from '../../../ui/Grid'; import { Modal } from '../../../ui/Modal'; import { Stack } from '../../../ui/Stack'; import { Tooltip } from '../../../ui/Tooltip'; -import { Caption, Text, Title } from '../../../ui/typography'; +import { Caption, Text } from '../../../ui/typography'; import { MODALS } from '../../constants/constants'; import { useCoreApiInfo } from '../../queries/useCoreApiInfo'; import { Badge } from '../Badge'; diff --git a/src/common/queries/usMempoolFee.ts b/src/common/queries/useMempoolFee.ts similarity index 71% rename from src/common/queries/usMempoolFee.ts rename to src/common/queries/useMempoolFee.ts index 5a05fd231..19e783187 100644 --- a/src/common/queries/usMempoolFee.ts +++ b/src/common/queries/useMempoolFee.ts @@ -1,14 +1,6 @@ -import { - UseQueryOptions, - UseSuspenseQueryOptions, - useQuery, - useSuspenseQuery, -} from '@tanstack/react-query'; +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { address } from 'bitcoinjs-lib'; -import { MempoolFeePriorities } from '@stacks/blockchain-api-client/src/generated/models'; -import { AddressBalanceResponse } from '@stacks/stacks-blockchain-api-types'; - import { useApi } from '../api/useApi'; import { ONE_MINUTE } from './query-stale-time'; diff --git a/src/common/queries/useMempoolTxStats.ts b/src/common/queries/useMempoolTxStats.ts new file mode 100644 index 000000000..2dbf68c62 --- /dev/null +++ b/src/common/queries/useMempoolTxStats.ts @@ -0,0 +1,30 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { MempoolTransactionStatsResponse } from '@stacks/blockchain-api-client'; + +import { useApi } from '../api/useApi'; +import { ONE_MINUTE } from './query-stale-time'; + +export function useMempoolTransactionStats(options: any = {}) { + const api = useApi(); + return useQuery({ + queryKey: ['mempoolTransactionStats'], + queryFn: () => { + return api.transactionsApi.getMempoolTransactionStats(); + }, + staleTime: ONE_MINUTE, + ...options, + }); +} + +export function useSuspenseMempoolTransactionStats(options: any = {}) { + const api = useApi(); + return useSuspenseQuery({ + queryKey: ['mempoolTransactionStats'], + queryFn: () => { + return api.transactionsApi.getMempoolTransactionStats(); + }, + staleTime: ONE_MINUTE, + ...options, + }); +} diff --git a/src/features/txs-list/tabs/CSVDownloadButton.tsx b/src/features/txs-list/tabs/CSVDownloadButton.tsx index dc97dcece..8edb91171 100644 --- a/src/features/txs-list/tabs/CSVDownloadButton.tsx +++ b/src/features/txs-list/tabs/CSVDownloadButton.tsx @@ -1,7 +1,7 @@ 'use client'; import { useColorMode } from '@chakra-ui/react'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { CSVDownload } from 'react-csv'; import { FiDownload } from 'react-icons/fi'; diff --git a/src/ui/Grid.tsx b/src/ui/Grid.tsx index da1f0f75a..69733e924 100644 --- a/src/ui/Grid.tsx +++ b/src/ui/Grid.tsx @@ -1,11 +1,6 @@ 'use client'; -import { - Grid as CUIGrid, - GridProps as CUIGridProps, - forwardRef, - useColorMode, -} from '@chakra-ui/react'; +import { Grid as CUIGrid, GridProps as CUIGridProps, forwardRef } from '@chakra-ui/react'; import { UIComponent } from './types'; diff --git a/src/ui/MenuButton.tsx b/src/ui/MenuButton.tsx index 7a378bd7d..8ca2fbe4c 100644 --- a/src/ui/MenuButton.tsx +++ b/src/ui/MenuButton.tsx @@ -6,10 +6,14 @@ import { forwardRef, useColorMode, } from '@chakra-ui/react'; +import { ReactNode } from 'react'; import { UIComponent } from './types'; -export type MenuButtonProps = CUIMenuButtonProps & UIComponent; +export type MenuButtonProps = CUIMenuButtonProps & + UIComponent & { + leftIcon?: ReactNode | null; + }; export const MenuButton = forwardRef( ({ children, size, ...rest }, ref) => (