diff --git a/src/app/api/getDelegationsV2.ts b/src/app/api/getDelegationsV2.ts index 36fe2011..4844292b 100644 --- a/src/app/api/getDelegationsV2.ts +++ b/src/app/api/getDelegationsV2.ts @@ -26,12 +26,15 @@ interface DelegationV2API { params_version: number; staker_btc_pk_hex: string; delegation_staking: { + staking_tx_hex: string; staking_tx_hash_hex: string; staking_time: number; staking_amount: number; start_height: number; end_height: number; - staking_tx_hex: string; + bbn_inception_height: number; + bbn_inception_time: number; + slashing_tx_hex: string; }; delegation_unbonding: { unbonding_time: number; @@ -40,6 +43,7 @@ interface DelegationV2API { covenant_btc_pk_hex: string; signature_hex: string; }[]; + slashing_tx_hex: string; }; state: string; } @@ -77,6 +81,12 @@ export const getDelegationV2 = async ( delegationAPIResponse.data.delegation_staking.staking_tx_hash_hex, startHeight: delegationAPIResponse.data.delegation_staking.start_height, endHeight: delegationAPIResponse.data.delegation_staking.end_height, + bbnInceptionHeight: + delegationAPIResponse.data.delegation_staking.bbn_inception_height, + bbnInceptionTime: + delegationAPIResponse.data.delegation_staking.bbn_inception_time, + stakingSlashingTxHex: + delegationAPIResponse.data.delegation_staking.slashing_tx_hex, state, unbondingTime: delegationAPIResponse.data.delegation_unbonding.unbonding_time, @@ -89,6 +99,8 @@ export const getDelegationV2 = async ( signatureHex: signature.signature_hex, }), ), + unbondingSlashingTxHex: + delegationAPIResponse.data.delegation_unbonding.slashing_tx_hex, }; }; @@ -126,9 +138,15 @@ export const getDelegationsV2 = async ( stakingTxHashHex: apiDelegation.delegation_staking.staking_tx_hash_hex, startHeight: apiDelegation.delegation_staking.start_height, endHeight: apiDelegation.delegation_staking.end_height, + bbnInceptionHeight: + apiDelegation.delegation_staking.bbn_inception_height, + bbnInceptionTime: apiDelegation.delegation_staking.bbn_inception_time, + stakingSlashingTxHex: apiDelegation.delegation_staking.slashing_tx_hex, state, unbondingTime: apiDelegation.delegation_unbonding.unbonding_time, unbondingTxHex: apiDelegation.delegation_unbonding.unbonding_tx, + unbondingSlashingTxHex: + apiDelegation.delegation_unbonding.slashing_tx_hex, covenantUnbondingSignatures: apiDelegation.delegation_unbonding.covenant_unbonding_signatures?.map( (signature) => ({ diff --git a/src/app/api/getFinalityProviders.ts b/src/app/api/getFinalityProviders.ts index f483dcd9..6d444d4e 100644 --- a/src/app/api/getFinalityProviders.ts +++ b/src/app/api/getFinalityProviders.ts @@ -3,10 +3,7 @@ import { encode } from "url-safe-base64"; import { isValidUrl } from "@/utils/url"; import { Pagination } from "../types/api"; -import { - FinalityProvider, - FinalityProviderState, -} from "../types/finalityProviders"; +import { FinalityProvider } from "../types/finalityProviders"; import { apiWrapper } from "./apiWrapper"; @@ -22,7 +19,6 @@ interface FinalityProvidersAPIResponse { interface FinalityProviderAPI { description: DescriptionAPI; - state: FinalityProviderState; commission: string; btc_pk: string; active_tvl: number; @@ -39,35 +35,21 @@ interface DescriptionAPI { details: string; } -export const getFinalityProviders = async ({ - key, - pk, - sortBy, - order, - name, -}: { - key: string; - name?: string; - sortBy?: string; - order?: "asc" | "desc"; - pk?: string; -}): Promise => { +export const getFinalityProviders = async ( + key: string, +): Promise => { // const limit = 100; // const reverse = false; const params = { pagination_key: encode(key), - finality_provider_pk: pk, - sort_by: sortBy, - order, - name, // "pagination_reverse": reverse, // "pagination_limit": limit, }; const response = await apiWrapper( "GET", - "/v2/finality-providers", + "/v1/finality-providers", "Error getting finality providers", params, ); @@ -88,7 +70,6 @@ export const getFinalityProviders = async ({ securityContact: fp.description.security_contact, details: fp.description.details, }, - state: fp.state, commission: fp.commission, btcPk: fp.btc_pk, activeTVLSat: fp.active_tvl, diff --git a/src/app/api/getFinalityProvidersV2.ts b/src/app/api/getFinalityProvidersV2.ts new file mode 100644 index 00000000..dae65b68 --- /dev/null +++ b/src/app/api/getFinalityProvidersV2.ts @@ -0,0 +1,106 @@ +import { encode } from "url-safe-base64"; + +import { isValidUrl } from "@/utils/url"; + +import { Pagination } from "../types/api"; +import { + FinalityProviderState, + FinalityProviderV2, +} from "../types/finalityProviders"; + +import { apiWrapper } from "./apiWrapper"; + +export interface PaginatedFinalityProvidersV2 { + finalityProviders: FinalityProviderV2[]; + pagination: Pagination; +} + +interface FinalityProvidersAPIResponse { + data: FinalityProviderAPI[]; + pagination: Pagination; +} + +interface FinalityProviderAPI { + description: DescriptionAPI; + state: FinalityProviderState; + commission: string; + btc_pk: string; + active_tvl: number; + total_tvl: number; + active_delegations: number; + total_delegations: number; +} + +interface DescriptionAPI { + moniker: string; + identity: string; + website: string; + security_contact: string; + details: string; +} + +export const getFinalityProvidersV2 = async ({ + key, + pk, + sortBy, + order, + name, +}: { + key: string; + name?: string; + sortBy?: string; + order?: "asc" | "desc"; + pk?: string; +}): Promise => { + // const limit = 100; + // const reverse = false; + + const params = { + pagination_key: encode(key), + finality_provider_pk: pk, + sort_by: sortBy, + order, + name, + // "pagination_reverse": reverse, + // "pagination_limit": limit, + }; + + const response = await apiWrapper( + "GET", + "/v2/finality-providers", + "Error getting finality providers", + params, + ); + + const finalityProvidersAPIResponse: FinalityProvidersAPIResponse = + response.data; + const finalityProvidersAPI: FinalityProviderAPI[] = + finalityProvidersAPIResponse.data; + + const finalityProviders = finalityProvidersAPI.map( + (fp: FinalityProviderAPI): FinalityProviderV2 => ({ + description: { + moniker: fp.description.moniker, + identity: fp.description.identity, + website: isValidUrl(fp.description.website) + ? fp.description.website + : "", + securityContact: fp.description.security_contact, + details: fp.description.details, + }, + state: fp.state, + commission: fp.commission, + btcPk: fp.btc_pk, + activeTVLSat: fp.active_tvl, + totalTVLSat: fp.total_tvl, + activeDelegations: fp.active_delegations, + totalDelegations: fp.total_delegations, + }), + ); + + const pagination: Pagination = { + next_key: finalityProvidersAPIResponse.pagination.next_key, + }; + + return { finalityProviders, pagination }; +}; diff --git a/src/app/components/Delegations/Delegation.tsx b/src/app/components/Delegations/Delegation.tsx index 971e7e89..2e3f441b 100644 --- a/src/app/components/Delegations/Delegation.tsx +++ b/src/app/components/Delegations/Delegation.tsx @@ -4,6 +4,7 @@ import { FaBitcoin } from "react-icons/fa"; import { IoIosWarning } from "react-icons/io"; import { Tooltip } from "react-tooltip"; +import { useFinalityProviderV2Service } from "@/app/hooks/services/useFinalityProviderV2Service"; import { type SigningStep, useTransactionService, @@ -13,7 +14,6 @@ import { type Delegation as DelegationInterface, DelegationState, } from "@/app/types/delegations"; -import { shouldDisplayPoints } from "@/config"; import { getNetworkConfig } from "@/config/network.config"; import { satoshiToBtc } from "@/utils/btc"; import { getState, getStateTooltip } from "@/utils/getState"; @@ -21,8 +21,6 @@ import { maxDecimals } from "@/utils/maxDecimals"; import { durationTillNow } from "@/utils/time"; import { trim } from "@/utils/trim"; -import { DelegationPoints } from "../Points/DelegationPoints"; - interface DelegationProps { delegation: DelegationInterface; onWithdraw: (id: string) => void; @@ -49,10 +47,8 @@ export const Delegation: React.FC = ({ const { startTimestamp } = stakingTx; const [currentTime, setCurrentTime] = useState(Date.now()); const { isApiNormal, isGeoBlocked } = useHealthCheck(); - const shouldShowPoints = - isApiNormal && !isGeoBlocked && shouldDisplayPoints(); const { transitionPhase1Delegation } = useTransactionService(); - + const { getFinalityProviderMoniker } = useFinalityProviderV2Service(); // get the moniker of the finality provider useEffect(() => { const timerId = setInterval(() => { setCurrentTime(Date.now()); @@ -145,27 +141,30 @@ export const Delegation: React.FC = ({ return (
{isOverflow && (
- +

overflow

)}
-
+
+ {durationTillNow(startTimestamp, currentTime)} +
+
+ {getFinalityProviderMoniker(finalityProviderPkHex)} +
+

{maxDecimals(satoshiToBtc(stakingValueSat), 8)} {coinName}

-

- {durationTillNow(startTimestamp, currentTime)} -

-
+ diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index 11ee18b3..bcc0a778 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -251,14 +251,12 @@ const DelegationsContent: React.FC = ({ Pending Transitions / Withdrawal (Phase 1) -
-

Amount

+

Inception

-

Transaction hash

+

Finality Provider

+

Amount

+

Transaction ID

Status

- {shouldShowPoints &&

Points

}

Action

= ({ fetchNextPage, handleSearch, handleSort, - } = useFinalityProviderService(); + } = useFinalityProviderV2Service(); useEffect(() => { if (finalityProviders) { diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 8f969e16..c1343480 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -16,8 +16,8 @@ import { useHealthCheck } from "@/app/hooks/useHealthCheck"; import { useAppState } from "@/app/state"; import { ErrorHandlerParam, ErrorState } from "@/app/types/errors"; import { - FinalityProvider, - FinalityProvider as FinalityProviderInterface, + FinalityProviderV2 as FinalityProviderInterface, + FinalityProviderV2, } from "@/app/types/finalityProviders"; import { getFeeRateFromMempool } from "@/utils/getFeeRateFromMempool"; import { isStakingSignReady } from "@/utils/isStakingSignReady"; @@ -58,7 +58,7 @@ export const Staking = () => { const [finalityProvider, setFinalityProvider] = useState(); const [finalityProviders, setFinalityProviders] = - useState(); + useState(); // Selected fee rate, comes from the user input const [selectedFeeRate, setSelectedFeeRate] = useState(0); const [awaitingWalletResponse, setAwaitingWalletResponse] = useState(false); diff --git a/src/app/hooks/api/useFinalityProviders.ts b/src/app/hooks/api/useFinalityProviders.ts index c34eddcb..14df4d7b 100644 --- a/src/app/hooks/api/useFinalityProviders.ts +++ b/src/app/hooks/api/useFinalityProviders.ts @@ -11,20 +11,12 @@ import { ErrorState } from "@/app/types/errors"; const FINALITY_PROVIDERS_KEY = "GET_FINALITY_PROVIDERS_KEY"; -interface Params { - pk?: string; - name?: string; - sortBy?: string; - order?: "asc" | "desc"; -} - -export function useFinalityProviders({ pk, sortBy, order, name }: Params = {}) { +export function useFinalityProviders() { const { isErrorOpen, handleError } = useError(); const query = useInfiniteQuery({ - queryKey: [FINALITY_PROVIDERS_KEY, pk, name, sortBy, order], - queryFn: ({ pageParam = "" }) => - getFinalityProviders({ key: pageParam, pk, sortBy, order, name }), + queryKey: [FINALITY_PROVIDERS_KEY], + queryFn: ({ pageParam = "" }) => getFinalityProviders(pageParam), getNextPageParam: (lastPage) => lastPage?.pagination?.next_key !== "" ? lastPage?.pagination?.next_key diff --git a/src/app/hooks/api/useFinalityProvidersV2.ts b/src/app/hooks/api/useFinalityProvidersV2.ts new file mode 100644 index 00000000..96f2182e --- /dev/null +++ b/src/app/hooks/api/useFinalityProvidersV2.ts @@ -0,0 +1,66 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useEffect } from "react"; + +import { + type PaginatedFinalityProvidersV2, + getFinalityProvidersV2, +} from "@/app/api/getFinalityProvidersV2"; +import { ONE_MINUTE } from "@/app/constants"; +import { useError } from "@/app/context/Error/ErrorContext"; +import { ErrorState } from "@/app/types/errors"; + +const FINALITY_PROVIDERS_KEY = "GET_FINALITY_PROVIDERS_V2_KEY"; + +interface Params { + pk?: string; + name?: string; + sortBy?: string; + order?: "asc" | "desc"; +} + +export function useFinalityProvidersV2({ + pk, + sortBy, + order, + name, +}: Params = {}) { + const { isErrorOpen, handleError } = useError(); + + const query = useInfiniteQuery({ + queryKey: [FINALITY_PROVIDERS_KEY, pk, name, sortBy, order], + queryFn: ({ pageParam = "" }) => + getFinalityProvidersV2({ key: pageParam, pk, sortBy, order, name }), + getNextPageParam: (lastPage) => + lastPage?.pagination?.next_key !== "" + ? lastPage?.pagination?.next_key + : null, + initialPageParam: "", + refetchInterval: ONE_MINUTE, + placeholderData: (prev) => prev, + select: (data) => { + const flattenedData = data.pages.reduce( + (acc, page) => { + acc.finalityProviders.push(...page.finalityProviders); + acc.pagination = page.pagination; + return acc; + }, + { finalityProviders: [], pagination: { next_key: "" } }, + ); + return flattenedData; + }, + retry: (failureCount) => { + return !isErrorOpen && failureCount <= 3; + }, + }); + + useEffect(() => { + handleError({ + error: query.error, + hasError: query.isError, + errorState: ErrorState.SERVER_ERROR, + refetchFunction: query.refetch, + }); + }, [query.isError, query.error, query.refetch, handleError]); + + return query; +} diff --git a/src/app/hooks/finalityProviders/useFinalityProvidersData.ts b/src/app/hooks/finalityProviders/useFinalityProvidersData.ts deleted file mode 100644 index 97d08c20..00000000 --- a/src/app/hooks/finalityProviders/useFinalityProvidersData.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; - -import { FinalityProvider as FinalityProviderInterface } from "@/app/types/finalityProviders"; - -export const useFinalityProvidersData = ( - initialProviders: FinalityProviderInterface[] | undefined, -) => { - const [filteredProviders, setFilteredProviders] = useState(initialProviders); - - useEffect(() => { - setFilteredProviders(initialProviders); - }, [initialProviders]); - - const handleSearch = useCallback( - (searchTerm: string) => { - const filtered = initialProviders?.filter( - (fp) => - fp.description?.moniker - ?.toLowerCase() - .includes(searchTerm.toLowerCase()) || - fp.btcPk.toLowerCase().includes(searchTerm.toLowerCase()), - ); - setFilteredProviders(filtered); - }, - [initialProviders], - ); - - return { - filteredProviders, - setFilteredProviders, - handleSearch, - }; -}; diff --git a/src/app/hooks/services/useFinalityProviderService.ts b/src/app/hooks/services/useFinalityProviderService.ts index 16b7ff6b..581fb803 100644 --- a/src/app/hooks/services/useFinalityProviderService.ts +++ b/src/app/hooks/services/useFinalityProviderService.ts @@ -1,56 +1,13 @@ -import { useDebounce } from "@uidotdev/usehooks"; -import { useCallback, useState } from "react"; - import { useFinalityProviders } from "@/app/hooks/api/useFinalityProviders"; -interface SortState { - field?: string; - direction?: "asc" | "desc"; -} - -const SORT_DIRECTIONS = { - undefined: "desc", - desc: "asc", - asc: undefined, -} as const; - export function useFinalityProviderService() { - const [searchValue, setSearchValue] = useState(""); - const [sortState, setSortState] = useState({}); - const name = useDebounce(searchValue, 300); - const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = - useFinalityProviders({ - sortBy: sortState.field, - order: sortState.direction, - name, - }); - - const handleSearch = useCallback((searchTerm: string) => { - setSearchValue(searchTerm); - }, []); - - const handleSort = useCallback((sortField: string) => { - setSortState(({ field, direction }) => - field === sortField - ? { - field: SORT_DIRECTIONS[`${direction}`] ? field : undefined, - direction: SORT_DIRECTIONS[`${direction}`], - } - : { - field: sortField, - direction: "desc", - }, - ); - }, []); + useFinalityProviders(); return { - searchValue, finalityProviders: data?.finalityProviders, hasNextPage, isLoading: isFetchingNextPage, fetchNextPage, - handleSearch, - handleSort, }; } diff --git a/src/app/hooks/services/useFinalityProviderV2Service.ts b/src/app/hooks/services/useFinalityProviderV2Service.ts new file mode 100644 index 00000000..0c03c816 --- /dev/null +++ b/src/app/hooks/services/useFinalityProviderV2Service.ts @@ -0,0 +1,71 @@ +import { useDebounce } from "@uidotdev/usehooks"; +import { useCallback, useState } from "react"; + +import { useFinalityProvidersV2 } from "@/app/hooks/api/useFinalityProvidersV2"; + +interface SortState { + field?: string; + direction?: "asc" | "desc"; +} + +const SORT_DIRECTIONS = { + undefined: "desc", + desc: "asc", + asc: undefined, +} as const; + +export function useFinalityProviderV2Service() { + const [searchValue, setSearchValue] = useState(""); + const [sortState, setSortState] = useState({}); + const name = useDebounce(searchValue, 300); + + const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = + useFinalityProvidersV2({ + sortBy: sortState.field, + order: sortState.direction, + name, + }); + + const handleSearch = useCallback((searchTerm: string) => { + setSearchValue(searchTerm); + }, []); + + const handleSort = useCallback((sortField: string) => { + setSortState(({ field, direction }) => + field === sortField + ? { + field: SORT_DIRECTIONS[`${direction}`] ? field : undefined, + direction: SORT_DIRECTIONS[`${direction}`], + } + : { + field: sortField, + direction: "desc", + }, + ); + }, []); + + const getFinalityProviderMoniker = useCallback( + (btcPkHex: string) => { + const moniker = data?.finalityProviders.find( + (fp) => fp.btcPk === btcPkHex, + )?.description?.moniker; + + if (moniker === undefined || moniker === null || moniker === "") { + return "-"; + } + return moniker; + }, + [data?.finalityProviders], + ); + + return { + searchValue, + finalityProviders: data?.finalityProviders, + hasNextPage, + isLoading: isFetchingNextPage, + fetchNextPage, + handleSearch, + handleSort, + getFinalityProviderMoniker, + }; +} diff --git a/src/app/types/delegationsV2.ts b/src/app/types/delegationsV2.ts index 342ca613..b1a5d985 100644 --- a/src/app/types/delegationsV2.ts +++ b/src/app/types/delegationsV2.ts @@ -1,15 +1,19 @@ export interface DelegationV2 { stakingTxHashHex: string; stakingTxHex: string; + stakingSlashingTxHex: string; paramsVersion: number; finalityProviderBtcPksHex: string[]; stakerBtcPkHex: string; stakingAmount: number; stakingTime: number; + bbnInceptionHeight: number; + bbnInceptionTime: number; startHeight: number; endHeight: number; unbondingTime: number; unbondingTxHex: string; + unbondingSlashingTxHex: string; state: DelegationV2StakingState; covenantUnbondingSignatures?: { covenantBtcPkHex: string; diff --git a/src/app/types/finalityProviders.ts b/src/app/types/finalityProviders.ts index 6e53e52e..79bf52c1 100644 --- a/src/app/types/finalityProviders.ts +++ b/src/app/types/finalityProviders.ts @@ -1,4 +1,14 @@ export interface FinalityProvider { + description: Description; + commission: string; + btcPk: string; + activeTVLSat: number; + totalTVLSat: number; + activeDelegations: number; + totalDelegations: number; +} + +export interface FinalityProviderV2 { description: Description; state: FinalityProviderState; commission: string; diff --git a/src/components/delegations/DelegationList/components/FinalityProviderMoniker.tsx b/src/components/delegations/DelegationList/components/FinalityProviderMoniker.tsx new file mode 100644 index 00000000..773182b1 --- /dev/null +++ b/src/components/delegations/DelegationList/components/FinalityProviderMoniker.tsx @@ -0,0 +1,15 @@ +import { useFinalityProviderV2Service } from "@/app/hooks/services/useFinalityProviderV2Service"; + +interface FinalityProviderMoniker { + value: string; +} + +export function FinalityProviderMoniker({ value }: FinalityProviderMoniker) { + const { getFinalityProviderMoniker } = useFinalityProviderV2Service(); + + const moniker = getFinalityProviderMoniker(value); + + return ( +
{moniker}
+ ); +} diff --git a/src/components/delegations/DelegationList/components/Inscription.tsx b/src/components/delegations/DelegationList/components/Inscription.tsx new file mode 100644 index 00000000..6e75c998 --- /dev/null +++ b/src/components/delegations/DelegationList/components/Inscription.tsx @@ -0,0 +1,14 @@ +import { durationTillNow } from "@/utils/time"; + +interface Inscription { + value: string; +} + +export function Inscription({ value }: Inscription) { + const currentTime = Date.now(); + return ( +
+ {durationTillNow(value, currentTime)} +
+ ); +} diff --git a/src/components/delegations/DelegationList/index.tsx b/src/components/delegations/DelegationList/index.tsx index a6f6868c..270331b3 100644 --- a/src/components/delegations/DelegationList/index.tsx +++ b/src/components/delegations/DelegationList/index.tsx @@ -6,11 +6,13 @@ import { DelegationV2StakingState, type DelegationV2, } from "@/app/types/delegationsV2"; +import { FinalityProviderMoniker } from "@/components/delegations/DelegationList/components/FinalityProviderMoniker"; import { GridTable, type TableColumn } from "../../common/GridTable"; import { ActionButton } from "./components/ActionButton"; import { Amount } from "./components/Amount"; +import { Inscription } from "./components/Inscription"; import { Status } from "./components/Status"; import { TxHash } from "./components/TxHash"; @@ -19,15 +21,26 @@ const columns: TableColumn< { handleActionClick: (action: string, txHash: string) => void } >[] = [ { - field: "stakingAmount", - headerName: "Amount", + field: "inscription", + headerName: "Inscription", width: "max-content", - renderCell: (row) => , + renderCell: (row) => ( + + ), }, { - field: "startHeight", - headerName: "Duration", + field: "finalityProvider", + headerName: "Finality Provider", width: "max-content", + renderCell: (row) => ( + + ), + }, + { + field: "stakingAmount", + headerName: "Amount", + width: "max-content", + renderCell: (row) => , }, { field: "stakingTxHashHex", @@ -155,7 +168,7 @@ export function DelegationList() { wrapperClassName: "max-h-[21rem] overflow-x-auto", bodyClassName: "gap-y-4 min-w-[1000px]", cellClassName: - "p-4 first:pl-4 first:rounded-l last:pr-4 last:rounded-r bg-secondary-contrast flex items-center text-base justify-start group-even:bg-[#F9F9F9] text-primary-dark", + "p-4 first:pl-4 first:rounded-l last:pr-4 last:rounded-r bg-secondary-contrast flex items-center text-sm justify-start group-even:bg-[#F9F9F9] text-primary-dark", }} params={{ handleActionClick }} fallback={
No delegations found
}