From bfccbadf1c4d3e82cbd00ea29d37d7c7d6f04d21 Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Fri, 17 Jul 2026 13:09:08 +0300 Subject: [PATCH 1/5] feat: add USDT0 migration campaign promo card to landing banner Campaign: ~10% APR via Merkl for USDT0/RBTC LPs, rewards every 8h. Enables carousel rotation (autoPlay/showDots/infinite) now that there is more than one promo card. Spec: tyrone-work usdt0-rollout docs 2026-07-13-usdt0-campaign-frontend-spec.md --- .../components/Banner/Banner.test.tsx | 37 +++++++++++++++++++ .../LandingPage/components/Banner/Banner.tsx | 23 ++++++++++-- .../frontend/src/locales/en/translations.json | 5 +++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 apps/frontend/src/app/5_pages/LandingPage/components/Banner/Banner.test.tsx 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" + /> Date: Fri, 17 Jul 2026 13:12:22 +0300 Subject: [PATCH 2/5] feat: add Merkl incentive badge with tooltip for USDT0 campaign Circled gift icon (design-final SVG, #F57118) with hover/tap tooltip linking to the Merkl campaign page. Outer wrapper stops click propagation so the pools-table row does not expand on tap. --- .../MerklIncentiveBadge.constants.ts | 4 ++ .../MerklIncentiveBadge.test.tsx | 50 ++++++++++++++ .../MerklIncentiveBadge.tsx | 67 +++++++++++++++++++ .../frontend/src/locales/en/translations.json | 7 +- 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.constants.ts create mode 100644 apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.test.tsx create mode 100644 apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/components/MerklIncentiveBadge/MerklIncentiveBadge.tsx 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 4c5e09b06..44343c9a0 100644 --- a/apps/frontend/src/locales/en/translations.json +++ b/apps/frontend/src/locales/en/translations.json @@ -1235,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", From 58b17cb878d36333b8cb9cb3532bca045283a0b2 Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Fri, 17 Jul 2026 13:34:26 +0300 Subject: [PATCH 3/5] feat: show boosted ~10.NN% Merkl rate on USDT0/RBTC pool row Adds campaign 10% APR on top of the pool's organic return rate for the USDT0/RBTC AMM pool only, identified by its exported pool token address. Covers desktop column and mobile row title (both render PoolsTableReturns). --- .../MarketMakingPage.constants.ts | 3 +- .../PoolsTableReturns.test.tsx | 54 +++++++++++++++++++ .../PoolsTableReturns/PoolsTableReturns.tsx | 25 ++++++++- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 apps/frontend/src/app/5_pages/MarketMakingPage/components/PoolsTable/components/PoolsTableReturns/PoolsTableReturns.test.tsx diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/MarketMakingPage.constants.ts b/apps/frontend/src/app/5_pages/MarketMakingPage/MarketMakingPage.constants.ts index d31f493ab..90168938c 100644 --- a/apps/frontend/src/app/5_pages/MarketMakingPage/MarketMakingPage.constants.ts +++ b/apps/frontend/src/app/5_pages/MarketMakingPage/MarketMakingPage.constants.ts @@ -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) 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}% + )} +
); }; From 4c12ef357fee9b91c3ed62f04509ac8ecd10ad5a Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Fri, 17 Jul 2026 19:55:15 +0300 Subject: [PATCH 4/5] dummy deploy for Netlify --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fcc89ddb9..60dca3a21 100644 --- a/README.md +++ b/README.md @@ -134,4 +134,4 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope ## Licence -The Sovryn dapp is open-sourced software licensed under the [MIT license](LICENSE). +The Sovryn dapp is open-sourced software licensed under the [MIT license](LICENSE) From 29c6c034c6c66db42b06a90888c5ed5543444d39 Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Fri, 17 Jul 2026 20:45:47 +0300 Subject: [PATCH 5/5] Revert "dummy deploy for Netlify" This reverts commit 4c12ef357fee9b91c3ed62f04509ac8ecd10ad5a. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 60dca3a21..fcc89ddb9 100644 --- a/README.md +++ b/README.md @@ -134,4 +134,4 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope ## Licence -The Sovryn dapp is open-sourced software licensed under the [MIT license](LICENSE) +The Sovryn dapp is open-sourced software licensed under the [MIT license](LICENSE).