Skip to content

Commit

Permalink
eoi modal, pending verification modal (#370)
Browse files Browse the repository at this point in the history
  • Loading branch information
gbarkhatov authored Nov 21, 2024
1 parent f0af90e commit ac95659
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 56 deletions.
57 changes: 26 additions & 31 deletions src/app/components/Modals/EOIModal.tsx
Original file line number Diff line number Diff line change
@@ -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: <AiOutlineSignature size={20} />,
signed: <IoMdCheckmark className="text-success" size={20} />,
processing: (
[EOIStepStatus.UNSIGNED]: <AiOutlineSignature size={20} />,
[EOIStepStatus.SIGNED]: <IoMdCheckmark className="text-success" size={20} />,
[EOIStepStatus.PROCESSING]: (
<span className="loading loading-spinner loading-xs text-primary" />
),
} as const;

export function EOIModal({ open, statuses, onClose }: EOIModalProps) {
return (
<GeneralModal open={open} onClose={onClose}>
<GeneralModal
open={open}
onClose={onClose}
closeOnOverlayClick={false}
closeOnEsc={false}
>
<div className="mb-4 flex items-center justify-between">
<h3 className="font-bold">Staking Express Of Interest</h3>
<button
className="btn btn-circle btn-ghost btn-sm"
onClick={() => onClose(false)}
>
<IoMdClose size={24} />
</button>
<h3 className="font-bold">Staking</h3>
</div>

<div className="py-4">
<p>
Please sign the messages below to prepare your staking
express-of-interest (EOI):
</p>
<p>Please sign the following messages</p>

<ul className="my-8 md:pl-6 text-primary">
<ul className="my-8 md:pl-6">
<li className="flex gap-1 mb-4 items-center">
{STATUS_ICON[statuses.slashing]}
Consent to slashing
Step 1: Consent to slashing
</li>
<li className="flex gap-1 mb-4 items-center">
{STATUS_ICON[statuses.unbonding]}
Consent to slashing during unbonding
Step 2: Consent to slashing during unbonding
</li>
<li className="flex gap-1 mb-4 items-center">
{STATUS_ICON[statuses.reward]}
BTC-BBN address binding for receiving staking rewards
Step 3: BTC-BBN address binding for receiving staking rewards
</li>
<li className="flex gap-1 mb-4 items-center">
{STATUS_ICON[statuses.eoi]}
EOI transaction
Step 4: Staking transaction registration
</li>
</ul>

<p>
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.
</p>
</div>
</GeneralModal>
);
Expand Down
10 changes: 8 additions & 2 deletions src/app/components/Modals/PendingVerificationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
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";

interface PendingVerificationModalProps {
open: boolean;
onClose: (value: boolean) => void;
verified: boolean;
onStake?: () => void;
awaitingWalletResponse: boolean;
stakingTxHash: string;
}

const Verified = () => (
Expand Down Expand Up @@ -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 (
<GeneralModal
open={open}
Expand Down
119 changes: 104 additions & 15 deletions src/app/components/Staking/Staking.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Transaction } from "bitcoinjs-lib";
import { useEffect, useMemo, useState } from "react";
import { Tooltip } from "react-tooltip";
import { useLocalStorage } from "usehooks-ts";

import { UTXO_KEY } from "@/app/common/constants";
import { LoadingView } from "@/app/components/Loading/Loading";
import { EOIModal } from "@/app/components/Modals/EOIModal";
import { EOIModal, EOIStepStatus } from "@/app/components/Modals/EOIModal";
import { useError } from "@/app/context/Error/ErrorContext";
import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider";
import {
Expand All @@ -23,6 +24,7 @@ import { getFeeRateFromMempool } from "@/utils/getFeeRateFromMempool";
import { isStakingSignReady } from "@/utils/isStakingSignReady";

import { FeedbackModal } from "../Modals/FeedbackModal";
import { PendingVerificationModal } from "../Modals/PendingVerificationModal";
import { PreviewModal } from "../Modals/PreviewModal";

import { FinalityProviders } from "./FinalityProviders/FinalityProviders";
Expand All @@ -47,6 +49,7 @@ export const Staking = () => {
publicKeyNoCoord,
network: btcWalletNetwork,
getNetworkFees,
pushTx,
} = useBTCWallet();

const disabled = isError;
Expand Down Expand Up @@ -76,6 +79,17 @@ export const Staking = () => {
const { params } = useAppState();
const latestParam = params?.bbnStakingParams?.latestParam;

const [pendingVerificationOpen, setPendingVerificationOpen] = useState(false);
const [stakingTx, setStakingTx] = useState<string | undefined>();

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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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 () => {
Expand All @@ -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: {
Expand All @@ -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 (
Expand Down Expand Up @@ -470,15 +555,19 @@ export const Staking = () => {
onClose={handleCloseFeedbackModal}
type={feedbackModal.type}
/>
{stakingTx && (
<PendingVerificationModal
open={pendingVerificationOpen}
onClose={handlePendingVerificationClose}
stakingTxHash={Transaction.fromHex(stakingTx).getId()}
awaitingWalletResponse={awaitingWalletResponse}
onStake={handleStake}
/>
)}
<EOIModal
statuses={{
slashing: "signed",
unbonding: "signed",
reward: "processing",
eoi: "unsigned",
}}
open={false}
onClose={() => null}
statuses={eoiStatuses}
open={eoiModalOpen}
onClose={() => setEoiModalOpen(false)}
/>
</div>
);
Expand Down
15 changes: 15 additions & 0 deletions src/app/hooks/api/useDelegationV2.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit ac95659

Please sign in to comment.