Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "1.1.7",
"@sentry/nextjs": "8.51.0",
"@shazow/whatsabi": "^0.19.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ interface Collection {
telegram_url: string | null;
marketplace_pages: MarketplacePage[];
floor_prices: FloorPrice[];
top_bids: any[];
top_bids: unknown[];
distinct_owner_count: number;
distinct_nft_count: number;
total_quantity: number;
Expand Down Expand Up @@ -148,7 +148,7 @@ interface NFT {
queried_wallet_balances: Owner[];
}

export interface SimpleHashResponse {
interface SimpleHashResponse {
next_cursor: null | string;
next: null | string;
previous: null | string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "@/constants/env";

interface Transaction {
chain_id: number;
hash: string;
nonce: number;
block_hash: string;
block_number: number;
block_timestamp: number;
transaction_index: number;
from_address: string;
to_address: string | null;
value: number;
gas: number;
gas_price: number | null;
data: string | null;
function_selector: string | null;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
transaction_type: number | null;
r: string | null;
s: string | null;
v: number | null;
access_list_json: string | null;
contract_address: string | null;
gas_used: number | null;
cumulative_gas_used: number | null;
effective_gas_price: number | null;
blob_gas_used: number | null;
blob_gas_price: number | null;
logs_bloom: string | null;
status: boolean | null; // true for success, false for failure
}

interface InsightsResponse {
meta: {
address: string;
signature: string;
page: number;
total_items: number;
total_pages: number;
limit_per_chain: number;
chain_ids: number[];
};
data: Transaction[];
}

export async function fetchTxActivity(args: {
chainId: number;
address: string;
limit_per_type?: number;
page?: number;
}): Promise<Transaction[]> {
let { chainId, address, limit_per_type, page } = args;
if (!limit_per_type) limit_per_type = 100;
if (!page) page = 0;

const outgoingTxsResponse = await fetch(
`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`,
{
headers: {
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
},
},
);

const incomingTxsResponse = await fetch(
`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`,
{
headers: {
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
},
},
);

if (!outgoingTxsResponse.ok || !incomingTxsResponse.ok) {
throw new Error("Failed to fetch transaction history");
}

const outgoingTxsData: InsightsResponse = await outgoingTxsResponse.json();
const incomingTxsData: InsightsResponse = await incomingTxsResponse.json();

return [...outgoingTxsData.data, ...incomingTxsData.data].sort(
(a, b) => b.block_number - a.block_number,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import {
TableRow,
} from "@/components/ui/table";
import { TabButtons } from "@/components/ui/tabs";
import {} from "@radix-ui/react-tabs";
import { useState } from "react";

interface Transaction {
id: number;
type: string;
id: string;
type: "out" | "in";
amount: string;
to?: string;
from?: string;
Expand Down Expand Up @@ -43,6 +42,17 @@ export function ActivityOverview({
const [activeTab, setActiveTab] = useState<"transactions" | "contracts">(
"transactions",
);
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;

// Calculate the index of the last transaction on the current page
const lastIndex = currentPage * itemsPerPage;
// Calculate the index of the first transaction on the current page
const firstIndex = lastIndex - itemsPerPage;
// Get the current transactions to display
const currentTransactions = transactions.slice(firstIndex, lastIndex);
// Calculate total pages
const totalPages = Math.ceil(transactions.length / itemsPerPage);

return (
<Card>
Expand Down Expand Up @@ -71,31 +81,62 @@ export function ActivityOverview({
{isLoading ? (
<Spinner />
) : activeTab === "transactions" ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Details</TableHead>
<TableHead>Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.map((tx) => (
<TableRow key={tx.id}>
<TableCell>{tx.type}</TableCell>
<TableCell>{tx.amount}</TableCell>
<TableCell>
{tx.to && `To: ${tx.to}`}
{tx.from && `From: ${tx.from}`}
{tx.contract && `Contract: ${tx.contract}`}
{tx.method && ` Method: ${tx.method}`}
</TableCell>
<TableCell>{tx.date}</TableCell>
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Details</TableHead>
<TableHead>Date</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{currentTransactions.map((tx) => (
<TableRow key={tx.id}>
<TableCell>{tx.type}</TableCell>
<TableCell>{tx.amount}</TableCell>
<TableCell>
{tx.to && `To: ${tx.to} `}
{tx.from && `From: ${tx.from} `}
{tx.contract && `Contract: ${tx.contract} `}
{tx.method && ` Method: ${tx.method}`}
</TableCell>
<TableCell>{tx.date}</TableCell>
</TableRow>
))}
</TableBody>
</Table>

{/* Pagination Controls */}
<div className="pagination">
<TabButtons
tabs={[
{
name: "Previous",
isActive: currentPage === 1,
isEnabled: currentPage > 1,
onClick: () =>
setCurrentPage((prev) => Math.max(prev - 1, 1)),
},
{
name: `Page ${currentPage} of ${totalPages}`,
isActive: true,
isEnabled: false,
onClick: () => {}, // No action needed
},
{
name: "Next",
isActive: currentPage === totalPages,
isEnabled: currentPage < totalPages,
onClick: () =>
setCurrentPage((prev) => Math.min(prev + 1, totalPages)),
},
]}
tabClassName="font-medium !text-sm"
/>
</div>
</>
) : activeTab === "contracts" ? (
<Table>
<TableHeader>
Expand All @@ -107,7 +148,7 @@ export function ActivityOverview({
</TableHeader>
<TableBody>
{contracts.map((contract, index) => (
<TableRow key={index}>
<TableRow key={`${contract.address}-${index}`}>
<TableCell>{contract.name}</TableCell>
<TableCell>{contract.address}</TableCell>
<TableCell>{contract.lastInteraction}</TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
TableRow,
} from "@/components/ui/table";
import { TabButtons } from "@/components/ui/tabs";
import {} from "@radix-ui/react-tabs";
import { useState } from "react";
import { toTokens } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
Expand Down Expand Up @@ -72,7 +71,7 @@ export function TokenHoldings({
<Spinner />
) : (
tokens.map((token, idx) => (
<TableRow key={idx}>
<TableRow key={`${token.name}-${idx}`}>
<TableCell>
{token.symbol} ({token.name})
</TableCell>
Expand All @@ -90,7 +89,11 @@ export function TokenHoldings({
) : activeTab === "nft" ? (
<div className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
{nfts.map((nft, idx) => (
<NFTCard key={idx} nft={nft} chain={chain} />
<NFTCard
key={`${nft.contractAddress}-${idx}`}
nft={nft}
chain={chain}
/>
))}
</div>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ChainMetadata } from "thirdweb/chains";
import { useBalance } from "../hooks/getBalance";
import { useGetERC20Tokens } from "../hooks/useGetERC20Tokens";
import { useGetNFTs } from "../hooks/useGetNFTs";
import { useGetTxActivity } from "../hooks/useGetTxActivity";
import { mockWalletData } from "../utils/mockData";
import { ActivityOverview } from "./ActivityOverview";
import { BalanceOverview } from "./BalanceOverview";
Expand All @@ -23,19 +24,24 @@ export function WalletDashboard(props: {
const {
tokens,
isLoading: isLoadingERC20,
error: errorERC20,
// error: errorERC20,
} = useGetERC20Tokens(props.chain.chainId, props.address);
if (errorERC20) {
console.error("Error fetching ERC20 tokens:", errorERC20);
}
// if (errorERC20) {
// console.error("Error fetching ERC20 tokens:", errorERC20);
// }
const {
nfts,
isLoading: isLoadingNFTs,
error: errorNFTs,
// error: errorNFTs,
} = useGetNFTs(props.chain.chainId, props.address);
if (errorNFTs) {
console.error("Error fetching NFTs:", errorNFTs);
}
// if (errorNFTs) {
// console.error("Error fetching NFTs:", errorNFTs);
// }

const { txActivity, isLoading: isLoadingActivity } = useGetTxActivity(
props.chain.chainId,
props.address,
);

return (
<div className="grid gap-6">
Expand All @@ -45,7 +51,8 @@ export function WalletDashboard(props: {
isLoading={isLoadingBalance}
/>
<ActivityOverview
transactions={mockWalletData.transactions}
transactions={txActivity}
isLoading={isLoadingActivity}
contracts={mockWalletData.contracts}
/>
<TokenHoldings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {

Check failure on line 16 in apps/dashboard/src/app/(dashboard)/hackweek/[chain_id]/[address]/hooks/getBalance.ts

View workflow job for this annotation

GitHub Actions / Lint Packages

Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery"
async function fetchBalance() {
try {
setIsLoading(true);
setError(null);
const client = createThirdwebClient({
clientId: process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID!,
clientId: process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
});
const rpcRequest = getRpcClient({
client,
chain: defineChain(chainId),

Check failure on line 26 in apps/dashboard/src/app/(dashboard)/hackweek/[chain_id]/[address]/hooks/getBalance.ts

View workflow job for this annotation

GitHub Actions / Lint Packages

Use useV5DashboardChain instead if you are using it inside a component
});
const result = await eth_getBalance(rpcRequest, { address });
setBalance(result);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useEffect, useState } from "react";
import { fetchTxActivity } from "../actions/fetchTxActivity";

interface TxActivityItem {
id: string;
// all txs we retrieve for now are outgoing
// TODO: add incoming
type: "out" | "in";
amount: string;
to?: string;
from?: string;
method?: string;
date: string;
}

export function useGetTxActivity(chainId: number, address: string) {
const [txActivity, setTxActivity] = useState<TxActivityItem[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {

Check failure on line 20 in apps/dashboard/src/app/(dashboard)/hackweek/[chain_id]/[address]/hooks/useGetTxActivity.ts

View workflow job for this annotation

GitHub Actions / Lint Packages

Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery"
(async () => {
const response = await fetchTxActivity({ chainId, address });
const activity = response.map((tx): TxActivityItem => {
const type =
tx.to_address?.toLowerCase() === address.toLowerCase() ? "in" : "out";
return {
id: tx.hash,
type,
amount: `${tx.value / 10 ** 18} ETH`,
to: tx.to_address || undefined,
from: tx.from_address,
method: tx.function_selector || undefined,
date: new Date(tx.block_timestamp * 1000).toLocaleString(),
};
});
setTxActivity(activity);
setIsLoading(false);
})();
Comment on lines +18 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace useEffect-based data fetching with useQuery from React Query. The implementation should look like: useQuery(['txActivity', chainId, address], () => fetchTxActivity({chainId, address}), { select: (data) => data.map(tx => ({ ... })) })

Spotted by Graphite Reviewer (based on CI logs)

Is this helpful? React 👍 or 👎 to let us know.

}, [address, chainId]);

return { txActivity, isLoading };
}
Loading
Loading