Skip to content

Commit

Permalink
Migration feature (#95)
Browse files Browse the repository at this point in the history
* feat: add migration feature

* Refactor component styles for responsive design

* Refactor component styles for responsive design

* remove migrate

* Refactor staking arguments in MigrationModal component

* Refactor MigrationStartModal component to improve user instructions

* Refactor Migration component to conditionally show start modal based on chain ID

* Refactor MigrationModal component to fix pointer events when loading

---------

Co-authored-by: ex_suzhonghui <ex_suzhonghui@tcl.com>
  • Loading branch information
snoopy1412 and ex_suzhonghui authored Sep 26, 2024
1 parent e8b0e5e commit a489637
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 5 deletions.
6 changes: 6 additions & 0 deletions public/images/status/success-white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions src/components/migration/deposits.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";
import Modal from "../modal";
import EnsureMatchNetworkButton from "../ensure-match-network-button";
import { Deposit } from "@/types";
import { formatBlanace } from "@/utils";

export function DepositsModal({
isOpen,
busy,
onClose,
deposits = [],
onConfirm,
}: {
isOpen: boolean;
deposits?: Deposit[];
busy: boolean;
onClose: () => void;
onConfirm: () => void;
}) {
const totalDeposits = deposits.length;
const maxMigratePerBatch = 50;
const batchesRequired = Math.ceil(totalDeposits / maxMigratePerBatch);
const currentBatchSize = Math.min(totalDeposits, maxMigratePerBatch);

return (
<Modal
title="Migrate deposits"
isOpen={isOpen}
okText="Start Migration"
onClose={onClose}
className="md:w-[450px]"
busy={busy}
>
<div className="max-h-1/2 flex w-full flex-col gap-5 overflow-y-auto pb-5">
{deposits.length ? (
deposits.map(({ id, value }) => (
<div className="flex justify-between" key={id}>
<span className="text-sm font-light text-white">ID#{id}</span>
<span className="text-sm font-light text-white">{formatBlanace(value)} RING</span>
</div>
))
) : (
<div className="text-sm font-light text-white">No deposits found</div>
)}
</div>
<div className="flex flex-col gap-5">
<div className="h-px bg-white/20" />
<div className="text-xs font-light text-white">Total: {totalDeposits} items</div>
<EnsureMatchNetworkButton
className="h-10 w-full border border-primary bg-primary text-sm font-bold text-white"
onClick={onConfirm}
disabled={totalDeposits === 0}
busy={busy}
>
Migrate({currentBatchSize})
</EnsureMatchNetworkButton>
<p className="m-0 text-xs font-light text-white">
You can migrate up to 50 deposit items at a time. To migrate all deposit items, please complete this process{" "}
<span className="text-[#FF0083]">{batchesRequired}</span> time(s).
</p>
</div>
</Modal>
);
}
30 changes: 30 additions & 0 deletions src/components/migration/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState, useEffect } from "react";
import { MigrationStartModal } from "./start";
import { MigrationModal } from "./migration";
import { useChainId } from "wagmi";
import { ChainID } from "@/types";

const NETWORKS_REQUIRING_MIGRATION = [ChainID.CRAB, ChainID.KOI];

export function Migration() {
const [modals, setModals] = useState({ start: false, migration: false });
const chainId = useChainId();

useEffect(() => {
if (!chainId) return;

if (NETWORKS_REQUIRING_MIGRATION.includes(chainId)) {
setModals({ start: true, migration: false });
}
}, [chainId]);

const handleNext = () => setModals({ start: false, migration: true });
const handleClose = (modal: keyof typeof modals) => setModals((prev) => ({ ...prev, [modal]: false }));

return (
<>
<MigrationStartModal isOpen={modals.start} onClose={() => handleClose("start")} onOk={handleNext} />
<MigrationModal isOpen={modals.migration} onClose={() => handleClose("migration")} />
</>
);
}
175 changes: 175 additions & 0 deletions src/components/migration/migration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use client";

import Image from "next/image";
import { PropsWithChildren, useCallback, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { CSSTransition } from "react-transition-group";
import { useStaking, useApp } from "@/hooks";
import { formatBalanceParts, getChainConfig, notifyTransaction } from "@/utils";
import { writeContract, waitForTransaction } from "@wagmi/core";
import { notification } from "../notification";
import EnsureMatchNetworkButton from "../ensure-match-network-button";
import CountLoading from "../count-loading";
import Link from "next/link";

interface Props {
isOpen: boolean;
maskClosable?: boolean;
className?: string;
onClose?: () => void;
}

export function MigrationModal({ isOpen, maskClosable = true, onClose = () => undefined }: PropsWithChildren<Props>) {
const [step1Busy, setStep1Busy] = useState(false);
const { activeChain } = useApp();
const { stakedRing, stakedDeposit, stakedDeposits, isLedgersInitialized, isDepositsInitialized } = useStaking();
const nodeRef = useRef<HTMLDivElement | null>(null);

const total = stakedRing + stakedDeposit;
const { integer, decimal } = formatBalanceParts(total);

const currentStep = total > 0n ? 1 : 2;

const handleUnbond = useCallback(async () => {
const { contract, explorer } = getChainConfig(activeChain);
setStep1Busy(true);

try {
const { hash } = await writeContract({
address: contract.staking.address,
abi: (await import(`@/config/abi/${contract.staking.abiFile}`)).default,
functionName: "unstake",
args: [stakedRing, stakedDeposits],
});

const receipt = await waitForTransaction({ hash });
notifyTransaction(receipt, explorer);
} catch (err) {
console.error(err);
notification.error({ description: (err as Error).message });
} finally {
setStep1Busy(false);
}
}, [activeChain, stakedRing, stakedDeposits]);

const isLoading = !isLedgersInitialized || !isDepositsInitialized;

return (
<>
{createPortal(
<CSSTransition
in={isOpen}
timeout={300}
nodeRef={nodeRef}
classNames="modal-fade"
unmountOnExit
onEnter={() => {
document.body.style.overflow = "hidden";
}}
onExited={() => {
document.body.style.overflow = "auto";
}}
>
<div
ref={nodeRef}
className="fixed inset-0 z-30 flex items-center justify-center bg-app-black/80 px-large"
onClick={() => maskClosable && onClose()}
>
<div className="relative flex w-full flex-col md:w-[450px]" onClick={(e) => e.stopPropagation()}>
<Image
alt="Close modal"
width={24}
height={24}
src="/images/close-white.svg"
className="absolute right-2.5 top-2.5 cursor-pointer transition-transform hover:scale-105 active:scale-95"
onClick={onClose}
/>
<div className={`h-full bg-component p-5`}>
<div className="flex flex-col items-center gap-5">
<Image src="/images/token/ring.svg" alt="ring" width={60} height={60} />
<div className="flex flex-col items-center gap-2.5">
<p className="text-sm font-light text-white">Reserved in Staking</p>
<p className="text-2xl font-bold">
<span className="text-[#FF0083]">{integer}</span>
<span className="text-white/50">.{decimal}</span>
</p>
</div>
</div>

<div
className="mt-5 flex w-full flex-col gap-5"
style={{ pointerEvents: !isLoading ? "auto" : "none" }}
>
<EnsureMatchNetworkButton
className="h-10 w-full border border-primary bg-primary text-sm font-bold text-white"
onClick={handleUnbond}
disabled={currentStep !== 1 || isLoading}
busy={step1Busy}
>
{total === 0n ? (
<span className="flex items-center justify-center gap-1">
<span>No Unstake required</span>
<Image src="/images/status/success-white.svg" alt="success" width={26} height={26} />
</span>
) : (
"Step1: Unstake"
)}
</EnsureMatchNetworkButton>

{currentStep === 2 ? (
<Link
href="https://darwinia.notion.site/How-do-you-migrate-your-deposit-10aaad1d671e80a8bf89e7c8b0b0e670?pvs=74"
passHref
target="_blank"
rel="noopener"
>
<EnsureMatchNetworkButton
className="h-10 w-full border border-primary bg-primary text-sm font-bold text-white"
disabled={isLoading}
>
Step2: Migrate
</EnsureMatchNetworkButton>
</Link>
) : (
<EnsureMatchNetworkButton
className="h-10 w-full border border-primary bg-primary text-sm font-bold text-white"
disabled
>
Step2: Migrate
</EnsureMatchNetworkButton>
)}

{currentStep === 2 ? (
<Link href="https://collator-staking.ringdao.com" passHref target="_blank" rel="noopener">
<EnsureMatchNetworkButton
className="h-10 w-full border border-primary bg-primary text-sm font-bold text-white"
disabled={isLoading}
>
Step3: Stake in new pool
</EnsureMatchNetworkButton>
</Link>
) : (
<EnsureMatchNetworkButton
className="h-10 w-full border border-primary bg-primary text-sm font-bold text-white"
disabled
>
Step3: Stake in new pool
</EnsureMatchNetworkButton>
)}
</div>
</div>
{isLoading && (
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center bg-white/5">
<CountLoading color="white" />
</div>
)}
</div>
</div>
</CSSTransition>,
document.body
)}
</>
);
}

//
50 changes: 50 additions & 0 deletions src/components/migration/start.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";
import Link from "next/link";
import Modal from "../modal";

export function MigrationStartModal({
isOpen,
onClose,
onOk,
}: {
isOpen: boolean;
onClose: () => void;
onOk: () => void;
}) {
return (
<Modal
title="We are migrating"
isOpen={isOpen}
okText="Start Migration"
onOk={onOk}
onClose={onClose}
className="md:w-[450px]"
busy={false}
>
<p className="text-sm leading-[22px]">
Please migrate to the new RING Pool to receive your rewards and participate in RingDAO governance. For more
information, please check{" "}
<Link
href="https://github.com/darwinia-network/DIPs/blob/main/DIPs/dip-7.md"
target="_blank"
rel="noopener noreferrer"
className="text-[#FF0083]"
>
DIP-7
</Link>
. To migrate, please follow the steps below:
</p>
<div className="flex flex-col gap-2.5">
{[
"to unstake your staked RING and Deposit.",
"to migrate your Deposit.",
"to go to the new staking DApp and create a new stake.",
].map((step, index) => (
<div key={index} className="bg-white/20 px-5 py-2.5 text-sm font-light leading-[22px] text-white">
Step {index + 1}: Click &quot;{["Unstake", "Migrate", "Stake in new pool"][index]}&quot; button {step}
</div>
))}
</div>
</Modal>
);
}
2 changes: 2 additions & 0 deletions src/components/staking-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useAccount } from "wagmi";
import LatestRewards from "./latest-rewards";
import ReservedInStaking from "./reserved-in-staking";
import StakingDepositTabs from "./staking-deposit-tabs";
import { Migration } from "./migration";

export default function StakingDashboard() {
const { address } = useAccount();
Expand All @@ -21,6 +22,7 @@ export default function StakingDashboard() {
</div>

<StakingDepositTabs />
<Migration />
</>
);
}
Loading

0 comments on commit a489637

Please sign in to comment.