Skip to content

Commit

Permalink
Feat/op users fusion (#324)
Browse files Browse the repository at this point in the history
* chore: add top user modal

* chore: localise format distance to now

* chore: wagmi ssr connect

* chore: hide top 100 spice users with feature flag

* chore: fix failing tests

* chore: feature flag fusion popover

* chore: update switch height multiplier

---------

Co-authored-by: Slava <[email protected]>
  • Loading branch information
danielsimao and slavastartsev authored Dec 9, 2024
1 parent 57a9afc commit 2c94656
Show file tree
Hide file tree
Showing 47 changed files with 1,538 additions and 445 deletions.
11 changes: 7 additions & 4 deletions apps/evm/lingui.config.js → apps/bob-pay/lingui.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @type {import('@lingui/conf').LinguiConfig} */
module.exports = {
locales: ['en', 'zh'],
import { LinguiConfig } from '@lingui/conf';

const config = {
locales: ['en', 'zh'] as const,
sourceLocale: 'en',
fallbackLocales: {
default: 'en'
Expand All @@ -11,4 +12,6 @@ module.exports = {
include: ['src/']
}
]
};
} as const satisfies LinguiConfig;

export default config;
5 changes: 3 additions & 2 deletions apps/evm/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ NEXT_PUBLIC_L1_CHAIN="11155111"
NEXT_PUBLIC_L2_CHAIN="808813"
NEXT_COINGECKO_API_KEY=
NEXT_PUBLIC_GEOBLOCK_ENABLED=false
NEXT_PUBLIC_FEATURE_FLAG_WALLET=enabled
NEXT_PUBLIC_INDEXER_URL=
NEXT_PUBLIC_SENTRY_AUTH_TOKEN=
NEXT_PUBLIC_SENTRY_URL=
SENTRY_AUTH_TOKEN=
KV_REST_API_URL=
KV_REST_API_TOKEN=
KV_REST_API_TOKEN=
NEXT_PUBLIC_FEATURE_FLAG_TOP_100_SPICE_USERS=enabled
NEXT_PUBLIC_FEATURE_FLAG_OP_SUPERUSER=enabled
9 changes: 6 additions & 3 deletions apps/bob-pay/lingui.config.js → apps/evm/lingui.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @type {import('@lingui/conf').LinguiConfig} */
module.exports = {
import { LinguiConfig } from '@lingui/conf';

const config = {
locales: ['en', 'zh'],
sourceLocale: 'en',
fallbackLocales: {
Expand All @@ -11,4 +12,6 @@ module.exports = {
include: ['src/']
}
]
};
} as const satisfies LinguiConfig;

export default config;
3 changes: 3 additions & 0 deletions apps/evm/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { withSentryConfig } from '@sentry/nextjs';

/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['raw.githubusercontent.com'] // Add the allowed hostname here
},
compiler: {
styledComponents: true
},
Expand Down
1 change: 1 addition & 0 deletions apps/evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"date-fns": "catalog:",
"graphql-request": "catalog:",
"lottie-react": "^2.4.0",
"react-calendly": "^4.3.1",
"negotiator": "catalog:",
"next": "catalog:",
"react": "catalog:",
Expand Down
Binary file added apps/evm/public/assets/optimism-city.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/evm/public/assets/top-100-spice.webp
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { Clock, Flex, FlexProps } from '@gobob/ui';
import { Trans } from '@lingui/macro';
import { formatDistanceToNow, isFuture } from 'date-fns';
import { ReactNode, useMemo } from 'react';
import { useParams } from 'next/navigation';

import { BridgeTransaction } from '../../hooks';

import { StyledTimePill } from './BridgeStatus.style';
import { BridgeStep } from './BridgeStep';

import { BridgeSteps } from '@/types';
import { getLocale } from '@/utils';

const TimeLabel = ({ label }: { label: ReactNode }) => (
<StyledTimePill size='xs'>
Expand All @@ -28,6 +30,8 @@ type InheritAttrs = Omit<FlexProps, keyof Props | 'children'>;
type TimeStepProps = Props & InheritAttrs;

const TimeStep = ({ step, data, currentStep }: TimeStepProps): JSX.Element => {
const { lang } = useParams();

const timeLabel = useMemo(() => {
// should only show step if it is not a complete step
const showTime =
Expand All @@ -44,7 +48,12 @@ const TimeStep = ({ step, data, currentStep }: TimeStepProps): JSX.Element => {
return step === 'challenge-period' ? <Trans>7 days</Trans> : <Trans>2 hours</Trans>;
}

return <Trans>{formatDistanceToNow(data.statusEndDate)} remaining</Trans>;
return (
<Trans>
{formatDistanceToNow(data.statusEndDate, { locale: getLocale(lang as Parameters<typeof getLocale>[0]) })}{' '}
remaining
</Trans>
);
}, [step, currentStep, data.statusEndDate]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { Currency, CurrencyAmount } from '@gobob/currency';
import { ArrowLongRight, Flex, FlexProps, P, UnstyledButton } from '@gobob/ui';
import { Trans } from '@lingui/macro';
import { formatDistanceToNow } from 'date-fns';
import { useParams } from 'next/navigation';

import { StyledDetailsButton, StyledExpandIcon } from './TransactionList.style';

import { Chain } from '@/components';
import { TransactionDirection } from '@/types';
import { getLocale } from '@/utils';

type Props = {
direction: TransactionDirection;
Expand Down Expand Up @@ -35,6 +37,7 @@ const TransactionDetails = ({
onExpand,
...props
}: TransactionDetailsProps): JSX.Element => {
const { lang } = useParams();
const directionLabel = direction === TransactionDirection.L1_TO_L2 ? <Trans>Deposit</Trans> : <Trans>Withdraw</Trans>;

const isExpandable = !!onExpand;
Expand All @@ -46,7 +49,7 @@ const TransactionDetails = ({
{directionLabel}
</P>
<P color='grey-50' size='xs' weight='semibold'>
<Trans>{formatDistanceToNow(date)} ago</Trans>
<Trans>{formatDistanceToNow(date, { locale: getLocale(lang as Parameters<typeof getLocale>[0]) })} ago</Trans>
</P>
</Flex>
<StyledDetailsButton elementType={onExpand ? UnstyledButton : undefined} {...{ onPress: onExpand }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,70 @@ const mockSegmentStrategy: GatewayStrategyContract = {
}
} as const;

const mockPellUniBTCStrategy: GatewayStrategyContract = {
id: 'pell-unibtc',
type: 'deposit',
address: '0xf5f2f90d3edc557b7ff0a285169a0b194df7b6f2',
method: '',
chain: {
id: '',
chainId: 60808,
slug: 'bob',
name: 'bob',
logo: '',
type: 'evm',
singleChainSwap: true,
singleChainStaking: true
},
integration: {
type: 'staking',
slug: 'pell-unibtc',
name: 'Pell (uniBTC)',
logo: '',
monetization: false
},
inputToken: {
symbol: 'WBTC',
address: '0x03c7054bcb39f7b2e5b2c7acb37583e32d70cfa3',
logo: 'https://ethereum-optimism.github.io/data/WBTC/logo.svg',
decimals: 8,
chain: 'bob'
},
outputToken: null
} as const;

const seTokensContractDataMock = {
seSOLVBTCBBN: [200420547971521033526791436n, 90941658309n, '0xCC0966D8418d412c599A6421b760a847eB169A8c'],
seTBTC: [207537267655402594884495418n, 8255486068n, '0xBBa2eF945D523C4e2608C9E1214C2Cc64D4fc2e2'],
seUNIBTC: [20020877878162857n, 8875128907n, '0x236f8c0a61dA474dB21B693fB2ea7AAB0c803894'],
seWBTC: [20083839753286961n, 63216624241n, '0x03C7054BCB39f7b2e5B2c7AcB37583e32D70Cfa3']
} as const;

const seTokensUnderlyingContractDataMock = {
seSOLVBTCBBN: 18,
seTBTC: 18,
seUNIBTC: 8,
seWBTC: 8
} as const;

const tokensContractDataMock = {
'SolvBTC.BBN': [1669199681543807681873n, 18],
uniBTC: [42320720383n, 8]
} as const;

const noOuputTokenContractDataMock = {
'0xdf3aa56f2626e253b5db7703ac7241e835140566': 8587056375410955041n,
'0xf5f2f90d3edc557b7ff0a285169a0b194df7b6f2': 20351418531n
} as const;

const noOuputTokenContractSharesToUnderlyingDataMock = {
'0xdf3aa56f2626e253b5db7703ac7241e835140566': 8587056375410955041n,
'0xf5f2f90d3edc557b7ff0a285169a0b194df7b6f2': 20351418531n
} as const;

describe('useGetStakingStrategies', () => {
afterEach(vi.clearAllMocks);

const mockExchangeRateStored = 207520794671396869399540716n;
const mockTotalSupply = 9036849246n;
const mockUnderlying = '0xabc';

const underlyingDecimals = 18;
const decimals = 8;

Expand All @@ -131,13 +188,19 @@ describe('useGetStakingStrategies', () => {
beforeEach(() => {
(useReadContracts as Mock)
.mockReturnValueOnce({
data: [mockExchangeRateStored, mockTotalSupply, mockUnderlying]
data: seTokensContractDataMock
})
.mockReturnValueOnce({
data: seTokensUnderlyingContractDataMock
})
.mockReturnValueOnce({
data: [underlyingDecimals]
data: tokensContractDataMock
})
.mockReturnValueOnce({
data: [mockTotalSupply, decimals]
data: noOuputTokenContractDataMock
})
.mockReturnValueOnce({
data: noOuputTokenContractSharesToUnderlyingDataMock
});

(usePrices as Mock).mockReturnValue({
Expand All @@ -148,13 +211,19 @@ describe('useGetStakingStrategies', () => {
it('should return strategy data', async () => {
(useReadContracts as Mock)
.mockReturnValueOnce({
data: [mockExchangeRateStored, mockTotalSupply, mockUnderlying]
data: seTokensContractDataMock
})
.mockReturnValueOnce({
data: [underlyingDecimals]
data: seTokensUnderlyingContractDataMock
})
.mockReturnValueOnce({
data: [mockTotalSupply, decimals]
data: tokensContractDataMock
})
.mockReturnValueOnce({
data: noOuputTokenContractDataMock
})
.mockReturnValueOnce({
data: noOuputTokenContractSharesToUnderlyingDataMock
});

(gatewaySDK.getStrategies as Mock).mockReturnValue([mockStrategy]);
Expand All @@ -177,7 +246,9 @@ describe('useGetStakingStrategies', () => {
mockStrategy.outputToken.symbol
)
: undefined,
tvl: new Big(mockTotalSupply.toString())
tvl: new Big(
tokensContractDataMock[mockStrategy.outputToken?.symbol as keyof typeof tokensContractDataMock][0].toString()
)
.mul(mockPrice)
.div(10 ** decimals)
.toNumber()
Expand All @@ -187,23 +258,25 @@ describe('useGetStakingStrategies', () => {
expect(result.current.data).toEqual([expectedData]);
});

it('should return undefined currency when outputToken is missing', async () => {
it('should return tvl value if output token is se* token', async () => {
(useReadContracts as Mock)
.mockReturnValueOnce({
data: [mockExchangeRateStored, mockTotalSupply, mockUnderlying]
data: seTokensContractDataMock
})
.mockReturnValueOnce({
data: seTokensUnderlyingContractDataMock
})
.mockReturnValueOnce({
data: tokensContractDataMock
})
.mockReturnValueOnce({
data: [underlyingDecimals]
data: noOuputTokenContractDataMock
})
.mockReturnValueOnce({
data: [mockTotalSupply, decimals]
data: noOuputTokenContractSharesToUnderlyingDataMock
});

const mockStrategyWithoutToken = {
outputToken: undefined
};

(gatewaySDK.getStrategies as Mock).mockReturnValue([mockStrategyWithoutToken]);
(gatewaySDK.getStrategies as Mock).mockReturnValue([mockSegmentStrategy]);

const { result, waitForValueToChange } = renderHook<PropsWithChildren, ReturnType<typeof useGetStakingStrategies>>(
() => useGetStakingStrategies(),
Expand All @@ -213,27 +286,52 @@ describe('useGetStakingStrategies', () => {
await waitForValueToChange(() => result.current.data);

const expectedData = {
raw: mockStrategyWithoutToken,
currency: undefined,
tvl: null
raw: mockSegmentStrategy,
currency: mockSegmentStrategy.outputToken
? new Token(
ChainId.BOB,
mockSegmentStrategy.outputToken.address as Address,
mockSegmentStrategy.outputToken.decimals,
mockSegmentStrategy.outputToken.symbol,
mockSegmentStrategy.outputToken.symbol
)
: undefined,
tvl: new Big(
(
seTokensContractDataMock[
mockSegmentStrategy.outputToken?.symbol as keyof typeof seTokensContractDataMock
][0] *
seTokensContractDataMock[mockSegmentStrategy.outputToken?.symbol as keyof typeof seTokensContractDataMock][1]
).toString()
)
.mul(mockPrice)
.div(1e18)
.div(10 ** underlyingDecimals)
.toNumber()
};

expect(result.current.data).toEqual([expectedData]);
});

it('should return tvl value if output token is se* token', async () => {
it('should return tvl value if no output', async () => {
(useReadContracts as Mock)
.mockReturnValueOnce({
data: [mockExchangeRateStored, mockTotalSupply, mockUnderlying]
data: seTokensContractDataMock
})
.mockReturnValueOnce({
data: [underlyingDecimals]
data: seTokensUnderlyingContractDataMock
})
.mockReturnValueOnce({
data: [mockTotalSupply, decimals]
data: tokensContractDataMock
})
.mockReturnValueOnce({
data: noOuputTokenContractDataMock
})
.mockReturnValueOnce({
data: noOuputTokenContractSharesToUnderlyingDataMock
});

(gatewaySDK.getStrategies as Mock).mockReturnValue([mockSegmentStrategy]);
(gatewaySDK.getStrategies as Mock).mockReturnValue([mockPellUniBTCStrategy]);

const { result, waitForValueToChange } = renderHook<PropsWithChildren, ReturnType<typeof useGetStakingStrategies>>(
() => useGetStakingStrategies(),
Expand All @@ -243,20 +341,15 @@ describe('useGetStakingStrategies', () => {
await waitForValueToChange(() => result.current.data);

const expectedData = {
raw: mockSegmentStrategy,
currency: mockSegmentStrategy.outputToken
? new Token(
ChainId.BOB,
mockSegmentStrategy.outputToken.address as Address,
mockSegmentStrategy.outputToken.decimals,
mockSegmentStrategy.outputToken.symbol,
mockSegmentStrategy.outputToken.symbol
)
: undefined,
tvl: new Big((mockExchangeRateStored * mockTotalSupply).toString())
raw: mockPellUniBTCStrategy,
currency: undefined,
tvl: new Big(
noOuputTokenContractSharesToUnderlyingDataMock[
mockPellUniBTCStrategy.address as keyof typeof noOuputTokenContractSharesToUnderlyingDataMock
].toString()
)
.mul(mockPrice)
.div(1e18)
.div(10 ** underlyingDecimals)
.div(10 ** (tokensContractDataMock.uniBTC[1]! as number))
.toNumber()
};

Expand Down
Loading

0 comments on commit 2c94656

Please sign in to comment.