diff --git a/package-lock.json b/package-lock.json index daa17320..b4c01139 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.3.10", + "version": "0.3.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.3.10", + "version": "0.3.9", "dependencies": { "@babylonlabs-io/babylon-proto-ts": "0.0.3-canary.2", "@babylonlabs-io/btc-staking-ts": "0.4.0-canary.2", diff --git a/src/app/api/getDelegations.ts b/src/app/api/getDelegations.ts index 135caceb..4507b70e 100644 --- a/src/app/api/getDelegations.ts +++ b/src/app/api/getDelegations.ts @@ -24,6 +24,7 @@ interface DelegationAPI { staking_tx: StakingTxAPI; unbonding_tx?: UnbondingTxAPI; is_overflow: boolean; + transitioned: boolean; } interface StakingTxAPI { @@ -87,6 +88,7 @@ export const getDelegations = async ( outputIndex: apiDelegation.unbonding_tx.output_index, } : undefined, + transitioned: apiDelegation.transitioned, }), ); diff --git a/src/app/api/getParams.ts b/src/app/api/getParams.ts index 777063a6..de66691a 100644 --- a/src/app/api/getParams.ts +++ b/src/app/api/getParams.ts @@ -52,7 +52,8 @@ export const getParams = async (): Promise => { maxStakingValueSat: v.max_staking_value_sat, minStakingTimeBlocks: v.min_staking_time_blocks, maxStakingTimeBlocks: v.max_staking_time_blocks, - unbondingTime: v.min_unbonding_time_blocks, + // TODO: To be reverted after https://github.com/babylonlabs-io/babylon/issues/263 + unbondingTime: v.min_unbonding_time_blocks + 1, unbondingFeeSat: v.unbonding_fee_sat, minCommissionRate: v.min_commission_rate, maxActiveFinalityProviders: v.max_active_finality_providers, @@ -66,14 +67,22 @@ export const getParams = async (): Promise => { minStakingAmountSat: v.min_staking_value_sat, })); - const latestVersion = versions.reduce((prev, current) => + const latestParam = versions.reduce((prev, current) => current.version > prev.version ? current : prev, ); + // Find the genesis params which is the first version + // This param can be used for transitioning phase-1 delegations + const genesisParam = versions.find((v) => v.version === 0); + if (!genesisParam) { + throw new Error("Genesis params not found"); + } + return { bbnStakingParams: { - latestVersion, - versions, + latestParam, + bbnStakingParams: versions, + genesisParam, }, }; }; diff --git a/src/app/components/Delegations/Delegation.tsx b/src/app/components/Delegations/Delegation.tsx index 05e7fb81..84d2bd59 100644 --- a/src/app/components/Delegations/Delegation.tsx +++ b/src/app/components/Delegations/Delegation.tsx @@ -4,9 +4,15 @@ import { FaBitcoin } from "react-icons/fa"; import { IoIosWarning } from "react-icons/io"; import { Tooltip } from "react-tooltip"; +import { + SigningStep, + useTransactionService, +} from "@/app/hooks/services/useTransactionService"; import { useHealthCheck } from "@/app/hooks/useHealthCheck"; -import { DelegationState, StakingTx } from "@/app/types/delegations"; -import { GlobalParamsVersion } from "@/app/types/globalParams"; +import { + Delegation as DelegationInterface, + DelegationState, +} from "@/app/types/delegations"; import { shouldDisplayPoints } from "@/config"; import { getNetworkConfig } from "@/config/network.config"; import { satoshiToBtc } from "@/utils/btcConversions"; @@ -18,36 +24,34 @@ import { trim } from "@/utils/trim"; import { DelegationPoints } from "../Points/DelegationPoints"; interface DelegationProps { - stakingTx: StakingTx; - stakingValueSat: number; - stakingTxHash: string; - state: string; - onUnbond: (id: string) => void; + delegation: DelegationInterface; onWithdraw: (id: string) => void; // This attribute is set when an action has been taken by the user // that should change the status but the back-end // has not had time to reflect this change yet intermediateState?: string; - isOverflow: boolean; - globalParamsVersion: GlobalParamsVersion; } export const Delegation: React.FC = ({ - stakingTx, - stakingTxHash, - state, - stakingValueSat, - onUnbond, + delegation, onWithdraw, intermediateState, - isOverflow, - globalParamsVersion, }) => { + const { + stakingTx, + stakingTxHashHex, + state, + stakingValueSat, + isOverflow, + finalityProviderPkHex, + } = delegation; + const { startTimestamp } = stakingTx; const [currentTime, setCurrentTime] = useState(Date.now()); const { isApiNormal, isGeoBlocked } = useHealthCheck(); const shouldShowPoints = isApiNormal && !isGeoBlocked && shouldDisplayPoints(); + const { transitionPhase1Delegation } = useTransactionService(); useEffect(() => { const timerId = setInterval(() => { @@ -57,8 +61,28 @@ export const Delegation: React.FC = ({ return () => clearInterval(timerId); }, []); + // TODO: Hook up with the transaction signing modal + const transitionCallback = async (step: SigningStep) => { + console.log(step); + }; + + const onTransition = async () => { + // TODO: Open the transaction signing modal + + await transitionPhase1Delegation( + stakingTx.txHex, + { + finalityProviderPkNoCoordHex: finalityProviderPkHex, + stakingAmountSat: stakingValueSat, + stakingTimeBlocks: stakingTx.timelock, + }, + transitionCallback, + ); + // TODO: Close the transaction signing modal and update the UI + }; + const generateActionButton = () => { - // This function generates the unbond or withdraw button + // This function generates the transition or withdraw button // based on the state of the delegation // It also disables the button if the delegation // is in an intermediate state (local storage) @@ -67,12 +91,12 @@ export const Delegation: React.FC = ({
); @@ -81,7 +105,7 @@ export const Delegation: React.FC = ({
{generateActionButton()}
diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index e923b7e8..12e66cd8 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -9,35 +9,26 @@ import { useError } from "@/app/context/Error/ErrorContext"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; import { useDelegations } from "@/app/hooks/api/useDelegations"; import { useHealthCheck } from "@/app/hooks/useHealthCheck"; -import { useAppState } from "@/app/state"; import { useDelegationState } from "@/app/state/DelegationState"; import { Delegation as DelegationInterface, DelegationState, } from "@/app/types/delegations"; import { ErrorState } from "@/app/types/errors"; -import { GlobalParamsVersion } from "@/app/types/globalParams"; import { shouldDisplayPoints } from "@/config"; -import { signUnbondingTx } from "@/utils/delegations/signUnbondingTx"; import { signWithdrawalTx } from "@/utils/delegations/signWithdrawalTx"; import { getIntermediateDelegationsLocalStorageKey } from "@/utils/local_storage/getIntermediateDelegationsLocalStorageKey"; import { toLocalStorageIntermediateDelegation } from "@/utils/local_storage/toLocalStorageIntermediateDelegation"; -import { - MODE, - MODE_UNBOND, - MODE_WITHDRAW, - UnbondWithdrawModal, -} from "../Modals/UnbondWithdrawModal"; +import { MODE, MODE_WITHDRAW, WithdrawModal } from "../Modals/WithdrawModal"; import { Delegation } from "./Delegation"; export const Delegations = () => { - const { currentVersion } = useAppState(); const { data: delegationsAPI } = useDelegations(); const { address, publicKeyNoCoord, connected, network } = useBTCWallet(); - if (!connected || !delegationsAPI || !currentVersion || !network) { + if (!connected || !delegationsAPI || !network) { return; } @@ -50,7 +41,6 @@ export const Delegations = () => { > { interface DelegationsContentProps { delegationsAPI: DelegationInterface[]; - globalParamsVersion: GlobalParamsVersion; publicKeyNoCoord: string; btcWalletNetwork: networks.Network; address: string; @@ -71,7 +60,6 @@ interface DelegationsContentProps { const DelegationsContent: React.FC = ({ delegationsAPI, - globalParamsVersion, address, btcWalletNetwork, publicKeyNoCoord, @@ -99,7 +87,7 @@ const DelegationsContent: React.FC = ({ const shouldShowPoints = isApiNormal && !isGeoBlocked && shouldDisplayPoints(); - // Local storage state for intermediate delegations (withdrawing, unbonding) + // Local storage state for intermediate delegations (transitioning, withdrawing) const intermediateDelegationsLocalStorageKey = getIntermediateDelegationsLocalStorageKey(publicKeyNoCoord); @@ -145,39 +133,6 @@ const DelegationsContent: React.FC = ({ }); }; - // Handles unbonding requests for Active delegations that want to be withdrawn early - // It constructs an unbonding transaction, creates a signature for it, and submits both to the back-end API - const handleUnbond = async (id: string) => { - try { - // Prevent the modal from closing - setAwaitingWalletResponse(true); - // Sign the unbonding transaction - const { delegation } = await signUnbondingTx( - id, - delegationsAPI, - publicKeyNoCoord, - btcWalletNetwork, - signPsbt, - ); - // Update the local state with the new intermediate delegation - updateLocalStorage(delegation, DelegationState.INTERMEDIATE_UNBONDING); - } catch (error: Error | any) { - showError({ - error: { - message: error.message, - errorState: ErrorState.UNBONDING, - }, - retryAction: () => handleModal(id, MODE_UNBOND), - }); - captureError(error); - } finally { - setModalOpen(false); - setTxID(""); - setModalMode(undefined); - setAwaitingWalletResponse(false); - } - }; - // 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 const handleWithdraw = async (id: string) => { @@ -243,13 +198,9 @@ const DelegationsContent: React.FC = ({ // conditions based on intermediate states if ( intermediateDelegation.state === - DelegationState.INTERMEDIATE_UNBONDING + DelegationState.INTERMEDIATE_TRANSITIONING ) { - return !( - matchingDelegation.state === DelegationState.UNBONDING_REQUESTED || - matchingDelegation.state === DelegationState.UNBONDING || - matchingDelegation.state === DelegationState.UNBONDED - ); + return !(matchingDelegation.state === DelegationState.TRANSITIONED); } if ( @@ -317,13 +268,7 @@ const DelegationsContent: React.FC = ({ > {combinedDelegationsData?.map((delegation) => { if (!delegation) return null; - const { - stakingValueSat, - stakingTx, - stakingTxHashHex, - state, - isOverflow, - } = delegation; + const { stakingTx, stakingTxHashHex } = delegation; const intermediateDelegation = intermediateDelegationsLocalStorage.find( (item) => item.stakingTxHashHex === stakingTxHashHex, @@ -332,17 +277,11 @@ const DelegationsContent: React.FC = ({ return ( handleModal(stakingTxHashHex, MODE_UNBOND)} + delegation={delegation} onWithdraw={() => handleModal(stakingTxHashHex, MODE_WITHDRAW) } intermediateState={intermediateDelegation?.state} - isOverflow={isOverflow} - globalParamsVersion={globalParamsVersion} /> ); })} @@ -351,13 +290,11 @@ const DelegationsContent: React.FC = ({ )} {modalMode && txID && delegation && ( - setModalOpen(false)} onProceed={() => { - modalMode === MODE_UNBOND - ? handleUnbond(txID) - : handleWithdraw(txID); + handleWithdraw(txID); }} mode={modalMode} awaitingWalletResponse={awaitingWalletResponse} diff --git a/src/app/components/FAQ/FAQ.tsx b/src/app/components/FAQ/FAQ.tsx index 42af7e71..15aeb44f 100644 --- a/src/app/components/FAQ/FAQ.tsx +++ b/src/app/components/FAQ/FAQ.tsx @@ -1,4 +1,3 @@ -import { useAppState } from "@/app/state"; import { getNetworkConfig } from "@/config/network.config"; import { questions } from "./data/questions"; @@ -7,18 +6,15 @@ import { Section } from "./Section"; interface FAQProps {} export const FAQ: React.FC = () => { - const { currentVersion } = useAppState(); const { coinName, networkName } = getNetworkConfig(); + // TODO: To be handled by https://github.com/babylonlabs-io/simple-staking/issues/325 + const confirmationDepth = 10; return (

FAQ

- {questions( - coinName, - networkName, - currentVersion?.confirmationDepth, - ).map((question) => ( + {questions(coinName, networkName, confirmationDepth).map((question) => (
= ({ +export const WithdrawModal: React.FC = ({ open, onClose, onProceed, - mode, awaitingWalletResponse, - delegation, }) => { - const { currentVersion: delegationGlobalParams } = useVersionByHeight( - delegation.stakingTx.startHeight ?? 0, - ); - - const unbondingFeeSat = delegationGlobalParams?.unbondingFeeSat ?? 0; - const unbondingTimeBlocks = delegationGlobalParams?.unbondingTime ?? 0; - - const unbondTitle = "Unbond"; - - const unbondContent = ( - <> - You are about to unbond your stake before its expiration.
A - transaction fee of{" "} - - {maxDecimals(satoshiToBtc(unbondingFeeSat), 8) || 0} {coinName} - {" "} - will be deduced from your stake by the {networkName} network.
- The expected unbonding time will be about{" "} - {blocksToDisplayTime(unbondingTimeBlocks)}.
- After unbonded, you will need to use this dashboard to withdraw your stake - for it to appear in your wallet. - - ); - const withdrawTitle = "Withdraw"; const withdrawContent = ( <> @@ -66,8 +36,8 @@ export const UnbondWithdrawModal: React.FC = ({ ); - const title = mode === MODE_UNBOND ? unbondTitle : withdrawTitle; - const content = mode === MODE_UNBOND ? unbondContent : withdrawContent; + const title = withdrawTitle; + const content = withdrawContent; return ( = ({ - stakingTxHash, + stakingTxHashHex, className, }) => { const { isApiNormal, isGeoBlocked } = useHealthCheck(); @@ -22,7 +22,7 @@ export const DelegationPoints: React.FC = ({ return null; } - const points = delegationPoints.get(stakingTxHash); + const points = delegationPoints.get(stakingTxHashHex); if (isLoading) { return (
diff --git a/src/app/components/Staking/Form/States/Message.tsx b/src/app/components/Staking/Form/States/Message.tsx index 8afb1bbe..cefc334f 100644 --- a/src/app/components/Staking/Form/States/Message.tsx +++ b/src/app/components/Staking/Form/States/Message.tsx @@ -6,10 +6,6 @@ interface MessageProps { icon: any; } -// Used for -// - Staking cap reached -// - Staking has not started yet -// - Staking params are upgrading export const Message: React.FC = ({ title, messages, icon }) => { return (
diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index d16762eb..5493cc31 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -8,11 +8,10 @@ import { LoadingView } from "@/app/components/Loading/Loading"; import { EOIModal } from "@/app/components/Modals/EOIModal"; import { useError } from "@/app/context/Error/ErrorContext"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; -import { useParams } from "@/app/hooks/api/useParams"; import { SigningStep, - useEoiCreationService, -} from "@/app/hooks/services/useEoiCreationService"; + useTransactionService, +} from "@/app/hooks/services/useTransactionService"; import { useHealthCheck } from "@/app/hooks/useHealthCheck"; import { useAppState } from "@/app/state"; import { ErrorHandlerParam, ErrorState } from "@/app/types/errors"; @@ -20,7 +19,6 @@ import { FinalityProvider, FinalityProvider as FinalityProviderInterface, } from "@/app/types/finalityProviders"; -import { createStakingTx } from "@/utils/delegations/signStakingTx"; import { getFeeRateFromMempool } from "@/utils/getFeeRateFromMempool"; import { isStakingSignReady } from "@/utils/isStakingSignReady"; @@ -39,7 +37,6 @@ import geoRestricted from "./Form/States/geo-restricted.svg"; export const Staking = () => { const { availableUTXOs, - currentVersion, totalBalance: btcWalletBalanceSat, isError, isLoading, @@ -75,8 +72,9 @@ export const Staking = () => { const [cancelFeedbackModalOpened, setCancelFeedbackModalOpened] = useLocalStorage("bbn-staking-cancelFeedbackModalOpened ", false); - const { createDelegationEoi } = useEoiCreationService(); - const { data: params } = useParams(); + const { createDelegationEoi, estimateStakingFee } = useTransactionService(); + const { params } = useAppState(); + const latestParam = params?.bbnStakingParams?.latestParam; // Mempool fee rates, comes from the network // Fetch fee rates, sat/vB @@ -160,12 +158,12 @@ export const Staking = () => { throw new Error("Finality provider not selected"); } const eoiInput = { - finalityProviderPublicKey: finalityProvider.btcPk, + finalityProviderPkNoCoordHex: finalityProvider.btcPk, stakingAmountSat, stakingTimeBlocks, feeRate, }; - await createDelegationEoi(eoiInput, signingCallback); + await createDelegationEoi(eoiInput, feeRate, signingCallback); // TODO: Hook up with the react pending verify modal handleResetState(); } catch (error: Error | any) { @@ -195,10 +193,10 @@ export const Staking = () => { if ( btcWalletNetwork && address && + latestParam && publicKeyNoCoord && stakingAmountSat && finalityProvider && - currentVersion && mempoolFeeRates && availableUTXOs ) { @@ -208,19 +206,15 @@ export const Staking = () => { throw new Error("Selected fee rate is lower than the hour fee"); } const memoizedFeeRate = selectedFeeRate || defaultFeeRate; - // Calculate the staking fee - const { stakingFeeSat } = createStakingTx( - currentVersion, + + const eoiInput = { + finalityProviderPkNoCoordHex: finalityProvider.btcPk, stakingAmountSat, stakingTimeBlocks, - finalityProvider.btcPk, - btcWalletNetwork, - address, - publicKeyNoCoord, - memoizedFeeRate, - availableUTXOs, - ); - return stakingFeeSat; + feeRate: memoizedFeeRate, + }; + // Calculate the staking fee + return estimateStakingFee(eoiInput, memoizedFeeRate); } catch (error: Error | any) { let errorMsg = error?.message; // Turn the error message into a user-friendly message @@ -248,17 +242,18 @@ export const Staking = () => { }, [ btcWalletNetwork, address, + latestParam, publicKeyNoCoord, stakingAmountSat, - stakingTimeBlocks, finalityProvider, - currentVersion, mempoolFeeRates, - selectedFeeRate, availableUTXOs, - showError, - defaultFeeRate, + selectedFeeRate, minFeeRate, + defaultFeeRate, + stakingTimeBlocks, + estimateStakingFee, + showError, ]); // Select the finality provider from the list @@ -352,8 +347,7 @@ export const Staking = () => { } // Staking form else { - const stakingParams = params?.bbnStakingParams.latestVersion; - if (!stakingParams) { + if (!latestParam) { throw new Error("Staking params not loaded"); } const { @@ -363,7 +357,7 @@ export const Staking = () => { maxStakingTimeBlocks, unbondingTime, unbondingFeeSat, - } = stakingParams; + } = latestParam; // Staking time is fixed const stakingTimeFixed = minStakingTimeBlocks === maxStakingTimeBlocks; diff --git a/src/app/components/Summary/Summary.tsx b/src/app/components/Summary/Summary.tsx index f84ce9f2..bc85a0e3 100644 --- a/src/app/components/Summary/Summary.tsx +++ b/src/app/components/Summary/Summary.tsx @@ -18,11 +18,13 @@ import { StakerPoints } from "../Points/StakerPoints"; export const Summary = () => { const { isApiNormal, isGeoBlocked } = useHealthCheck(); const { totalStaked } = useDelegationState(); - const { totalBalance, currentVersion, isLoading: loading } = useAppState(); + const { totalBalance, isLoading: loading } = useAppState(); const { address, publicKeyNoCoord } = useBTCWallet(); const { coinName } = getNetworkConfig(); const onMainnet = getNetworkConfig().network === Network.MAINNET; + // TODO: To be handled by https://github.com/babylonlabs-io/simple-staking/issues/325 + const confirmationDepth = 10; if (!address) return; @@ -38,7 +40,7 @@ export const Summary = () => { diff --git a/src/app/hooks/services/useEoiCreationService.tsx b/src/app/hooks/services/useEoiCreationService.tsx deleted file mode 100644 index 03b5a567..00000000 --- a/src/app/hooks/services/useEoiCreationService.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; -import { - BTCSigType, - ProofOfPossessionBTC, -} from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop"; -import { Staking } from "@babylonlabs-io/btc-staking-ts"; -import { fromBech32 } from "@cosmjs/encoding"; -import { Psbt } from "bitcoinjs-lib"; -import { useCallback } from "react"; - -import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; -import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; -import { useAppState } from "@/app/state"; -import { - clearTxSignatures, - extractSchnorrSignaturesFromTransaction, - uint8ArrayToHex, -} from "@/utils/delegations"; - -import { useParams } from "../api/useParams"; - -export interface BtcStakingInputs { - finalityProviderPublicKey: string; - stakingAmountSat: number; - stakingTimeBlocks: number; - feeRate: number; -} - -export enum SigningStep { - STAKING_SLASHING = "staking_slashing", - UNBONDING_SLASHING = "unbonding_slashing", - PROOF_OF_POSSESSION = "proof_of_possession", - SEND_BBN = "send_bbn", -} - -export const useEoiCreationService = () => { - const { availableUTXOs: inputUTXOs } = useAppState(); - const { - connected: cosmosConnected, - bech32Address, - signingStargateClient, - } = useCosmosWallet(); - const { - connected: btcConnected, - signPsbt, - publicKeyNoCoord, - address, - signMessage, - network: btcNetwork, - } = useBTCWallet(); - - const { data: params } = useParams(); - - const createDelegationEoi = useCallback( - async ( - btcInput: BtcStakingInputs, - signingCallback: (step: SigningStep) => Promise, - ) => { - const stakingParams = params?.bbnStakingParams.latestVersion; - if (!stakingParams) { - throw new Error("Staking params not loaded"); - } - // Perform initial validation - if ( - !cosmosConnected || - !btcConnected || - !btcNetwork || - !signingStargateClient - ) { - throw new Error("Wallet not connected"); - } - if (!params) { - throw new Error("Staking params not loaded"); - } - if (!btcInput.finalityProviderPublicKey) { - throw new Error("Finality provider not selected"); - } - if (!btcInput.stakingAmountSat) { - throw new Error("Staking amount not set"); - } - if (!btcInput.stakingTimeBlocks) { - throw new Error("Staking time not set"); - } - if (!inputUTXOs || inputUTXOs.length === 0) { - throw new Error("No input UTXOs"); - } - if (!btcInput.feeRate) { - throw new Error("Fee rate not set"); - } - - const staking = new Staking( - btcNetwork, - { - address, - publicKeyNoCoordHex: publicKeyNoCoord, - }, - stakingParams, - btcInput.finalityProviderPublicKey, - btcInput.stakingTimeBlocks, - ); - - // Create and sign staking transaction - const { psbt: stakingPsbt } = staking.createStakingTransaction( - btcInput.stakingAmountSat, - inputUTXOs, - btcInput.feeRate, - ); - // TODO: This is temporary solution until we have - // https://github.com/babylonlabs-io/btc-staking-ts/issues/40 - const signedStakingPsbtHex = await signPsbt(stakingPsbt.toHex()); - const stakingTx = Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); - const cleanedStakingTx = clearTxSignatures(stakingTx); - - // Create and sign unbonding transactionst - const { psbt: unbondingPsbt } = - staking.createUnbondingTransaction(cleanedStakingTx); - // TODO: This is temporary solution until we have - // https://github.com/babylonlabs-io/btc-staking-ts/issues/40 - const signedUnbondingPsbtHex = await signPsbt(unbondingPsbt.toHex()); - const unbondingTx = Psbt.fromHex( - signedUnbondingPsbtHex, - ).extractTransaction(); - const cleanedUnbondingTx = clearTxSignatures(unbondingTx); - - // Create slashing transactions and extract signatures - const { psbt: slashingPsbt } = - staking.createStakingOutputSlashingTransaction(cleanedStakingTx); - const signedSlashingPsbtHex = await signPsbt(slashingPsbt.toHex()); - const signedSlashingTx = Psbt.fromHex( - signedSlashingPsbtHex, - ).extractTransaction(); - const slashingSig = - extractSchnorrSignaturesFromTransaction(signedSlashingTx); - if (!slashingSig) { - throw new Error( - "No signature found in the staking output slashing PSBT", - ); - } - await signingCallback(SigningStep.STAKING_SLASHING); - - const { psbt: unbondingSlashingPsbt } = - staking.createUnbondingOutputSlashingTransaction(unbondingTx); - const signedUnbondingSlashingPsbtHex = await signPsbt( - unbondingSlashingPsbt.toHex(), - ); - const signedUnbondingSlashingTx = Psbt.fromHex( - signedUnbondingSlashingPsbtHex, - ).extractTransaction(); - const unbondingSignatures = extractSchnorrSignaturesFromTransaction( - signedUnbondingSlashingTx, - ); - if (!unbondingSignatures) { - throw new Error( - "No signature found in the unbonding output slashing PSBT", - ); - } - await signingCallback(SigningStep.UNBONDING_SLASHING); - - // Create Proof of Possession - const bech32AddressHex = uint8ArrayToHex(fromBech32(bech32Address).data); - const signedBbnAddress = await signMessage(bech32AddressHex, "ecdsa"); - const ecdsaSig = Uint8Array.from( - globalThis.Buffer.from(signedBbnAddress, "base64"), - ); - const proofOfPossession: ProofOfPossessionBTC = { - btcSigType: BTCSigType.ECDSA, - btcSig: ecdsaSig, - }; - await signingCallback(SigningStep.PROOF_OF_POSSESSION); - - // Prepare and send protobuf message - const msg: btcstakingtx.MsgCreateBTCDelegation = - btcstakingtx.MsgCreateBTCDelegation.fromPartial({ - stakerAddr: bech32Address, - pop: proofOfPossession, - btcPk: Uint8Array.from(Buffer.from(publicKeyNoCoord, "hex")), - fpBtcPkList: [ - Uint8Array.from( - Buffer.from(btcInput.finalityProviderPublicKey, "hex"), - ), - ], - stakingTime: btcInput.stakingTimeBlocks, - stakingValue: btcInput.stakingAmountSat, - stakingTx: Uint8Array.from(cleanedStakingTx.toBuffer()), - slashingTx: Uint8Array.from( - Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex"), - ), - delegatorSlashingSig: Uint8Array.from(slashingSig), - // TODO: Confirm with core on the value whether its inclusive or exclusive - unbondingTime: stakingParams.unbondingTime + 1, - unbondingTx: Uint8Array.from(cleanedUnbondingTx.toBuffer()), - unbondingValue: - btcInput.stakingAmountSat - stakingParams.unbondingFeeSat, - unbondingSlashingTx: Uint8Array.from( - Buffer.from( - clearTxSignatures(signedUnbondingSlashingTx).toHex(), - "hex", - ), - ), - delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures), - stakingTxInclusionProof: undefined, - }); - - const protoMsg = { - typeUrl: "/babylon.btcstaking.v1.MsgCreateBTCDelegation", - value: msg, - }; - - // estimate gas - const gasEstimate = await signingStargateClient.simulate( - bech32Address, - [protoMsg], - "estimate fee", - ); - const gasWanted = Math.ceil(gasEstimate * 1.2); - const fee = { - amount: [{ denom: "ubbn", amount: (gasWanted * 0.01).toFixed(0) }], - gas: gasWanted.toString(), - }; - // sign it - await signingStargateClient.signAndBroadcast( - bech32Address, - [protoMsg], - fee, - ); - await signingCallback(SigningStep.SEND_BBN); - }, - [ - cosmosConnected, - btcConnected, - btcNetwork, - params, - inputUTXOs, - address, - publicKeyNoCoord, - signPsbt, - signMessage, - bech32Address, - signingStargateClient, - ], - ); - - return { createDelegationEoi }; -}; diff --git a/src/app/hooks/services/useTransactionService.tsx b/src/app/hooks/services/useTransactionService.tsx new file mode 100644 index 00000000..a46e3377 --- /dev/null +++ b/src/app/hooks/services/useTransactionService.tsx @@ -0,0 +1,390 @@ +import { btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; +import { InclusionProof } from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/btcstaking"; +import { + BTCSigType, + ProofOfPossessionBTC, +} from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop"; +import { Staking } from "@babylonlabs-io/btc-staking-ts"; +import { fromBech32 } from "@cosmjs/encoding"; +import { SigningStargateClient } from "@cosmjs/stargate"; +import { Network, Psbt, Transaction } from "bitcoinjs-lib"; +import { useCallback } from "react"; + +import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; +import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; +import { useAppState } from "@/app/state"; +import { BbnStakingParamsVersion } from "@/app/types/params"; +import { + clearTxSignatures, + extractSchnorrSignaturesFromTransaction, + uint8ArrayToHex, +} from "@/utils/delegations"; + +export interface BtcStakingInputs { + finalityProviderPkNoCoordHex: string; + stakingAmountSat: number; + stakingTimeBlocks: number; +} + +interface BtcSigningFuncs { + signPsbt: (psbtHex: string) => Promise; + signMessage: ( + message: string, + type?: "ecdsa" | "bip322-simple", + ) => Promise; + signingCallback: (step: SigningStep) => Promise; +} + +export enum SigningStep { + STAKING_SLASHING = "staking_slashing", + UNBONDING_SLASHING = "unbonding_slashing", + PROOF_OF_POSSESSION = "proof_of_possession", + SEND_BBN = "send_bbn", +} + +export const useTransactionService = () => { + const { availableUTXOs: inputUTXOs } = useAppState(); + const { + connected: cosmosConnected, + bech32Address, + signingStargateClient, + } = useCosmosWallet(); + const { + connected: btcConnected, + signPsbt, + publicKeyNoCoord, + address, + signMessage, + network: btcNetwork, + } = useBTCWallet(); + + const { params } = useAppState(); + const latestParam = params?.bbnStakingParams?.latestParam; + const genesisParam = params?.bbnStakingParams?.genesisParam; + + const createDelegationEoi = useCallback( + async ( + stakingInput: BtcStakingInputs, + feeRate: number, + signingCallback: (step: SigningStep) => Promise, + ) => { + // Perform checks + checkWalletConnection( + cosmosConnected, + btcConnected, + btcNetwork, + signingStargateClient, + ); + validateStakingInput(stakingInput); + if (!latestParam) throw new Error("Staking params not loaded"); + + const staking = new Staking( + btcNetwork!, + { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + }, + latestParam, + stakingInput.finalityProviderPkNoCoordHex, + stakingInput.stakingTimeBlocks, + ); + + // Create and sign staking transaction + const { psbt: stakingPsbt } = staking.createStakingTransaction( + stakingInput.stakingAmountSat, + inputUTXOs!, + feeRate, + ); + // TODO: This is temporary solution until we have + // https://github.com/babylonlabs-io/btc-staking-ts/issues/40 + const signedStakingPsbtHex = await signPsbt(stakingPsbt.toHex()); + const stakingTx = Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); + + const delegationMsg = await createBtcDelegationMsg( + staking, + stakingInput, + stakingTx, + bech32Address, + { address, publicKeyNoCoordHex: publicKeyNoCoord }, + latestParam, + { signPsbt, signMessage, signingCallback }, + ); + await sendBbnTx(signingStargateClient!, bech32Address, delegationMsg); + await signingCallback(SigningStep.SEND_BBN); + }, + [ + cosmosConnected, + btcConnected, + btcNetwork, + signingStargateClient, + latestParam, + address, + publicKeyNoCoord, + inputUTXOs, + signPsbt, + bech32Address, + signMessage, + ], + ); + + const estimateStakingFee = useCallback( + (stakingInput: BtcStakingInputs, feeRate: number): number => { + // Perform checks + checkWalletConnection( + cosmosConnected, + btcConnected, + btcNetwork, + signingStargateClient, + ); + if (!latestParam) throw new Error("Staking params not loaded"); + validateStakingInput(stakingInput); + + const staking = new Staking( + btcNetwork!, + { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + }, + latestParam, + stakingInput.finalityProviderPkNoCoordHex, + stakingInput.stakingTimeBlocks, + ); + + const { fee: stakingFee } = staking.createStakingTransaction( + stakingInput.stakingAmountSat, + inputUTXOs!, + feeRate, + ); + return stakingFee; + }, + [ + cosmosConnected, + btcConnected, + btcNetwork, + signingStargateClient, + latestParam, + address, + publicKeyNoCoord, + inputUTXOs, + ], + ); + + const transitionPhase1Delegation = useCallback( + async ( + stakingTxHex: string, + stakingInput: BtcStakingInputs, + signingCallback: (step: SigningStep) => Promise, + ) => { + // Perform checks + checkWalletConnection( + cosmosConnected, + btcConnected, + btcNetwork, + signingStargateClient, + ); + if (!genesisParam) throw new Error("Genesis params not loaded"); + validateStakingInput(stakingInput); + + const stakingTx = Transaction.fromHex(stakingTxHex); + const stakingInstance = new Staking( + btcNetwork!, + { address, publicKeyNoCoordHex: publicKeyNoCoord }, + genesisParam, + stakingInput.finalityProviderPkNoCoordHex, + stakingInput.stakingTimeBlocks, + ); + + const delegationMsg = await createBtcDelegationMsg( + stakingInstance, + stakingInput, + stakingTx, + bech32Address, + { address, publicKeyNoCoordHex: publicKeyNoCoord }, + genesisParam, + { + signPsbt, + signMessage, + signingCallback, + }, + ); + await sendBbnTx(signingStargateClient!, bech32Address, delegationMsg); + await signingCallback(SigningStep.SEND_BBN); + }, + [ + cosmosConnected, + btcConnected, + btcNetwork, + signingStargateClient, + genesisParam, + bech32Address, + address, + publicKeyNoCoord, + signPsbt, + signMessage, + ], + ); + + return { + createDelegationEoi, + estimateStakingFee, + transitionPhase1Delegation, + }; +}; + +const sendBbnTx = async ( + signingStargateClient: SigningStargateClient, + bech32Address: string, + delegationMsg: any, +) => { + // estimate gas + const gasEstimate = await signingStargateClient!.simulate( + bech32Address, + [delegationMsg], + "estimate fee", + ); + // TODO: The gas calculation need to be improved + // https://github.com/babylonlabs-io/simple-staking/issues/320 + const gasWanted = Math.ceil(gasEstimate * 1.2); + const fee = { + amount: [{ denom: "ubbn", amount: (gasWanted * 0.01).toFixed(0) }], + gas: gasWanted.toString(), + }; + // sign it + await signingStargateClient!.signAndBroadcast( + bech32Address, + [delegationMsg], + fee, + ); +}; + +const createBtcDelegationMsg = async ( + stakingInstance: Staking, + stakingInput: BtcStakingInputs, + stakingTx: Transaction, + bech32Address: string, + stakerInfo: { + address: string; + publicKeyNoCoordHex: string; + }, + param: BbnStakingParamsVersion, + btcSigningFuncs: BtcSigningFuncs, + inclusionProof?: InclusionProof, +) => { + const cleanedStakingTx = clearTxSignatures(stakingTx); + + const { psbt: unbondingPsbt } = + stakingInstance.createUnbondingTransaction(cleanedStakingTx); + // TODO: This is temporary solution until we have + // https://github.com/babylonlabs-io/btc-staking-ts/issues/40 + const signedUnbondingPsbtHex = await btcSigningFuncs.signPsbt( + unbondingPsbt.toHex(), + ); + const unbondingTx = Psbt.fromHex(signedUnbondingPsbtHex).extractTransaction(); + const cleanedUnbondingTx = clearTxSignatures(unbondingTx); + + // Create slashing transactions and extract signatures + const { psbt: slashingPsbt } = + stakingInstance.createStakingOutputSlashingTransaction(cleanedStakingTx); + const signedSlashingPsbtHex = await btcSigningFuncs.signPsbt( + slashingPsbt.toHex(), + ); + const signedSlashingTx = Psbt.fromHex( + signedSlashingPsbtHex, + ).extractTransaction(); + const slashingSig = extractSchnorrSignaturesFromTransaction(signedSlashingTx); + if (!slashingSig) { + throw new Error("No signature found in the staking output slashing PSBT"); + } + await btcSigningFuncs.signingCallback(SigningStep.STAKING_SLASHING); + + const { psbt: unbondingSlashingPsbt } = + stakingInstance.createUnbondingOutputSlashingTransaction(unbondingTx); + const signedUnbondingSlashingPsbtHex = await btcSigningFuncs.signPsbt( + unbondingSlashingPsbt.toHex(), + ); + const signedUnbondingSlashingTx = Psbt.fromHex( + signedUnbondingSlashingPsbtHex, + ).extractTransaction(); + const unbondingSignatures = extractSchnorrSignaturesFromTransaction( + signedUnbondingSlashingTx, + ); + if (!unbondingSignatures) { + throw new Error("No signature found in the unbonding output slashing PSBT"); + } + await btcSigningFuncs.signingCallback(SigningStep.UNBONDING_SLASHING); + + // Create Proof of Possession + const bech32AddressHex = uint8ArrayToHex(fromBech32(bech32Address).data); + const signedBbnAddress = await btcSigningFuncs.signMessage( + bech32AddressHex, + "ecdsa", + ); + const ecdsaSig = Uint8Array.from( + globalThis.Buffer.from(signedBbnAddress, "base64"), + ); + const proofOfPossession: ProofOfPossessionBTC = { + btcSigType: BTCSigType.ECDSA, + btcSig: ecdsaSig, + }; + await btcSigningFuncs.signingCallback(SigningStep.PROOF_OF_POSSESSION); + + // Prepare and send protobuf message + const msg: btcstakingtx.MsgCreateBTCDelegation = + btcstakingtx.MsgCreateBTCDelegation.fromPartial({ + stakerAddr: bech32Address, + pop: proofOfPossession, + btcPk: Uint8Array.from( + Buffer.from(stakerInfo.publicKeyNoCoordHex, "hex"), + ), + fpBtcPkList: [ + Uint8Array.from( + Buffer.from(stakingInput.finalityProviderPkNoCoordHex, "hex"), + ), + ], + stakingTime: stakingInput.stakingTimeBlocks, + stakingValue: stakingInput.stakingAmountSat, + stakingTx: Uint8Array.from(cleanedStakingTx.toBuffer()), + slashingTx: Uint8Array.from( + Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex"), + ), + delegatorSlashingSig: Uint8Array.from(slashingSig), + unbondingTime: param.unbondingTime, + unbondingTx: Uint8Array.from(cleanedUnbondingTx.toBuffer()), + unbondingValue: stakingInput.stakingAmountSat - param.unbondingFeeSat, + unbondingSlashingTx: Uint8Array.from( + Buffer.from( + clearTxSignatures(signedUnbondingSlashingTx).toHex(), + "hex", + ), + ), + delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures), + stakingTxInclusionProof: inclusionProof, + }); + + return { + typeUrl: "/babylon.btcstaking.v1.MsgCreateBTCDelegation", + value: msg, + }; +}; + +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.stakingTimeBlocks) throw new Error("Staking time not set"); +}; + +const checkWalletConnection = ( + cosmosConnected: boolean, + btcConnected: boolean, + btcNetwork: Network | undefined, + signingStargateClient: SigningStargateClient | undefined, +) => { + if ( + !cosmosConnected || + !btcConnected || + !btcNetwork || + !signingStargateClient + ) + throw new Error("Wallet not connected"); +}; diff --git a/src/app/state/index.tsx b/src/app/state/index.tsx index 302816f2..99dc1ca5 100644 --- a/src/app/state/index.tsx +++ b/src/app/state/index.tsx @@ -5,16 +5,14 @@ import { useOrdinals } from "@/app/hooks/api/useOrdinals"; import { useParams } from "@/app/hooks/api/useParams"; import { useUTXOs } from "@/app/hooks/api/useUTXOs"; import { useVersions } from "@/app/hooks/api/useVersions"; -import { GlobalParamsVersion } from "@/app/types/globalParams"; import { createStateUtils } from "@/utils/createStateUtils"; -import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; import { filterDust } from "@/utils/wallet"; import type { InscriptionIdentifier, UTXO, } from "@/utils/wallet/btc_wallet_provider"; -import { BbnStakingParamsVersion } from "../types/params"; +import { Params } from "../types/params"; import { DelegationState } from "./DelegationState"; @@ -23,13 +21,8 @@ const STATE_LIST = [DelegationState]; export interface AppState { availableUTXOs?: UTXO[]; totalBalance: number; - nextVersion?: GlobalParamsVersion; - currentVersion?: GlobalParamsVersion; - latestBbnStakingParamsVersion?: BbnStakingParamsVersion; - bbnStakingParamsVersions?: BbnStakingParamsVersion[]; + params?: Params; currentHeight?: number; - isApprochingNextVersion: boolean; - firstActivationHeight: number; isError: boolean; isLoading: boolean; ordinalsExcluded: boolean; @@ -42,20 +35,11 @@ const { StateProvider, useState: useApplicationState } = isLoading: false, isError: false, totalBalance: 0, - isApprochingNextVersion: false, - firstActivationHeight: 0, ordinalsExcluded: true, includeOrdinals: () => {}, excludeOrdinals: () => {}, }); -const defaultVersionParams = { - isApprochingNextVersion: false, - firstActivationHeight: 0, - currentVersion: undefined, - nextVersion: undefined, -}; - export function AppState({ children }: PropsWithChildren) { const [ordinalsExcluded, setOrdinalsExcluded] = useState(true); @@ -127,16 +111,6 @@ export function AppState({ children }: PropsWithChildren) { [availableUTXOs], ); - const versionInfo = useMemo( - () => - versions && height - ? getCurrentGlobalParamsVersion(height + 1, versions) - : defaultVersionParams, - [versions, height], - ); - - const bbnStakingParams = useMemo(() => params?.bbnStakingParams, [params]); - // Handlers const includeOrdinals = useCallback(() => setOrdinalsExcluded(false), []); const excludeOrdinals = useCallback(() => setOrdinalsExcluded(true), []); @@ -147,9 +121,7 @@ export function AppState({ children }: PropsWithChildren) { availableUTXOs, currentHeight: height, totalBalance, - ...versionInfo, - latestBbnStakingParamsVersion: bbnStakingParams?.latestVersion, - bbnStakingParamsVersions: bbnStakingParams?.versions, + params, isError, isLoading, ordinalsExcluded, @@ -160,8 +132,7 @@ export function AppState({ children }: PropsWithChildren) { availableUTXOs, height, totalBalance, - versionInfo, - bbnStakingParams, + params, isError, isLoading, ordinalsExcluded, diff --git a/src/app/types/delegations.ts b/src/app/types/delegations.ts index 323cea94..dc5016b4 100644 --- a/src/app/types/delegations.ts +++ b/src/app/types/delegations.ts @@ -7,6 +7,7 @@ export interface Delegation { stakingTx: StakingTx; unbondingTx: UnbondingTx | undefined; isOverflow: boolean; + transitioned: boolean; } export interface StakingTx { @@ -32,6 +33,8 @@ export const OVERFLOW = "overflow"; export const EXPIRED = "expired"; export const INTERMEDIATE_UNBONDING = "intermediate_unbonding"; export const INTERMEDIATE_WITHDRAWAL = "intermediate_withdrawal"; +export const TRANSITIONED = "transitioned"; +export const INTERMEDIATE_TRANSITIONING = "intermediate_transitioning"; // Define the state of a delegation as per API export const DelegationState = { @@ -45,4 +48,6 @@ export const DelegationState = { EXPIRED, INTERMEDIATE_UNBONDING, INTERMEDIATE_WITHDRAWAL, + TRANSITIONED, + INTERMEDIATE_TRANSITIONING, }; diff --git a/src/app/types/errors.ts b/src/app/types/errors.ts index 6a032b1b..9a0fa8ad 100644 --- a/src/app/types/errors.ts +++ b/src/app/types/errors.ts @@ -4,6 +4,7 @@ export enum ErrorState { WALLET = "WALLET", WITHDRAW = "WITHDRAW", STAKING = "STAKING", + TRANSITION = "TRANSITION", } export interface ErrorType { diff --git a/src/app/types/params.ts b/src/app/types/params.ts index cc31e707..d13d00b7 100644 --- a/src/app/types/params.ts +++ b/src/app/types/params.ts @@ -8,8 +8,12 @@ export interface BbnStakingParamsVersion extends StakingParams { } export interface BbnStakingParams { - latestVersion: BbnStakingParamsVersion; - versions: BbnStakingParamsVersion[]; + // The genesis params is the version 0 of the staking params which is + // compatible with the phase-1 global params + genesisParam: BbnStakingParamsVersion; + // The latest params is the param with the highest version number + latestParam: BbnStakingParamsVersion; + bbnStakingParams: BbnStakingParamsVersion[]; } export interface Params { diff --git a/src/components/delegations/DelegationList/components/Status.tsx b/src/components/delegations/DelegationList/components/Status.tsx index d9a3d366..db9babf9 100644 --- a/src/components/delegations/DelegationList/components/Status.tsx +++ b/src/components/delegations/DelegationList/components/Status.tsx @@ -1,5 +1,5 @@ import { useAppState } from "@/app/state"; -import { GlobalParamsVersion } from "@/app/types/globalParams"; +import { BbnStakingParamsVersion } from "@/app/types/params"; import { Hint } from "@/components/common/Hint"; import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; @@ -11,7 +11,7 @@ interface StatusProps { const STATUSES: Record< string, - (state?: GlobalParamsVersion) => { label: string; tooltip: string } + (state?: BbnStakingParamsVersion) => { label: string; tooltip: string } > = { [DelegationState.ACTIVE]: () => ({ label: "Active", @@ -35,7 +35,9 @@ const STATUSES: Record< }), [DelegationState.PENDING]: (state) => ({ label: "Pending", - tooltip: `Stake that is pending ${state?.confirmationDepth || 10} Bitcoin confirmations will only be visible from this device`, + // TODO: get confirmation depth from params + // https://github.com/babylonlabs-io/simple-staking/issues/325 + tooltip: `Stake that is pending ${10} Bitcoin confirmations will only be visible from this device`, }), [DelegationState.OVERFLOW]: () => ({ label: "Overflow", @@ -56,9 +58,9 @@ const STATUSES: Record< }; export function Status({ value }: StatusProps) { - const { currentVersion } = useAppState(); + const { params } = useAppState(); const { label = "unknown", tooltip = "unknown" } = - STATUSES[value](currentVersion) ?? {}; + STATUSES[value](params?.bbnStakingParams?.latestParam) ?? {}; return {label}; } diff --git a/src/components/delegations/DelegationList/type.ts b/src/components/delegations/DelegationList/type.ts index 894833f8..a46c3cae 100644 --- a/src/components/delegations/DelegationList/type.ts +++ b/src/components/delegations/DelegationList/type.ts @@ -22,4 +22,6 @@ export enum DelegationState { EXPIRED = "expired", INTERMEDIATE_UNBONDING = "intermediate_unbonding", INTERMEDIATE_WITHDRAWAL = "intermediate_withdrawal", + INTERMEDIATE_TRANSITIONING = "intermediate_transitioning", + TRANSITIONED = "transitioned", } diff --git a/src/utils/delegations/signStakingTx.ts b/src/utils/delegations/signStakingTx.ts deleted file mode 100644 index 2897b6b0..00000000 --- a/src/utils/delegations/signStakingTx.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { stakingTransaction } from "@babylonlabs-io/btc-staking-ts"; -import { Psbt, Transaction, networks } from "bitcoinjs-lib"; - -import { GlobalParamsVersion } from "@/app/types/globalParams"; -import { apiDataToStakingScripts } from "@/utils/apiDataToStakingScripts"; -import { isTaproot } from "@/utils/wallet"; -import { UTXO } from "@/utils/wallet/btc_wallet_provider"; - -import { getStakingTerm } from "../getStakingTerm"; - -import { txFeeSafetyCheck } from "./fee"; - -// Returns: -// - unsignedStakingPsbt: the unsigned staking transaction -// - stakingTerm: the staking term -// - stakingFee: the staking fee -export const createStakingTx = ( - globalParamsVersion: GlobalParamsVersion, - stakingAmountSat: number, - stakingTimeBlocks: number, - finalityProviderPublicKey: string, - btcWalletNetwork: networks.Network, - address: string, - publicKeyNoCoord: string, - feeRate: number, - inputUTXOs: UTXO[], -) => { - // Get the staking term, it will ignore the `stakingTimeBlocks` and use the value from params - // if the min and max staking time blocks are the same - const stakingTerm = getStakingTerm(globalParamsVersion, stakingTimeBlocks); - - // Check the staking data - if ( - stakingAmountSat < globalParamsVersion.minStakingAmountSat || - stakingAmountSat > globalParamsVersion.maxStakingAmountSat || - stakingTerm < globalParamsVersion.minStakingTimeBlocks || - stakingTerm > globalParamsVersion.maxStakingTimeBlocks - ) { - throw new Error("Invalid staking data"); - } - - if (inputUTXOs.length == 0) { - throw new Error("Not enough usable balance"); - } - - if (feeRate <= 0) { - throw new Error("Invalid fee rate"); - } - - // Create the staking scripts - let scripts; - try { - scripts = apiDataToStakingScripts( - finalityProviderPublicKey, - stakingTerm, - globalParamsVersion, - publicKeyNoCoord, - ); - } catch (error: Error | any) { - throw new Error(error?.message || "Cannot build staking scripts"); - } - - // Create the staking transaction - let unsignedStakingPsbt; - let stakingFeeSat; - try { - const { psbt, fee } = stakingTransaction( - scripts, - stakingAmountSat, - address, - inputUTXOs, - btcWalletNetwork, - feeRate, - isTaproot(address) ? Buffer.from(publicKeyNoCoord, "hex") : undefined, - // `lockHeight` is exclusive of the provided value. - // For example, if a Bitcoin height of X is provided, - // the transaction will be included starting from height X+1. - // https://learnmeabitcoin.com/technical/transaction/locktime/ - globalParamsVersion.activationHeight - 1, - ); - unsignedStakingPsbt = psbt; - stakingFeeSat = fee; - } catch (error: Error | any) { - throw new Error( - error?.message || "Cannot build unsigned staking transaction", - ); - } - - return { unsignedStakingPsbt, stakingTerm, stakingFeeSat }; -}; - -// Sign a staking transaction -// Returns: -// - stakingTxHex: the signed staking transaction -// - stakingTerm: the staking term -export const signStakingTx = async ( - signPsbt: (psbtHex: string) => Promise, - pushTx: any, - globalParamsVersion: GlobalParamsVersion, - stakingAmountSat: number, - stakingTimeBlocks: number, - finalityProviderPublicKey: string, - btcWalletNetwork: networks.Network, - address: string, - publicKeyNoCoord: string, - feeRate: number, - inputUTXOs: UTXO[], -): Promise<{ stakingTxHex: string; stakingTerm: number }> => { - // Create the staking transaction - let { unsignedStakingPsbt, stakingTerm, stakingFeeSat } = createStakingTx( - globalParamsVersion, - stakingAmountSat, - stakingTimeBlocks, - finalityProviderPublicKey, - btcWalletNetwork, - address, - publicKeyNoCoord, - feeRate, - inputUTXOs, - ); - - // Sign the staking transaction - let stakingTx: Transaction; - try { - const signedStakingPsbtHex = await signPsbt(unsignedStakingPsbt.toHex()); - stakingTx = Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); - } catch (error: Error | any) { - throw new Error(error?.message || "Staking transaction signing PSBT error"); - } - - // Get the staking transaction hex - const stakingTxHex = stakingTx.toHex(); - - txFeeSafetyCheck(stakingTx, feeRate, stakingFeeSat); - - // Broadcast the staking transaction - await pushTx(stakingTxHex); - - return { stakingTxHex, stakingTerm }; -}; diff --git a/src/utils/delegations/signUnbondingTx.ts b/src/utils/delegations/signUnbondingTx.ts deleted file mode 100644 index 7f95aa65..00000000 --- a/src/utils/delegations/signUnbondingTx.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { unbondingTransaction } from "@babylonlabs-io/btc-staking-ts"; -import { Psbt, Transaction, networks } from "bitcoinjs-lib"; - -import { getGlobalParams } from "@/app/api/getGlobalParams"; -import { getUnbondingEligibility } from "@/app/api/getUnbondingEligibility"; -import { postUnbonding } from "@/app/api/postUnbonding"; -import { Delegation as DelegationInterface } from "@/app/types/delegations"; -import { apiDataToStakingScripts } from "@/utils/apiDataToStakingScripts"; -import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; - -// 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"); - } -}; - -// Sign an unbonding transaction -// Returns: -// - unbondingTx: the signed unbonding transaction -// - delegation: the initial delegation -export const signUnbondingTx = async ( - id: string, - delegationsAPI: DelegationInterface[], - publicKeyNoCoord: string, - btcWalletNetwork: networks.Network, - signPsbt: (psbtHex: string) => Promise, -): Promise<{ unbondingTxHex: 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"); - } - - // Check if the unbonding is possible - const unbondingEligibility = await getUnbondingEligibility( - delegation.stakingTxHashHex, - ); - if (!unbondingEligibility) { - throw new Error("Not eligible for unbonding"); - } - - const paramVersions = await getGlobalParams(); - // 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 scripts = apiDataToStakingScripts( - delegation.finalityProviderPkHex, - delegation.stakingTx.timelock, - globalParamsWhenStaking, - publicKeyNoCoord, - ); - - // Create the unbonding transaction - const { psbt: unsignedUnbondingTx } = unbondingTransaction( - scripts, - Transaction.fromHex(delegation.stakingTx.txHex), - globalParamsWhenStaking.unbondingFeeSat, - btcWalletNetwork, - delegation.stakingTx.outputIndex, - ); - - // Sign the unbonding transaction - let unbondingTx: Transaction; - try { - const signedUnbondingPsbtHex = await signPsbt(unsignedUnbondingTx.toHex()); - unbondingTx = Psbt.fromHex(signedUnbondingPsbtHex).extractTransaction(); - } catch (error) { - throw new Error("Failed to sign PSBT for the unbonding transaction"); - } - - // Get the staker signature - const stakerSignature = getStakerSignature(unbondingTx); - - // Get the unbonding transaction hex - const unbondingTxHex = unbondingTx.toHex(); - - // POST unbonding to the API - await postUnbonding( - stakerSignature, - delegation.stakingTxHashHex, - unbondingTx.getId(), - unbondingTxHex, - ); - - return { unbondingTxHex, delegation }; -}; diff --git a/src/utils/getStakingTerm.ts b/src/utils/getStakingTerm.ts index eb0a1797..4bb53c7b 100644 --- a/src/utils/getStakingTerm.ts +++ b/src/utils/getStakingTerm.ts @@ -1,6 +1,6 @@ -import { GlobalParamsVersion } from "@/app/types/globalParams"; +import { StakingParams } from "@babylonlabs-io/btc-staking-ts"; -export const getStakingTerm = (params: GlobalParamsVersion, term: number) => { +export const getStakingTerm = (params: StakingParams, term: number) => { // check if term is fixed let termWithFixed; if (params && params.minStakingTimeBlocks === params.maxStakingTimeBlocks) { diff --git a/src/utils/getState.ts b/src/utils/getState.ts index 4e724269..f971c053 100644 --- a/src/utils/getState.ts +++ b/src/utils/getState.ts @@ -1,7 +1,5 @@ import { DelegationState } from "@/app/types/delegations"; -import { blocksToDisplayTime } from "./blocksToDisplayTime"; - // Convert state to human readable format export const getState = (state: string) => { switch (state) { @@ -26,29 +24,24 @@ export const getState = (state: string) => { return "Requesting Unbonding"; case DelegationState.INTERMEDIATE_WITHDRAWAL: return "Withdrawal Submitted"; + case DelegationState.INTERMEDIATE_TRANSITIONING: + return "Transitioning"; + case DelegationState.TRANSITIONED: + return "Transitioned"; default: return "Unknown"; } }; // Create state tooltips for the additional information -export const getStateTooltip = ( - state: string, - params?: { confirmationDepth: number; unbondingTime: number }, -) => { +export const getStateTooltip = (state: string) => { switch (state) { case DelegationState.ACTIVE: return "Stake is active"; - case DelegationState.UNBONDING_REQUESTED: - return "Unbonding requested"; - case DelegationState.UNBONDING: - return `Unbonding process of ${blocksToDisplayTime(params?.unbondingTime)} has started`; case DelegationState.UNBONDED: return "Stake has been unbonded"; case DelegationState.WITHDRAWN: return "Stake has been withdrawn"; - case DelegationState.PENDING: - return `Stake that is pending ${params?.confirmationDepth || 10} Bitcoin confirmations will only be visible from this device`; case DelegationState.OVERFLOW: return "Stake is over the staking cap"; case DelegationState.EXPIRED: @@ -58,6 +51,10 @@ export const getStateTooltip = ( return "Stake is requesting unbonding"; case DelegationState.INTERMEDIATE_WITHDRAWAL: return "Withdrawal transaction pending confirmation on Bitcoin"; + case DelegationState.INTERMEDIATE_TRANSITIONING: + return "Stake is transitioning to the Babylon chain network"; + case DelegationState.TRANSITIONED: + return "Stake has been transitioned to the Babylon chain network"; default: return "Unknown"; } diff --git a/src/utils/local_storage/toLocalStorageDelegation.ts b/src/utils/local_storage/toLocalStorageDelegation.ts deleted file mode 100644 index cf8dfd32..00000000 --- a/src/utils/local_storage/toLocalStorageDelegation.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Delegation, DelegationState } from "@/app/types/delegations"; - -export const toLocalStorageDelegation = ( - stakingTxHashHex: string, - stakerPkHex: string, - finalityProviderPkHex: string, - stakingValueSat: number, - stakingTxHex: string, - timelock: number, -): Delegation => ({ - stakingTxHashHex: stakingTxHashHex, - stakerPkHex: stakerPkHex, - finalityProviderPkHex: finalityProviderPkHex, - state: DelegationState.PENDING, - stakingValueSat: stakingValueSat, - stakingTx: { - txHex: stakingTxHex, - outputIndex: 0, - startTimestamp: new Date().toISOString(), - startHeight: 0, - timelock, - }, - isOverflow: false, - unbondingTx: undefined, -}); diff --git a/src/utils/local_storage/toLocalStorageIntermediateDelegation.ts b/src/utils/local_storage/toLocalStorageIntermediateDelegation.ts index 12ab7ba7..edbd85d0 100644 --- a/src/utils/local_storage/toLocalStorageIntermediateDelegation.ts +++ b/src/utils/local_storage/toLocalStorageIntermediateDelegation.ts @@ -23,4 +23,5 @@ export const toLocalStorageIntermediateDelegation = ( }, isOverflow: false, unbondingTx: undefined, + transitioned: false, }); diff --git a/src/utils/mempool_api.ts b/src/utils/mempool_api.ts index 79323d4f..1db82f12 100644 --- a/src/utils/mempool_api.ts +++ b/src/utils/mempool_api.ts @@ -4,6 +4,12 @@ import { Fees, UTXO } from "./wallet/btc_wallet_provider"; const { mempoolApiUrl } = getNetworkConfig(); +interface MerkleProof { + blockHeight: number; + merkle: string[]; + pos: number; +} + export class ServerError extends Error { constructor( message: string, @@ -57,6 +63,11 @@ function txInfoUrl(txId: string): URL { return new URL(mempoolAPI + "tx/" + txId); } +// URL for the transaction merkle proof endpoint +function txMerkleProofUrl(txId: string): URL { + return new URL(mempoolAPI + "tx/" + txId + "/merkle-proof"); +} + /** * Pushes a transaction to the Bitcoin network. * @param txHex - The hex string corresponding to the full transaction. @@ -212,3 +223,22 @@ export async function getTxInfo(txId: string): Promise { } return await response.json(); } + +/** + * Retrieve the merkle proof for a transaction. + * @param txId - The transaction ID in string format. + * @returns A promise that resolves into the merkle proof. + */ +export async function getTxMerkleProof(txId: string): Promise { + const response = await fetch(txMerkleProofUrl(txId)); + if (!response.ok) { + const err = await response.text(); + throw new ServerError(err, response.status); + } + const data = await response.json(); + return { + blockHeight: data.block_height, + merkle: data.merkle, + pos: data.pos, + }; +} diff --git a/tests/helper/generateMockDelegation.ts b/tests/helper/generateMockDelegation.ts index 96e2cedc..678f9de8 100644 --- a/tests/helper/generateMockDelegation.ts +++ b/tests/helper/generateMockDelegation.ts @@ -25,5 +25,6 @@ export function generateMockDelegation( }, isOverflow: false, unbondingTx: undefined, + transitioned: false, }; } diff --git a/tests/helper/index.ts b/tests/helper/index.ts index 1aee4a31..26370053 100644 --- a/tests/helper/index.ts +++ b/tests/helper/index.ts @@ -8,8 +8,6 @@ import * as bitcoin from "bitcoinjs-lib"; import ECPairFactory from "ecpair"; import { GlobalParamsVersion } from "@/app/types/globalParams"; -import { createStakingTx } from "@/utils/delegations/signStakingTx"; -import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; import { getPublicKeyNoCoord } from "@/utils/wallet"; import { UTXO } from "@/utils/wallet/btc_wallet_provider"; @@ -260,60 +258,6 @@ export class DataGenerator { }; }; - createRandomStakingPsbt = ( - globalParams: GlobalParamsVersion[], - txHeight: number, - stakerKeysPairs?: KeyPairs, - ) => { - const { currentVersion: selectedParam } = getCurrentGlobalParamsVersion( - txHeight, - globalParams, - ); - if (!selectedParam) { - throw new Error("Current version not found"); - } - const stakerKeys = stakerKeysPairs - ? stakerKeysPairs - : this.generateRandomKeyPair(); - const { scriptPubKey, address } = this.getAddressAndScriptPubKey( - stakerKeys.publicKey, - ).nativeSegwit; - const stakingAmount = this.getRandomIntegerBetween( - selectedParam.minStakingAmountSat, - selectedParam.maxStakingAmountSat, - ); - const stakingTerm = this.getRandomIntegerBetween( - selectedParam.minStakingTimeBlocks, - selectedParam.maxStakingTimeBlocks, - ); - const { unsignedStakingPsbt } = createStakingTx( - selectedParam, - stakingAmount, - stakingTerm, - this.generateRandomKeyPair().noCoordPublicKey, // FP key - this.network, - address, - stakerKeys.noCoordPublicKey, // staker public key - DEFAULT_TEST_FEE_RATE, - this.generateRandomUTXOs( - stakingAmount + 1000000, // add some extra satoshis to cover the fee - this.getRandomIntegerBetween(1, 10), - scriptPubKey, - ), - ); - - const unsignedPsbt = unsignedStakingPsbt; - - const signedPsbt = unsignedStakingPsbt - .signAllInputs(stakerKeys.keyPair) - .finalizeAllInputs(); - - return { - unsignedPsbt, - signedPsbt, - }; - }; - private getTaprootAddress = (publicKey: string) => { // Remove the prefix if it exists if (publicKey.length == COMPRESSED_PUBLIC_KEY_HEX_LENGTH) { diff --git a/tests/utils/delegations/createStakingTx.test.ts b/tests/utils/delegations/createStakingTx.test.ts deleted file mode 100644 index a85649bf..00000000 --- a/tests/utils/delegations/createStakingTx.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { createStakingTx } from "@/utils/delegations/signStakingTx"; - -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; - -describe.each(testingNetworks)( - "utils/delegations/createStakingTx", - ({ network, dataGenerator: dataGen }) => { - describe.each([true, false])("isFixed %s", (isFixed) => { - const randomFpKeys = dataGen.generateRandomKeyPair(); - const randomStakerKeys = dataGen.generateRandomKeyPair(); - const feeRate = DEFAULT_TEST_FEE_RATE; - const { address: stakerTaprootAddress, scriptPubKey } = - dataGen.getAddressAndScriptPubKey(randomStakerKeys.publicKey).taproot; - const randomParam = dataGen.generateRandomGlobalParams(isFixed); - const randomStakingAmount = dataGen.getRandomIntegerBetween( - randomParam.minStakingAmountSat, - randomParam.maxStakingAmountSat, - ); - const randomStakingTimeBlocks = dataGen.getRandomIntegerBetween( - randomParam.minStakingTimeBlocks, - randomParam.maxStakingTimeBlocks, - ); - const randomInputUTXOs = dataGen.generateRandomUTXOs( - randomStakingAmount + dataGen.getRandomIntegerBetween(0, 100000000), - dataGen.getRandomIntegerBetween(1, 10), - scriptPubKey, - ); - const testTermDescription = isFixed ? "fixed term" : "variable term"; - - it("should successfully create a staking transaction", () => { - const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.noCoordPublicKey, - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - feeRate, - randomInputUTXOs, - ); - - expect(stakingTerm).toBe(randomStakingTimeBlocks); - const matchedStakingOutput = unsignedStakingPsbt.txOutputs.find( - (output) => { - return output.value === randomStakingAmount; - }, - ); - expect(matchedStakingOutput).toBeDefined(); - expect(stakingFeeSat).toBeGreaterThan(0); - }); - - it(`${testTermDescription} - should successfully create a staking transaction with change amount in output`, () => { - const utxo = dataGen.generateRandomUTXOs( - randomStakingAmount + Math.floor(Math.random() * 1000000), - 1, // make it a single utxo so we always have change - scriptPubKey, - ); - const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.noCoordPublicKey, - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - feeRate, - utxo, - ); - - expect(stakingTerm).toBe(randomStakingTimeBlocks); - const matchedStakingOutput = unsignedStakingPsbt.txOutputs.find( - (output) => { - return output.value === randomStakingAmount; - }, - ); - expect(matchedStakingOutput).toBeDefined(); - expect(stakingFeeSat).toBeGreaterThan(0); - - const changeOutput = unsignedStakingPsbt.txOutputs.find((output) => { - return output.address == stakerTaprootAddress; - }); - expect(changeOutput).toBeDefined(); - }); - - it(`${testTermDescription} - should throw an error if the staking amount is less than the minimum staking amount`, () => { - expect(() => - createStakingTx( - randomParam, - randomParam.minStakingAmountSat - 1, - randomStakingTimeBlocks, - randomFpKeys.noCoordPublicKey, - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - feeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid staking data"); - }); - - it(`${testTermDescription} - should throw an error if the staking amount is greater than the maximum staking amount`, () => { - expect(() => - createStakingTx( - randomParam, - randomParam.maxStakingAmountSat + 1, - randomStakingTimeBlocks, - randomFpKeys.noCoordPublicKey, - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - feeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid staking data"); - }); - - it(`${testTermDescription} - should throw an error if the fee rate is less than or equal to 0`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.noCoordPublicKey, - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - 0, - randomInputUTXOs, - ), - ).toThrow("Invalid fee rate"); - }); - - it(`${testTermDescription} - should throw an error if there are no usable balance`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.noCoordPublicKey, - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - feeRate, - [], - ), - ).toThrow("Not enough usable balance"); - }); - - // This test only test createStakingTx when apiDataToStakingScripts throw error. - // The different error cases are tested in the apiDataToStakingScripts.test.ts - it(`${testTermDescription} - should throw an error if the staking scripts cannot be built`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - dataGen.generateRandomKeyPair().publicKey, // invalid finality provider key - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - feeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid script data provided"); - }); - - // More test cases when we have variable staking terms - if (!isFixed) { - it(`${testTermDescription} - should throw an error if the staking term is less than the minimum staking time blocks`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomParam.minStakingTimeBlocks - 1, - randomFpKeys.noCoordPublicKey, - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - feeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid staking data"); - }); - - it(`${testTermDescription} - should throw an error if the staking term is greater than the maximum staking time blocks`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomParam.maxStakingTimeBlocks + 1, - randomFpKeys.noCoordPublicKey, - network, - stakerTaprootAddress, - randomStakerKeys.noCoordPublicKey, - feeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid staking data"); - }); - } - }); - }, -); diff --git a/tests/utils/delegations/fee.test.ts b/tests/utils/delegations/fee.test.ts index 746ee41c..f21edd06 100644 --- a/tests/utils/delegations/fee.test.ts +++ b/tests/utils/delegations/fee.test.ts @@ -1,21 +1,14 @@ import { txFeeSafetyCheck } from "@/utils/delegations/fee"; -import { testingNetworks } from "../../helper"; - -describe.each(testingNetworks)("txFeeSafetyCheck", ({ dataGenerator }) => { - const feeRate = dataGenerator.generateRandomFeeRates(); - const globalParams = dataGenerator.generateGlobalPramsVersions( - dataGenerator.getRandomIntegerBetween(1, 10), - ); - const randomParam = - globalParams[ - dataGenerator.getRandomIntegerBetween(0, globalParams.length - 1) - ]; - const { signedPsbt } = dataGenerator.createRandomStakingPsbt( - globalParams, - randomParam.activationHeight + 1, - ); - const tx = signedPsbt.extractTransaction(); +describe("txFeeSafetyCheck", () => { + const feeRate = Math.floor(Math.random() * 100) + 1; + const tx: any = { + virtualSize: jest.fn(), + }; + beforeEach(() => { + const randomVirtualSize = Math.floor(Math.random() * 1000) + 1; + tx.virtualSize.mockReturnValue(randomVirtualSize); + }); test("should not throw an error if the estimated fee is within the acceptable range", () => { let estimatedFee = (tx.virtualSize() * feeRate) / 2 + 1; diff --git a/tests/utils/delegations/signStakingTx.test.ts b/tests/utils/delegations/signStakingTx.test.ts deleted file mode 100644 index 29f3c5fc..00000000 --- a/tests/utils/delegations/signStakingTx.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Psbt } from "bitcoinjs-lib"; - -jest.mock("@/utils/delegations/fee", () => ({ - txFeeSafetyCheck: jest.fn().mockReturnValue(undefined), -})); - -import { signStakingTx } from "@/utils/delegations/signStakingTx"; - -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; - -describe.each(testingNetworks)( - "txFeeSafetyCheck", - ({ network, dataGenerator: dataGen }) => { - const randomFpKeys = dataGen.generateRandomKeyPair(); - const randomUserKeys = dataGen.generateRandomKeyPair(); - const randomParam = dataGen.generateRandomGlobalParams(true); - const randomStakingAmount = dataGen.getRandomIntegerBetween( - randomParam.minStakingAmountSat, - randomParam.maxStakingAmountSat, - ); - const randomStakingTimeBlocks = dataGen.getRandomIntegerBetween( - randomParam.minStakingTimeBlocks, - randomParam.maxStakingTimeBlocks, - ); - const { address: randomTaprootAddress, scriptPubKey } = - dataGen.getAddressAndScriptPubKey( - dataGen.generateRandomKeyPair().publicKey, - ).taproot; - const randomInputUTXOs = dataGen.generateRandomUTXOs( - randomStakingAmount + Math.floor(Math.random() * 100000000), - Math.floor(Math.random() * 10) + 1, - scriptPubKey, - ); - const txHex = dataGen.generateRandomTxId(); - const btcWallet = { - signPsbt: jest.fn(), - getWalletProviderName: jest.fn(), - pushTx: jest.fn().mockResolvedValue(true), - }; - - it("should successfully sign a staking transaction", async () => { - const mockTransaction = { - toHex: jest.fn().mockReturnValue(txHex), - }; - - jest.spyOn(Psbt, "fromHex").mockReturnValue({ - extractTransaction: jest.fn().mockReturnValue(mockTransaction), - } as any); - - const mockSignPsbt = jest.fn().mockResolvedValue(txHex); - - const result = await signStakingTx( - mockSignPsbt, - btcWallet.pushTx, - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.noCoordPublicKey, - network, - randomTaprootAddress, - randomUserKeys.noCoordPublicKey, - DEFAULT_TEST_FEE_RATE, - randomInputUTXOs, - ); - - expect(result).toBeDefined(); - expect(btcWallet.pushTx).toHaveBeenCalled(); - // check the signed transaction hex - expect(result.stakingTxHex).toBe(txHex); - // check the staking term - expect(result.stakingTerm).toBe(randomStakingTimeBlocks); - }); - - it("should throw an error when signing a staking transaction", async () => { - const mockSignPsbt = jest - .fn() - .mockRejectedValue(new Error("signing error")); - - try { - await signStakingTx( - mockSignPsbt, - btcWallet.pushTx, - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.noCoordPublicKey, - network, - randomTaprootAddress, - randomUserKeys.noCoordPublicKey, - DEFAULT_TEST_FEE_RATE, - randomInputUTXOs, - ); - } catch (error: any) { - expect(error).toBeDefined(); - expect(error.message).toBe("signing error"); - } - }); - }, -); diff --git a/tests/utils/delegations/signUnbondingTx.test.ts b/tests/utils/delegations/signUnbondingTx.test.ts deleted file mode 100644 index 1ba44eb1..00000000 --- a/tests/utils/delegations/signUnbondingTx.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -jest.mock("@/app/api/getUnbondingEligibility", () => ({ - getUnbondingEligibility: jest.fn(), -})); -jest.mock("@/app/api/getGlobalParams", () => ({ - getGlobalParams: jest.fn(), -})); -jest.mock("@/app/api/postUnbonding", () => ({ - postUnbonding: jest.fn(), -})); - -import { Psbt, Transaction } from "bitcoinjs-lib"; - -import { getGlobalParams } from "@/app/api/getGlobalParams"; -import { getUnbondingEligibility } from "@/app/api/getUnbondingEligibility"; -import { postUnbonding } from "@/app/api/postUnbonding"; -import { signUnbondingTx } from "@/utils/delegations/signUnbondingTx"; - -import { testingNetworks } from "../../helper"; - -describe("signUnbondingTx", () => { - const { network, dataGenerator } = testingNetworks[0]; - const randomTxId = dataGenerator.generateRandomTxId(); - const randomGlobalParamsVersions = dataGenerator.generateGlobalPramsVersions( - dataGenerator.getRandomIntegerBetween(1, 20), - ); - const stakerKeys = dataGenerator.generateRandomKeyPair(); - const randomStakingTxHeight = - randomGlobalParamsVersions[ - dataGenerator.getRandomIntegerBetween( - 0, - randomGlobalParamsVersions.length - 1, - ) - ].activationHeight + 1; - const { signedPsbt } = dataGenerator.createRandomStakingPsbt( - randomGlobalParamsVersions, - randomStakingTxHeight, - stakerKeys, - ); - const signedPsbtTx = signedPsbt.extractTransaction(); - const mockedDelegationApi = [ - { - stakingTxHashHex: randomTxId, - finalityProviderPkHex: - dataGenerator.generateRandomKeyPair().noCoordPublicKey, - stakingTx: { - startHeight: randomStakingTxHeight, - timelock: 150, - //timelock: signedPsbtTx.locktime, - txHex: signedPsbtTx.toHex(), - outputIndex: 0, - }, - }, - ] as any; - const mockedSignPsbtHex = jest - .fn() - .mockImplementation(async (psbtHex: string) => { - const psbt = Psbt.fromHex(psbtHex); - psbt.signAllInputs(stakerKeys.keyPair).finalizeAllInputs(); - - return psbt.toHex(); - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should throw an error if no back-end API data is available", async () => { - expect( - signUnbondingTx( - randomTxId, - [], - stakerKeys.noCoordPublicKey, - network, - mockedSignPsbtHex, - ), - ).rejects.toThrow("Delegation not found"); - }); - - it("should throw an error if txId not found in delegationApi", async () => { - const delegationApi = [ - { stakingTxHashHex: dataGenerator.generateRandomTxId() }, - ] as any; - expect( - signUnbondingTx( - randomTxId, - delegationApi, - stakerKeys.noCoordPublicKey, - network, - mockedSignPsbtHex, - ), - ).rejects.toThrow("Delegation not found"); - }); - - it("should throw an error if the stakingTx is not eligible for unbonding", async () => { - (getUnbondingEligibility as any).mockImplementationOnce(async () => { - return false; - }); - - expect( - signUnbondingTx( - randomTxId, - mockedDelegationApi, - stakerKeys.noCoordPublicKey, - network, - mockedSignPsbtHex, - ), - ).rejects.toThrow("Not eligible for unbonding"); - }); - - it("should throw an error if global param is not loaded", async () => { - (getUnbondingEligibility as any).mockImplementationOnce(async () => { - return true; - }); - (getGlobalParams as any).mockImplementationOnce(async () => { - return []; - }); - - expect( - signUnbondingTx( - randomTxId, - mockedDelegationApi, - stakerKeys.noCoordPublicKey, - network, - mockedSignPsbtHex, - ), - ).rejects.toThrow("No global params versions found"); - }); - - it("should throw an error if the current version is not found", async () => { - (getUnbondingEligibility as any).mockImplementationOnce(async () => { - return true; - }); - (getGlobalParams as any).mockImplementationOnce(async () => { - return randomGlobalParamsVersions; - }); - - const firstVersion = randomGlobalParamsVersions[0]; - const delegationApi = [ - { - stakingTxHashHex: randomTxId, - stakingTx: { - // Make height lower than the first version - activationHeight: firstVersion.activationHeight - 1, - }, - }, - ] as any; - expect( - signUnbondingTx( - randomTxId, - delegationApi, - stakerKeys.noCoordPublicKey, - network, - mockedSignPsbtHex, - ), - ).rejects.toThrow("Current version not found"); - }); - - it("should throw error if fail to signPsbtTx", async () => { - (getUnbondingEligibility as any).mockImplementationOnce(async () => { - return true; - }); - (getGlobalParams as any).mockImplementationOnce(async () => { - return randomGlobalParamsVersions; - }); - mockedSignPsbtHex.mockRejectedValueOnce(new Error("oops!")); - - expect( - signUnbondingTx( - randomTxId, - mockedDelegationApi, - stakerKeys.noCoordPublicKey, - network, - mockedSignPsbtHex, - ), - ).rejects.toThrow("Failed to sign PSBT for the unbonding transaction"); - }); - - it("should return the signed unbonding transaction", async () => { - (getUnbondingEligibility as any).mockImplementationOnce(async () => { - return true; - }); - (getGlobalParams as any).mockImplementationOnce(async () => { - return randomGlobalParamsVersions; - }); - (postUnbonding as any).mockImplementationOnce(async () => { - return; - }); - - const { unbondingTxHex, delegation } = await signUnbondingTx( - randomTxId, - mockedDelegationApi, - stakerKeys.noCoordPublicKey, - network, - mockedSignPsbtHex, - ); - const unbondingTx = Transaction.fromHex(unbondingTxHex); - const sig = unbondingTx.ins[0].witness[0].toString("hex"); - const unbondingTxId = unbondingTx.getId(); - expect(postUnbonding).toHaveBeenCalledWith( - sig, - delegation.stakingTxHashHex, - unbondingTxId, - unbondingTxHex, - ); - }); -}); diff --git a/tests/utils/local_storage/toLocalStorageDelegation.test.ts b/tests/utils/local_storage/toLocalStorageDelegation.test.ts deleted file mode 100644 index 0a26c2fe..00000000 --- a/tests/utils/local_storage/toLocalStorageDelegation.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DelegationState } from "@/app/types/delegations"; -import { toLocalStorageDelegation } from "@/utils/local_storage/toLocalStorageDelegation"; - -describe("utils/local_storage/toLocalStorageDelegation", () => { - it("should create a delegation object with the correct values", () => { - const stakingTxHashHex = "hash1"; - const stakerPkHex = "staker1"; - const finalityProviderPkHex = "provider1"; - const stakingValueSat = 1000; - const stakingTxHex = "txHex1"; - const timelock = 3600; - - const result = toLocalStorageDelegation( - stakingTxHashHex, - stakerPkHex, - finalityProviderPkHex, - stakingValueSat, - stakingTxHex, - timelock, - ); - - expect(result).toEqual({ - stakingTxHashHex, - stakerPkHex, - finalityProviderPkHex, - state: DelegationState.PENDING, - stakingValueSat, - stakingTx: { - txHex: stakingTxHex, - outputIndex: 0, - startTimestamp: expect.any(String), - startHeight: 0, - timelock, - }, - isOverflow: false, - unbondingTx: undefined, - }); - - // Validate startTimestamp is a valid ISO date string - expect(new Date(result.stakingTx.startTimestamp).toISOString()).toBe( - result.stakingTx.startTimestamp, - ); - }); -}); diff --git a/tests/utils/local_storage/toLocalStorageIntermediateDelegation.test.ts b/tests/utils/local_storage/toLocalStorageIntermediateDelegation.test.ts index c72b0655..330392d2 100644 --- a/tests/utils/local_storage/toLocalStorageIntermediateDelegation.test.ts +++ b/tests/utils/local_storage/toLocalStorageIntermediateDelegation.test.ts @@ -36,6 +36,7 @@ describe("utils/local_storage/toLocalStorageIntermediateDelegation", () => { }, isOverflow: false, unbondingTx: undefined, + transitioned: false, }); // Validate startTimestamp is a valid ISO date string @@ -78,6 +79,7 @@ describe("utils/local_storage/toLocalStorageIntermediateDelegation", () => { }, isOverflow: false, unbondingTx: undefined, + transitioned: false, }); // Validate startTimestamp is a valid ISO date string