From 0d452961187497325c79082dc4bdcb6c7881f6de Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Fri, 14 Feb 2025 10:11:28 +0700 Subject: [PATCH 1/2] chore: import geist txn table --- apps/web/package.json | 5 +- .../components/geist/transaction-table.tsx | 169 ++++++++++++++++++ apps/web/src/components/ui/data-table.tsx | 148 +++++++++++++++ .../lib/domain/chain/chain-resolver.test.ts | 32 ++++ .../src/lib/domain/chain/chain-resolver.ts | 77 ++++++++ .../src/lib/domain/transaction/transaction.ts | 14 ++ apps/web/src/lib/explorer/url.test.ts | 116 ++++++++++++ apps/web/src/lib/explorer/url.ts | 110 ++++++++++++ apps/web/src/lib/utils/address.test.ts | 10 ++ apps/web/src/lib/utils/address.ts | 22 +++ apps/web/src/lib/utils/hex.ts | 20 +++ apps/web/src/lib/utils/wagmi-config.tsx | 17 ++ pnpm-lock.yaml | 20 +++ 13 files changed, 758 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/geist/transaction-table.tsx create mode 100644 apps/web/src/components/ui/data-table.tsx create mode 100644 apps/web/src/lib/domain/chain/chain-resolver.test.ts create mode 100644 apps/web/src/lib/domain/chain/chain-resolver.ts create mode 100644 apps/web/src/lib/domain/transaction/transaction.ts create mode 100644 apps/web/src/lib/explorer/url.test.ts create mode 100644 apps/web/src/lib/explorer/url.ts create mode 100644 apps/web/src/lib/utils/address.test.ts create mode 100644 apps/web/src/lib/utils/address.ts create mode 100644 apps/web/src/lib/utils/hex.ts create mode 100644 apps/web/src/lib/utils/wagmi-config.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 68601af..3721603 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,6 +28,8 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-tooltip": "^1.1.8", + "@repo/nillion": "workspace:*", + "@tanstack/react-table": "^8.21.2", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "alchemy-sdk": "^3.5.2", @@ -52,8 +54,7 @@ "viem": "^2.22.22", "wagmi": "^2.14.11", "zod": "^3.24.1", - "zod-to-json-schema": "^3.24.1", - "@repo/nillion": "workspace:*" + "zod-to-json-schema": "^3.24.1" }, "devDependencies": { "@types/react-grid-layout": "^1.3.5", diff --git a/apps/web/src/components/geist/transaction-table.tsx b/apps/web/src/components/geist/transaction-table.tsx new file mode 100644 index 0000000..2d29736 --- /dev/null +++ b/apps/web/src/components/geist/transaction-table.tsx @@ -0,0 +1,169 @@ +import { resolveChainById } from "../../lib/domain/chain/chain-resolver"; +import type { TransactionMeta } from "../../lib/domain/transaction/transaction"; +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, ExternalLink } from "lucide-react"; +import { formatEther } from "viem"; +import { DataTable } from "../../components/ui/data-table"; +import { Badge } from "../../components/ui/badge"; +import { + Explorer, + ExplorerEntity, + blockExplorerUrlFactory, +} from "../../lib/explorer/url"; +import { getShortAddress } from "../../lib/utils/address"; +import { getShortHex } from "../../lib/utils/hex"; + +export const txnTypeMap: Record = { + contract_call: "Contract Call", + native_transfer: "Native Token Transfer", + token_transfer: "Token Transfer", + coin_transfer: "Token Transfer", +}; + +export interface TransactionTableProps { + transactions: TransactionMeta[]; + chainId?: number; + explorer?: Explorer; + itemsPerPage?: number; + withPagination?: boolean; + withSorting?: boolean; +} + +const getTxnTableCols = ({ + chainId, + explorer, +}: { + chainId: number; + explorer: Explorer; +}): ColumnDef[] => { + const chain = resolveChainById(chainId); + const createTxnUrl = blockExplorerUrlFactory({ + chain, + config: { + name: explorer, + }, + }); + + return [ + { + accessorKey: "hash", + header: "Txn Hash", + cell: ({ row }) => { + const txnHash = row.getValue("hash") as `0x${string}`; + const txnUrl = createTxnUrl({ + chain, + entity: ExplorerEntity.Transaction, + params: { + txnHash, + }, + }); + return ( + + {txnHash && getShortHex(txnHash)} + + + ); + }, + }, + { + accessorKey: "from", + header: "From", + cell: ({ row }) => { + const address = row.getValue("from") as `0x${string}`; + return <>{getShortAddress(address)}; + }, + }, + { + accessorKey: "to", + header: "To", + cell: ({ row }) => { + const address = row.getValue("to") as `0x${string}`; + return <>{getShortAddress(address)}; + }, + }, + { + accessorKey: "displayedTxType", + header: "Txn Type", + cell: ({ row }) => { + const txnType = row.getValue("displayedTxType"); + return {txnTypeMap[txnType]}; + }, + }, + { + accessorKey: "value", + header: ({ column }) => ( +
column.toggleSorting(column.getIsSorted() === "asc")} + className="cursor-pointer hover:text-black flex items-center gap-2" + > + Value (ETH) + +
+ ), + cell: ({ row }) => { + const rawValue = row.getValue("value"); + return <>{formatEther(rawValue)} ETH; + }, + }, + { + accessorKey: "gas", + header: ({ column }) => ( +
column.toggleSorting(column.getIsSorted() === "asc")} + className="cursor-pointer hover:text-black flex items-center gap-2" + > + Gas Used (ETH) + +
+ ), + cell: ({ row }) => { + const rawValue = row.getValue("gas"); + return <>{formatEther(rawValue)} ETH; + }, + }, + { + accessorKey: "blockNumber", + header: ({ column }) => ( +
column.toggleSorting(column.getIsSorted() === "asc")} + className="cursor-pointer hover:text-black flex items-center gap-2" + > + Block Number + +
+ ), + cell: ({ row }) => { + const rawValue = row.getValue("blockNumber"); + return <>{Number(rawValue)}; + }, + }, + ] as ColumnDef[]; +}; + +export const TransactionTable = ({ + transactions, + + // defaults to ETH mainnet + chainId = 1, + explorer = Explorer.Blockscout, +}: TransactionTableProps) => { + console.log("printing hookx", { chainId, explorer }); + + return ( +
+ +
+ ); +}; diff --git a/apps/web/src/components/ui/data-table.tsx b/apps/web/src/components/ui/data-table.tsx new file mode 100644 index 0000000..215b4fc --- /dev/null +++ b/apps/web/src/components/ui/data-table.tsx @@ -0,0 +1,148 @@ +import { + type ColumnDef, + type ExpandedState, + type SortingState, + flexRender, + getCoreRowModel, + getExpandedRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { useState } from "react"; +import { Button } from "./button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "./table"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + tableConfig: any; +} + +export const DataTable = ({ + columns, + data, + tableConfig, +}: DataTableProps) => { + const [sorting, setSorting] = useState([]); + const [expanded, setExpanded] = useState(true); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + onExpandedChange: setExpanded, + getSortedRowModel: getSortedRowModel(), + getExpandedRowModel: getExpandedRowModel(), + state: { + sorting, + expanded, + }, + ...tableConfig, + }); + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {/* separated row easier to style */} + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <> + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + {/* {row.getIsExpanded() && ( + + + {row.subRows.map((subRow) => ( + + {subRow.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + ))} + + + )} */} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+ + +
+
+ ); +}; diff --git a/apps/web/src/lib/domain/chain/chain-resolver.test.ts b/apps/web/src/lib/domain/chain/chain-resolver.test.ts new file mode 100644 index 0000000..a4fabfe --- /dev/null +++ b/apps/web/src/lib/domain/chain/chain-resolver.test.ts @@ -0,0 +1,32 @@ +import { + base, + baseGoerli, + baseSepolia, + optimism, + optimismSepolia, +} from "viem/chains"; +import { describe, expect, test } from "vitest"; +import { + resolveChainById, + resolveChainWithName, + resolveProductionChain, +} from "./chain-resolver"; + +describe("ChainResolver", () => { + test("should resolve production chain", () => { + expect(resolveProductionChain(optimismSepolia)).toEqual(optimism); + expect(resolveProductionChain(baseGoerli)).toEqual(base); + }); + test("should resolve chain with name", () => { + expect(resolveChainWithName("optimism")!.name).toEqual(optimism.name); + expect( + resolveChainWithName("optimismsepolia", [optimismSepolia, baseSepolia])! + .name, + ).toEqual(optimismSepolia.name); + expect(resolveChainWithName("base")).toEqual(base); + }); + + test("should resolve chain with id", () => { + expect(resolveChainById(1).name).toBe("Ethereum"); + }); +}); diff --git a/apps/web/src/lib/domain/chain/chain-resolver.ts b/apps/web/src/lib/domain/chain/chain-resolver.ts new file mode 100644 index 0000000..35cdd2a --- /dev/null +++ b/apps/web/src/lib/domain/chain/chain-resolver.ts @@ -0,0 +1,77 @@ +import { type Chain, extractChain } from "viem"; +import { + arbitrum, + arbitrumGoerli, + arbitrumSepolia, + base, + baseGoerli, + baseSepolia, + mainnet, + optimism, + optimismGoerli, + optimismSepolia, + zora, +} from "viem/chains"; +import * as chains from "viem/chains"; +// importing chains has bundle size impact +// https://viem.sh/docs/utilities/extractChain.html + +export const DEFAULT_SUPPORTED_CHAINS = [mainnet, base, optimism, zora]; +// TODO build time over run-time +// no standard in chain naming, delimiter can be space, hyphen or none +const CHAIN_ID_BY_NAME = Object.fromEntries( + Object.values(chains) + .map((chain) => [chain.name.toLowerCase().replace(/ /, ""), chain.id]) + .concat([ + // alias + // opmainnet + ["optimism", optimism.id], + // arbitrumone + ["arbitrum", arbitrum.id], + // OP Sepolia + ["optimismsepolia", optimismSepolia.id], + ]), +); + +export const resolveChainById = (chainId: number) => { + return Object.fromEntries( + Object.values(chains).map((chain) => [chain.id, chain]), + )[chainId]; +}; + +/** + * Non reliable and best effort basis + */ +export const resolveChainWithName = ( + name: string, + chains: Chain[] = DEFAULT_SUPPORTED_CHAINS, +): Chain | null => { + const chainId = CHAIN_ID_BY_NAME[name]; + + return extractChain({ + chains, + id: chainId, + }); +}; + +/** + * Many scenarios we want to find production chain of a testnet chain + * e.g. to find logo to use + * https://wagmi.sh/react/api/chains + * + * We refers to "ProductionChain" as mainnet refers to Ethereum mainnet + * + * That is not deducible from chain definition https://github.com/wevm/viem/blob/main/src/chains/definitions/optimismSepolia.ts + */ +export const resolveProductionChain = (chain: Chain) => { + return ( + { + [optimismSepolia.id]: optimism, + [optimismGoerli.id]: optimism, + [baseSepolia.id]: base, + [baseGoerli.id]: base, + [arbitrumGoerli.id]: arbitrum, + [arbitrumSepolia.id]: arbitrum, + }[chain.id] || chain + ); +}; diff --git a/apps/web/src/lib/domain/transaction/transaction.ts b/apps/web/src/lib/domain/transaction/transaction.ts new file mode 100644 index 0000000..91e4d04 --- /dev/null +++ b/apps/web/src/lib/domain/transaction/transaction.ts @@ -0,0 +1,14 @@ +import type { Transaction } from "viem"; +import type { Token } from "#token/token"; + +// ignore other meta such as supply / volume for now +export type TokenTransfer = Token & { + amount: bigint; +}; + +export type TransactionMeta = Partial & { + displayedTxType: string; + isSuccess: boolean; + value: bigint; + tokenTransfers: TokenTransfer[]; +}; diff --git a/apps/web/src/lib/explorer/url.test.ts b/apps/web/src/lib/explorer/url.test.ts new file mode 100644 index 0000000..231c719 --- /dev/null +++ b/apps/web/src/lib/explorer/url.test.ts @@ -0,0 +1,116 @@ +import { BY_USER, TRANSACTION } from "@geist/domain/user.fixture"; +import { filecoin, filecoinCalibration, mainnet, sepolia } from "viem/chains"; +import { describe, expect, test } from "vitest"; +import { createOverrideStrategies as createBlockscoutOverrideStrategies } from "#lib/blockscout/url"; +import { + Explorer, + ExplorerEntity, + blockExplorerUrlFactory, +} from "#lib/explorer/url"; +import { createOverrideStrategies as createFilecoinOverrideStrategies } from "#lib/filecoin/url"; + +describe("BlockExplorer", () => { + test.each([ + [ + mainnet, + ExplorerEntity.Transaction, + { + txnHash: TRANSACTION.VITALIK_TRANSFER.txnHash, + }, + { + name: Explorer.Etherscan, + }, + "https://etherscan.io/tx/0xed46c306a58821dd2dcbc78d7b7af9715c809c71d7a53b0dfc90c42dfdf59b67", + ], + [ + mainnet, + ExplorerEntity.Transaction, + { + txnHash: TRANSACTION.VITALIK_TRANSFER.txnHash, + }, + null, + "https://eth.blockscout.com/tx/0xed46c306a58821dd2dcbc78d7b7af9715c809c71d7a53b0dfc90c42dfdf59b67", + ], + [ + mainnet, + ExplorerEntity.Address, + { + address: BY_USER.vitalik.address, + }, + { + name: Explorer.Etherscan, + }, + "https://etherscan.io/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + ], + [ + mainnet, + ExplorerEntity.Address, + { + address: BY_USER.vitalik.address, + }, + { + name: Explorer.Etherscan, + }, + "https://etherscan.io/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + ], + [ + sepolia, + ExplorerEntity.Address, + { + address: BY_USER.vitalik.address, + }, + { + name: Explorer.Blockscout, + }, + "https://eth-sepolia.blockscout.com/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + ], + [ + filecoin, + ExplorerEntity.Address, + { + address: BY_USER.filecoinTopHolder.filAddress, + }, + { + name: Explorer.Filfox, + locale: "en", + }, + "https://filfox.info/en/address/f1m2swr32yrlouzs7ijui3jttwgc6lxa5n5sookhi", + ], + + // This is necessary to override 1. use filfox over filescan 2. locale + [ + filecoinCalibration, + ExplorerEntity.Address, + { + address: BY_USER.filecoinTopHolder.filAddress, + }, + { + name: Explorer.Filfox, + locale: "zh", + }, + "https://calibration.filfox.info/zh/address/f1m2swr32yrlouzs7ijui3jttwgc6lxa5n5sookhi", + ], + ])("#createBlockExplorerUrl", (chain, entity, params, config, expected) => { + const name = config?.name || Explorer.Blockscout; + const defaultConfig = [Explorer.Blockscout, Explorer.Filfox].includes(name) + ? { + overrideStrategies: { + ...createFilecoinOverrideStrategies(), + ...createBlockscoutOverrideStrategies(), + }, + } + : {}; + + const createUrl = blockExplorerUrlFactory({ + chain, + config: { + name, + ...defaultConfig, + ...config, + }, + }); + + const url = createUrl({ chain, entity, params }); + expect(url).toEqual(expected); + }); +}); diff --git a/apps/web/src/lib/explorer/url.ts b/apps/web/src/lib/explorer/url.ts new file mode 100644 index 0000000..abc299e --- /dev/null +++ b/apps/web/src/lib/explorer/url.ts @@ -0,0 +1,110 @@ +import { getBlock } from "viem/actions"; +import { + type Chain, + filecoin, + filecoinCalibration, + mainnet, +} from "viem/chains"; + +export enum Explorer { + Blockscout = "blockscout", + Etherscan = "etherscan", + Filfox = "filfox", + Filscan = "filscan", +} + +export type ExplorerParams = { + address?: string; + txnHash?: string; +} & { [key: string]: any }; + +export enum ExplorerEntity { + Block = "block", + Transaction = "transaction", + Address = "address", + Token = "token", +} + +/** + * Model after https://eips.ethereum.org/EIPS/eip-3091 + */ + +export type CreateExplorerUrl = ( + createUrlArgs: CreateBlockExplorerUrlArgs, +) => string; + +export type ExplorerConfig = { + name: Explorer; + overrideStrategies?: { + [chainId: number]: CreateExplorerUrl; + default?: CreateExplorerUrl; + }; + locale?: string; +}; + +export const createBlockExplorerUrlWithEip3091 = ( + endpoint: string, + entity: ExplorerEntity, + params: ExplorerParams, +) => { + if (entity === ExplorerEntity.Transaction) { + return endpoint + "/tx/" + params.txnHash; + } + return endpoint + "/address/" + params.address; +}; + +export type CreateBlockExplorerUrlArgs = { + chain: Chain; + entity: ExplorerEntity; + params: ExplorerParams; +}; + +/** + * inefficient implementation, will optimize later with currying + * + * Currying as generally we don't need run-time dynamic choice of explorer but a handy api to create corrs. urls + * run-time params are txn hash, or locale etc which populated from config + * + * viem blockExplorers, or any obj-based config cannot handle locale or more complicated logic + * prefer single functional override to simplify the logic here + * we might want e.g. filecoin specific chain with blockscout for the rest + * + * TODO figure out best way to inject overrideBlockExplorers / overrideStrategies after we have more non standard examples + */ + +export const blockExplorerUrlFactory = ({ + chain, + config, +}: { + chain: Chain; + config?: ExplorerConfig; +}) => { + const name = config?.name || Explorer.Blockscout; + + const strategy = + config?.overrideStrategies?.[chain.id] || + config?.overrideStrategies?.default; + + if (strategy) { + return (createUrlArgs: CreateBlockExplorerUrlArgs) => + strategy({ + ...createUrlArgs, + params: { + ...createUrlArgs.params, + locale: config?.locale, + }, + }); + } + const blockExplorer = + chain.blockExplorers?.[name] || chain.blockExplorers?.default; + + return (createUrlArgs: CreateBlockExplorerUrlArgs) => { + const { entity, params } = createUrlArgs; + + return createBlockExplorerUrlWithEip3091( + blockExplorer?.url || "", + entity, + params, + ); + }; +}; diff --git a/apps/web/src/lib/utils/address.test.ts b/apps/web/src/lib/utils/address.test.ts new file mode 100644 index 0000000..b6c4ce2 --- /dev/null +++ b/apps/web/src/lib/utils/address.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest"; +import { getShortAddress } from "#lib/utils/address"; + +describe("utils", () => { + it("getShortAddress", () => { + expect( + getShortAddress("0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678ac"), + ).toEqual("0xa5cc...78AC"); + }); +}); diff --git a/apps/web/src/lib/utils/address.ts b/apps/web/src/lib/utils/address.ts new file mode 100644 index 0000000..47025eb --- /dev/null +++ b/apps/web/src/lib/utils/address.ts @@ -0,0 +1,22 @@ +// return shortened or "sliced" address that is checksumed, middle shortened to ..., from the 42-chars ethereum address +// Not to be confused with https://viem.sh/docs/utilities/slice + +// checksum encoded + +import { type Address, getAddress, isHex } from "viem"; +import { getShortHex } from "#lib/utils/hex"; + +export type GetShortAddressReturnType = string | null; + +export const getShortAddress = ( + address: Address, + sectionLength: number = 4, +): GetShortAddressReturnType => { + if (!isHex(address) || (address as string)?.length !== 42) { + throw new Error("Invalid Address"); + } + + const checksumed = getAddress(address); + + return getShortHex(checksumed, sectionLength); +}; diff --git a/apps/web/src/lib/utils/hex.ts b/apps/web/src/lib/utils/hex.ts new file mode 100644 index 0000000..ffaf386 --- /dev/null +++ b/apps/web/src/lib/utils/hex.ts @@ -0,0 +1,20 @@ +import type { Hex } from "viem"; + +export type GetShortAddressReturnType = string | null; + +export const getShortHex = ( + hex: Hex, + sectionLength: number = 4, +): GetShortAddressReturnType => { + return [hex.slice(0, sectionLength + 2), hex.slice(-sectionLength)].join( + "...", + ); +}; + +// TODO +export const truncate = (stringToTruncate: string, threshold: number = 15) => { + if (stringToTruncate.length <= threshold) { + return stringToTruncate; + } + return stringToTruncate.slice(-threshold).concat("..."); +}; diff --git a/apps/web/src/lib/utils/wagmi-config.tsx b/apps/web/src/lib/utils/wagmi-config.tsx new file mode 100644 index 0000000..39e7ff8 --- /dev/null +++ b/apps/web/src/lib/utils/wagmi-config.tsx @@ -0,0 +1,17 @@ +// TODO extract sample wagmi config + +import { base, mainnet, optimism, optimismSepolia } from "viem/chains"; +import { http, createConfig } from "wagmi"; +import { injected } from "wagmi/connectors"; + +export const WAGMI_CONFIG = createConfig({ + chains: [mainnet, base, optimism, optimismSepolia], + connectors: [injected()], + ssr: true, + transports: { + [mainnet.id]: http(), + [base.id]: http(), + [optimism.id]: http(), + [optimismSepolia.id]: http(), + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e73ff8e..fbc27b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: '@repo/nillion': specifier: workspace:* version: link:../../packages/nillion + '@tanstack/react-table': + specifier: ^8.21.2 + version: 8.21.2(react-dom@19.0.0)(react@19.0.0) '@types/react': specifier: ^19.0.8 version: 19.0.8 @@ -3786,6 +3789,23 @@ packages: react: 19.0.0 dev: false + /@tanstack/react-table@8.21.2(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@tanstack/table-core': 8.21.2 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + dev: false + + /@tanstack/table-core@8.21.2: + resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} + engines: {node: '>=12'} + dev: false + /@tsconfig/node10@1.0.11: resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} From 220d727e3a0acbdfd1e8fa139342634b4df899c0 Mon Sep 17 00:00:00 2001 From: metalboyrick Date: Fri, 14 Feb 2025 10:29:26 +0700 Subject: [PATCH 2/2] refactor: use geist txn table --- apps/web/src/components/Transactions.tsx | 252 ++++++++++------------- apps/web/src/lib/utils/address.ts | 16 +- 2 files changed, 119 insertions(+), 149 deletions(-) diff --git a/apps/web/src/components/Transactions.tsx b/apps/web/src/components/Transactions.tsx index bd600de..2f4699b 100644 --- a/apps/web/src/components/Transactions.tsx +++ b/apps/web/src/components/Transactions.tsx @@ -1,164 +1,134 @@ -import { useStore } from "@nanostores/react"; -import { Alchemy, Network } from "alchemy-sdk"; +import { Alchemy, AssetTransfersCategory, Network } from "alchemy-sdk"; import React from "react"; -import { $messages } from "../store/messages"; -import { YieldHistoricalChart } from "./YieldHistoricalChart"; +import { TransactionTable } from "./geist/transaction-table"; +import type { TransactionMeta } from "@/lib/domain/transaction/transaction"; +import { parseEther } from "viem"; +import { baseSepolia } from "viem/chains"; +import { Explorer } from "@/lib/explorer/url"; const alchemy = new Alchemy({ - apiKey: import.meta.env.PUBLIC_ALCHEMY_API_KEY, - network: Network.BASE_SEPOLIA, + apiKey: import.meta.env.PUBLIC_ALCHEMY_API_KEY, + network: Network.BASE_SEPOLIA, }); export const invokeApi = async (endpoint: string, body?: any) => { - return fetch(endpoint, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }).then((res) => { - return res.json(); - }); + return fetch(endpoint, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }).then((res) => { + return res.json(); + }); }; export const getTxnsByFilter = async ({ - filter, - type, - method, - address, + filter, + type, + method, + address, }: { - filter?: string; - type?: string[]; - method?: string; - chainId?: number; - address?: string; + filter?: string; + type?: string[]; + method?: string; + chainId?: number; + address?: string; }) => { - if (address) { - try { - const txns = await alchemy.core.getAssetTransfers({ - fromBlock: "0x0", - fromAddress: address, - category: ["external", "erc20", "erc721", "erc1155"], - }); + if (address) { + try { + const txns = await alchemy.core.getAssetTransfers({ + fromBlock: "0x0", + fromAddress: address, + category: [ + AssetTransfersCategory.EXTERNAL, + AssetTransfersCategory.ERC20, + AssetTransfersCategory.ERC721, + AssetTransfersCategory.ERC1155, + ], + }); - // Transform Alchemy response to match existing format - return { - items: txns.transfers.map((tx: any) => ({ - hash: tx.hash, - method: tx.category, - status: "ok", // Alchemy doesn't provide status directly - from: { hash: tx.from }, - to: { hash: tx.to }, - value: tx.value, - })), - }; - } catch (error) { - console.error("Alchemy API error:", error); - throw error; - } - } + console.log("txns", txns); - // Fallback to original Blockscout API if no address provided - const queryString = new URLSearchParams({ - ...(filter && { filter }), - ...(method && { method }), - ...(type && { type: type.join(",") }), - }); - const endpoint = `https://base-sepolia.blockscout.com/api/v2/transactions?${queryString.toString()}`; - return await invokeApi(endpoint); + const formatted = txns.transfers.map( + (tx) => + ({ + hash: tx.hash as `0x${string}`, + from: tx.from as `0x${string}`, + to: tx.to as `0x${string}`, + value: tx.value ? BigInt(tx.value * 10 ** 18) : 0n, + gas: 0n, + blockNumber: tx.blockNum ? BigInt(tx.blockNum) : 0n, + isSuccess: true, + tokenTransfers: [], + } satisfies Partial), + ); + + console.log({ formatted }); + + // Transform Alchemy response to match existing format + return { + items: formatted, + }; + } catch (error) { + console.error("Alchemy API error:", error); + throw error; + } + } + + // Fallback to original Blockscout API if no address provided + const queryString = new URLSearchParams({ + ...(filter && { filter }), + ...(method && { method }), + ...(type && { type: type.join(",") }), + }); + const endpoint = `https://base-sepolia.blockscout.com/api/v2/transactions?${queryString.toString()}`; + return await invokeApi(endpoint); }; const TransactionsWidget = ({ address }: { address: string }) => { - const [transactions, setTransactions] = React.useState([]); - const [isLoading, setIsLoading] = React.useState(true); + const [transactions, setTransactions] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(true); - React.useEffect(() => { - const fetchTransactions = async () => { - try { - setIsLoading(true); - const response = await getTxnsByFilter({ - filter: "contract_call", - method: "supply,withdraw", - type: ["validated"], - address: address, - }); - console.log(response); - setTransactions(response.items || []); - } catch (error) { - console.error("Failed to fetch transactions:", error); - setTransactions([]); - } finally { - setIsLoading(false); - } - }; + React.useEffect(() => { + const fetchTransactions = async () => { + try { + setIsLoading(true); + const response = await getTxnsByFilter({ + filter: "contract_call", + method: "supply,withdraw", + type: ["validated"], + address: address, + }); + console.log(response); + setTransactions(response.items || []); + } catch (error) { + console.error("Failed to fetch transactions:", error); + setTransactions([]); + } finally { + setIsLoading(false); + } + }; - fetchTransactions(); - }, [address]); + fetchTransactions(); + }, [address]); - if (isLoading) { - return ( -
-
-
- ); - } + if (isLoading) { + return ( +
+
+
+ ); + } - return ( -
- - - - - - - - - - - {transactions.map((tx) => ( - - - - - - - ))} - -
Transaction HashMethodFromStatus
- - {tx.hash.slice(0, 10)}...{tx.hash.slice(-8)} - - - - supply - - - {/* {tx.from.hash.slice(0, 6)}...{tx.from.hash.slice(-4)} */} - {address.slice(0, 6)}...{address.slice(-4)} - - - {tx.status} - -
-
- ); + return ( +
+ +
+ ); }; export default TransactionsWidget; diff --git a/apps/web/src/lib/utils/address.ts b/apps/web/src/lib/utils/address.ts index 47025eb..2df9d53 100644 --- a/apps/web/src/lib/utils/address.ts +++ b/apps/web/src/lib/utils/address.ts @@ -4,19 +4,19 @@ // checksum encoded import { type Address, getAddress, isHex } from "viem"; -import { getShortHex } from "#lib/utils/hex"; +import { getShortHex } from "./hex"; export type GetShortAddressReturnType = string | null; export const getShortAddress = ( - address: Address, - sectionLength: number = 4, + address: Address, + sectionLength: number = 4, ): GetShortAddressReturnType => { - if (!isHex(address) || (address as string)?.length !== 42) { - throw new Error("Invalid Address"); - } + if (!isHex(address) || (address as string)?.length !== 42) { + throw new Error("Invalid Address"); + } - const checksumed = getAddress(address); + const checksumed = getAddress(address); - return getShortHex(checksumed, sectionLength); + return getShortHex(checksumed, sectionLength); };