diff --git a/src/components/Molecules/Address/NFTApprovalList/NFTApprovalList.stories.tsx b/src/components/Molecules/Address/NFTApprovalList/NFTApprovalList.stories.tsx new file mode 100644 index 00000000..ef9c48e9 --- /dev/null +++ b/src/components/Molecules/Address/NFTApprovalList/NFTApprovalList.stories.tsx @@ -0,0 +1,18 @@ +import { type Meta, type StoryObj } from "@storybook/react"; +import { NFTApprovalList as NFTApprovalListComponent } from "./NFTApprovalList"; + +type Story = StoryObj; + +const meta: Meta = { + title: "Molecules/Address/Approval/NFT Approval List", + component: NFTApprovalListComponent, +}; + +export default meta; + +export const NFTApprovalList: Story = { + args: { + chain_name: "eth-mainnet", + address: "demo.eth", + }, +}; diff --git a/src/components/Molecules/Address/NFTApprovalList/NFTApprovalList.tsx b/src/components/Molecules/Address/NFTApprovalList/NFTApprovalList.tsx new file mode 100644 index 00000000..d2d3ead2 --- /dev/null +++ b/src/components/Molecules/Address/NFTApprovalList/NFTApprovalList.tsx @@ -0,0 +1,188 @@ +import { type Option, None, Some } from "@/utils/option"; +import type { NftApprovalsItem } from "@covalenthq/client-sdk"; +import { useEffect, useState } from "react"; +import { type NFTApprovalListProps } from "@/utils/types/molecules.types"; +import { useGoldRush } from "@/utils/store"; +import { type CovalentAPIError } from "@/utils/types/shared.types"; +import { defaultErrorMessage } from "@/utils/constants/shared.constants"; +import { TableHeaderSorting, TableList } from "@/components/Shared"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Address } from "@/components/Atoms"; +import { Button } from "@/components/ui/button"; + +export const NFTApprovalList: React.FC = ({ + chain_name, + address, + on_revoke_approval, +}) => { + const { covalentClient } = useGoldRush(); + + const [maybeResult, setMaybeResult] = + useState>(None); + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + (async () => { + setMaybeResult(None); + setErrorMessage(null); + try { + const { data, ...error } = + await covalentClient.SecurityService.getNftApprovals( + chain_name, + address.trim() + ); + if (error.error) { + throw error; + } + setMaybeResult(new Some(data.items)); + } catch (error: CovalentAPIError | any) { + setErrorMessage(error?.error_message ?? defaultErrorMessage); + setMaybeResult(new Some(null)); + console.error(error); + } + })(); + }, [chain_name, address]); + + const columns: ColumnDef[] = [ + { + id: "token_details", + accessorKey: "token_details", + header: ({ column }) => ( + + align="left" + header={"Token"} + column={column} + /> + ), + cell: ({ row }) => { + return ( +
+
+ {row.original.contract_ticker_symbol || ( +
+ )} +
+

+

+

+
+ ); + }, + }, + { + id: "token_balance", + accessorKey: "token_balance", + header: ({ column }) => ( + + align="left" + header={"Wallet Balance"} + column={column} + /> + ), + cell: ({ row }) => row.original.token_balances.length, + }, + { + id: "token_id", + accessorKey: "token_id", + header: ({ column }) => ( + + align="left" + header={"Token ID"} + column={column} + /> + ), + cell: ({ row }) => { + const token_ids = row.original.token_balances.map( + (balance) => balance.token_id + ); + return ( +

{token_ids.join(", ")}

+ ); + }, + }, + { + id: "spender_address_label", + accessorKey: "spender_address_label", + header: ({ column }) => ( + + align="left" + header={"Spender"} + column={column} + /> + ), + cell: ({ row }) => { + return ( +

+ {row.original.spenders.map((spender) => + spender.spender_address_label ? ( + spender.spender_address_label + ) : ( +

+ ) + )} +

+ ); + }, + }, + { + id: "risk_factor", + accessorKey: "risk_factor", + header: ({ column }) => ( + + align="left" + header={"Risk Factor"} + column={column} + /> + ), + cell: ({ row }) => { + return ( + + {row.original.spenders[0].allowance === "Unlimited" + ? "High" + : "Low"} + + ); + }, + }, + ]; + + if (on_revoke_approval) { + columns.push({ + id: "revoke", + accessorKey: "revoke", + header: () =>
, + cell: ({ row }) => { + return ( + + ); + }, + }); + } + + return ( + + columns={columns} + errorMessage={errorMessage} + maybeData={maybeResult} + sorting_state={[ + { + id: "value_at_risk", + desc: true, + }, + ]} + /> + ); +}; diff --git a/src/components/Molecules/Address/TokenApprovalList/TokenApprovalList.stories.tsx b/src/components/Molecules/Address/TokenApprovalList/TokenApprovalList.stories.tsx new file mode 100644 index 00000000..518a6d7d --- /dev/null +++ b/src/components/Molecules/Address/TokenApprovalList/TokenApprovalList.stories.tsx @@ -0,0 +1,18 @@ +import { type Meta, type StoryObj } from "@storybook/react"; +import { TokenApprovalList as TokenApprovalListComponent } from "./TokenApprovalList"; + +type Story = StoryObj; + +const meta: Meta = { + title: "Molecules/Address/Approval/Token Approval List", + component: TokenApprovalListComponent, +}; + +export default meta; + +export const TokenApprovalList: Story = { + args: { + chain_name: "eth-mainnet", + address: "demo.eth", + }, +}; diff --git a/src/components/Molecules/Address/TokenApprovalList/TokenApprovalList.tsx b/src/components/Molecules/Address/TokenApprovalList/TokenApprovalList.tsx new file mode 100644 index 00000000..013baf99 --- /dev/null +++ b/src/components/Molecules/Address/TokenApprovalList/TokenApprovalList.tsx @@ -0,0 +1,219 @@ +import { type Option, None, Some } from "@/utils/option"; +import { type TokensApprovalItem } from "@covalenthq/client-sdk"; +import { useEffect, useState } from "react"; +import { type TokenApprovalListProps } from "@/utils/types/molecules.types"; +import { useGoldRush } from "@/utils/store"; +import { type CovalentAPIError } from "@/utils/types/shared.types"; +import { + GRK_SIZES, + defaultErrorMessage, +} from "@/utils/constants/shared.constants"; +import { CardDetail, TableHeaderSorting, TableList } from "@/components/Shared"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Address, TokenAvatar } from "@/components/Atoms"; +import { Button } from "@/components/ui/button"; + +export const TokenApprovalList: React.FC = ({ + chain_name, + address, + on_revoke_approval, +}) => { + const { covalentClient } = useGoldRush(); + + const [maybeResult, setMaybeResult] = + useState>(None); + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + (async () => { + setMaybeResult(None); + setErrorMessage(null); + try { + const { data, ...error } = + await covalentClient.SecurityService.getApprovals( + chain_name, + address.trim() + ); + if (error.error) { + throw error; + } + setMaybeResult(new Some(data.items)); + } catch (error: CovalentAPIError | any) { + setErrorMessage(error?.error_message ?? defaultErrorMessage); + setMaybeResult(new Some(null)); + console.error(error); + } + })(); + }, [chain_name, address]); + + const columns: ColumnDef[] = [ + { + id: "token_details", + accessorKey: "token_details", + header: ({ column }) => ( + + align="left" + header={"Token"} + column={column} + /> + ), + cell: ({ row }) => { + return ( +
+ + + {row.original.ticker_symbol || ( +
+ )} +
+ } + /> + + } + /> + + ); + }, + }, + { + id: "balance_quote", + accessorKey: "balance_quote", + header: ({ column }) => ( + + align="left" + header={"Wallet Balance"} + column={column} + /> + ), + cell: ({ row }) => { + return ( +
+ + +
+ ); + }, + }, + { + id: "pretty_value_at_risk_quote", + accessorKey: "pretty_value_at_risk_quote", + header: ({ column }) => ( + + align="left" + header={"Value at Risk"} + column={column} + /> + ), + cell: ({ row }) => { + return ( +

+ {row.original.pretty_value_at_risk_quote + ? row.original.pretty_value_at_risk_quote + : Number(row.original.value_at_risk) / + Math.pow(10, row.original.contract_decimals)} +

+ ); + }, + }, + { + id: "spender_address_label", + accessorKey: "spender_address_label", + header: ({ column }) => ( + + align="left" + header={"Spender(s)"} + column={column} + /> + ), + cell: ({ row }) => { + return ( +

+ {row.original.spenders.map((spender) => + spender.spender_address_label ? ( + spender.spender_address_label + ) : ( +

+ ) + )} +

+ ); + }, + }, + { + id: "risk_factor", + accessorKey: "risk_factor", + header: ({ column }) => ( + + align="left" + header={"Risk Factor"} + column={column} + /> + ), + cell: ({ row }) => { + return ( + + {row.original.spenders[0].risk_factor === + "CONSIDER REVOKING" + ? "High" + : "Low"} + + ); + }, + }, + ]; + + if (on_revoke_approval) { + columns.push({ + id: "revoke", + accessorKey: "revoke", + header: () =>
, + cell: ({ row }) => { + return ( + + ); + }, + }); + } + + return ( + + columns={columns} + errorMessage={errorMessage} + maybeData={maybeResult} + sorting_state={[ + { + id: "value_at_risk", + desc: true, + }, + ]} + /> + ); +}; diff --git a/src/components/Molecules/index.ts b/src/components/Molecules/index.ts index a689dfe9..44ee0f43 100644 --- a/src/components/Molecules/index.ts +++ b/src/components/Molecules/index.ts @@ -8,6 +8,8 @@ export { LatestBlocks } from "./Block/LatestBlocks/LatestBlocks"; export { ChainSelector } from "./ChainSelector/ChainSelector"; export { GasCard } from "./GasCard/GasCard"; export { LatestPrice } from "./LatestPrice/LatestPrice"; +export { TokenApprovalList } from "./Address/TokenApprovalList/TokenApprovalList"; +export { NFTApprovalList } from "./Address/NFTApprovalList/NFTApprovalList"; export { NFTCollectionDetails } from "./NFT/NFTCollectionDetails/NFTCollectionDetails"; export { NFTCollectionTokensList } from "./NFT/NFTCollectionTokensList/NFTCollectionTokensList"; export { NFTFloorPrice } from "./NFT/NFTFloorPrice/NFTFloorPrice"; diff --git a/src/utils/types/molecules.types.ts b/src/utils/types/molecules.types.ts index 7e2c8730..ed3357d7 100644 --- a/src/utils/types/molecules.types.ts +++ b/src/utils/types/molecules.types.ts @@ -1,4 +1,8 @@ import { type Option } from "@/utils/option"; +import type { + NftApprovalsItem, + TokensApprovalItem, +} from "@covalenthq/client-sdk"; import { type BalanceItem, type Block, @@ -30,6 +34,18 @@ export interface AddressActivityListProps { errorMessage?: string | null; } +export interface NFTApprovalListProps { + chain_name: Chain; + address: string; + on_revoke_approval?: (approval: NftApprovalsItem) => void; +} + +export interface TokenApprovalListProps { + chain_name: Chain; + address: string; + on_revoke_approval?: (approval: TokensApprovalItem) => void; +} + export interface BlockDetailsProps { chain_name: Chain; height: number; diff --git a/src/utils/types/shared.types.ts b/src/utils/types/shared.types.ts index cfde739a..1b7eff6c 100644 --- a/src/utils/types/shared.types.ts +++ b/src/utils/types/shared.types.ts @@ -1,4 +1,8 @@ import { type Option } from "@/utils/option"; +import type { + NftApprovalsItem, + TokensApprovalItem, +} from "@covalenthq/client-sdk"; import { type Pagination, type Transaction } from "@covalenthq/client-sdk"; import { type Column, @@ -45,6 +49,16 @@ export interface TransactionsProps { errorMessage: string | null; } +export interface TokenApprovalsTableProps { + maybeResult: Option; + errorMessage: string | null; +} + +export interface NFTApprovalsTableProps { + maybeResult: Option; + errorMessage: string | null; +} + export interface SkeletonTableProps { rows?: number; cols?: number;