Skip to content

Commit dca48c9

Browse files
authored
wallet txns activity (#6090)
Adds a table with `in/out` txns for a wallet <!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on updating dependencies, enhancing the handling of transactions, and improving the UI components related to token holdings and activity overview in the dashboard. ### Detailed summary - Removed `@radix-ui/react-tabs` from `package.json`. - Updated `clientId` handling in `getBalance.ts`. - Changed `top_bids` type from `any[]` to `unknown[]` in `fetchNFTs.ts`. - Modified `SimpleHashResponse` interface in `fetchNFTs.ts`. - Updated `TokenHoldings` component to use unique keys for `TableRow`. - Enhanced NFT mapping in `TokenHoldings` with unique keys. - Added `useGetTxActivity` hook for fetching transaction activities. - Integrated transaction activity in `WalletDashboard`. - Updated `ActivityOverview` to include pagination for transactions. - Modified transaction structure in `fetchTxActivity.ts`. - Improved UI structure and key handling in `ActivityOverview`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 0b4045b commit dca48c9

File tree

9 files changed

+232
-73
lines changed

9 files changed

+232
-73
lines changed

apps/dashboard/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
"@radix-ui/react-separator": "^1.1.1",
4646
"@radix-ui/react-slot": "^1.1.1",
4747
"@radix-ui/react-switch": "^1.1.2",
48-
"@radix-ui/react-tabs": "^1.1.2",
4948
"@radix-ui/react-tooltip": "1.1.7",
5049
"@sentry/nextjs": "8.51.0",
5150
"@shazow/whatsabi": "^0.19.0",

apps/dashboard/src/app/(dashboard)/hackweek/[chain_id]/[address]/actions/fetchNFTs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ interface Collection {
8484
telegram_url: string | null;
8585
marketplace_pages: MarketplacePage[];
8686
floor_prices: FloorPrice[];
87-
top_bids: any[];
87+
top_bids: unknown[];
8888
distinct_owner_count: number;
8989
distinct_nft_count: number;
9090
total_quantity: number;
@@ -148,7 +148,7 @@ interface NFT {
148148
queried_wallet_balances: Owner[];
149149
}
150150

151-
export interface SimpleHashResponse {
151+
interface SimpleHashResponse {
152152
next_cursor: null | string;
153153
next: null | string;
154154
previous: null | string;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "@/constants/env";
2+
3+
interface Transaction {
4+
chain_id: number;
5+
hash: string;
6+
nonce: number;
7+
block_hash: string;
8+
block_number: number;
9+
block_timestamp: number;
10+
transaction_index: number;
11+
from_address: string;
12+
to_address: string | null;
13+
value: number;
14+
gas: number;
15+
gas_price: number | null;
16+
data: string | null;
17+
function_selector: string | null;
18+
max_fee_per_gas: number | null;
19+
max_priority_fee_per_gas: number | null;
20+
transaction_type: number | null;
21+
r: string | null;
22+
s: string | null;
23+
v: number | null;
24+
access_list_json: string | null;
25+
contract_address: string | null;
26+
gas_used: number | null;
27+
cumulative_gas_used: number | null;
28+
effective_gas_price: number | null;
29+
blob_gas_used: number | null;
30+
blob_gas_price: number | null;
31+
logs_bloom: string | null;
32+
status: boolean | null; // true for success, false for failure
33+
}
34+
35+
interface InsightsResponse {
36+
meta: {
37+
address: string;
38+
signature: string;
39+
page: number;
40+
total_items: number;
41+
total_pages: number;
42+
limit_per_chain: number;
43+
chain_ids: number[];
44+
};
45+
data: Transaction[];
46+
}
47+
48+
export async function fetchTxActivity(args: {
49+
chainId: number;
50+
address: string;
51+
limit_per_type?: number;
52+
page?: number;
53+
}): Promise<Transaction[]> {
54+
let { chainId, address, limit_per_type, page } = args;
55+
if (!limit_per_type) limit_per_type = 100;
56+
if (!page) page = 0;
57+
58+
const outgoingTxsResponse = await fetch(
59+
`https://insight.thirdweb-dev.com/v1/transactions?chain=${chainId}&filter_from_address=${address}&page=${page}&limit=${limit_per_type}&sort_by=block_number&sort_order=desc`,
60+
{
61+
headers: {
62+
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
63+
},
64+
},
65+
);
66+
67+
const incomingTxsResponse = await fetch(
68+
`https://insight.thirdweb-dev.com/v1/transactions?chain=${chainId}&filter_to_address=${address}&page=${page}&limit=${limit_per_type}&sort_by=block_number&sort_order=desc`,
69+
{
70+
headers: {
71+
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
72+
},
73+
},
74+
);
75+
76+
if (!outgoingTxsResponse.ok || !incomingTxsResponse.ok) {
77+
throw new Error("Failed to fetch transaction history");
78+
}
79+
80+
const outgoingTxsData: InsightsResponse = await outgoingTxsResponse.json();
81+
const incomingTxsData: InsightsResponse = await incomingTxsResponse.json();
82+
83+
return [...outgoingTxsData.data, ...incomingTxsData.data].sort(
84+
(a, b) => b.block_number - a.block_number,
85+
);
86+
}

apps/dashboard/src/app/(dashboard)/hackweek/[chain_id]/[address]/components/ActivityOverview.tsx

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import {
99
TableRow,
1010
} from "@/components/ui/table";
1111
import { TabButtons } from "@/components/ui/tabs";
12-
import {} from "@radix-ui/react-tabs";
1312
import { useState } from "react";
1413

1514
interface Transaction {
16-
id: number;
17-
type: string;
15+
id: string;
16+
type: "out" | "in";
1817
amount: string;
1918
to?: string;
2019
from?: string;
@@ -43,6 +42,17 @@ export function ActivityOverview({
4342
const [activeTab, setActiveTab] = useState<"transactions" | "contracts">(
4443
"transactions",
4544
);
45+
const [currentPage, setCurrentPage] = useState(1);
46+
const itemsPerPage = 5;
47+
48+
// Calculate the index of the last transaction on the current page
49+
const lastIndex = currentPage * itemsPerPage;
50+
// Calculate the index of the first transaction on the current page
51+
const firstIndex = lastIndex - itemsPerPage;
52+
// Get the current transactions to display
53+
const currentTransactions = transactions.slice(firstIndex, lastIndex);
54+
// Calculate total pages
55+
const totalPages = Math.ceil(transactions.length / itemsPerPage);
4656

4757
return (
4858
<Card>
@@ -71,31 +81,62 @@ export function ActivityOverview({
7181
{isLoading ? (
7282
<Spinner />
7383
) : activeTab === "transactions" ? (
74-
<Table>
75-
<TableHeader>
76-
<TableRow>
77-
<TableHead>Type</TableHead>
78-
<TableHead>Amount</TableHead>
79-
<TableHead>Details</TableHead>
80-
<TableHead>Date</TableHead>
81-
</TableRow>
82-
</TableHeader>
83-
<TableBody>
84-
{transactions.map((tx) => (
85-
<TableRow key={tx.id}>
86-
<TableCell>{tx.type}</TableCell>
87-
<TableCell>{tx.amount}</TableCell>
88-
<TableCell>
89-
{tx.to && `To: ${tx.to}`}
90-
{tx.from && `From: ${tx.from}`}
91-
{tx.contract && `Contract: ${tx.contract}`}
92-
{tx.method && ` Method: ${tx.method}`}
93-
</TableCell>
94-
<TableCell>{tx.date}</TableCell>
84+
<>
85+
<Table>
86+
<TableHeader>
87+
<TableRow>
88+
<TableHead>Type</TableHead>
89+
<TableHead>Amount</TableHead>
90+
<TableHead>Details</TableHead>
91+
<TableHead>Date</TableHead>
9592
</TableRow>
96-
))}
97-
</TableBody>
98-
</Table>
93+
</TableHeader>
94+
<TableBody>
95+
{currentTransactions.map((tx) => (
96+
<TableRow key={tx.id}>
97+
<TableCell>{tx.type}</TableCell>
98+
<TableCell>{tx.amount}</TableCell>
99+
<TableCell>
100+
{tx.to && `To: ${tx.to} `}
101+
{tx.from && `From: ${tx.from} `}
102+
{tx.contract && `Contract: ${tx.contract} `}
103+
{tx.method && ` Method: ${tx.method}`}
104+
</TableCell>
105+
<TableCell>{tx.date}</TableCell>
106+
</TableRow>
107+
))}
108+
</TableBody>
109+
</Table>
110+
111+
{/* Pagination Controls */}
112+
<div className="pagination">
113+
<TabButtons
114+
tabs={[
115+
{
116+
name: "Previous",
117+
isActive: currentPage === 1,
118+
isEnabled: currentPage > 1,
119+
onClick: () =>
120+
setCurrentPage((prev) => Math.max(prev - 1, 1)),
121+
},
122+
{
123+
name: `Page ${currentPage} of ${totalPages}`,
124+
isActive: true,
125+
isEnabled: false,
126+
onClick: () => {}, // No action needed
127+
},
128+
{
129+
name: "Next",
130+
isActive: currentPage === totalPages,
131+
isEnabled: currentPage < totalPages,
132+
onClick: () =>
133+
setCurrentPage((prev) => Math.min(prev + 1, totalPages)),
134+
},
135+
]}
136+
tabClassName="font-medium !text-sm"
137+
/>
138+
</div>
139+
</>
99140
) : activeTab === "contracts" ? (
100141
<Table>
101142
<TableHeader>
@@ -107,7 +148,7 @@ export function ActivityOverview({
107148
</TableHeader>
108149
<TableBody>
109150
{contracts.map((contract, index) => (
110-
<TableRow key={index}>
151+
<TableRow key={`${contract.address}-${index}`}>
111152
<TableCell>{contract.name}</TableCell>
112153
<TableCell>{contract.address}</TableCell>
113154
<TableCell>{contract.lastInteraction}</TableCell>

apps/dashboard/src/app/(dashboard)/hackweek/[chain_id]/[address]/components/TokenHoldings.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
TableRow,
1010
} from "@/components/ui/table";
1111
import { TabButtons } from "@/components/ui/tabs";
12-
import {} from "@radix-ui/react-tabs";
1312
import { useState } from "react";
1413
import { toTokens } from "thirdweb";
1514
import type { ChainMetadata } from "thirdweb/chains";
@@ -72,7 +71,7 @@ export function TokenHoldings({
7271
<Spinner />
7372
) : (
7473
tokens.map((token, idx) => (
75-
<TableRow key={idx}>
74+
<TableRow key={`${token.name}-${idx}`}>
7675
<TableCell>
7776
{token.symbol} ({token.name})
7877
</TableCell>
@@ -90,7 +89,11 @@ export function TokenHoldings({
9089
) : activeTab === "nft" ? (
9190
<div className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
9291
{nfts.map((nft, idx) => (
93-
<NFTCard key={idx} nft={nft} chain={chain} />
92+
<NFTCard
93+
key={`${nft.contractAddress}-${idx}`}
94+
nft={nft}
95+
chain={chain}
96+
/>
9497
))}
9598
</div>
9699
) : null}

apps/dashboard/src/app/(dashboard)/hackweek/[chain_id]/[address]/components/WalletDashboard.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ChainMetadata } from "thirdweb/chains";
33
import { useBalance } from "../hooks/getBalance";
44
import { useGetERC20Tokens } from "../hooks/useGetERC20Tokens";
55
import { useGetNFTs } from "../hooks/useGetNFTs";
6+
import { useGetTxActivity } from "../hooks/useGetTxActivity";
67
import { mockWalletData } from "../utils/mockData";
78
import { ActivityOverview } from "./ActivityOverview";
89
import { BalanceOverview } from "./BalanceOverview";
@@ -23,19 +24,24 @@ export function WalletDashboard(props: {
2324
const {
2425
tokens,
2526
isLoading: isLoadingERC20,
26-
error: errorERC20,
27+
// error: errorERC20,
2728
} = useGetERC20Tokens(props.chain.chainId, props.address);
28-
if (errorERC20) {
29-
console.error("Error fetching ERC20 tokens:", errorERC20);
30-
}
29+
// if (errorERC20) {
30+
// console.error("Error fetching ERC20 tokens:", errorERC20);
31+
// }
3132
const {
3233
nfts,
3334
isLoading: isLoadingNFTs,
34-
error: errorNFTs,
35+
// error: errorNFTs,
3536
} = useGetNFTs(props.chain.chainId, props.address);
36-
if (errorNFTs) {
37-
console.error("Error fetching NFTs:", errorNFTs);
38-
}
37+
// if (errorNFTs) {
38+
// console.error("Error fetching NFTs:", errorNFTs);
39+
// }
40+
41+
const { txActivity, isLoading: isLoadingActivity } = useGetTxActivity(
42+
props.chain.chainId,
43+
props.address,
44+
);
3945

4046
return (
4147
<div className="grid gap-6">
@@ -45,7 +51,8 @@ export function WalletDashboard(props: {
4551
isLoading={isLoadingBalance}
4652
/>
4753
<ActivityOverview
48-
transactions={mockWalletData.transactions}
54+
transactions={txActivity}
55+
isLoading={isLoadingActivity}
4956
contracts={mockWalletData.contracts}
5057
/>
5158
<TokenHoldings

apps/dashboard/src/app/(dashboard)/hackweek/[chain_id]/[address]/hooks/getBalance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function useBalance(chainId: number, address: string): UseBalanceResult {
1919
setIsLoading(true);
2020
setError(null);
2121
const client = createThirdwebClient({
22-
clientId: process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID!,
22+
clientId: process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
2323
});
2424
const rpcRequest = getRpcClient({
2525
client,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useEffect, useState } from "react";
2+
import { fetchTxActivity } from "../actions/fetchTxActivity";
3+
4+
interface TxActivityItem {
5+
id: string;
6+
// all txs we retrieve for now are outgoing
7+
// TODO: add incoming
8+
type: "out" | "in";
9+
amount: string;
10+
to?: string;
11+
from?: string;
12+
method?: string;
13+
date: string;
14+
}
15+
16+
export function useGetTxActivity(chainId: number, address: string) {
17+
const [txActivity, setTxActivity] = useState<TxActivityItem[]>([]);
18+
const [isLoading, setIsLoading] = useState(true);
19+
20+
useEffect(() => {
21+
(async () => {
22+
const response = await fetchTxActivity({ chainId, address });
23+
const activity = response.map((tx): TxActivityItem => {
24+
const type =
25+
tx.to_address?.toLowerCase() === address.toLowerCase() ? "in" : "out";
26+
return {
27+
id: tx.hash,
28+
type,
29+
amount: `${tx.value / 10 ** 18} ETH`,
30+
to: tx.to_address || undefined,
31+
from: tx.from_address,
32+
method: tx.function_selector || undefined,
33+
date: new Date(tx.block_timestamp * 1000).toLocaleString(),
34+
};
35+
});
36+
setTxActivity(activity);
37+
setIsLoading(false);
38+
})();
39+
}, [address, chainId]);
40+
41+
return { txActivity, isLoading };
42+
}

0 commit comments

Comments
 (0)