diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/Banner/Banner.test.tsx b/apps/frontend/src/app/5_pages/LandingPage/components/Banner/Banner.test.tsx
new file mode 100644
index 000000000..e038672aa
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LandingPage/components/Banner/Banner.test.tsx
@@ -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 }) => (
+
{children}
+ ),
+}));
+
+describe('Banner', () => {
+ beforeAll(async () => {
+ await i18n;
+ });
+
+ it('renders the USDT0 migration promo card first with CTA to market making', () => {
+ render( , { 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( , { wrapper: MemoryRouter });
+
+ expect(screen.getByText('0% INTEREST LOANS')).toBeInTheDocument();
+ });
+});
diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/Banner/Banner.tsx b/apps/frontend/src/app/5_pages/LandingPage/components/Banner/Banner.tsx
index 1f456760d..733cee9e9 100644
--- a/apps/frontend/src/app/5_pages/LandingPage/components/Banner/Banner.tsx
+++ b/apps/frontend/src/app/5_pages/LandingPage/components/Banner/Banner.tsx
@@ -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
>
+
+
+ {t(translations.landingPage.promotions.usdt0Migration.cta)}
+
+ >
+ }
+ className="border-primary"
+ />
/^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)
diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/PoolsTableReturns.test.tsx b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/PoolsTableReturns.test.tsx
new file mode 100644
index 000000000..db80ef3ea
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/PoolsTableReturns.test.tsx
@@ -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( );
+
+ expect(screen.getByText('~10.42%')).toBeInTheDocument();
+ expect(screen.getByLabelText('Merkl incentives')).toBeInTheDocument();
+ });
+
+ it('shows the plain rate without badge for other pools', () => {
+ render( );
+
+ expect(screen.getByText('0.42%')).toBeInTheDocument();
+ expect(screen.queryByLabelText('Merkl incentives')).toBeNull();
+ });
+});
diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/PoolsTableReturns.tsx b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/PoolsTableReturns.tsx
index 6f5cdff3e..4dd9d3e39 100644
--- a/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/PoolsTableReturns.tsx
+++ b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/PoolsTableReturns.tsx
@@ -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;
@@ -23,7 +26,27 @@ export const PoolsTableReturns: FC = ({
[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 (
- {returnRate}%
+
+ {hasMerklIncentive ? (
+
+ ~{boostedReturnRate}%
+
+
+ ) : (
+ <>{returnRate}%>
+ )}
+
);
};
diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.constants.ts b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.constants.ts
new file mode 100644
index 000000000..f8339fbc5
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.constants.ts
@@ -0,0 +1,4 @@
+export const MERKL_USDT0_CAMPAIGN_URL =
+ 'https://app.merkl.xyz/opportunities/213019951942253509';
+
+export const MERKL_USDT0_CAMPAIGN_APR_BOOST = 10;
diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.test.tsx b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.test.tsx
new file mode 100644
index 000000000..f8d7f8a09
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.test.tsx
@@ -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( );
+
+ expect(screen.getByLabelText('Merkl incentives')).toBeInTheDocument();
+ });
+
+ it('opens the tooltip on hover with campaign info and Merkl link', async () => {
+ render( );
+
+ 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(
+
+
+
,
+ );
+
+ fireEvent.click(screen.getByLabelText('Merkl incentives'));
+
+ expect(onRowClick).not.toHaveBeenCalled();
+ });
+});
diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.tsx b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.tsx
new file mode 100644
index 000000000..7a34eeb68
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.tsx
@@ -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 = () => (
+ event.stopPropagation()}
+ >
+
+
+ {t(translations.marketMakingPage.poolsTable.merklIncentive.title)}
+
+
+ {t(
+ translations.marketMakingPage.poolsTable.merklIncentive
+ .description,
+ )}
+
+
+ {t(translations.marketMakingPage.poolsTable.merklIncentive.cta)} ↗
+
+
+ }
+ trigger={TooltipTrigger.hover}
+ placement={TooltipPlacement.top}
+ dataAttribute="merkl-incentive-badge"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/apps/frontend/src/locales/en/translations.json b/apps/frontend/src/locales/en/translations.json
index 1243d5b1d..44343c9a0 100644
--- a/apps/frontend/src/locales/en/translations.json
+++ b/apps/frontend/src/locales/en/translations.json
@@ -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": {
@@ -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",