diff --git a/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem.tsx b/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem.tsx
index a03df9200..11e03ac06 100644
--- a/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem.tsx
+++ b/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem.tsx
@@ -4,6 +4,8 @@ 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;
@@ -11,11 +13,29 @@ interface Props {
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 (
-
+
-
+
{position.ammName}:
@@ -23,7 +43,10 @@ const V3ExternalHoldingsItem = ({ position }: Props) => {
Rekt Status:
-
{position.rektStatus}
+
+ {position.rektStatus !== 'At risk' && '-'}
+ {position.rektStatus}
+
{
position={position}
isOpen={isOpen}
setIsOpen={setIsOpen}
+ nonBancorToken={nonBancorToken}
/>
);
diff --git a/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsModal.tsx b/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsModal.tsx
index b68c9f8ba..f220bc3a3 100644
--- a/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsModal.tsx
+++ b/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsModal.tsx
@@ -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 () => {
@@ -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);
}
@@ -96,19 +101,21 @@ export const V3ExternalHoldingsModal = ({
return (
-
-
- Protect this {position.ammName} holding from impermanent loss
-
-
-
- {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.
-
-
-
Moving to Bancor
+
+
+
+ Protect this {position.ammName} holding from impermanent loss
+
+
+
+ {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.
+
+
+
Moving to Bancor
+
{position.tokens.map((t) => (
@@ -122,20 +129,46 @@ export const V3ExternalHoldingsModal = ({
))}
+ {!!position.nonBancorToken && (
+
+
Exit risky position
+
+
+
+ {prettifyNumber(position.nonBancorToken.tokenCurrentBalance)}{' '}
+ {position.nonBancorToken.tokenName}
+
+
HODL in your wallet
+
+
+ {prettifyNumber(
+ toBigNumber(position.nonBancorToken.tokenCurrentPrice).times(
+ position.nonBancorToken.tokenCurrentBalance
+ ),
+ true
+ )}
+
+
+ )}
+
+
+ Amounts might vary on execution
+
{txBusy ? '... waiting for confirmation' : 'Migrate and Protect'}
-
- 100% Protected • {lockDurationInDays} day cooldown •{' '}
- {withdrawalFeeInPercent}% withdrawal fee
-
+
{ApproveModal}
diff --git a/src/elements/earn/portfolio/v3/externalHoldings/externalHoldings.ts b/src/elements/earn/portfolio/v3/externalHoldings/externalHoldings.ts
index 85696e397..a8813a455 100644
--- a/src/elements/earn/portfolio/v3/externalHoldings/externalHoldings.ts
+++ b/src/elements/earn/portfolio/v3/externalHoldings/externalHoldings.ts
@@ -5,6 +5,7 @@ import { prettifyNumber } from 'utils/helperFunctions';
import {
ApyVisionData,
ApyVisionNonUniPosition,
+ ApyVisionNonUniPositionToken,
ApyVisionNonUniResponse,
ApyVisionUniPosition,
ExternalHolding,
@@ -33,7 +34,7 @@ const fetchApyVisionUniswap = async (
const fetchApyVisionNonUniswap = async (
user: string
): Promise => {
- 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(url);
return data.userPools;
@@ -115,6 +116,7 @@ export const getExternalHoldingsUni = (
// TODO add poolTokenAddress
poolTokenAddress: '',
poolTokenBalanceWei: '',
+ name: '',
};
return externalHolding;
})
@@ -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) {
@@ -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;
}
@@ -172,10 +176,12 @@ export const getExternalHoldingsNonUni = (
ammKey: pos.poolProviderKey,
ammName,
tokens,
+ nonBancorToken,
rektStatus,
usdValue,
poolTokenAddress: pos.address,
poolTokenBalanceWei,
+ name: pos.name,
};
return newPos;
})
diff --git a/src/elements/earn/portfolio/v3/externalHoldings/externalHoldings.types.ts b/src/elements/earn/portfolio/v3/externalHoldings/externalHoldings.types.ts
index 68c172e79..f32f969b5 100644
--- a/src/elements/earn/portfolio/v3/externalHoldings/externalHoldings.types.ts
+++ b/src/elements/earn/portfolio/v3/externalHoldings/externalHoldings.types.ts
@@ -54,6 +54,17 @@ export interface ApyVisionUniPosition {
};
}
+export interface ApyVisionNonUniPositionToken {
+ tokenAddress: string;
+ tokenName: string;
+ tokenStartingBalance: number;
+ tokenCurrentBalance: number;
+ tokenCurrentPrice: number;
+ tokenUsdGain: number;
+ weight: number;
+ averageWeightedExecutedPrice: number;
+}
+
export interface ApyVisionNonUniPosition {
poolProviderKey: AMMProvider;
networkId: number;
@@ -71,16 +82,7 @@ export interface ApyVisionNonUniPosition {
initialCapitalValueUsd: number;
totalFeeUsd: number;
hasPartialSessions: boolean;
- tokens: {
- tokenAddress: string;
- tokenName: string;
- tokenStartingBalance: number;
- tokenCurrentBalance: number;
- tokenCurrentPrice: number;
- tokenUsdGain: number;
- weight: number;
- averageWeightedExecutedPrice: number;
- }[];
+ tokens: ApyVisionNonUniPositionToken[];
netGainUsd: number;
netGainPct: number;
}
@@ -114,10 +116,12 @@ export interface ExternalHolding {
ammKey: AMMProvider;
ammName: string;
tokens: Token[];
+ nonBancorToken?: ApyVisionNonUniPositionToken;
usdValue: number;
rektStatus: string;
poolTokenAddress: string;
poolTokenBalanceWei: string;
+ name: string;
}
export interface ApyVisionData {
diff --git a/src/elements/earn/portfolio/v3/externalHoldings/useExternalHoldings.ts b/src/elements/earn/portfolio/v3/externalHoldings/useExternalHoldings.ts
index 515999eb9..d4d606d93 100644
--- a/src/elements/earn/portfolio/v3/externalHoldings/useExternalHoldings.ts
+++ b/src/elements/earn/portfolio/v3/externalHoldings/useExternalHoldings.ts
@@ -12,6 +12,7 @@ import {
ExternalHolding,
} from 'elements/earn/portfolio/v3/externalHoldings/externalHoldings.types';
import { getV3Tokens } from 'store/bancor/token';
+import { orderBy } from 'lodash';
const initialApyVisionData: ApyVisionData = {
positionsUni: [],
@@ -40,7 +41,7 @@ export const useExternalHoldings = () => {
);
const positions: ExternalHolding[] = useMemo(
- () => [...positionsUni, ...positionsNonUni],
+ () => orderBy([...positionsUni, ...positionsNonUni], 'usdValue', 'desc'),
[positionsUni, positionsNonUni]
);
diff --git a/src/services/notifications/notifications.ts b/src/services/notifications/notifications.ts
index 1bf0552b6..784cda053 100644
--- a/src/services/notifications/notifications.ts
+++ b/src/services/notifications/notifications.ts
@@ -661,3 +661,38 @@ export const rewardsClaimedNotification = (
},
dispatch
);
+
+export const confirmMigrateExtHoldingNotification = (
+ dispatch: any,
+ txHash: string,
+ name: string
+) =>
+ showNotification(
+ {
+ type: NotificationType.pending,
+ title: 'Pending Confirmation',
+ msg: `${name} migration is pending confirmation`,
+ txHash,
+ updatedInfo: {
+ successTitle: 'Success',
+ successMsg: `Your ${name} migration has been successfully completed.`,
+ errorTitle: 'Transaction Failed',
+ errorMsg: `Your ${name} migration has failed.`,
+ },
+ },
+ dispatch
+ );
+
+export const failedNotification = (
+ dispatch: any,
+ title = 'Unknown Error',
+ msg = `Something went wrong. Please try again or contact support.`
+) =>
+ showNotification(
+ {
+ type: NotificationType.error,
+ title,
+ msg,
+ },
+ dispatch
+ );