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 d7ea8936c..4034e8d3f 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 54066 years
+ in 54086 years
@@ -256,7 +256,7 @@ exports[`BlockListWithControls renders correctly 1`] = `
- in 54066 years
+ in 54086 years
@@ -303,7 +303,7 @@ exports[`BlockListWithControls renders correctly 1`] = `
- in 54066 years
+ in 54086 years
@@ -377,7 +377,7 @@ exports[`BlockListWithControls renders correctly 1`] = `
- in 54066 years
+ in 54086 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) => (