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

Return 1inch to aggregators #230

Merged
merged 9 commits into from
Feb 20, 2024
Merged
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ SUBGRAPH_REQUEST_TIMEOUT=5000
# allow some state overrides from browser console for QA
ENABLE_QA_HELPERS=false

# 1inch API token to power /api/oneinch-rate
ONE_INCH_API_KEY=

REWARDS_BACKEND=http://127.0.0.1:4000

# rate limit
Expand Down
6 changes: 6 additions & 0 deletions config/matomoClickEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const enum MATOMO_CLICK_EVENTS_TYPES {
clickExploreDeFi = 'clickExploreDeFi',
// / page
openOceanDiscount = 'openOceanDiscount',
oneInchDiscount = 'oneInchDiscount',
viewEtherscanOnStakePage = 'viewEtherscanOnStakePage',
l2BannerStake = 'l2BannerStake',
l2LowFeeStake = 'l2LowFeeStake',
Expand Down Expand Up @@ -93,6 +94,11 @@ export const MATOMO_CLICK_EVENTS: Record<
'Push "Get discount" on OpenOcean banner on widget',
'eth_widget_openocean_discount',
],
[MATOMO_CLICK_EVENTS_TYPES.oneInchDiscount]: [
'Ethereum_Staking_Widget',
'Push "Get discount" on 1inch banner on widget',
'eth_widget_oneinch_discount',
],
[MATOMO_CLICK_EVENTS_TYPES.viewEtherscanOnStakePage]: [
'Ethereum_Staking_Widget',
'Push «View on Etherscan» on the right side of Lido Statistics',
Expand Down
4 changes: 4 additions & 0 deletions config/stake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BigNumber } from 'ethers';
import dynamics from './dynamics';
import { IPFS_REFERRAL_ADDRESS } from './ipfs';
import { AddressZero } from '@ethersproject/constants';
import { StakeSwapDiscountIntegrationKey } from 'features/stake/swap-discount-banner';

export const PRECISION = 10 ** 6;

Expand All @@ -22,3 +23,6 @@ export const STAKE_GASLIMIT_FALLBACK = BigNumber.from(
export const STAKE_FALLBACK_REFERRAL_ADDRESS = dynamics.ipfsMode
? IPFS_REFERRAL_ADDRESS
: AddressZero;

export const STAKE_SWAP_INTEGRATION: StakeSwapDiscountIntegrationKey =
'one-inch';
13 changes: 3 additions & 10 deletions features/stake/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { useMemo } from 'react';
import { isDesktop } from 'react-device-detect';
import { useConnectorInfo } from 'reef-knot/web3-react';

const ONE_INCH_URL = 'https://app.1inch.io/#/1/swap/ETH/steth';
const LEDGER_LIVE_ONE_INCH_DESKTOP_DEEPLINK = 'ledgerlive://discover/1inch-lld';
const LEDGER_LIVE_ONE_INCH_MOBILE_DEEPLINK = 'ledgerlive://discover/1inch-llm';

export const use1inchLinkProps = () => {
export const use1inchDeepLinkProps = () => {
const { isLedgerLive } = useConnectorInfo();

const linkProps = useMemo(() => {
return useMemo(() => {
if (isLedgerLive) {
const href = isDesktop
? LEDGER_LIVE_ONE_INCH_DESKTOP_DEEPLINK
Expand All @@ -20,13 +19,7 @@ export const use1inchLinkProps = () => {
target: '_self',
};
} else {
return {
href: ONE_INCH_URL,
target: '_blank',
rel: 'noopener noreferrer',
};
return {};
DiRaiks marked this conversation as resolved.
Show resolved Hide resolved
}
}, [isLedgerLive]);

return linkProps;
};
1 change: 1 addition & 0 deletions features/stake/swap-discount-banner/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { SwapDiscountBanner } from './swap-discount-banner';
export type { StakeSwapDiscountIntegrationKey } from './types';
68 changes: 68 additions & 0 deletions features/stake/swap-discount-banner/integrations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { TOKENS } from '@lido-sdk/constants';
import { getOpenOceanRate } from 'utils/get-open-ocean-rate';
import {
StakeSwapDiscountIntegrationKey,
StakeSwapDiscountIntegrationMap,
} from './types';
import { OpenOceanIcon, OneInchIcon, OverlayLink } from './styles';
import { parseEther } from '@ethersproject/units';
import { OPEN_OCEAN_REFERRAL_ADDRESS } from 'config/external-links';
import { MATOMO_CLICK_EVENTS } from 'config/matomoClickEvents';
import { getOneInchRate } from 'utils/get-one-inch-rate';
import { use1inchDeepLinkProps } from 'features/stake/hooks';

const DEFAULT_AMOUNT = parseEther('1');

const STAKE_SWAP_INTEGRATION_CONFIG: StakeSwapDiscountIntegrationMap = {
'open-ocean': {
title: 'OpenOcean',
async getRate() {
const { rate } = await getOpenOceanRate(
DEFAULT_AMOUNT,
'ETH',
TOKENS.STETH,
);
return rate;
},
BannerText({ discountPercent }) {
return (
<>
Get a <b>{discountPercent.toFixed(2)}% discount</b> by swapping to
stETH&nbsp;on the OpenOcean platform
</>
);
},
Icon: OpenOceanIcon,
linkHref: `https://app.openocean.finance/classic?referrer=${OPEN_OCEAN_REFERRAL_ADDRESS}#/ETH/ETH/STETH`,
matomoEvent: MATOMO_CLICK_EVENTS.openOceanDiscount,
},
'one-inch': {
title: '1inch',
async getRate() {
const { rate } = await getOneInchRate({ token: 'ETH' });
return rate;
},
BannerText({ discountPercent }) {
return (
<>
Get a <b>{discountPercent.toFixed(2)}% discount</b> by swapping to
stETH&nbsp;on the 1inch platform
</>
);
},
Icon: OneInchIcon,
linkHref: `https://app.1inch.io/#/1/simple/swap/ETH/stETH`,
matomoEvent: MATOMO_CLICK_EVENTS.oneInchDiscount,
CustomLink({ children, ...props }) {
const customProps = use1inchDeepLinkProps();
return (
<OverlayLink {...props} {...customProps}>
{children}
</OverlayLink>
);
},
},
};

export const getSwapIntegration = (key: StakeSwapDiscountIntegrationKey) =>
STAKE_SWAP_INTEGRATION_CONFIG[key];
12 changes: 11 additions & 1 deletion features/stake/swap-discount-banner/styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import BgSrc from 'assets/icons/swap-banner-bg.svg';
import OpenOcean from 'assets/icons/open-ocean.svg';
import OneInch from 'assets/icons/oneinch-circle.svg';

export const Wrap = styled.div`
position: relative;
Expand Down Expand Up @@ -42,13 +43,22 @@ export const OverlayLink = styled.a`

export const OpenOceanIcon = styled.img.attrs({
src: OpenOcean,
alt: 'openOcean',
alt: 'OpenOcean',
})`
width: 40px;
height: 40px;
display: block;
`;

export const OneInchIcon = styled.img.attrs({
src: OneInch,
alt: '1inch',
})`
display: block;
width: 40px;
height: 40px;
`;

export const TextWrap = styled.p`
flex: 1 1 auto;
color: #fff;
Expand Down
91 changes: 22 additions & 69 deletions features/stake/swap-discount-banner/swap-discount-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,43 @@
import { Button } from '@lidofinance/lido-ui';
import { trackEvent } from '@lidofinance/analytics-matomo';

import { MATOMO_CLICK_EVENTS } from 'config';
import { OPEN_OCEAN_REFERRAL_ADDRESS } from 'config/external-links';
import { STRATEGY_LAZY } from 'utils/swrStrategies';
import { getOpenOceanRate } from 'utils/get-open-ocean-rate';
import { parseEther } from '@ethersproject/units';
import { TOKENS } from '@lido-sdk/constants';
import { useLidoSWR } from '@lido-sdk/react';
import { enableQaHelpers } from 'utils';

import { Wrap, TextWrap, OpenOceanIcon, OverlayLink } from './styles';

const SWAP_URL = `https://app.openocean.finance/classic?referrer=${OPEN_OCEAN_REFERRAL_ADDRESS}#/ETH/ETH/STETH`;
const DISCOUNT_THRESHOLD = 1.004;
const DEFAULT_AMOUNT = parseEther('1');
const MOCK_LS_KEY = 'mock-qa-helpers-discount-rate';

type FetchRateResult = {
rate: number;
shouldShowDiscount: boolean;
discountPercent: number;
};

const calculateDiscountState = (rate: number): FetchRateResult => ({
rate,
shouldShowDiscount: rate > DISCOUNT_THRESHOLD,
discountPercent: (1 - 1 / rate) * 100,
});

// we show banner if STETH is considerably cheaper to get on dex than staking
// ETH -> stETH rate > THRESHOLD
const fetchRate = async (): Promise<FetchRateResult> => {
const { rate } = await getOpenOceanRate(DEFAULT_AMOUNT, 'ETH', TOKENS.STETH);
return calculateDiscountState(rate);
};

const linkClickHandler = () =>
trackEvent(...MATOMO_CLICK_EVENTS.openOceanDiscount);

if (enableQaHelpers && typeof window !== 'undefined') {
(window as any).setMockDiscountRate = (rate?: number) =>
rate === undefined
? localStorage.removeItem(MOCK_LS_KEY)
: localStorage.setItem(MOCK_LS_KEY, rate.toString());
}

const getData = (data: FetchRateResult | undefined) => {
if (!enableQaHelpers || typeof window == 'undefined') return data;
const mock = localStorage.getItem(MOCK_LS_KEY);
if (mock) {
return calculateDiscountState(parseFloat(mock));
}
return data;
};
import { useSwapDiscount } from './use-swap-discount';
import { Wrap, TextWrap, OverlayLink } from './styles';

export const SwapDiscountBanner = ({ children }: React.PropsWithChildren) => {
const swr = useLidoSWR<FetchRateResult>(
['swr:open-ocean-rate'],
fetchRate,
STRATEGY_LAZY,
);

const data = getData(swr.data);
const { data, initialLoading } = useSwapDiscount();

if (swr.initialLoading) return null;
if (initialLoading) return null;

if (!data?.shouldShowDiscount) return <>{children}</>;
if (!data || !data.shouldShowDiscount) return <>{children}</>;

const {
BannerText,
Icon,
discountPercent,
matomoEvent,
linkHref,
CustomLink,
} = data;
const Link = CustomLink ?? OverlayLink;
return (
<Wrap>
<OpenOceanIcon />
<Icon />
<TextWrap>
Get a <b>{data?.discountPercent.toFixed(2)}% discount</b> by swapping to
stETH&nbsp;on the OpenOcean platform
<BannerText discountPercent={discountPercent} />
</TextWrap>
<OverlayLink
<Link
target="_blank"
rel="noreferrer"
href={SWAP_URL}
onClick={linkClickHandler}
href={linkHref}
onClick={() => {
trackEvent(...matomoEvent);
}}
>
<Button fullwidth size="xs">
Get discount
</Button>
</OverlayLink>
</Link>
</Wrap>
);
};
24 changes: 24 additions & 0 deletions features/stake/swap-discount-banner/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MatomoEventType } from '@lidofinance/analytics-matomo';

export type StakeSwapDiscountIntegrationKey = 'open-ocean' | 'one-inch';

export type StakeSwapDiscountIntegrationValue = {
title: string;
getRate: () => Promise<number>;
linkHref: string;
CustomLink?: React.FC<React.PropsWithoutRef<React.ComponentProps<'a'>>>;
matomoEvent: MatomoEventType;
BannerText: React.FC<{ discountPercent: number }>;
Icon: React.FC;
};

export type StakeSwapDiscountIntegrationMap = Record<
StakeSwapDiscountIntegrationKey,
StakeSwapDiscountIntegrationValue
>;

export type FetchRateResult = {
rate: number;
shouldShowDiscount: boolean;
discountPercent: number;
};
60 changes: 60 additions & 0 deletions features/stake/swap-discount-banner/use-swap-discount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { STRATEGY_LAZY } from 'utils/swrStrategies';
import { useLidoSWR } from '@lido-sdk/react';
import { enableQaHelpers } from 'utils';
import type {
FetchRateResult,
StakeSwapDiscountIntegrationKey,
StakeSwapDiscountIntegrationValue,
} from './types';
import { STAKE_SWAP_INTEGRATION } from 'config';
import { getSwapIntegration } from './integrations';

const DISCOUNT_THRESHOLD = 1.004;
const MOCK_LS_KEY = 'mock-qa-helpers-discount-rate';

if (enableQaHelpers && typeof window !== 'undefined') {
(window as any).setMockDiscountRate = (rate?: number) =>
rate === undefined
? localStorage.removeItem(MOCK_LS_KEY)
: localStorage.setItem(MOCK_LS_KEY, rate.toString());
}

// we show banner if STETH is considerably cheaper to get on dex than staking
// ETH -> stETH rate > THRESHOLD
const fetchRate = async (
_: string,
integrationKey: StakeSwapDiscountIntegrationKey,
): Promise<FetchRateResult & StakeSwapDiscountIntegrationValue> => {
const integration = getSwapIntegration(integrationKey);
let rate: number;
const mock = localStorage.getItem(MOCK_LS_KEY);
if (enableQaHelpers && mock) {
rate = parseFloat(mock);
} else {
rate = await integration.getRate();
}
return {
...integration,
rate,
shouldShowDiscount: rate > DISCOUNT_THRESHOLD,
discountPercent: (1 - 1 / rate) * 100,
};
};

export const useSwapDiscount = () => {
return useLidoSWR(
['swr:swap-discount-rate', STAKE_SWAP_INTEGRATION],
// @ts-expect-error useLidoSWR has broken fetcher-key type signature
fetchRate,
{
...STRATEGY_LAZY,
onError(error, key) {
console.warn(
`[useSwapDiscount] Error fetching ETH->Steth:`,
key,
error,
);
},
},
);
};
Loading
Loading