From c2b83644dc0c419d8c5106480fb4c4d0e4a38e53 Mon Sep 17 00:00:00 2001 From: jeremy-babylonchain Date: Tue, 10 Dec 2024 16:06:12 +0800 Subject: [PATCH] update finality provider components --- package-lock.json | 41 ++++- package.json | 2 +- .../FinalityProviders/FinalityProvider.tsx | 145 ------------------ .../FinalityProviderFilter.tsx | 22 +++ .../FinalityProviderSearch.tsx | 52 +++---- .../FinalityProviderSubtitle.tsx | 9 ++ .../FinalityProviderTable.tsx | 99 ++++++++++++ .../FinalityProviderTitle.tsx | 9 ++ .../FinalityProviders/FinalityProviders.tsx | 113 ++------------ .../FinalityProviders/components/FPInfo.tsx | 34 ++++ src/app/components/Staking/Staking.tsx | 10 +- ...ProvidersV2.ts => useFinalityProviders.ts} | 0 .../services/useFinalityProviderService.ts | 54 +++++-- .../hooks/services/useTransactionService.ts | 2 - .../state/FinalityProviderServiceState.tsx | 39 +++++ src/app/state/index.tsx | 7 +- 16 files changed, 344 insertions(+), 294 deletions(-) delete mode 100644 src/app/components/Staking/FinalityProviders/FinalityProvider.tsx create mode 100644 src/app/components/Staking/FinalityProviders/FinalityProviderFilter.tsx create mode 100644 src/app/components/Staking/FinalityProviders/FinalityProviderSubtitle.tsx create mode 100644 src/app/components/Staking/FinalityProviders/FinalityProviderTable.tsx create mode 100644 src/app/components/Staking/FinalityProviders/FinalityProviderTitle.tsx create mode 100644 src/app/components/Staking/FinalityProviders/components/FPInfo.tsx rename src/app/hooks/client/api/{useFinalityProvidersV2.ts => useFinalityProviders.ts} (100%) create mode 100644 src/app/state/FinalityProviderServiceState.tsx diff --git a/package-lock.json b/package-lock.json index bd0db4bd..f3fbef22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.3.9", "dependencies": { "@babylonlabs-io/babylon-proto-ts": "0.0.3-canary.4", - "@babylonlabs-io/bbn-core-ui": "^0.3.2", - "@babylonlabs-io/bbn-wallet-connect": "^0.1.7", + "@babylonlabs-io/bbn-core-ui": "^0.4.0", + "@babylonlabs-io/bbn-wallet-connect": "^0.1.6", "@babylonlabs-io/btc-staking-ts": "0.4.0-canary.3", "@bitcoin-js/tiny-secp256k1-asmjs": "2.2.3", "@bitcoinerlab/secp256k1": "^1.1.1", @@ -2075,9 +2075,9 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@babylonlabs-io/bbn-core-ui": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@babylonlabs-io/bbn-core-ui/-/bbn-core-ui-0.3.2.tgz", - "integrity": "sha512-IpGJZ7FJcS3sbqio3rMXO1lG77T/4fY40QkKl6JrNjl/YqzK1dUfr/aeNmVZP7tl5I9h+hSlZMr3WdTQ8XrNWA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@babylonlabs-io/bbn-core-ui/-/bbn-core-ui-0.2.0.tgz", + "integrity": "sha512-dM3GDBXfDgsjJxd8aB1pTOng5piKOYdRtQMq0PkgS3Q80ikvU8MdH4EZoLzUGusLoed/EcH40ei8QSPwa9RaRA==", "peerDependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", @@ -5373,6 +5373,16 @@ "node": ">=18" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@prisma/instrumentation": { "version": "5.19.1", "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.19.1.tgz", @@ -16651,6 +16661,12 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, "node_modules/react-icons": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", @@ -16714,6 +16730,21 @@ "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "license": "MIT", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-responsive-modal": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/react-responsive-modal/-/react-responsive-modal-6.4.2.tgz", diff --git a/package.json b/package.json index 3fb3c1fb..f10e29f3 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@babylonlabs-io/babylon-proto-ts": "0.0.3-canary.4", - "@babylonlabs-io/bbn-core-ui": "^0.3.2", + "@babylonlabs-io/bbn-core-ui": "^0.4.0", "@babylonlabs-io/bbn-wallet-connect": "^0.1.7", "@babylonlabs-io/btc-staking-ts": "0.4.0-canary.3", "@bitcoin-js/tiny-secp256k1-asmjs": "2.2.3", diff --git a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx deleted file mode 100644 index 9b0379e3..00000000 --- a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import Image from "next/image"; -import { AiOutlineInfoCircle } from "react-icons/ai"; -import { FiExternalLink } from "react-icons/fi"; -import { Tooltip } from "react-tooltip"; -import { twJoin } from "tailwind-merge"; - -import blue from "@/app/assets/blue-check.svg"; -import { Hash } from "@/app/components/Hash/Hash"; -import { FinalityProviderState } from "@/app/types/finalityProviders"; -import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btc"; -import { maxDecimals } from "@/utils/maxDecimals"; - -interface FinalityProviderProps { - moniker: string; - pkHex: string; - state: FinalityProviderState; - stakeSat: number; - commission: string; - onClick: () => void; - selected: boolean; - website?: string; -} -const stateMap = { - FINALITY_PROVIDER_STATUS_INACTIVE: "Inactive", - FINALITY_PROVIDER_STATUS_ACTIVE: "Active", - FINALITY_PROVIDER_STATUS_JAILED: "Jailed", - FINALITY_PROVIDER_STATUS_SLASHED: "Slashed", -} as const; - -export const FinalityProvider: React.FC = ({ - moniker, - state, - pkHex, - stakeSat, - commission, - onClick, - selected, - website, -}) => { - const generalStyles = - "card relative cursor-pointer border bg-base-300 p-4 text-sm transition-shadow hover:shadow-md dark:border-transparent dark:bg-base-200"; - - const { coinName } = getNetworkConfig(); - - const finalityProviderHasData = moniker && pkHex && commission; - - const handleClick = () => { - if (finalityProviderHasData) { - onClick(); - } - }; - - return ( -
-
-
- {finalityProviderHasData ? ( -
- verified -

- {moniker} - {website && ( - - - - )} -

-
- ) : ( -
- - - - - No data provided -
- )} -
- -
- -
- -
-

Delegation:

-

- {maxDecimals(satoshiToBtc(stakeSat), 8)} {coinName} -

- - - - -
- -
-

Commission:

- {finalityProviderHasData - ? `${maxDecimals(Number(commission) * 100, 2)}%` - : "-"} - - - - -
-
- Status: {stateMap[state]} -
-
-
- ); -}; diff --git a/src/app/components/Staking/FinalityProviders/FinalityProviderFilter.tsx b/src/app/components/Staking/FinalityProviders/FinalityProviderFilter.tsx new file mode 100644 index 00000000..ad460e8c --- /dev/null +++ b/src/app/components/Staking/FinalityProviders/FinalityProviderFilter.tsx @@ -0,0 +1,22 @@ +import { Select } from "@babylonlabs-io/bbn-core-ui"; + +import { useFinalityProviderService } from "@/app/hooks/services/useFinalityProviderService"; + +const options = [ + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, +]; + +export const FinalityProviderFilter = () => { + const { filterValue, handleFilter } = useFinalityProviderService(); + + return ( + - - + ); }; diff --git a/src/app/components/Staking/FinalityProviders/FinalityProviderSubtitle.tsx b/src/app/components/Staking/FinalityProviders/FinalityProviderSubtitle.tsx new file mode 100644 index 00000000..e8e56c5d --- /dev/null +++ b/src/app/components/Staking/FinalityProviders/FinalityProviderSubtitle.tsx @@ -0,0 +1,9 @@ +import { Text } from "@babylonlabs-io/bbn-core-ui"; + +export const FinalityProviderSubtitle = () => { + return ( + + Select a Finality Provider + + ); +}; diff --git a/src/app/components/Staking/FinalityProviders/FinalityProviderTable.tsx b/src/app/components/Staking/FinalityProviders/FinalityProviderTable.tsx new file mode 100644 index 00000000..47ead675 --- /dev/null +++ b/src/app/components/Staking/FinalityProviders/FinalityProviderTable.tsx @@ -0,0 +1,99 @@ +import { Avatar, Table } from "@babylonlabs-io/bbn-core-ui"; +import { useMemo } from "react"; + +import { Hash } from "@/app/components/Hash/Hash"; +import { useFinalityProviderService } from "@/app/hooks/services/useFinalityProviderService"; +import { FinalityProvider } from "@/app/types/finalityProviders"; +import { getNetworkConfig } from "@/config/network.config"; +import { satoshiToBtc } from "@/utils/btc"; +import { maxDecimals } from "@/utils/maxDecimals"; + +export const FinalityProviderTable = () => { + const { + isLoading, + finalityProviders, + hasNextPage, + fetchNextPage, + searchValue, + filterValue, + } = useFinalityProviderService(); + + const { coinName } = getNetworkConfig(); + + const columns = useMemo( + () => [ + { + key: "moniker", + header: "Finality Provider", + render: (_: unknown, row: FinalityProvider) => ( +
+ {row.description?.identity && ( + + )} + + {row.description?.moniker || "No name provided"} + +
+ ), + sorter: (a: FinalityProvider, b: FinalityProvider) => + (a.description?.moniker || "").localeCompare( + b.description?.moniker || "", + ), + }, + { + key: "btcPk", + header: "BTC PK", + render: (_: unknown, row: FinalityProvider) => ( + + ), + }, + { + key: "activeTVLSat", + header: "Total Delegation", + render: (value: unknown) => + `${maxDecimals(satoshiToBtc(value as number), 8)} ${coinName}`, + sorter: (a: FinalityProvider, b: FinalityProvider) => + a.activeTVLSat - b.activeTVLSat, + }, + { + key: "commission", + header: "Commission", + render: (value: unknown) => `${maxDecimals(Number(value) * 100, 2)}%`, + sorter: (a: FinalityProvider, b: FinalityProvider) => + Number(a.commission) - Number(b.commission), + }, + { + key: "state", + header: "Status", + render: (value: unknown) => + (value as string).replace("FINALITY_PROVIDER_STATUS_", ""), + }, + ], + [coinName], + ); + + const tableData = useMemo(() => { + return ( + finalityProviders?.map((fp) => ({ + ...fp, + id: fp.btcPk, + })) || [] + ); + }, [finalityProviders]); + + return ( + + ); +}; diff --git a/src/app/components/Staking/FinalityProviders/FinalityProviderTitle.tsx b/src/app/components/Staking/FinalityProviders/FinalityProviderTitle.tsx new file mode 100644 index 00000000..34776958 --- /dev/null +++ b/src/app/components/Staking/FinalityProviders/FinalityProviderTitle.tsx @@ -0,0 +1,9 @@ +import { Heading } from "@babylonlabs-io/bbn-core-ui"; + +export const FinalityProviderTitle = () => { + return ( + + Step 1 + + ); +}; diff --git a/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx b/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx index 81120a20..e4594ea2 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx @@ -1,104 +1,23 @@ -import { Heading, Text } from "@babylonlabs-io/bbn-core-ui"; -import { useEffect } from "react"; -import InfiniteScroll from "react-infinite-scroll-component"; - -import { - LoadingTableList, - LoadingView, -} from "@/app/components/Loading/Loading"; -import { useFinalityProviderService } from "@/app/hooks/services/useFinalityProviderService"; -import type { FinalityProvider as FinalityProviderInterface } from "@/app/types/finalityProviders"; - -import { FinalityProvider } from "./FinalityProvider"; +import { FinalityProviderFilter } from "./FinalityProviderFilter"; import { FinalityProviderSearch } from "./FinalityProviderSearch"; +import { FinalityProviderSubtitle } from "./FinalityProviderSubtitle"; +import { FinalityProviderTable } from "./FinalityProviderTable"; +import { FinalityProviderTitle } from "./FinalityProviderTitle"; -export interface FinalityProvidersProps { - onFinalityProvidersLoad: (data: FinalityProviderInterface[]) => void; - selectedFinalityProvider: FinalityProviderInterface | undefined; - onFinalityProviderChange: (btcPkHex: string) => void; -} - -export const FinalityProviders: React.FC = ({ - selectedFinalityProvider, - onFinalityProviderChange, - onFinalityProvidersLoad, -}) => { - const { - isLoading, - finalityProviders, - searchValue, - hasNextPage, - fetchNextPage, - handleSearch, - handleSort, - } = useFinalityProviderService(); - - useEffect(() => { - if (finalityProviders) { - onFinalityProvidersLoad(finalityProviders); - } - }, [finalityProviders, onFinalityProvidersLoad]); - - if (!finalityProviders?.length) { - return ; - } - +export const FinalityProviders = () => { return ( -
- - Step 1 - - - Select a Finality Provider - -
- -
-
-

handleSort("name")}> - Finality Provider -

-

BTC PK

-

handleSort("active_tvl")}> - Total Delegation -

-

handleSort("commission")}> - Commission -

-

Status

-
-
- : null} - scrollableTarget="finality-providers" - > - {finalityProviders?.map((fp) => ( - { - onFinalityProviderChange(fp.btcPk); - }} - /> - ))} - +
+ + +
+
+ +
+
+ +
+
); }; diff --git a/src/app/components/Staking/FinalityProviders/components/FPInfo.tsx b/src/app/components/Staking/FinalityProviders/components/FPInfo.tsx new file mode 100644 index 00000000..7a21fd9f --- /dev/null +++ b/src/app/components/Staking/FinalityProviders/components/FPInfo.tsx @@ -0,0 +1,34 @@ +import Image from "next/image"; +import { FiExternalLink } from "react-icons/fi"; + +import blue from "@/app/assets/blue-check.svg"; + +interface FPInfoProps { + moniker?: string; + website?: string; +} + +export const FPInfo = ({ moniker, website }: FPInfoProps) => { + if (!moniker) { + return No data provided; + } + + return ( +
+ + + {moniker} + {website && ( + + + + )} + +
+ ); +}; diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index c3ac8182..4b8a1259 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -556,13 +556,9 @@ export const Staking = () => { Bitcoin Staking -
-
- +
+
+
{renderStakingForm()} diff --git a/src/app/hooks/client/api/useFinalityProvidersV2.ts b/src/app/hooks/client/api/useFinalityProviders.ts similarity index 100% rename from src/app/hooks/client/api/useFinalityProvidersV2.ts rename to src/app/hooks/client/api/useFinalityProviders.ts diff --git a/src/app/hooks/services/useFinalityProviderService.ts b/src/app/hooks/services/useFinalityProviderService.ts index 5c30021a..d77ce222 100644 --- a/src/app/hooks/services/useFinalityProviderService.ts +++ b/src/app/hooks/services/useFinalityProviderService.ts @@ -1,7 +1,8 @@ import { useDebounce } from "@uidotdev/usehooks"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; -import { useFinalityProviders } from "@/app/hooks/client/api/useFinalityProvidersV2"; +import { useFinalityProviders } from "@/app/hooks/client/api/useFinalityProviders"; +import { FinalityProvider } from "@/app/types/finalityProviders"; interface SortState { field?: string; @@ -14,16 +15,18 @@ const SORT_DIRECTIONS = { asc: undefined, } as const; -export function useFinalityProviderService() { +export function useFinalityProviderServiceState() { const [searchValue, setSearchValue] = useState(""); + const [filterValue, setFilterValue] = useState("active"); const [sortState, setSortState] = useState({}); - const name = useDebounce(searchValue, 300); + + const debouncedSearch = useDebounce(searchValue, 300); // debounce for 300ms const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = useFinalityProviders({ sortBy: sortState.field, order: sortState.direction, - name, + name: debouncedSearch, }); const handleSearch = useCallback((searchTerm: string) => { @@ -44,9 +47,38 @@ export function useFinalityProviderService() { ); }, []); + const handleFilter = useCallback((value: string | number) => { + setFilterValue(value); + }, []); + + const filteredFinalityProviders = useMemo(() => { + if (!data?.finalityProviders) return []; + + return data.finalityProviders.filter((fp: FinalityProvider) => { + const statusMatch = + filterValue === "active" + ? fp.state === "FINALITY_PROVIDER_STATUS_ACTIVE" + : filterValue === "inactive" + ? fp.state !== "FINALITY_PROVIDER_STATUS_ACTIVE" + : true; + + if (!statusMatch) return false; + + if (searchValue) { + const searchLower = searchValue.toLowerCase(); + return ( + fp.description?.moniker?.toLowerCase().includes(searchLower) || + fp.btcPk.toLowerCase().includes(searchLower) + ); + } + + return true; + }); + }, [data?.finalityProviders, filterValue, searchValue]); + const getFinalityProviderMoniker = useCallback( (btcPkHex: string) => { - const moniker = data?.finalityProviders.find( + const moniker = filteredFinalityProviders.find( (fp) => fp.btcPk === btcPkHex, )?.description?.moniker; @@ -55,17 +87,21 @@ export function useFinalityProviderService() { } return moniker; }, - [data?.finalityProviders], + [filteredFinalityProviders], ); return { searchValue, - finalityProviders: data?.finalityProviders, + filterValue, + finalityProviders: filteredFinalityProviders, hasNextPage, isLoading: isFetchingNextPage, - fetchNextPage, handleSearch, handleSort, + handleFilter, getFinalityProviderMoniker, + fetchNextPage, }; } + +export { useFinalityProviderService } from "@/app/state/FinalityProviderServiceState"; diff --git a/src/app/hooks/services/useTransactionService.ts b/src/app/hooks/services/useTransactionService.ts index 474ea0a9..6f146526 100644 --- a/src/app/hooks/services/useTransactionService.ts +++ b/src/app/hooks/services/useTransactionService.ts @@ -330,7 +330,6 @@ export const useTransactionService = () => { ); } await pushTx(signedStakingTx.toHex()); - console.log("staking tx id", signedStakingTx.getId()); }, [ btcConnected, @@ -413,7 +412,6 @@ export const useTransactionService = () => { ); } await pushTx(signedUnbondingTx.toHex()); - console.log("unbonding tx id", signedUnbondingTx.getId()); }, [ networkInfo, diff --git a/src/app/state/FinalityProviderServiceState.tsx b/src/app/state/FinalityProviderServiceState.tsx new file mode 100644 index 00000000..f5613071 --- /dev/null +++ b/src/app/state/FinalityProviderServiceState.tsx @@ -0,0 +1,39 @@ +import { type PropsWithChildren } from "react"; + +import { useFinalityProviderServiceState } from "@/app/hooks/services/useFinalityProviderService"; +import type { FinalityProvider } from "@/app/types/finalityProviders"; +import { createStateUtils } from "@/utils/createStateUtils"; + +interface FinalityProviderServiceState { + searchValue: string; + filterValue: string | number; + finalityProviders: FinalityProvider[]; + hasNextPage: boolean; + isLoading: boolean; + handleSearch: (searchTerm: string) => void; + handleSort: (sortField: string) => void; + handleFilter: (value: string | number) => void; + getFinalityProviderMoniker: (btcPkHex: string) => string; + fetchNextPage: () => void; +} + +const { StateProvider, useState: useFinalityProviderService } = + createStateUtils({ + searchValue: "", + filterValue: "active", + finalityProviders: [], + hasNextPage: false, + isLoading: false, + handleSearch: () => {}, + handleSort: () => {}, + handleFilter: () => {}, + getFinalityProviderMoniker: () => "-", + fetchNextPage: () => {}, + }); + +export function FinalityProviderServiceState({ children }: PropsWithChildren) { + const state = useFinalityProviderServiceState(); + return {children}; +} + +export { useFinalityProviderService }; diff --git a/src/app/state/index.tsx b/src/app/state/index.tsx index d1898a2e..366d3040 100644 --- a/src/app/state/index.tsx +++ b/src/app/state/index.tsx @@ -16,8 +16,13 @@ import { NetworkInfo } from "../types/networkInfo"; import { DelegationState } from "./DelegationState"; import { DelegationV2State } from "./DelegationV2State"; +import { FinalityProviderServiceState } from "./FinalityProviderServiceState"; -const STATE_LIST = [DelegationState, DelegationV2State]; +const STATE_LIST = [ + DelegationState, + DelegationV2State, + FinalityProviderServiceState, +]; export interface AppState { availableUTXOs?: UTXO[];