Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: External Migration (Portal) #398

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/tokensOverlap/TokensOverlap.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Token } from 'services/observables/tokens';
import { Token, TokenMinimal } from 'services/observables/tokens';
import { Reserve } from 'services/observables/pools';
import { Image } from 'components/image/Image';

export const TokensOverlap = ({
tokens,
maxLogos = 4,
}: {
tokens: Token[] | Reserve[];
tokens: Token[] | Reserve[] | TokenMinimal[];
maxLogos?: number;
}) => {
const tokenCount = tokens.length;
Expand Down
8 changes: 8 additions & 0 deletions src/elements/earn/portfolio/v3/V3Portfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ReactComponent as HoldingsLight } from 'assets/holdingsLight.svg';
import { ReactComponent as HoldingsDark } from 'assets/holdingsDark.svg';
import { V3Holdings } from 'elements/earn/portfolio/v3/holdings/V3Holdings';
import { useWalletConnect } from 'elements/walletConnect/useWalletConnect';
import V3ExternalHoldings from 'elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldings';

const V3Portfolio = () => {
const account = useAppSelector((state) => state.user.account);
Expand Down Expand Up @@ -55,6 +56,13 @@ const V3Portfolio = () => {
</h2>
<V3Withdraw />
</div>

<div>
<h2 className="md:hidden max-w-[300px] rounded-20 h-[35px] mb-10">
External Holdings
</h2>
<V3ExternalHoldings />
</div>
</div>
</div>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { memo, useState } from 'react';
import { memo } from 'react';
import V3ExternalHoldingsItem from 'elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react/swiper-react';
import { NavigationOptions } from 'swiper/types';
import { useExternalHoldings } from 'elements/earn/portfolio/v3/externalHoldings/useExternalHoldings';
import { ReactComponent as IconArrow } from 'assets/icons/arrow.svg';

const navOptions: NavigationOptions = {
nextEl: '.external-holding-swiper-next-btn',
Expand All @@ -13,23 +14,22 @@ const navOptions: NavigationOptions = {
};

const V3ExternalHoldings = () => {
const [activeIndex, setActiveIndex] = useState(1);
const { positions } = useExternalHoldings();

return positions.length ? (
<section className="content-block p-20">
<h2>External Holdings at risk</h2>
<p className="mb-10 text-graphite">
<h2>
External Holdings at risk{' '}
<span className="text-14 text-secondary">({positions.length})</span>
</h2>
<p className="mt-10 mb-20 text-secondary">
Your holdings on other platforms are vulnerable to impermanent loss
</p>
<Swiper
modules={[Navigation]}
spaceBetween={20}
slidesPerView={1}
grabCursor
onActiveIndexChange={({ activeIndex }) =>
setActiveIndex(activeIndex + 1)
}
navigation={navOptions}
>
{positions.map((pos, i) => (
Expand All @@ -39,17 +39,16 @@ const V3ExternalHoldings = () => {
))}
</Swiper>

<div className="space-x-10 flex items-center mt-10">
<button className="external-holding-swiper-prev-btn hover:text-primary">
{'<--'}
</button>
<button className="external-holding-swiper-next-btn hover:text-primary">
{'-->'}
</button>
<div>
{activeIndex} of {positions.length}
{positions.length > 1 && (
<div className="space-x-30 flex items-center mt-10">
<button className="external-holding-swiper-prev-btn hover:text-primary">
<IconArrow className="w-10 rotate-[-90deg]" />
</button>
<button className="external-holding-swiper-next-btn hover:text-primary">
<IconArrow className="w-10 rotate-[90deg]" />
</button>
</div>
</div>
)}
</section>
) : (
<div className="hidden" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,49 @@ import { Button, ButtonSize, ButtonVariant } from 'components/button/Button';
import { ExternalHolding } from 'elements/earn/portfolio/v3/externalHoldings/externalHoldings.types';
import { V3ExternalHoldingsModal } from 'elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsModal';
import { TokensOverlap } from 'components/tokensOverlap/TokensOverlap';
import { useAppSelector } from 'store/index';
import { utils } from 'ethers';

interface Props {
position: ExternalHolding;
}

const V3ExternalHoldingsItem = ({ position }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const allTokenListTokens = useAppSelector(
(state) => state.bancor.allTokenListTokens
);

const nonBancorToken =
position.nonBancorToken !== undefined
? allTokenListTokens.find(
(t) =>
utils.getAddress(t.address) ===
utils.getAddress(position.nonBancorToken?.tokenAddress ?? '')
)
: undefined;

return (
<div className="rounded-20 border border-fog p-20">
<div className="rounded-20 border border-fog dark:border-grey p-20">
<div className="h-30">
<TokensOverlap tokens={position.tokens} />
<TokensOverlap
tokens={
nonBancorToken
? [...position.tokens, nonBancorToken]
: position.tokens
}
/>
</div>
<div className="mt-20 flex justify-between">
<div>{position.ammName}: </div>
<div>{prettifyNumber(position.usdValue, true)}</div>
</div>
<div className="mt-6 flex justify-between">
<div>Rekt Status: </div>
<div className="text-error">{position.rektStatus}</div>
<div className="text-error">
{position.rektStatus !== 'At risk' && '-'}
{position.rektStatus}
</div>
</div>
<Button
variant={ButtonVariant.Secondary}
Expand All @@ -38,6 +61,7 @@ const V3ExternalHoldingsItem = ({ position }: Props) => {
position={position}
isOpen={isOpen}
setIsOpen={setIsOpen}
nonBancorToken={nonBancorToken}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,46 @@ import { useApproveModal } from 'hooks/useApproveModal';
import { mockToken } from 'utils/mocked';
import { getMigrateFnByAmmProvider } from 'elements/earn/portfolio/v3/externalHoldings/externalHoldings';
import { shrinkToken } from 'utils/formulas';
import { ProtectedSettingsV3 } from 'components/protectedSettingsV3/ProtectedSettingsV3';
import { prettifyNumber, toBigNumber } from 'utils/helperFunctions';
import { TokenMinimal } from 'services/observables/tokens';
import { Image } from 'components/image/Image';
import {
confirmMigrateExtHoldingNotification,
failedNotification,
rejectNotification,
} from 'services/notifications/notifications';
import { ErrorCode } from 'services/web3/types';

interface Props {
position: ExternalHolding;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
nonBancorToken?: TokenMinimal;
}

export const V3ExternalHoldingsModal = ({
position,
isOpen,
setIsOpen,
nonBancorToken,
}: Props) => {
const [txBusy, setTxBusy] = useState(false);
const account = useAppSelector((state) => state.user.account);
const dispatch = useDispatch();

const { withdrawalFee, lockDuration } = useAppSelector(
(state) => state.v3Portfolio.withdrawalSettings
);

const lockDurationInDays = useMemo(
() => lockDuration / 60 / 60 / 24,
[lockDuration]
);

const withdrawalFeeInPercent = useMemo(
() => (withdrawalFee * 100).toFixed(2),
[withdrawalFee]
);

const tokensToApprove = useMemo(
() => [
{
token: {
...mockToken,
address: position.poolTokenAddress,
symbol: 'lpTKN',
symbol: position.name,
},
amount: shrinkToken(position.poolTokenBalanceWei, 18),
},
],
[position.poolTokenAddress, position.poolTokenBalanceWei]
[position.name, position.poolTokenAddress, position.poolTokenBalanceWei]
);

const migrate = async () => {
Expand All @@ -68,16 +66,23 @@ export const V3ExternalHoldingsModal = ({
}

try {
const res = await migrateFn(
const tx = await migrateFn(
position.tokens[0].address,
position.tokens[1].address,
position.tokens[1]?.address ?? position.nonBancorToken?.tokenAddress,
position.poolTokenBalanceWei
);
await res.wait();
setIsOpen(false);
confirmMigrateExtHoldingNotification(dispatch, tx.hash, position.name);
await tx.wait();
await updatePortfolioData(dispatch);
} catch (e) {
console.error(e);
} catch (e: any) {
console.error('failed to migrate position', e);
if (e.code === ErrorCode.DeniedTx) {
rejectNotification(dispatch);
} else {
failedNotification(dispatch, 'Migration Failed');
}
setIsOpen(false);
} finally {
setTxBusy(false);
}
Expand All @@ -96,19 +101,21 @@ export const V3ExternalHoldingsModal = ({

return (
<Modal title={'Migrate'} setIsOpen={setIsOpen} isOpen={isOpen} large>
<div className="p-30 pt-0">
<h2 className="text-[24px] leading-9">
Protect this {position.ammName} holding from impermanent loss
</h2>

<p className="mt-16 mb-20 text-secondary">
{position.rektStatus === 'At risk'
? 'Your position is at risk of impermanent loss'
: `You’ve lost ${position.rektStatus} in impermanent loss so far`}
, get 100% protected on Bancor.
</p>

<h3 className="mb-10">Moving to Bancor</h3>
<div className="px-20 pb-10">
<div className="px-20">
<h2 className="text-[24px] leading-9">
Protect this {position.ammName} holding from impermanent loss
</h2>

<p className="mt-16 mb-20 text-secondary">
{position.rektStatus === 'At risk'
? 'Your position is at risk of impermanent loss'
: `You’ve lost ${position.rektStatus} in impermanent loss so far`}
, get 100% protected on Bancor.
</p>

<h3 className="mb-10">Moving to Bancor</h3>
</div>

<div className="bg-fog dark:bg-grey p-20 rounded space-y-20">
{position.tokens.map((t) => (
Expand All @@ -122,20 +129,46 @@ export const V3ExternalHoldingsModal = ({
</div>
))}
</div>
{!!position.nonBancorToken && (
<div className="px-20">
<h3 className="mb-10 mt-20 text-secondary">Exit risky position</h3>
<div className="flex items-center justify-between">
<div className="flex items-center">
<Image
alt={'Token Logo'}
src={nonBancorToken?.logoURI}
className="w-20 h-20 !rounded-full mr-10"
/>
{prettifyNumber(position.nonBancorToken.tokenCurrentBalance)}{' '}
{position.nonBancorToken.tokenName}
</div>
<div className="text-secondary">HODL in your wallet</div>
</div>
<div className="text-secondary ml-30">
{prettifyNumber(
toBigNumber(position.nonBancorToken.tokenCurrentPrice).times(
position.nonBancorToken.tokenCurrentBalance
),
true
)}
</div>
</div>
)}

<div className="mt-20 text-secondary text-center">
Amounts might vary on execution
</div>

<Button
onClick={handleButtonClick}
size={ButtonSize.Full}
className="mt-20"
className="mt-20 mb-10"
disabled={txBusy}
>
{txBusy ? '... waiting for confirmation' : 'Migrate and Protect'}
</Button>

<p className="text-secondary text-center mt-10">
100% Protected • {lockDurationInDays} day cooldown •{' '}
{withdrawalFeeInPercent}% withdrawal fee
</p>
<ProtectedSettingsV3 />

{ApproveModal}
</div>
Expand Down
Loading