diff --git a/src/app/components/Modals/EOIModal.tsx b/src/app/components/Modals/EOIModal.tsx index e8ca5ced..a93a3508 100644 --- a/src/app/components/Modals/EOIModal.tsx +++ b/src/app/components/Modals/EOIModal.tsx @@ -1,71 +1,66 @@ import { AiOutlineSignature } from "react-icons/ai"; -import { IoMdCheckmark, IoMdClose } from "react-icons/io"; +import { IoMdCheckmark } from "react-icons/io"; import { GeneralModal } from "./GeneralModal"; -type Status = "unsigned" | "signed" | "processing"; +export enum EOIStepStatus { + UNSIGNED = "UNSIGNED", + SIGNED = "SIGNED", + PROCESSING = "PROCESSING", +} interface EOIModalProps { statuses: { - slashing: Status; - unbonding: Status; - reward: Status; - eoi: Status; + slashing: EOIStepStatus; + unbonding: EOIStepStatus; + reward: EOIStepStatus; + eoi: EOIStepStatus; }; open: boolean; onClose: (value: boolean) => void; } const STATUS_ICON = { - unsigned: , - signed: , - processing: ( + [EOIStepStatus.UNSIGNED]: , + [EOIStepStatus.SIGNED]: , + [EOIStepStatus.PROCESSING]: ( ), } as const; export function EOIModal({ open, statuses, onClose }: EOIModalProps) { return ( - +
-

Staking Express Of Interest

- +

Staking

-

- Please sign the messages below to prepare your staking - express-of-interest (EOI): -

+

Please sign the following messages

-
    +
    • {STATUS_ICON[statuses.slashing]} - Consent to slashing + Step 1: Consent to slashing
    • {STATUS_ICON[statuses.unbonding]} - Consent to slashing during unbonding + Step 2: Consent to slashing during unbonding
    • {STATUS_ICON[statuses.reward]} - BTC-BBN address binding for receiving staking rewards + Step 3: BTC-BBN address binding for receiving staking rewards
    • {STATUS_ICON[statuses.eoi]} - EOI transaction + Step 4: Staking transaction registration
    - -

    - Please come back in a minute to check your EOI status. Once it is - accepted, you can proceed to submit the staking transaction to BTC. -

); diff --git a/src/app/components/Modals/PendingVerificationModal.tsx b/src/app/components/Modals/PendingVerificationModal.tsx index 95766b54..27e79003 100644 --- a/src/app/components/Modals/PendingVerificationModal.tsx +++ b/src/app/components/Modals/PendingVerificationModal.tsx @@ -1,5 +1,7 @@ import { FaCheckCircle } from "react-icons/fa"; +import { useDelegationV2 } from "@/app/hooks/api/useDelegationV2"; +import { DelegationV2StakingState as state } from "@/app/types/delegationsV2"; import { getNetworkConfig } from "@/config/network.config"; import { GeneralModal } from "./GeneralModal"; @@ -7,9 +9,9 @@ import { GeneralModal } from "./GeneralModal"; interface PendingVerificationModalProps { open: boolean; onClose: (value: boolean) => void; - verified: boolean; onStake?: () => void; awaitingWalletResponse: boolean; + stakingTxHash: string; } const Verified = () => ( @@ -37,12 +39,16 @@ const NotVerified = () => ( export function PendingVerificationModal({ open, onClose, - verified, onStake, awaitingWalletResponse, + stakingTxHash, }: PendingVerificationModalProps) { const { networkName } = getNetworkConfig(); + const { data: delegation = null } = useDelegationV2(stakingTxHash); + + const verified = delegation?.state === state.VERIFIED; + return ( { publicKeyNoCoord, network: btcWalletNetwork, getNetworkFees, + pushTx, } = useBTCWallet(); const disabled = isError; @@ -76,6 +79,17 @@ export const Staking = () => { const { params } = useAppState(); const latestParam = params?.bbnStakingParams?.latestParam; + const [pendingVerificationOpen, setPendingVerificationOpen] = useState(false); + const [stakingTx, setStakingTx] = useState(); + + const [eoiModalOpen, setEoiModalOpen] = useState(false); + const [eoiStatuses, setEoiStatuses] = useState({ + slashing: EOIStepStatus.UNSIGNED, + unbonding: EOIStepStatus.UNSIGNED, + reward: EOIStepStatus.UNSIGNED, + eoi: EOIStepStatus.UNSIGNED, + }); + // Mempool fee rates, comes from the network // Fetch fee rates, sat/vB const { @@ -131,6 +145,15 @@ export const Staking = () => { ]); const handleResetState = () => { + setStakingTx(undefined); + setPendingVerificationOpen(false); + setEoiModalOpen(false); + setEoiStatuses({ + slashing: EOIStepStatus.UNSIGNED, + unbonding: EOIStepStatus.UNSIGNED, + reward: EOIStepStatus.UNSIGNED, + eoi: EOIStepStatus.UNSIGNED, + }); setAwaitingWalletResponse(false); setFinalityProvider(undefined); setStakingAmountSat(0); @@ -147,9 +170,40 @@ export const Staking = () => { const queryClient = useQueryClient(); - // TODO: To hook up with the react signing modal - const signingCallback = async (step: SigningStep) => { - console.log("Signing step:", step); + const signingStepToStatusKey = ( + step: SigningStep, + ): keyof typeof eoiStatuses | null => { + switch (step) { + case SigningStep.STAKING_SLASHING: + return "slashing"; + case SigningStep.UNBONDING_SLASHING: + return "unbonding"; + case SigningStep.PROOF_OF_POSSESSION: + return "reward"; + case SigningStep.SEND_BBN: + return "eoi"; + default: + return null; + } + }; + + const signingCallback = async (step: SigningStep, status: EOIStepStatus) => { + setEoiStatuses((prevStatuses) => { + const statusKey = signingStepToStatusKey(step); + if (statusKey) { + return { + ...prevStatuses, + [statusKey]: status, + }; + } + return prevStatuses; + }); + }; + + const handlePendingVerificationClose = () => { + setPendingVerificationOpen(false); + handleFeedbackModal("cancel"); + handleResetState(); }; const handleDelegationEoiCreation = async () => { @@ -163,9 +217,16 @@ export const Staking = () => { stakingTimeBlocks, feeRate, }; - await createDelegationEoi(eoiInput, feeRate, signingCallback); - // TODO: Hook up with the react pending verify modal - handleResetState(); + setAwaitingWalletResponse(true); + setEoiModalOpen(true); + const stakingTX = await createDelegationEoi( + eoiInput, + feeRate, + signingCallback, + ); + setEoiModalOpen(false); + setStakingTx(stakingTX.toHex()); + setPendingVerificationOpen(true); } catch (error: Error | any) { showError({ error: { @@ -188,6 +249,30 @@ export const Staking = () => { } }; + const handleStake = async () => { + if (!stakingTx) { + throw new Error("Staking transaction not found"); + } + try { + // Right now we have staking tx + // later on this step should be changed to building and signing + await pushTx(stakingTx); + handleResetState(); + } catch (error: Error | any) { + showError({ + error: { + message: error.message, + errorState: ErrorState.STAKING, + }, + noCancel: true, + retryAction: async () => { + await pushTx(stakingTx); + handleResetState(); + }, + }); + } + }; + // Memoize the staking fee calculation const stakingFeeSat = useMemo(() => { if ( @@ -470,15 +555,19 @@ export const Staking = () => { onClose={handleCloseFeedbackModal} type={feedbackModal.type} /> + {stakingTx && ( + + )} null} + statuses={eoiStatuses} + open={eoiModalOpen} + onClose={() => setEoiModalOpen(false)} /> ); diff --git a/src/app/hooks/api/useDelegationV2.ts b/src/app/hooks/api/useDelegationV2.ts new file mode 100644 index 00000000..8ef5fd7c --- /dev/null +++ b/src/app/hooks/api/useDelegationV2.ts @@ -0,0 +1,15 @@ +import { getDelegationV2 } from "@/app/api/getDelegationsV2"; +import { ONE_SECOND } from "@/app/constants"; + +import { useAPIQuery } from "./useApi"; + +export function useDelegationV2(stakingTxHashHex: string) { + const data = useAPIQuery({ + queryKey: ["DELEGATION_BY_TX_HASH", stakingTxHashHex], + queryFn: () => getDelegationV2(stakingTxHashHex), + enabled: Boolean(stakingTxHashHex), + refetchInterval: 5 * ONE_SECOND, + }); + + return data; +} diff --git a/src/app/hooks/services/useTransactionService.tsx b/src/app/hooks/services/useTransactionService.tsx index a15df5ec..224a0b76 100644 --- a/src/app/hooks/services/useTransactionService.tsx +++ b/src/app/hooks/services/useTransactionService.tsx @@ -13,6 +13,7 @@ import { SigningStargateClient } from "@cosmjs/stargate"; import { Network, Psbt, Transaction } from "bitcoinjs-lib"; import { useCallback } from "react"; +import { EOIStepStatus } from "@/app/components/Modals/EOIModal"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; import { useAppState } from "@/app/state"; @@ -39,7 +40,7 @@ interface BtcSigningFuncs { message: string, type?: "ecdsa" | "bip322-simple", ) => Promise; - signingCallback: (step: SigningStep) => Promise; + signingCallback: (step: SigningStep, status: EOIStepStatus) => Promise; } export enum SigningStep { @@ -76,7 +77,10 @@ export const useTransactionService = () => { async ( stakingInput: BtcStakingInputs, feeRate: number, - signingCallback: (step: SigningStep) => Promise, + signingCallback: ( + step: SigningStep, + status: EOIStepStatus, + ) => Promise, ) => { // Perform checks checkWalletConnection( @@ -121,8 +125,11 @@ export const useTransactionService = () => { latestParam, { signPsbt, signMessage, signingCallback }, ); + await signingCallback(SigningStep.SEND_BBN, EOIStepStatus.PROCESSING); await sendBbnTx(signingStargateClient!, bech32Address, delegationMsg); - await signingCallback(SigningStep.SEND_BBN); + await signingCallback(SigningStep.SEND_BBN, EOIStepStatus.SIGNED); + + return stakingTx; }, [ cosmosConnected, @@ -186,7 +193,10 @@ export const useTransactionService = () => { async ( stakingTxHex: string, stakingInput: BtcStakingInputs, - signingCallback: (step: SigningStep) => Promise, + signingCallback: ( + step: SigningStep, + status: EOIStepStatus, + ) => Promise, ) => { // Perform checks checkWalletConnection( @@ -225,8 +235,9 @@ export const useTransactionService = () => { }, inclusionProof, ); + await signingCallback(SigningStep.SEND_BBN, EOIStepStatus.PROCESSING); await sendBbnTx(signingStargateClient!, bech32Address, delegationMsg); - await signingCallback(SigningStep.SEND_BBN); + await signingCallback(SigningStep.SEND_BBN, EOIStepStatus.SIGNED); }, [ cosmosConnected, @@ -492,6 +503,10 @@ const createBtcDelegationMsg = async ( const cleanedUnbondingTx = clearTxSignatures(unbondingTx); // Create slashing transactions and extract signatures + await btcSigningFuncs.signingCallback( + SigningStep.STAKING_SLASHING, + EOIStepStatus.PROCESSING, + ); const { psbt: slashingPsbt } = stakingInstance.createStakingOutputSlashingTransaction(cleanedStakingTx); const signedSlashingPsbtHex = await btcSigningFuncs.signPsbt( @@ -504,8 +519,15 @@ const createBtcDelegationMsg = async ( if (!slashingSig) { throw new Error("No signature found in the staking output slashing PSBT"); } - await btcSigningFuncs.signingCallback(SigningStep.STAKING_SLASHING); + await btcSigningFuncs.signingCallback( + SigningStep.STAKING_SLASHING, + EOIStepStatus.SIGNED, + ); + await btcSigningFuncs.signingCallback( + SigningStep.UNBONDING_SLASHING, + EOIStepStatus.PROCESSING, + ); const { psbt: unbondingSlashingPsbt } = stakingInstance.createUnbondingOutputSlashingTransaction(unbondingTx); const signedUnbondingSlashingPsbtHex = await btcSigningFuncs.signPsbt( @@ -520,8 +542,15 @@ const createBtcDelegationMsg = async ( if (!unbondingSignatures) { throw new Error("No signature found in the unbonding output slashing PSBT"); } - await btcSigningFuncs.signingCallback(SigningStep.UNBONDING_SLASHING); + await btcSigningFuncs.signingCallback( + SigningStep.UNBONDING_SLASHING, + EOIStepStatus.SIGNED, + ); + await btcSigningFuncs.signingCallback( + SigningStep.PROOF_OF_POSSESSION, + EOIStepStatus.PROCESSING, + ); // Create Proof of Possession const bech32AddressHex = uint8ArrayToHex(fromBech32(bech32Address).data); const signedBbnAddress = await btcSigningFuncs.signMessage( @@ -535,7 +564,10 @@ const createBtcDelegationMsg = async ( btcSigType: BTCSigType.ECDSA, btcSig: ecdsaSig, }; - await btcSigningFuncs.signingCallback(SigningStep.PROOF_OF_POSSESSION); + await btcSigningFuncs.signingCallback( + SigningStep.PROOF_OF_POSSESSION, + EOIStepStatus.SIGNED, + ); // Prepare and send protobuf message const msg: btcstakingtx.MsgCreateBTCDelegation =