Skip to content

Commit 57553b7

Browse files
committed
[Dashboard] Feature: Breakdown sponsored transactions by chain ID (#5252)
![Screenshot 2024-10-31 at 12 41 52 PM](https://github.com/user-attachments/assets/1eb6234c-9c5a-4e82-9ab7-4252dc1c1a8c) <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the `SponsoredTransactionsChartCard` component to handle user operation statistics by chain, improving data visualization and functionality. ### Detailed summary - Changed `UserOpStats` to `UserOpStatsByChain` in `createUserOpStatsStub`. - Updated chart data structure to support multiple chains. - Modified `SponsoredTransactionsChartCard` to visualize data by chain. - Enhanced data aggregation and display logic. - Updated chart configuration to include dynamic chains. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 13fc6f5 commit 57553b7

File tree

3 files changed

+95
-47
lines changed

3 files changed

+95
-47
lines changed

apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
44
import {
5+
type ChartConfig,
56
ChartContainer,
67
ChartLegend,
78
ChartLegendContent,
89
ChartTooltip,
910
ChartTooltipContent,
1011
} from "@/components/ui/chart";
11-
import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi";
12+
import type { UserOpStatsByChain } from "@3rdweb-sdk/react/hooks/useApi";
1213
import {
1314
EmptyChartState,
1415
LoadingChartState,
@@ -20,53 +21,94 @@ import { UnityIcon } from "components/icons/brand-icons/UnityIcon";
2021
import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
2122
import { DocLink } from "components/shared/DocLink";
2223
import { format } from "date-fns";
24+
import { useAllChainsData } from "hooks/chains/allChains";
2325
import { useMemo } from "react";
2426
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
2527
import { formatTickerNumber } from "../../../lib/format-utils";
2628

27-
type ChartData = {
29+
type ChartData = Record<string, number> & {
2830
time: string; // human readable date
29-
failed: number;
30-
successful: number;
3131
};
3232

33-
const chartConfig = {
34-
successful: {
35-
label: "Successful",
36-
color: "hsl(var(--chart-1))",
37-
},
38-
failed: {
39-
label: "Failed",
40-
color: "red",
41-
},
42-
};
4333
export function SponsoredTransactionsChartCard(props: {
44-
userOpStats: UserOpStats[];
34+
userOpStats: UserOpStatsByChain[];
4535
isPending: boolean;
4636
}) {
4737
const { userOpStats } = props;
38+
const topChainsToShow = 10;
39+
const chainsStore = useAllChainsData();
4840

49-
const barChartData: ChartData[] = useMemo(() => {
50-
const chartDataMap: Map<string, ChartData> = new Map();
41+
const { chartConfig, chartData } = useMemo(() => {
42+
const _chartConfig: ChartConfig = {};
43+
const _chartDataMap: Map<string, ChartData> = new Map();
44+
const chainIdToVolumeMap: Map<string, number> = new Map();
45+
// for each stat, add it in _chartDataMap
46+
for (const stat of userOpStats) {
47+
const chartData = _chartDataMap.get(stat.date);
48+
const { chainId } = stat;
49+
const chain = chainsStore.idToChain.get(Number(chainId));
5150

52-
for (const data of userOpStats) {
53-
const chartData = chartDataMap.get(data.date);
51+
// if no data for current day - create new entry
5452
if (!chartData) {
55-
chartDataMap.set(data.date, {
56-
time: format(new Date(data.date), "MMM dd"),
57-
successful: data.successful,
58-
failed: data.failed,
59-
});
53+
_chartDataMap.set(stat.date, {
54+
time: format(new Date(stat.date), "MMM dd"),
55+
[chain?.name || chainId || "Unknown"]:
56+
Math.round(stat.sponsoredUsd * 100) / 100,
57+
} as ChartData);
6058
} else {
61-
chartData.successful += data.successful;
62-
chartData.failed += data.failed;
59+
chartData[chain?.name || chainId || "Unknown"] =
60+
(chartData[chain?.name || chainId || "Unknown"] || 0) +
61+
Math.round(stat.sponsoredUsd * 100) / 100;
6362
}
63+
64+
chainIdToVolumeMap.set(
65+
chain?.name || chainId || "Unknown",
66+
stat.sponsoredUsd + (chainIdToVolumeMap.get(chainId || "Unknown") || 0),
67+
);
6468
}
6569

66-
return Array.from(chartDataMap.values());
67-
}, [userOpStats]);
70+
const chainsSorted = Array.from(chainIdToVolumeMap.entries())
71+
.sort((a, b) => b[1] - a[1])
72+
.map((w) => w[0]);
73+
74+
const chainsToShow = chainsSorted.slice(0, topChainsToShow);
75+
const chainsToTagAsOthers = chainsSorted.slice(topChainsToShow);
76+
77+
// replace chainIdsToTagAsOther chainId with "other"
78+
for (const data of _chartDataMap.values()) {
79+
for (const chainId in data) {
80+
if (chainsToTagAsOthers.includes(chainId)) {
81+
data.others = (data.others || 0) + (data[chainId] || 0);
82+
delete data[chainId];
83+
}
84+
}
85+
}
86+
87+
chainsToShow.forEach((walletType, i) => {
88+
_chartConfig[walletType] = {
89+
label: chainsToShow[i],
90+
color: `hsl(var(--chart-${(i % 10) + 1}))`,
91+
};
92+
});
93+
94+
// Add Other
95+
chainsToShow.push("others");
96+
_chartConfig.others = {
97+
label: "Others",
98+
color: "hsl(var(--muted-foreground))",
99+
};
100+
101+
return {
102+
chartData: Array.from(_chartDataMap.values()),
103+
chartConfig: _chartConfig,
104+
};
105+
}, [userOpStats, chainsStore]);
68106

69-
const disableActions = props.isPending || barChartData.length === 0;
107+
const uniqueChainIds = Object.keys(chartConfig);
108+
const disableActions =
109+
props.isPending ||
110+
chartData.length === 0 ||
111+
chartData.every((data) => data.transactions === 0);
70112

71113
return (
72114
<div className="relative w-full rounded-lg border border-border bg-muted/50 p-4 md:p-6">
@@ -83,10 +125,13 @@ export function SponsoredTransactionsChartCard(props: {
83125
fileName="Sponsored Transactions"
84126
disabled={disableActions}
85127
getData={async () => {
86-
const header = ["Date", "Successful", "Failed"];
87-
const rows = barChartData.map((data) => {
88-
const { time, successful, failed } = data;
89-
return [time, successful.toString(), failed.toString()];
128+
const header = ["Date", ...uniqueChainIds];
129+
const rows = chartData.map((data) => {
130+
const { time, ...rest } = data;
131+
return [
132+
time,
133+
...uniqueChainIds.map((w) => (rest[w] || 0).toString()),
134+
];
90135
});
91136
return { header, rows };
92137
}}
@@ -97,10 +142,8 @@ export function SponsoredTransactionsChartCard(props: {
97142
<ChartContainer config={chartConfig} className="h-[400px] w-full">
98143
{props.isPending ? (
99144
<LoadingChartState />
100-
) : barChartData.length === 0 ||
101-
barChartData.every(
102-
(data) => data.failed === 0 && data.successful === 0,
103-
) ? (
145+
) : chartData.length === 0 ||
146+
chartData.every((data) => data.transactions === 0) ? (
104147
<EmptyChartState>
105148
<div className="flex flex-col items-center justify-center">
106149
<span className="mb-6 text-lg">
@@ -143,7 +186,7 @@ export function SponsoredTransactionsChartCard(props: {
143186
) : (
144187
<BarChart
145188
accessibilityLayer
146-
data={barChartData}
189+
data={chartData}
147190
margin={{
148191
top: 20,
149192
}}
@@ -158,7 +201,12 @@ export function SponsoredTransactionsChartCard(props: {
158201
/>
159202

160203
<YAxis
161-
dataKey={(data) => data.successful + data.failed}
204+
dataKey={(data) => {
205+
return Object.entries(data)
206+
.filter(([key]) => key !== "time")
207+
.map(([, value]) => value)
208+
.reduce((acc, current) => Number(acc) + Number(current), 0);
209+
}}
162210
tickLine={false}
163211
axisLine={false}
164212
tickFormatter={(value) => formatTickerNumber(value)}
@@ -173,12 +221,12 @@ export function SponsoredTransactionsChartCard(props: {
173221
}
174222
/>
175223
<ChartLegend content={<ChartLegendContent />} />
176-
{(["failed", "successful"] as const).map((result) => {
224+
{uniqueChainIds.map((chainId) => {
177225
return (
178226
<Bar
179-
key={result}
180-
dataKey={result}
181-
fill={chartConfig[result].color}
227+
key={chainId}
228+
dataKey={chainId}
229+
fill={chartConfig[chainId]?.color}
182230
radius={4}
183231
stackId="a"
184232
strokeWidth={1.5}

apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ export function TotalSponsoredChartCard(props: {
204204

205205
<YAxis
206206
dataKey={(data) => {
207-
console.log(data);
208207
return Object.entries(data)
209208
.filter(([key]) => key !== "time")
210209
.map(([, value]) => value)

apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/storyUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi";
1+
import type { UserOpStatsByChain } from "@3rdweb-sdk/react/hooks/useApi";
22

3-
export function createUserOpStatsStub(days: number): UserOpStats[] {
4-
const stubbedData: UserOpStats[] = [];
3+
export function createUserOpStatsStub(days: number): UserOpStatsByChain[] {
4+
const stubbedData: UserOpStatsByChain[] = [];
55

66
let d = days;
77
while (d !== 0) {
@@ -13,6 +13,7 @@ export function createUserOpStatsStub(days: number): UserOpStats[] {
1313
successful,
1414
failed,
1515
sponsoredUsd,
16+
chainId: Math.floor(Math.random() * 100).toString(),
1617
});
1718

1819
if (Math.random() > 0.7) {

0 commit comments

Comments
 (0)