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 9 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
Expand Up @@ -19,7 +19,7 @@ const V3ExternalHoldings = () => {
return positions.length ? (
<section className="content-block p-20">
<h2>External Holdings at risk</h2>
<p className="mb-10 text-graphite">
<p className="mt-10 mb-20 text-secondary">
Your holdings on other platforms are vulnerable to impermanent loss
</p>
<Swiper
Expand All @@ -39,17 +39,19 @@ 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-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}
</div>
</div>
</div>
)}
</section>
) : (
<div className="hidden" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,38 @@ 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>
Expand All @@ -38,6 +58,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,42 @@ 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>
)}

<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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { prettifyNumber } from 'utils/helperFunctions';
import {
ApyVisionData,
ApyVisionNonUniPosition,
ApyVisionNonUniPositionToken,
ApyVisionNonUniResponse,
ApyVisionUniPosition,
ExternalHolding,
Expand Down Expand Up @@ -33,7 +34,7 @@ const fetchApyVisionUniswap = async (
const fetchApyVisionNonUniswap = async (
user: string
): Promise<ApyVisionNonUniPosition[]> => {
const url = `https://api.apy.vision/portfolio/1/core/${user}?accessToken=${process.env.REACT_APP_APY_VISION_TOKEN}`;
const url = `https://api.apy.vision/portfolio/1/core/${user}?accessToken=${process.env.REACT_APP_APY_VISION_TOKEN}&isInWallet=true`;
try {
const { data } = await axios.get<ApyVisionNonUniResponse>(url);
return data.userPools;
Expand Down Expand Up @@ -115,6 +116,7 @@ export const getExternalHoldingsUni = (
// TODO add poolTokenAddress
poolTokenAddress: '',
poolTokenBalanceWei: '',
name: '',
};
return externalHolding;
})
Expand All @@ -130,12 +132,15 @@ export const getExternalHoldingsNonUni = (
// TODO Remove this filter once we support more than 2 reseves
.filter((pos) => pos.tokens.length === 2)
.map((pos) => {
let nonBancorToken: ApyVisionNonUniPositionToken | undefined =
undefined;
const tokens = pos.tokens
.map((token) => {
const address = utils.getAddress(token.tokenAddress);
const isETH = address === utils.getAddress(wethToken);
const tkn = tokensMap.get(isETH ? ethToken : address);
if (!tkn) {
nonBancorToken = token;
return undefined;
}
if (isETH) {
Expand All @@ -152,8 +157,7 @@ export const getExternalHoldingsNonUni = (
})
.filter((t) => !!t) as Token[];

// TODO once we support pools with more than 2 reserve tokens we need to update this
if (tokens.length !== 2) {
if (tokens.length === 0) {
return undefined;
}

Expand All @@ -172,10 +176,12 @@ export const getExternalHoldingsNonUni = (
ammKey: pos.poolProviderKey,
ammName,
tokens,
nonBancorToken,
rektStatus,
usdValue,
poolTokenAddress: pos.address,
poolTokenBalanceWei,
name: pos.name,
};
return newPos;
})
Expand Down
Loading