Skip to content

Commit

Permalink
feat: add phase-1 unbonding method (#475)
Browse files Browse the repository at this point in the history
* feat: add phase-1 unbonding method
  • Loading branch information
jrwbabylonlab authored Dec 9, 2024
1 parent 931f830 commit aedf75a
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 7 deletions.
38 changes: 36 additions & 2 deletions src/app/components/Delegations/Delegations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions src/app/components/Modals/UnbondModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
} from "@babylonlabs-io/bbn-core-ui";

import { useIsMobileView } from "@/app/hooks/useBreakpoint";
import { getNetworkConfig } from "@/config/network.config";

interface UnbondModalProps {
isOpen: boolean;
onClose: () => void;
onProceed: () => void;
awaitingWalletResponse: boolean;
}
const { networkName, coinName } = getNetworkConfig();

export const UnbondModal = ({
isOpen,
Expand All @@ -25,7 +27,6 @@ export const UnbondModal = ({
awaitingWalletResponse,
}: UnbondModalProps) => {
const isMobileView = useIsMobileView();

const DialogComponent = isMobileView ? MobileDialog : Dialog;

return (
Expand All @@ -38,8 +39,8 @@ export const UnbondModal = ({
<DialogBody className="pb-8 pt-4 text-primary-dark">
<Text variant="body1">
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.
<br />
<br />
The expected unbonding time will be about 7 days. After unbonded, you
Expand Down
102 changes: 100 additions & 2 deletions src/app/hooks/services/useV1TransactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,7 +21,6 @@ export function useV1TransactionService() {
signPsbt,
publicKeyNoCoord,
address,
signMessage,
network: btcNetwork,
pushTx,
} = useBTCWallet();
Expand All @@ -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
Expand Down Expand Up @@ -72,7 +160,7 @@ export function useV1TransactionService() {
}

// Warning: We using the "Staking" instead of "ObservableStaking"
// because we only perform withdraw or unbonding transactions
// because withdrawal transactions does not require phase-1 specific tags
const staking = new Staking(
btcNetwork!,
{
Expand Down Expand Up @@ -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");
}
};

0 comments on commit aedf75a

Please sign in to comment.