diff --git a/.env.example b/.env.example index 6b59863c..d244241d 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ NEXT_PUBLIC_MEMPOOL_API=https://mempool.space NEXT_PUBLIC_API_URL=https://staking-api.phase-2-devnet.babylonlabs.io -NEXT_PUBLIC_POINTS_API_URL=https://points.testnet.babylonchain.io NEXT_PUBLIC_NETWORK=signet NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES=true NEXT_PUBLIC_SENTRY_DSN=http://f4de35dcc28cf83853663a4a6d2ee496@127.0.0.1:9000/1 \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 55c9243f..12bfd8c5 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -39,7 +39,6 @@ jobs: build-args: | NEXT_PUBLIC_MEMPOOL_API=${{ vars.NEXT_PUBLIC_MEMPOOL_API }} NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }} - NEXT_PUBLIC_POINTS_API_URL=${{ vars.NEXT_PUBLIC_POINTS_API_URL }} NEXT_PUBLIC_NETWORK=${{ vars.NEXT_PUBLIC_NETWORK }} NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES=${{ vars.NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES }} NEXT_PUBLIC_SENTRY_DSN=${{ vars.NEXT_PUBLIC_SENTRY_DSN }} diff --git a/README.md b/README.md index 9b97fce3..f370b0ef 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ where, node queries - `NEXT_PUBLIC_API_URL` specifies the back-end API to use for the staking system queries -- `NEXT_PUBLIC_POINTS_API_URL` specifies the Points API to use for the points - system (Optional) - `NEXT_PUBLIC_NETWORK` specifies the BTC network environment - `NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES` boolean value to indicate whether display testing network related message. Default to true diff --git a/docker/Dockerfile b/docker/Dockerfile index 5d6e2328..04b677fc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,9 +20,6 @@ ENV NEXT_PUBLIC_MEMPOOL_API=${NEXT_PUBLIC_MEMPOOL_API} ARG NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} -ARG NEXT_PUBLIC_POINTS_API_URL -ENV NEXT_PUBLIC_POINTS_API_URL=${NEXT_PUBLIC_POINTS_API_URL} - ARG NEXT_PUBLIC_NETWORK ENV NEXT_PUBLIC_NETWORK=${NEXT_PUBLIC_NETWORK} diff --git a/src/app/api/getPoints.ts b/src/app/api/getPoints.ts deleted file mode 100644 index df9601f5..00000000 --- a/src/app/api/getPoints.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { encode } from "url-safe-base64"; - -import { Pagination } from "../types/api"; - -import { pointsApiWrapper } from "./pointsApiWrapper"; - -interface StakerPointsAPIResponse { - data: StakerPointsAPI[]; -} - -export interface StakerPointsAPI { - staker_btc_pk: string; - points: number; -} - -export interface DelegationPointsAPI { - staking_tx_hash_hex: string; - staker: { - pk: string; - points: number; - }; - finality_provider: { - pk: string; - points: number; - }; - staking_height: number; - unbonding_height: number | null; - expiry_height: number; -} - -interface PaginatedDelegationsPointsAPIResponse { - data: DelegationPointsAPI[]; - pagination: Pagination; -} - -export const getStakersPoints = async ( - stakerBtcPks: string[], -): Promise => { - const params = new URLSearchParams(); - - stakerBtcPks.forEach((pk) => { - params.append("staker_btc_pk", encode(pk)); - }); - - const response = await pointsApiWrapper( - "GET", - "/v1/points/stakers", - "Error getting staker points", - params, - ); - - const responseData: StakerPointsAPIResponse = response.data; - return responseData.data; -}; - -// Get delegation points by staking transaction hash hex -export const getDelegationPointsByStakingTxHashHexes = async ( - stakingTxHashHexes: string[], -): Promise => { - try { - const response = await pointsApiWrapper( - "POST", - "/v1/points/delegations", - "Error getting delegation points by staking transaction hashes", - { staking_tx_hash_hex: stakingTxHashHexes }, - ); - - const responseData: PaginatedDelegationsPointsAPIResponse = response.data; - return responseData.data; - } catch (error) { - throw error; - } -}; diff --git a/src/app/api/pointsApiWrapper.ts b/src/app/api/pointsApiWrapper.ts deleted file mode 100644 index c48b9566..00000000 --- a/src/app/api/pointsApiWrapper.ts +++ /dev/null @@ -1,45 +0,0 @@ -import axios from "axios"; - -export const pointsApiWrapper = async ( - method: "GET" | "POST", - path: string, - generalErrorMessage: string, - params?: any, - timeout?: number, -) => { - let response; - let handler; - switch (method) { - case "GET": - handler = axios.get; - break; - case "POST": - handler = axios.post; - break; - default: - throw new Error("Invalid method"); - } - - try { - // destructure params in case of post request - response = await handler( - `${process.env.NEXT_PUBLIC_POINTS_API_URL}${path}`, - method === "POST" - ? { ...params } - : { - params, - }, - { - timeout: timeout || 0, // 0 is no timeout - }, - ); - } catch (error) { - if (axios.isAxiosError(error)) { - const message = error?.response?.data?.message; - throw new Error(message || generalErrorMessage); - } else { - throw new Error(generalErrorMessage); - } - } - return response; -}; diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index a0528ecf..589704c2 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -1,6 +1,5 @@ import { Heading } from "@babylonlabs-io/bbn-core-ui"; -import type { networks } from "bitcoinjs-lib"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import InfiniteScroll from "react-infinite-scroll-component"; import { useLocalStorage } from "usehooks-ts"; @@ -10,73 +9,30 @@ import { MODE_WITHDRAW, WithdrawModal, } from "@/app/components/Modals/WithdrawModal"; -import { DelegationsPointsProvider } from "@/app/context/api/DelegationsPointsProvider"; import { useError } from "@/app/context/Error/ErrorContext"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; import { useDelegations } from "@/app/hooks/client/api/useDelegations"; -import { useHealthCheck } from "@/app/hooks/useHealthCheck"; +import { useNetworkFees } from "@/app/hooks/client/api/useNetworkFees"; +import { useV1TransactionService } from "@/app/hooks/services/useV1TransactionService"; import { useDelegationState } from "@/app/state/DelegationState"; import { Delegation as DelegationInterface, DelegationState, } from "@/app/types/delegations"; import { ErrorState } from "@/app/types/errors"; -import { shouldDisplayPoints } from "@/config"; -import { signWithdrawalTx } from "@/utils/delegations/signWithdrawalTx"; import { getIntermediateDelegationsLocalStorageKey } from "@/utils/local_storage/getIntermediateDelegationsLocalStorageKey"; import { toLocalStorageIntermediateDelegation } from "@/utils/local_storage/toLocalStorageIntermediateDelegation"; import { Delegation } from "./Delegation"; -export const Delegations = () => { - const { data: delegationsAPI } = useDelegations(); - const { address, publicKeyNoCoord, connected, network } = useBTCWallet(); - - if (!connected || !delegationsAPI || !network) { - return; - } - - return ( - - {/* If there are no delegations, don't render the content */} - {delegationsAPI.delegations.length > 0 && ( - - )} - - ); -}; - -interface DelegationsContentProps { - delegationsAPI: DelegationInterface[]; - publicKeyNoCoord: string; - btcWalletNetwork: networks.Network; - address: string; - isWalletConnected: boolean; -} - -const DelegationsContent: React.FC = ({ - delegationsAPI, - address, - btcWalletNetwork, - publicKeyNoCoord, -}) => { +export const Delegations = ({}) => { + const { publicKeyNoCoord, connected, network } = useBTCWallet(); const [modalOpen, setModalOpen] = useState(false); const [txID, setTxID] = useState(""); const [modalMode, setModalMode] = useState(); - const { showError, captureError } = useError(); - const { isApiNormal, isGeoBlocked } = useHealthCheck(); + const { showError } = useError(); const [awaitingWalletResponse, setAwaitingWalletResponse] = useState(false); + const { data: delegationsAPI } = useDelegations(); const { delegations = [], fetchMoreDelegations, @@ -84,16 +40,13 @@ const DelegationsContent: React.FC = ({ isLoading, } = useDelegationState(); - const { signPsbt, getNetworkFees, pushTx } = useBTCWallet(); + const { submitWithdrawalTx } = useV1TransactionService(); + const { data: networkFees } = useNetworkFees(); - const delegation = useMemo( - () => - delegationsAPI.find((delegation) => delegation.stakingTxHashHex === txID), - [delegationsAPI, txID], + const selectedDelegation = delegationsAPI?.delegations.find( + (delegation) => delegation.stakingTxHashHex === txID, ); - const shouldShowPoints = - isApiNormal && !isGeoBlocked && shouldDisplayPoints(); // Local storage state for intermediate delegations (transitioning, withdrawing) const intermediateDelegationsLocalStorageKey = getIntermediateDelegationsLocalStorageKey(publicKeyNoCoord); @@ -141,24 +94,37 @@ const DelegationsContent: React.FC = ({ }; // Handles withdrawing requests for delegations that have expired timelocks - // It constructs a withdrawal transaction, creates a signature for it, and submits it to the Bitcoin network + // It constructs a withdrawal transaction, creates a signature for it, + // and submits it to the Bitcoin network const handleWithdraw = async (id: string) => { try { + if (!networkFees) { + throw new Error("Network fees not found"); + } // Prevent the modal from closing setAwaitingWalletResponse(true); + + if (selectedDelegation?.stakingTxHashHex != id) { + throw new Error("Wrong delegation selected for withdrawal"); + } // Sign the withdrawal transaction - const { delegation } = await signWithdrawalTx( - id, - delegationsAPI, - publicKeyNoCoord, - btcWalletNetwork, - signPsbt, - address, - getNetworkFees, - pushTx, + const { stakingTx, finalityProviderPkHex, stakingValueSat, unbondingTx } = + selectedDelegation; + submitWithdrawalTx( + { + stakingTimelock: stakingTx.timelock, + finalityProviderPkNoCoordHex: finalityProviderPkHex, + stakingAmountSat: stakingValueSat, + }, + stakingTx.startHeight, + stakingTx.txHex, + unbondingTx?.txHex, ); // Update the local state with the new intermediate delegation - updateLocalStorage(delegation, DelegationState.INTERMEDIATE_WITHDRAWAL); + updateLocalStorage( + selectedDelegation, + DelegationState.INTERMEDIATE_WITHDRAWAL, + ); } catch (error: Error | any) { showError({ error: { @@ -192,7 +158,7 @@ const DelegationsContent: React.FC = ({ } return intermediateDelegations.filter((intermediateDelegation) => { - const matchingDelegation = delegationsAPI.find( + const matchingDelegation = delegationsAPI.delegations.find( (delegation) => delegation?.stakingTxHashHex === intermediateDelegation?.stakingTxHashHex, @@ -223,7 +189,7 @@ const DelegationsContent: React.FC = ({ }, [delegationsAPI, setIntermediateDelegationsLocalStorage]); useEffect(() => { - if (modalOpen && !delegation) { + if (modalOpen && !selectedDelegation) { showError({ error: { message: "Delegation not found", @@ -235,11 +201,15 @@ const DelegationsContent: React.FC = ({ setTxID(""); setModalMode(undefined); } - }, [modalOpen, delegation, showError]); + }, [modalOpen, selectedDelegation, showError]); + + if (!connected || !delegationsAPI || !network) { + return; + } // combine delegations from the API and local storage, prioritizing API data const combinedDelegationsData = delegationsAPI - ? [...delegations, ...delegationsAPI] + ? [...delegations, ...delegationsAPI.delegations] : // if no API data, fallback to using only local storage delegations delegations; @@ -296,7 +266,7 @@ const DelegationsContent: React.FC = ({ - {modalMode && txID && delegation && ( + {modalMode && txID && selectedDelegation && ( setModalOpen(false)} diff --git a/src/app/components/FAQ/data/questions.tsx b/src/app/components/FAQ/data/questions.tsx index 2a028d99..d59cd26b 100644 --- a/src/app/components/FAQ/data/questions.tsx +++ b/src/app/components/FAQ/data/questions.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react"; -import { shouldDisplayPoints, shouldDisplayTestingMsg } from "@/config"; +import { shouldDisplayTestingMsg } from "@/config"; export interface Question { title: string; @@ -291,21 +291,6 @@ export const questions = ( ), }, ]; - if (shouldDisplayPoints()) { - questionList.push({ - title: "What are the points for?", - content: ( -

- We use points to track staking activity. Points are not blockchain - tokens. Points do not, and may never, convert to, accrue to, be used - as a basis to calculate, or become tokens, other digital assets, or - distributions thereof. Points are virtual calculations with no - monetary value. Points do not constitute any currency or property of - any type and are not redeemable, refundable, or transferable. -

- ), - }); - } if (shouldDisplayTestingMsg()) { questionList.push({ title: "What is the goal of this testnet?", diff --git a/src/app/components/Modals/PreviewModal.tsx b/src/app/components/Modals/PreviewModal.tsx index a7069c7c..445a3f26 100644 --- a/src/app/components/Modals/PreviewModal.tsx +++ b/src/app/components/Modals/PreviewModal.tsx @@ -25,7 +25,7 @@ interface PreviewModalProps { finalityProvider: string | undefined; finalityProviderAvatar: string | undefined; stakingAmountSat: number; - stakingTimeBlocks: number; + stakingTimelock: number; stakingFeeSat: number; feeRate: number; unbondingFeeSat: number; @@ -38,7 +38,7 @@ export const PreviewModal = ({ finalityProvider, finalityProviderAvatar, stakingAmountSat, - stakingTimeBlocks, + stakingTimelock, onSign, stakingFeeSat, feeRate, @@ -99,7 +99,7 @@ export const PreviewModal = ({ { key: "Term", value: ( - {blocksToDisplayTime(stakingTimeBlocks)} + {blocksToDisplayTime(stakingTimelock)} ), }, { diff --git a/src/app/components/PersonalBalance/PersonalBalance.tsx b/src/app/components/PersonalBalance/PersonalBalance.tsx index afede1d4..3f224aad 100644 --- a/src/app/components/PersonalBalance/PersonalBalance.tsx +++ b/src/app/components/PersonalBalance/PersonalBalance.tsx @@ -24,17 +24,17 @@ export function PersonalBalance() { const { getRewards } = useRewardsService(); const { claimRewards } = useRewardsService(); - const { data: rewards } = useQuery({ + const { data: rewards, isLoading: rewardsLoading } = useQuery({ queryKey: [QUERY_KEYS.REWARDS], queryFn: getRewards, }); - const { data: btcBalance } = useQuery({ + const { data: btcBalance, isLoading: btcBalanceLoading } = useQuery({ queryKey: [QUERY_KEYS.BTC_BALANCE], queryFn: getBTCBalance, }); - const { data: cosmosBalance } = useQuery({ + const { data: cosmosBalance, isLoading: cosmosBalanceLoading } = useQuery({ queryKey: [QUERY_KEYS.COSMOS_BALANCE], queryFn: getBalance, }); @@ -52,7 +52,7 @@ export function PersonalBalance() { {/* TODO: Need to add the staker tvl value for the bitcoin balance as well as remove the filtering on inscription balance*/} @@ -60,7 +60,7 @@ export function PersonalBalance() {
@@ -68,7 +68,7 @@ export function PersonalBalance() {
@@ -76,7 +76,7 @@ export function PersonalBalance() {
= ({ - stakingTxHashHex, - className, -}) => { - const { isApiNormal, isGeoBlocked } = useHealthCheck(); - const { delegationPoints, isLoading } = useDelegationsPoints(); - // Early return if the API is not normal or the user is geo-blocked - if (!isApiNormal || isGeoBlocked || !shouldDisplayPoints()) { - return null; - } - - const points = delegationPoints.get(stakingTxHashHex); - if (isLoading) { - return ( -
-
-
-
-
- ); - } - - return ( -
-
-

- Points: - -

-
-
- ); -}; diff --git a/src/app/components/Points/Points.tsx b/src/app/components/Points/Points.tsx deleted file mode 100644 index 815cb2a8..00000000 --- a/src/app/components/Points/Points.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { NumericFormat } from "react-number-format"; -import { Tooltip } from "react-tooltip"; - -interface PointsProps { - points: number | undefined; -} - -export const Points: React.FC = ({ points }) => { - if (points === undefined || points === 0) { - return <>n.a.; - } - - if (points < 0.001 && points > 0) { - return ( - <> - - <0.001 - - - - ); - } - - return ( - - ); -}; diff --git a/src/app/components/Points/StakerPoints.tsx b/src/app/components/Points/StakerPoints.tsx deleted file mode 100644 index b3ab859a..00000000 --- a/src/app/components/Points/StakerPoints.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import React from "react"; - -import { getStakersPoints } from "@/app/api/getPoints"; - -import { Points } from "./Points"; - -interface StakerPointsProps { - publicKeyNoCoord: string; -} - -export const StakerPoints: React.FC = ({ - publicKeyNoCoord, -}) => { - const { data: stakerPoints, isLoading } = useQuery({ - queryKey: ["stakerPoints", publicKeyNoCoord], - queryFn: () => getStakersPoints([publicKeyNoCoord]), - enabled: Boolean(publicKeyNoCoord), - refetchInterval: 300000, // Refresh every 5 minutes - refetchOnWindowFocus: false, - retry: 1, - }); - - if (isLoading) { - return ( -
-
-
- ); - } - - const points = stakerPoints?.[0]?.points; - - return ( -
-

- -

-
- ); -}; diff --git a/src/app/components/Staking/Form/StakingTime.tsx b/src/app/components/Staking/Form/StakingTime.tsx index 8d6ea79b..42eb1b45 100644 --- a/src/app/components/Staking/Form/StakingTime.tsx +++ b/src/app/components/Staking/Form/StakingTime.tsx @@ -14,7 +14,6 @@ interface StakingTimeProps { export const StakingTime: React.FC = ({ minStakingTimeBlocks, maxStakingTimeBlocks, - unbondingTimeBlocks, onStakingTimeBlocksChange, reset, }) => { diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index bf518f8d..349e8236 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -542,7 +542,7 @@ export const Staking = () => { finalityProvider?.description.identity } stakingAmountSat={stakingAmountSat} - stakingTimeBlocks={stakingTimeBlocksWithFixed} + stakingTimelock={stakingTimeBlocksWithFixed} stakingFeeSat={stakingFeeSat} feeRate={feeRate} unbondingFeeSat={unbondingFeeSat} diff --git a/src/app/components/Summary/Summary.tsx b/src/app/components/Summary/Summary.tsx index 1e8d2589..c9762fc2 100644 --- a/src/app/components/Summary/Summary.tsx +++ b/src/app/components/Summary/Summary.tsx @@ -4,23 +4,19 @@ import { Tooltip } from "react-tooltip"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; import { useNetworkInfo } from "@/app/hooks/client/api/useNetworkInfo"; -import { useHealthCheck } from "@/app/hooks/useHealthCheck"; import { useAppState } from "@/app/state"; import { useDelegationState } from "@/app/state/DelegationState"; -import { shouldDisplayPoints } from "@/config"; import { getNetworkConfig } from "@/config/network.config"; import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; import { Network } from "@/utils/wallet/btc_wallet_provider"; import { LoadingSmall } from "../Loading/Loading"; -import { StakerPoints } from "../Points/StakerPoints"; export const Summary = () => { - const { isApiNormal, isGeoBlocked } = useHealthCheck(); const { totalStaked } = useDelegationState(); const { totalBalance, isLoading: loading } = useAppState(); - const { address, publicKeyNoCoord } = useBTCWallet(); + const { address } = useBTCWallet(); const { coinName } = getNetworkConfig(); const onMainnet = getNetworkConfig().network === Network.MAINNET; @@ -58,30 +54,6 @@ export const Summary = () => {

- {isApiNormal && !isGeoBlocked && shouldDisplayPoints() && ( - <> -
-
-
-

Total points

- - - - -
-
- -
-
- - )}
diff --git a/src/app/context/api/DelegationsPointsProvider.tsx b/src/app/context/api/DelegationsPointsProvider.tsx deleted file mode 100644 index 40a906eb..00000000 --- a/src/app/context/api/DelegationsPointsProvider.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import React, { createContext, useContext, useEffect, useState } from "react"; - -import { getDelegationPointsByStakingTxHashHexes } from "@/app/api/getPoints"; -import { useHealthCheck } from "@/app/hooks/useHealthCheck"; -import { Delegation } from "@/app/types/delegations"; -import { shouldDisplayPoints } from "@/config"; - -interface PointsContextType { - delegationPoints: Map; - isLoading: boolean; - error: Error | null; -} - -interface DelegationsPointsProviderProps { - children: React.ReactNode; - publicKeyNoCoord: string; - delegationsAPI: Delegation[]; - isWalletConnected: boolean; - address: string; -} - -const MAX_DELEGATION_POINTS_BATCH_SIZE = 200; -const DelegationsPointsContext = createContext( - undefined, -); - -export const useDelegationsPoints = () => { - const context = useContext(DelegationsPointsContext); - if (context === undefined) { - throw new Error( - "useDelegationsPoints must be used within a DelegationsPointsProvider", - ); - } - return context; -}; - -export const DelegationsPointsProvider: React.FC< - DelegationsPointsProviderProps -> = ({ - children, - publicKeyNoCoord, - delegationsAPI, - isWalletConnected, - address, -}) => { - const [delegationPoints, setDelegationPoints] = useState>( - new Map(), - ); - const { isApiNormal, isGeoBlocked } = useHealthCheck(); - - const fetchAllPoints = async () => { - const stakingTxHashHexes = delegationsAPI - .filter((delegation) => !delegation.isOverflow) - .map((delegation) => delegation.stakingTxHashHex); - - const chunks = []; - for ( - let i = 0; - i < stakingTxHashHexes.length; - i += MAX_DELEGATION_POINTS_BATCH_SIZE - ) { - chunks.push( - stakingTxHashHexes.slice(i, i + MAX_DELEGATION_POINTS_BATCH_SIZE), - ); - } - - const results = await Promise.all( - chunks.map((chunk) => getDelegationPointsByStakingTxHashHexes(chunk)), - ); - - return results.flatMap((result) => result); - }; - - const { data, isLoading, error } = useQuery({ - queryKey: ["delegationPoints", address, publicKeyNoCoord], - queryFn: fetchAllPoints, - enabled: - isWalletConnected && - delegationsAPI.length > 0 && - isApiNormal && - !isGeoBlocked && - shouldDisplayPoints(), - refetchInterval: 300000, // Refetch every 5 minutes - refetchOnWindowFocus: false, - retry: 1, - }); - - useEffect(() => { - if (data) { - const newDelegationPoints = new Map(); - data.forEach((point) => { - if (point) { - newDelegationPoints.set( - point.staking_tx_hash_hex, - point.staker.points, - ); - } - }); - setDelegationPoints(newDelegationPoints); - } - }, [data]); - - const value = { - delegationPoints, - isLoading, - error, - }; - - return ( - - {children} - - ); -}; diff --git a/src/app/hooks/client/api/useVersions.ts b/src/app/hooks/client/api/useVersions.ts deleted file mode 100644 index e5a35ad2..00000000 --- a/src/app/hooks/client/api/useVersions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getGlobalParams } from "@/app/api/getGlobalParams"; -import { useAPIQuery } from "@/app/hooks/client/api/useApi"; - -export const VERSIONS_KEY = "VERSIONS"; - -export function useVersions({ enabled = true }: { enabled?: boolean } = {}) { - const data = useAPIQuery({ - queryKey: [VERSIONS_KEY], - queryFn: getGlobalParams, - enabled, - }); - - return data; -} diff --git a/src/app/hooks/services/useTransactionService.ts b/src/app/hooks/services/useTransactionService.ts index ad7b33a0..474ea0a9 100644 --- a/src/app/hooks/services/useTransactionService.ts +++ b/src/app/hooks/services/useTransactionService.ts @@ -24,6 +24,7 @@ import { clearTxSignatures, extractSchnorrSignaturesFromTransaction, uint8ArrayToHex, + validateStakingInput, } from "@/utils/delegations"; import { getFeeRateFromMempool } from "@/utils/getFeeRateFromMempool"; import { getTxInfo, getTxMerkleProof } from "@/utils/mempool_api"; @@ -689,13 +690,6 @@ const createBtcDelegationMsg = async ( }; }; -const validateStakingInput = (stakingInput: BtcStakingInputs) => { - if (!stakingInput.finalityProviderPkNoCoordHex) - throw new Error("Finality provider not selected"); - if (!stakingInput.stakingAmountSat) throw new Error("Staking amount not set"); - if (!stakingInput.stakingTimelock) throw new Error("Staking time not set"); -}; - const checkWalletConnection = ( cosmosConnected: boolean, btcConnected: boolean, diff --git a/src/app/hooks/services/useV1TransactionService.ts b/src/app/hooks/services/useV1TransactionService.ts new file mode 100644 index 00000000..2e940d08 --- /dev/null +++ b/src/app/hooks/services/useV1TransactionService.ts @@ -0,0 +1,125 @@ +import { PsbtResult, Staking } from "@babylonlabs-io/btc-staking-ts"; +import { Psbt, Transaction } from "bitcoinjs-lib"; +import { useCallback } from "react"; + +import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; +import { useAppState } from "@/app/state"; +import { validateStakingInput } from "@/utils/delegations"; +import { txFeeSafetyCheck } from "@/utils/delegations/fee"; +import { getFeeRateFromMempool } from "@/utils/getFeeRateFromMempool"; +import { getBbnParamByBtcHeight } from "@/utils/params"; + +import { useNetworkFees } from "../client/api/useNetworkFees"; + +import { BtcStakingInputs } from "./useTransactionService"; + +export function useV1TransactionService() { + const { + connected: btcConnected, + signPsbt, + publicKeyNoCoord, + address, + signMessage, + network: btcNetwork, + pushTx, + } = useBTCWallet(); + const { data: networkFees } = useNetworkFees(); + const { defaultFeeRate } = getFeeRateFromMempool(networkFees); + const { networkInfo } = useAppState(); + + // We use phase-2 parameters instead of legacy global parameters. + // Phase-2 BBN parameters include all phase-1 global parameters, + // except for the "tag" field which is only used for staking transactions. + // The "tag" is not needed for withdrawal or unbonding transactions. + const bbnStakingParams = networkInfo?.params.bbnStakingParams.versions; + + /** + * Submit the withdrawal transaction + * For withdrawal from a staking transaction that has expired, or from an early + * unbonding transaction + * If earlyUnbondingTxHex is provided, the early unbonding transaction will be used, + * otherwise the staking transaction will be used + * + * @param stakingInput - The staking inputs + * @param stakingTxHex - The staking transaction hex + * @param earlyUnbondingTxHex - The early unbonding transaction hex + */ + const submitWithdrawalTx = useCallback( + async ( + stakingInput: BtcStakingInputs, + stakingHeight: number, + stakingTxHex: string, + earlyUnbondingTxHex?: string, + ) => { + // Perform checks + if (!bbnStakingParams) { + throw new Error("Staking params not loaded"); + } + if (!btcConnected || !btcNetwork) + throw new Error("BTC Wallet not connected"); + validateStakingInput(stakingInput); + + // Get the staking params at the time of the staking transaction + const stakingParam = getBbnParamByBtcHeight( + stakingHeight, + bbnStakingParams, + ); + + if (!stakingParam) { + throw new Error( + `Unable to find staking params for height ${stakingHeight}`, + ); + } + + // Warning: We using the "Staking" instead of "ObservableStaking" + // because we only perform withdraw or unbonding transactions + const staking = new Staking( + btcNetwork!, + { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + }, + stakingParam, + stakingInput.finalityProviderPkNoCoordHex, + stakingInput.stakingTimelock, + ); + + let psbtResult: PsbtResult; + if (earlyUnbondingTxHex) { + psbtResult = staking.createWithdrawEarlyUnbondedTransaction( + Transaction.fromHex(earlyUnbondingTxHex), + defaultFeeRate, + ); + } else { + psbtResult = staking.createWithdrawStakingExpiredTransaction( + Transaction.fromHex(stakingTxHex), + defaultFeeRate, + ); + } + + const signedWithdrawalPsbtHex = await signPsbt(psbtResult.psbt.toHex()); + const signedWithdrawalTx = Psbt.fromHex( + signedWithdrawalPsbtHex, + ).extractTransaction(); + + // Perform a safety check on the estimated transaction fee + txFeeSafetyCheck(signedWithdrawalTx, defaultFeeRate, psbtResult.fee); + + await pushTx(signedWithdrawalTx.toHex()); + }, + [ + bbnStakingParams, + btcConnected, + btcNetwork, + address, + publicKeyNoCoord, + signPsbt, + pushTx, + defaultFeeRate, + ], + ); + + return { + submitWithdrawalTx, + }; +} diff --git a/src/app/hooks/useVersionByHeight.ts b/src/app/hooks/useVersionByHeight.ts deleted file mode 100644 index 24677a3d..00000000 --- a/src/app/hooks/useVersionByHeight.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useMemo } from "react"; - -import { BbnStakingParamsVersion } from "../types/networkInfo"; - -import { useNetworkInfo } from "./client/api/useNetworkInfo"; - -export function useParamByHeight(height: number) { - const { data: networkInfo } = useNetworkInfo(); - return useMemo( - () => - getBbnParamByBtcHeight( - height, - networkInfo?.params.bbnStakingParams.versions ?? [], - ), - [networkInfo, height], - ); -} - -const getBbnParamByBtcHeight = ( - height: number, - bbnParams: BbnStakingParamsVersion[], -) => { - // Sort by btcActivationHeight in ascending order - const sortedParams = [...bbnParams].sort( - (a, b) => a.btcActivationHeight - b.btcActivationHeight, - ); - - // Find first param where height is >= btcActivationHeight - return sortedParams.find((param) => height >= param.btcActivationHeight); -}; diff --git a/src/config/index.ts b/src/config/index.ts index f7e92546..711a1beb 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -17,12 +17,6 @@ export const getNetworkAppUrl = (): string => { : "https://btcstaking.babylonlabs.io"; }; -// shouldDisplayPoints function is used to check if the application should -// display points or not based on the existence of the environment variable. -export const shouldDisplayPoints = (): boolean => { - return Boolean(process.env.NEXT_PUBLIC_POINTS_API_URL); -}; - // getBtcNetwork function is used to get the BTC network based on the environment export const getBtcNetwork = (): Network => { return network; diff --git a/src/utils/delegations/index.ts b/src/utils/delegations/index.ts index 2767cafd..46a432fb 100644 --- a/src/utils/delegations/index.ts +++ b/src/utils/delegations/index.ts @@ -1,5 +1,7 @@ import { Transaction } from "bitcoinjs-lib"; +import { BtcStakingInputs } from "@/app/hooks/services/useTransactionService"; + /** * Clears the signatures from a transaction. * @param tx - The transaction to clear the signatures from. @@ -44,3 +46,15 @@ export const uint8ArrayToHex = (uint8Array: Uint8Array): string => { .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); }; + +/** + * Validates the staking input for a delegation. + * @param stakingInput - The staking input to validate. + * @throws An error if the staking input is invalid. + */ +export const validateStakingInput = (stakingInput: BtcStakingInputs) => { + if (!stakingInput.finalityProviderPkNoCoordHex) + throw new Error("Finality provider not selected"); + if (!stakingInput.stakingAmountSat) throw new Error("Staking amount not set"); + if (!stakingInput.stakingTimelock) throw new Error("Staking time not set"); +}; diff --git a/src/utils/delegations/signWithdrawalTx.ts b/src/utils/delegations/signWithdrawalTx.ts deleted file mode 100644 index 9789c211..00000000 --- a/src/utils/delegations/signWithdrawalTx.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - PsbtResult, - withdrawEarlyUnbondedTransaction, - withdrawTimelockUnbondedTransaction, -} from "@babylonlabs-io/btc-staking-ts"; -import { Psbt, Transaction, networks } from "bitcoinjs-lib"; - -import { getGlobalParams } from "@/app/api/getGlobalParams"; -import { Delegation as DelegationInterface } from "@/app/types/delegations"; -import { apiDataToStakingScripts } from "@/utils/apiDataToStakingScripts"; -import { getCurrentGlobalParamsVersion } from "@/utils/params/globalParams"; - -import { getFeeRateFromMempool } from "../getFeeRateFromMempool"; -import { Fees } from "../wallet/btc_wallet_provider"; - -import { txFeeSafetyCheck } from "./fee"; - -// Sign a withdrawal transaction -// Returns: -// - withdrawalTx: the signed withdrawal transaction -// - delegation: the initial delegation -export const signWithdrawalTx = async ( - id: string, - delegationsAPI: DelegationInterface[], - publicKeyNoCoord: string, - btcWalletNetwork: networks.Network, - signPsbt: (psbtHex: string) => Promise, - address: string, - getNetworkFees: () => Promise, - pushTx: (txHex: string) => Promise, -): Promise<{ - withdrawalTxHex: string; - delegation: DelegationInterface; -}> => { - // Check if the data is available - if (!delegationsAPI) { - throw new Error("No back-end API data available"); - } - - // Find the delegation in the delegations retrieved from the API - const delegation = delegationsAPI.find( - (delegation) => delegation.stakingTxHashHex === id, - ); - if (!delegation) { - throw new Error("Delegation not found"); - } - - // Get the required data - const [paramVersions, fees] = await Promise.all([ - getGlobalParams(), - getNetworkFees(), - ]); - - // State of global params when the staking transaction was submitted - const { currentVersion: globalParamsWhenStaking } = - getCurrentGlobalParamsVersion( - delegation.stakingTx.startHeight, - paramVersions, - ); - - if (!globalParamsWhenStaking) { - throw new Error("Current version not found"); - } - - // Recreate the staking scripts - const { - timelockScript, - slashingScript, - unbondingScript, - unbondingTimelockScript, - } = apiDataToStakingScripts( - delegation.finalityProviderPkHex, - delegation.stakingTx.timelock, - globalParamsWhenStaking, - publicKeyNoCoord, - ); - - const feeRate = getFeeRateFromMempool(fees); - - // Create the withdrawal transaction - let withdrawPsbtTxResult: PsbtResult; - if (delegation?.unbondingTx) { - // Withdraw funds from an unbonding transaction that was submitted for early unbonding and the unbonding period has passed - withdrawPsbtTxResult = withdrawEarlyUnbondedTransaction( - { - unbondingTimelockScript, - slashingScript, - }, - Transaction.fromHex(delegation.unbondingTx.txHex), - address, - btcWalletNetwork, - feeRate.defaultFeeRate, - ); - } else { - // Withdraw funds from a staking transaction in which the timelock naturally expired - withdrawPsbtTxResult = withdrawTimelockUnbondedTransaction( - { - timelockScript, - slashingScript, - unbondingScript, - }, - Transaction.fromHex(delegation.stakingTx.txHex), - address, - btcWalletNetwork, - feeRate.defaultFeeRate, - delegation.stakingTx.outputIndex, - ); - } - - // Sign the withdrawal transaction - let withdrawalTx: Transaction; - - try { - const { psbt } = withdrawPsbtTxResult; - const signedWithdrawalPsbtHex = await signPsbt(psbt.toHex()); - withdrawalTx = Psbt.fromHex(signedWithdrawalPsbtHex).extractTransaction(); - } catch (error) { - throw new Error("Failed to sign PSBT for the withdrawal transaction"); - } - - // Get the withdrawal transaction hex - const withdrawalTxHex = withdrawalTx.toHex(); - // Perform a safety check on the estimated transaction fee - txFeeSafetyCheck( - withdrawalTx, - feeRate.defaultFeeRate, - withdrawPsbtTxResult.fee, - ); - - // Broadcast withdrawal transaction - await pushTx(withdrawalTxHex); - - return { withdrawalTxHex, delegation }; -}; diff --git a/src/utils/params/index.ts b/src/utils/params/index.ts new file mode 100644 index 00000000..28a061c8 --- /dev/null +++ b/src/utils/params/index.ts @@ -0,0 +1,14 @@ +import { BbnStakingParamsVersion } from "@/app/types/networkInfo"; + +export const getBbnParamByBtcHeight = ( + height: number, + bbnParams: BbnStakingParamsVersion[], +) => { + // Sort by btcActivationHeight in ascending order + const sortedParams = [...bbnParams].sort( + (a, b) => a.btcActivationHeight - b.btcActivationHeight, + ); + + // Find first param where height is >= btcActivationHeight + return sortedParams.find((param) => height >= param.btcActivationHeight); +};