From 7486503e7a31e195eab8b2f4b2fb1cb01569ef4b Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Mon, 9 Dec 2024 18:33:57 +1100 Subject: [PATCH] feat: add phase-1 unbonding method --- .../components/Delegations/Delegations.tsx | 38 ++++++- src/app/components/Modals/UnbondModal.tsx | 6 +- .../hooks/services/useV1TransactionService.ts | 100 +++++++++++++++++- 3 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index 589704c2..14256513 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -40,7 +40,7 @@ export const Delegations = ({}) => { isLoading, } = useDelegationState(); - const { submitWithdrawalTx } = useV1TransactionService(); + const { submitWithdrawalTx, submitUnbondingTx } = useV1TransactionService(); const { data: networkFees } = useNetworkFees(); const selectedDelegation = delegationsAPI?.delegations.find( @@ -93,6 +93,40 @@ export const Delegations = ({}) => { }); }; + // TODO: Hook up with unbonding modal + const handleUnbond = async (id: string) => { + try { + if (selectedDelegation?.stakingTxHashHex != id) { + throw new Error("Wrong delegation selected for withdrawal"); + } + // Sign the withdrawal transaction + const { stakingTx, finalityProviderPkHex, stakingValueSat } = + selectedDelegation; + + await submitUnbondingTx( + { + stakingTimelock: stakingTx.timelock, + finalityProviderPkNoCoordHex: finalityProviderPkHex, + stakingAmountSat: stakingValueSat, + }, + stakingTx.startHeight, + stakingTx.txHex, + ); + // Update the local state with the new intermediate delegation + updateLocalStorage( + selectedDelegation, + DelegationState.INTERMEDIATE_UNBONDING, + ); + } catch (error: Error | any) { + showError({ + error: { + message: error.message, + errorState: ErrorState.UNBONDING, + }, + }); + } + }; + // 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 @@ -110,7 +144,7 @@ export const Delegations = ({}) => { // Sign the withdrawal transaction const { stakingTx, finalityProviderPkHex, stakingValueSat, unbondingTx } = selectedDelegation; - submitWithdrawalTx( + await submitWithdrawalTx( { stakingTimelock: stakingTx.timelock, finalityProviderPkNoCoordHex: finalityProviderPkHex, diff --git a/src/app/components/Modals/UnbondModal.tsx b/src/app/components/Modals/UnbondModal.tsx index 2a6396da..23a68c8b 100644 --- a/src/app/components/Modals/UnbondModal.tsx +++ b/src/app/components/Modals/UnbondModal.tsx @@ -10,6 +10,7 @@ import { } from "@babylonlabs-io/bbn-core-ui"; import { useIsMobileView } from "@/app/hooks/useBreakpoint"; +import { getNetworkConfig } from "@/config/network.config"; interface UnbondModalProps { isOpen: boolean; @@ -24,6 +25,7 @@ export const UnbondModal = ({ onProceed, awaitingWalletResponse, }: UnbondModalProps) => { + const { networkName, coinName } = getNetworkConfig(); const isMobileView = useIsMobileView(); const DialogComponent = isMobileView ? MobileDialog : Dialog; @@ -38,8 +40,8 @@ export const UnbondModal = ({ You are about to unbond your stake before its expiration. A - transaction fee of 0.00005 Signet BTC will be deduced from your stake - by the BTC signet network. + transaction fee of 0.00005 {coinName} will be deduced from your stake + by the {networkName} network.

The expected unbonding time will be about 7 days. After unbonded, you diff --git a/src/app/hooks/services/useV1TransactionService.ts b/src/app/hooks/services/useV1TransactionService.ts index 2e940d08..b639caec 100644 --- a/src/app/hooks/services/useV1TransactionService.ts +++ b/src/app/hooks/services/useV1TransactionService.ts @@ -2,6 +2,8 @@ import { PsbtResult, Staking } from "@babylonlabs-io/btc-staking-ts"; import { Psbt, Transaction } from "bitcoinjs-lib"; import { useCallback } from "react"; +import { getUnbondingEligibility } from "@/app/api/getUnbondingEligibility"; +import { postUnbonding } from "@/app/api/postUnbonding"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; import { useAppState } from "@/app/state"; import { validateStakingInput } from "@/utils/delegations"; @@ -19,7 +21,6 @@ export function useV1TransactionService() { signPsbt, publicKeyNoCoord, address, - signMessage, network: btcNetwork, pushTx, } = useBTCWallet(); @@ -33,6 +34,93 @@ export function useV1TransactionService() { // The "tag" is not needed for withdrawal or unbonding transactions. const bbnStakingParams = networkInfo?.params.bbnStakingParams.versions; + /** + * Submit the unbonding transaction to babylon API for further processing + * The system will gather covenant signatures and submit the unbonding transaction + * to the Bitcoin network + * + * @param stakingInput - The staking inputs + * @param stakingHeight - The height of the staking transaction + * @param stakingTxHex - The staking transaction hex + */ + const submitUnbondingTx = useCallback( + async ( + stakingInput: BtcStakingInputs, + stakingHeight: number, + stakingTxHex: 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 unbonding transactions does not require phase-1 specific tags + const staking = new Staking( + btcNetwork!, + { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + }, + stakingParam, + stakingInput.finalityProviderPkNoCoordHex, + stakingInput.stakingTimelock, + ); + + const stakingTx = Transaction.fromHex(stakingTxHex); + + // Check if this staking transaction is eligible for unbonding + const eligibility = await getUnbondingEligibility(stakingTx.getId()); + if (!eligibility) { + throw new Error("Staking transaction is not eligible for unbonding"); + } + + const txResult = staking.createUnbondingTransaction(stakingTx); + + const psbt = staking.toUnbondingPsbt(txResult.transaction, stakingTx); + + const signedUnbondingPsbtHex = await signPsbt(psbt.toHex()); + const signedUnbondingTx = Psbt.fromHex( + signedUnbondingPsbtHex, + ).extractTransaction(); + + const stakerSignatureHex = getStakerSignature(signedUnbondingTx); + try { + await postUnbonding( + stakerSignatureHex, + stakingTx.getId(), + signedUnbondingTx.getId(), + signedUnbondingTx.toHex(), + ); + } catch (error) { + throw new Error(`Error submitting unbonding transaction: ${error}`); + } + }, + [ + bbnStakingParams, + btcConnected, + btcNetwork, + address, + publicKeyNoCoord, + signPsbt, + ], + ); + /** * Submit the withdrawal transaction * For withdrawal from a staking transaction that has expired, or from an early @@ -120,6 +208,16 @@ export function useV1TransactionService() { ); return { + submitUnbondingTx, submitWithdrawalTx, }; } + +// Get the staker signature from the unbonding transaction +const getStakerSignature = (unbondingTx: Transaction): string => { + try { + return unbondingTx.ins[0].witness[0].toString("hex"); + } catch (error) { + throw new Error("Failed to get staker signature"); + } +};