Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render, screen } from '@testing-library/react';

import React from 'react';

import 'jest-canvas-mock';
import { MemoryRouter } from 'react-router-dom';

import { i18n } from '../../../../../locales/i18n';
import { Banner } from './Banner';

jest.mock('react-multi-carousel', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));

describe('Banner', () => {
beforeAll(async () => {
await i18n;
});

it('renders the USDT0 migration promo card first with CTA to market making', () => {
render(<Banner />, { wrapper: MemoryRouter });

expect(screen.getByText('USDT0 MIGRATION: ~10% APR')).toBeInTheDocument();

const cta = screen.getByText('Start migration');
expect(cta.closest('a')).toHaveAttribute('href', '/earn/market-making');
});

it('still renders the zero interest loans promo card', () => {
render(<Banner />, { wrapper: MemoryRouter });

expect(screen.getByText('0% INTEREST LOANS')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,29 @@ export const Banner: FC = () => {
swipeable
className="static"
renderDotsOutside
// showDots
autoPlay={false} // Needs to be true when we have more than 1 promo
showDots
autoPlay
dotListClass={styles.dot}
autoPlaySpeed={15000}
// infinite
infinite
>
<LandingPromoCard
heading={t(translations.landingPage.promotions.usdt0Migration.title)}
description={t(
translations.landingPage.promotions.usdt0Migration.description,
)}
actions={
<>
<Link
to="/earn/market-making"
className="inline-flex box-border items-center justify-center text-center border font-body font-semibold no-underline rounded cursor-pointer px-5 py-2 bg-gray-80 border-gray-50 text-gray-10 text-sm hover:bg-gray-50"
>
{t(translations.landingPage.promotions.usdt0Migration.cta)}
</Link>
</>
}
className="border-primary"
/>
<LandingPromoCard
heading={t(
translations.landingPage.promotions.zeroInterestLoans.title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const isAddress = (value?: string) => /^0x[a-fA-F0-9]{40}$/.test(value || '');

// USDT0/RBTC pool activation. Leave empty until the pool is deployed.
const USDT0_BTC_AMM_CONVERTER = '0xd107e06964112d3f70cfb386565dfbda16ae71f3';
const USDT0_BTC_AMM_POOL_TOKEN = '0x591e07d721c2e22eeb4bf33d0b3377daca886fcc';
export const USDT0_BTC_AMM_POOL_TOKEN =
'0x591e07d721c2e22eeb4bf33d0b3377daca886fcc';

const MAINNET_AMM_USDT0_WRBTC =
isAddress(USDT0_BTC_AMM_CONVERTER) && isAddress(USDT0_BTC_AMM_POOL_TOKEN)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render, screen } from '@testing-library/react';

import React from 'react';

import 'jest-canvas-mock';

import { ChainIds } from '@sovryn/ethers-provider';

import { i18n } from '../../../../../../../locales/i18n';
import { USDT0_BTC_AMM_POOL_TOKEN } from '../../../../MarketMakingPage.constants';
import { AmmLiquidityPool } from '../../../../utils/AmmLiquidityPool';
import { PoolsTableReturns } from './PoolsTableReturns';

jest.mock('../../../../hooks/useGetReturnRate', () => ({
useGetReturnRate: () => ({ beforeRewards: '0.42', afterRewards: '1.00' }),
}));

const usdt0Pool = new AmmLiquidityPool(
'USDT0',
'BTC',
1,
ChainIds.RSK_MAINNET,
'0xd107e06964112d3f70cfb386565dfbda16ae71f3',
USDT0_BTC_AMM_POOL_TOKEN,
);

const dllrPool = new AmmLiquidityPool(
'DLLR',
'BTC',
1,
ChainIds.RSK_MAINNET,
'0xe81373285eb8cdee2e0108e98c5aa022948da9d2',
'0x3D5eDF3201876BF6935090C319FE3Ff36ED3D494',
);

describe('PoolsTableReturns', () => {
beforeAll(async () => {
await i18n;
});

it('shows the boosted rate with Merkl badge for the USDT0/RBTC pool', () => {
render(<PoolsTableReturns pool={usdt0Pool} />);

expect(screen.getByText('~10.42%')).toBeInTheDocument();
expect(screen.getByLabelText('Merkl incentives')).toBeInTheDocument();
});

it('shows the plain rate without badge for other pools', () => {
render(<PoolsTableReturns pool={dllrPool} />);

expect(screen.getByText('0.42%')).toBeInTheDocument();
expect(screen.queryByLabelText('Merkl incentives')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import React, { FC, useMemo } from 'react';

import classNames from 'classnames';

import { USDT0_BTC_AMM_POOL_TOKEN } from '../../../../MarketMakingPage.constants';
import { useGetReturnRate } from '../../../../hooks/useGetReturnRate';
import { AmmLiquidityPool } from '../../../../utils/AmmLiquidityPool';
import styles from './PoolsTableReturns.module.css';
import { MerklIncentiveBadge } from './components/MerklIncentiveBadge/MerklIncentiveBadge';
import { MERKL_USDT0_CAMPAIGN_APR_BOOST } from './components/MerklIncentiveBadge/MerklIncentiveBadge.constants';

type PoolsTableReturnsProps = {
pool: AmmLiquidityPool;
Expand All @@ -23,7 +26,27 @@ export const PoolsTableReturns: FC<PoolsTableReturnsProps> = ({
[returnRates],
);

// USDT0 migration campaign (Merkl, ~10% APR) — remove once the campaign ends
const hasMerklIncentive = useMemo(
() => pool.poolTokenA === USDT0_BTC_AMM_POOL_TOKEN.toLowerCase(),
[pool.poolTokenA],
);

const boostedReturnRate = useMemo(
() => (MERKL_USDT0_CAMPAIGN_APR_BOOST + Number(returnRate)).toFixed(2),
[returnRate],
);

return (
<div className={classNames(styles.rewards, className)}>{returnRate}%</div>
<div className={classNames(styles.rewards, className)}>
{hasMerklIncentive ? (
<span className="inline-flex items-center gap-1">
<span>~{boostedReturnRate}%</span>
<MerklIncentiveBadge />
</span>
) : (
<>{returnRate}%</>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const MERKL_USDT0_CAMPAIGN_URL =
'https://app.merkl.xyz/opportunities/213019951942253509';

export const MERKL_USDT0_CAMPAIGN_APR_BOOST = 10;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { fireEvent, render, screen } from '@testing-library/react';

import React from 'react';

import 'jest-canvas-mock';

import { i18n } from '../../../../../../../../../locales/i18n';
import { MerklIncentiveBadge } from './MerklIncentiveBadge';
import { MERKL_USDT0_CAMPAIGN_URL } from './MerklIncentiveBadge.constants';

describe('MerklIncentiveBadge', () => {
beforeAll(async () => {
await i18n;
});

it('renders the gift icon', () => {
render(<MerklIncentiveBadge />);

expect(screen.getByLabelText('Merkl incentives')).toBeInTheDocument();
});

it('opens the tooltip on hover with campaign info and Merkl link', async () => {
render(<MerklIncentiveBadge />);

const trigger = screen.getByLabelText('Merkl incentives').closest('span');
fireEvent.mouseEnter(trigger!);

expect(
await screen.findByText('Merkl incentives live on this pool'),
).toBeInTheDocument();

const link = screen.getByText(/View & claim on Merkl/).closest('a');
expect(link).toHaveAttribute('href', MERKL_USDT0_CAMPAIGN_URL);
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noreferrer');
});

it('does not propagate clicks to the table row', () => {
const onRowClick = jest.fn();
render(
<div onClick={onRowClick}>
<MerklIncentiveBadge />
</div>,
);

fireEvent.click(screen.getByLabelText('Merkl incentives'));

expect(onRowClick).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { FC } from 'react';

import { t } from 'i18next';

import { Tooltip, TooltipPlacement, TooltipTrigger } from '@sovryn/ui';

import { translations } from '../../../../../../../../../locales/i18n';
import { MERKL_USDT0_CAMPAIGN_URL } from './MerklIncentiveBadge.constants';

export const MerklIncentiveBadge: FC = () => (
<span
className="prevent-row-click inline-flex"
onClick={event => event.stopPropagation()}
>
<Tooltip
content={
<div className="max-w-xs">
<div className="font-semibold mb-1">
{t(translations.marketMakingPage.poolsTable.merklIncentive.title)}
</div>
<div className="mb-2">
{t(
translations.marketMakingPage.poolsTable.merklIncentive
.description,
)}
</div>
<a
href={MERKL_USDT0_CAMPAIGN_URL}
target="_blank"
rel="noreferrer"
className="font-semibold underline"
>
{t(translations.marketMakingPage.poolsTable.merklIncentive.cta)} ↗
</a>
</div>
}
trigger={TooltipTrigger.hover}
placement={TooltipPlacement.top}
dataAttribute="merkl-incentive-badge"
>
<span className="inline-flex cursor-pointer">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Merkl incentives"
>
<circle cx="8" cy="8" r="7.25" stroke="#F57118" strokeWidth="1.5" />
<g
stroke="#F57118"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4.8 7.4h6.4v3.9a.7.7 0 0 1-.7.7H5.5a.7.7 0 0 1-.7-.7V7.4Z" />
<path d="M4.3 5.8h7.4v1.6H4.3z" />
<path d="M8 5.8V12" />
<path d="M8 5.6c-.5-1.2-1.5-1.9-2.3-1.4-.7.4-.4 1.4.4 1.6L8 5.6Zm0 0c.5-1.2 1.5-1.9 2.3-1.4.7.4.4 1.4-.4 1.6L8 5.6Z" />
</g>
</svg>
</span>
</Tooltip>
</span>
);
12 changes: 11 additions & 1 deletion apps/frontend/src/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,11 @@
"title": "0% INTEREST LOANS",
"description": "Take a loan with Sovryn Zero at 0% interest. ",
"cta": "Sovryn Zero"
},
"usdt0Migration": {
"title": "USDT0 MIGRATION: ~10% APR",
"description": "Migrate your USDT to USDT0 1:1 and provide liquidity to the USDT0/RBTC pool. Rewards paid every 8 hours via Merkl, for as long as the reward pool lasts.",
"cta": "Start migration"
}
},
"titleSection": {
Expand Down Expand Up @@ -1230,7 +1235,12 @@
"returnsRate": "Return rate",
"returnsInfo": "The return rate shown is based on swap fees, plus liquidity mining rewards (if applicable), earned by the given pool in the previous 24 hour period, extrapolated to show an estimated annual percentage rate of return.",
"volume": "24H volume",
"balance": "Balance"
"balance": "Balance",
"merklIncentive": {
"title": "Merkl incentives live on this pool",
"description": "USDT0/RBTC liquidity providers currently earn ~10% APR on top of pool fees through the USDT0 migration campaign. Rewards are tracked from the LP tokens in your wallet and paid out every 8 hours, for as long as the reward pool lasts.",
"cta": "View & claim on Merkl"
}
},
"poolsTableReturns": {
"title": "Up to {{percent}}% APR",
Expand Down
Loading