diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml index a5bce51478..1f5fda5f15 100644 --- a/.github/workflows/jest.yml +++ b/.github/workflows/jest.yml @@ -37,6 +37,6 @@ jobs: NEXT_PUBLIC_WALLETCONNECT_V2_ID: ${{ secrets.NEXT_PUBLIC_WALLETCONNECT_V2_ID }} - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/package.json b/package.json index 2cd99369c1..000da02eb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kwenta", - "version": "7.6.0", + "version": "7.7.0", "description": "Kwenta", "main": "index.js", "scripts": { diff --git a/packages/app/.env.local.example b/packages/app/.env.local.example index 5787c1f831..9013549790 100644 --- a/packages/app/.env.local.example +++ b/packages/app/.env.local.example @@ -10,4 +10,4 @@ NEXT_PUBLIC_CLOSE_ONLY=false NEXT_PUBLIC_WALLETCONNECT_V2_ID="WALLETCONNECT_V2_ID" NEXT_PUBLIC_DEVNET_ENABLED=true NEXT_PUBLIC_DEVNET_RPC_URL="NEXT_PUBLIC_DEVNET_RPC_URL" -NEXT_PUBLIC_ONE_INCH_COINGECKO_PROXY="http://[HOST]:[PORT]" # http://localhost:5001 \ No newline at end of file +NEXT_PUBLIC_SERVICES_PROXY="https://proxy.kwenta.io:443/proxy" # http://localhost:5001 \ No newline at end of file diff --git a/packages/app/next.config.js b/packages/app/next.config.js index 9080e43fdb..a90ec0e78c 100644 --- a/packages/app/next.config.js +++ b/packages/app/next.config.js @@ -78,6 +78,12 @@ const baseConfig = { destination: '/exchange/?quote=:quote&base=:base', permanent: true, }, + { + source: '/', + has: [{ type: 'query', key: 'ref' }], + destination: '/market/?ref=:ref', + permanent: true, + }, ] }, productionBrowserSourceMaps: true, diff --git a/packages/app/package.json b/packages/app/package.json index 757bd2d9fd..522bf94c4f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@kwenta/app", - "version": "7.6.0", + "version": "7.7.0", "scripts": { "dev": "next", "build": "next build", diff --git a/packages/app/public/images/referrals/bronze.gif b/packages/app/public/images/referrals/bronze.gif new file mode 100644 index 0000000000..527324ddd9 Binary files /dev/null and b/packages/app/public/images/referrals/bronze.gif differ diff --git a/packages/app/public/images/referrals/gold.gif b/packages/app/public/images/referrals/gold.gif new file mode 100644 index 0000000000..9ce24da97e Binary files /dev/null and b/packages/app/public/images/referrals/gold.gif differ diff --git a/packages/app/public/images/referrals/silver.gif b/packages/app/public/images/referrals/silver.gif new file mode 100644 index 0000000000..8a72d23ec6 Binary files /dev/null and b/packages/app/public/images/referrals/silver.gif differ diff --git a/packages/app/src/assets/svg/referrals/bronze-small.svg b/packages/app/src/assets/svg/referrals/bronze-small.svg new file mode 100644 index 0000000000..4ed21df6f9 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/bronze-small.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app/src/assets/svg/referrals/bronze.svg b/packages/app/src/assets/svg/referrals/bronze.svg new file mode 100644 index 0000000000..988c7b0be9 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/bronze.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/app/src/assets/svg/referrals/copy-check.svg b/packages/app/src/assets/svg/referrals/copy-check.svg new file mode 100644 index 0000000000..d9381880cc --- /dev/null +++ b/packages/app/src/assets/svg/referrals/copy-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/app/src/assets/svg/referrals/deep-liquidity.svg b/packages/app/src/assets/svg/referrals/deep-liquidity.svg new file mode 100644 index 0000000000..40785a1818 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/deep-liquidity.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/assets/svg/referrals/gold-small.svg b/packages/app/src/assets/svg/referrals/gold-small.svg new file mode 100644 index 0000000000..adaf8c033c --- /dev/null +++ b/packages/app/src/assets/svg/referrals/gold-small.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app/src/assets/svg/referrals/gold.svg b/packages/app/src/assets/svg/referrals/gold.svg new file mode 100644 index 0000000000..5f1241f0a7 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/gold.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/app/src/assets/svg/referrals/inactive-tier.svg b/packages/app/src/assets/svg/referrals/inactive-tier.svg new file mode 100644 index 0000000000..e6a37130dd --- /dev/null +++ b/packages/app/src/assets/svg/referrals/inactive-tier.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/app/src/assets/svg/referrals/low-fees.svg b/packages/app/src/assets/svg/referrals/low-fees.svg new file mode 100644 index 0000000000..f03e87a2a0 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/low-fees.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/assets/svg/referrals/minting.svg b/packages/app/src/assets/svg/referrals/minting.svg new file mode 100644 index 0000000000..e362943e4e --- /dev/null +++ b/packages/app/src/assets/svg/referrals/minting.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/app/src/assets/svg/referrals/modal-background.svg b/packages/app/src/assets/svg/referrals/modal-background.svg new file mode 100644 index 0000000000..73be991ef4 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/modal-background.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/svg/referrals/profile-background-mobile.svg b/packages/app/src/assets/svg/referrals/profile-background-mobile.svg new file mode 100644 index 0000000000..81e16f8f17 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/profile-background-mobile.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/svg/referrals/profile-background.svg b/packages/app/src/assets/svg/referrals/profile-background.svg new file mode 100644 index 0000000000..b42e77ae57 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/profile-background.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/svg/referrals/referrals.svg b/packages/app/src/assets/svg/referrals/referrals.svg new file mode 100644 index 0000000000..6e9ee2eef1 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/referrals.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/assets/svg/referrals/silver-small.svg b/packages/app/src/assets/svg/referrals/silver-small.svg new file mode 100644 index 0000000000..dfe9d9a65e --- /dev/null +++ b/packages/app/src/assets/svg/referrals/silver-small.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app/src/assets/svg/referrals/silver.svg b/packages/app/src/assets/svg/referrals/silver.svg new file mode 100644 index 0000000000..6b28010b95 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/silver.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/app/src/assets/svg/referrals/success.svg b/packages/app/src/assets/svg/referrals/success.svg new file mode 100644 index 0000000000..f13f1e0c82 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/success.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/assets/svg/referrals/trading-rewards.svg b/packages/app/src/assets/svg/referrals/trading-rewards.svg new file mode 100644 index 0000000000..586273a0f7 --- /dev/null +++ b/packages/app/src/assets/svg/referrals/trading-rewards.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/components/Badge.tsx b/packages/app/src/components/Badge.tsx index f55233cf9a..8f7fc3f9a2 100644 --- a/packages/app/src/components/Badge.tsx +++ b/packages/app/src/components/Badge.tsx @@ -2,28 +2,46 @@ import { FC, ReactNode } from 'react' import styled, { css } from 'styled-components' type BadgeProps = { - color?: 'yellow' | 'red' | 'gray' + color?: 'yellow' | 'red' | 'gray' | 'primary' size?: 'small' | 'regular' + font?: 'regular' | 'black' dark?: boolean children?: ReactNode + textTransform?: boolean } -const Badge: FC = ({ color = 'yellow', size = 'regular', dark, ...props }) => { - return +const Badge: FC = ({ + color = 'yellow', + size = 'regular', + font = 'black', + dark, + textTransform = true, + ...props +}) => { + return ( + + ) } const BaseBadge = styled.span<{ - $color: 'yellow' | 'red' | 'gray' - $dark?: boolean + $color: 'yellow' | 'red' | 'gray' | 'primary' $size: 'small' | 'regular' + $font?: 'regular' | 'black' + $dark?: boolean + $textTransform?: boolean }>` - text-transform: uppercase; - text-align: center; + border-radius: 6px; ${(props) => css` - padding: 2px 6px; padding: ${props.$size === 'small' ? '2px 4px' : '2px 6px'}; font-size: ${props.$size === 'small' ? 10 : 12}px; - font-family: ${props.theme.fonts.black}; + font-family: ${props.$font === 'black' ? props.theme.fonts.black : props.theme.fonts.regular}; color: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].text}; background: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].background}; ${props.$dark && @@ -33,9 +51,15 @@ const BaseBadge = styled.span<{ border: 1px solid ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].dark.border}; `} `} - border-radius: 100px; + ${(props) => + !!props.$textTransform && + css` + text-transform: uppercase; + font-variant: all-small-caps; + border-radius: 100px; + `} + text-align: center; line-height: unset; - font-variant: all-small-caps; opacity: 1; user-select: none; display: flex; diff --git a/packages/app/src/components/BaseModal.tsx b/packages/app/src/components/BaseModal.tsx index 2cf52ec021..683789f42f 100644 --- a/packages/app/src/components/BaseModal.tsx +++ b/packages/app/src/components/BaseModal.tsx @@ -17,6 +17,7 @@ type BaseModalProps = { showCross?: boolean lowercase?: boolean rndProps?: Props + headerBackground?: ReactNode } type ModalContentWrapperProps = { @@ -41,12 +42,14 @@ export const BaseModal: FC = memo( showCross = true, lowercase, rndProps = { disableDragging: true, enableResizing: false }, + headerBackground, ...rest }) => ( + {headerBackground} {title} {showCross && ( @@ -106,7 +109,14 @@ const StyledCard = styled(Card)` width: 100%; border-radius: 10px 10px 0 0; } + svg.bg { + margin-left: -16px; + } `} + overflow: hidden; + svg.bg { + margin-bottom: -120px; + } ` const StyledCardHeader = styled(CardHeader)` diff --git a/packages/app/src/components/TVChart/TVChart.tsx b/packages/app/src/components/TVChart/TVChart.tsx index b5361bc824..18bcf6c2ba 100644 --- a/packages/app/src/components/TVChart/TVChart.tsx +++ b/packages/app/src/components/TVChart/TVChart.tsx @@ -83,7 +83,7 @@ export function TVChart({ } const [marketAsset, marketAssetLoaded] = useMemo(() => { - return router.query.asset ? [router.query.asset, true] : [null, false] + return router.query.asset ? [router.query.asset, true] : ['sETH', false] }, [router.query.asset]) const clearOrderLines = () => { diff --git a/packages/app/src/components/Table/Pagination.tsx b/packages/app/src/components/Table/Pagination.tsx index 5bbf692d20..1cf29a3061 100644 --- a/packages/app/src/components/Table/Pagination.tsx +++ b/packages/app/src/components/Table/Pagination.tsx @@ -6,9 +6,9 @@ import LeftEndArrowIcon from 'assets/svg/app/caret-left-end.svg' import LeftArrowIcon from 'assets/svg/app/caret-left.svg' import RightEndArrowIcon from 'assets/svg/app/caret-right-end.svg' import RightArrowIcon from 'assets/svg/app/caret-right.svg' +import { FlexDivRow } from 'components/layout/flex' import { GridDivCenteredCol } from 'components/layout/grid' import { resetButtonCSS } from 'styles/common' -import media from 'styles/media' export type PaginationProps = { pageIndex: number @@ -42,14 +42,14 @@ const Pagination: FC = React.memo( return ( <> - + - + {t('common.pagination.page')}{' '} {t('common.pagination.page-of-total-pages', { @@ -57,14 +57,14 @@ const Pagination: FC = React.memo( totalPages: pageCount, })} - + - + {extra} @@ -72,14 +72,6 @@ const Pagination: FC = React.memo( } ) -const ArrowButtonContainer = styled.div` - ${media.lessThan('lg')` - display: flex; - felx-direction: row; - column-gap: 5px; - `} -` - const PageInfo = styled.span` color: ${(props) => props.theme.colors.selectedTheme.gray}; ` @@ -96,10 +88,7 @@ const PaginationContainer = styled(GridDivCenteredCol)<{ border-top: ${(props) => props.theme.colors.selectedTheme.border}; border-bottom: ${(props) => props.$bottomBorder ? props.theme.colors.selectedTheme.border : 'none'}; - - ${media.lessThan('lg')` - border-radius: 0px; - `} + border-radius: 0px; ` const ArrowButton = styled.button` @@ -110,24 +99,15 @@ const ArrowButton = styled.button` opacity: 0.5; } svg { - width: 14px; - height: 14px; + width: 9px; + height: 9px; fill: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; } - ${media.lessThan('lg')` - border: none; - border-radius: 100px; - padding: 4px; - width: 24px; - height: 24px; - background: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.background}; - - svg { - width: 9px; - height: 9px; - } - `} + width: 24px; + height: 24px; + background: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.background}; + border-radius: 100px; ` export default Pagination diff --git a/packages/app/src/components/Table/Table.tsx b/packages/app/src/components/Table/Table.tsx index f45a67376d..0a57ce7455 100644 --- a/packages/app/src/components/Table/Table.tsx +++ b/packages/app/src/components/Table/Table.tsx @@ -53,6 +53,11 @@ function calculatePageSize( return showShortList ? pageSize ?? SHORT_PAGE_SIZE : MAX_TOTAL_ROWS } +export type GridLayoutStyles = { + row?: string + cell?: string +} + type TableProps = { data: T[] columns: ColumnDef[] @@ -76,6 +81,7 @@ type TableProps = { autoResetPageIndex?: boolean paginationExtra?: React.ReactNode CustomPagination?: FC + gridLayout?: GridLayoutStyles } const Table = ({ @@ -100,6 +106,7 @@ const Table = ({ columnsDeps = [], paginationExtra, CustomPagination, + gridLayout = {}, }: TableProps) => { const [sorting, setSorting] = useState(sortBy) const [pagination, setPagination] = useState({ @@ -130,7 +137,7 @@ const Table = ({ const defaultRef = useRef(null) const shouldShowPagination = useMemo( - () => showPagination && !showShortList && data.length > table.getState().pagination.pageSize, + () => showPagination || (!showShortList && data.length > table.getState().pagination.pageSize), [data.length, showPagination, showShortList, table] ) @@ -201,7 +208,8 @@ const Table = ({ localRef={localRef} highlightRowsOnHover={highlightRowsOnHover} row={row} - onClick={handleRowClick(row)} + onClick={onTableRowClick ? handleRowClick(row) : undefined} + gridLayout={gridLayout} /> ) })} @@ -211,7 +219,7 @@ const Table = ({ = { row: Row localRef: any highlightRowsOnHover?: boolean onClick?: () => void + gridLayout?: GridLayoutStyles } const TableBodyRow = genericMemo( - ({ row, localRef, highlightRowsOnHover, onClick }: TableBodyRowProps) => ( + ({ row, localRef, highlightRowsOnHover, onClick, gridLayout }: TableBodyRowProps) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -33,7 +38,7 @@ const TableBodyRow = genericMemo( ) ) -const BaseTableBodyRow = styled.div<{ $highlightRowsOnHover?: boolean }>` +const BaseTableBodyRow = styled.div<{ $highlightRowsOnHover?: boolean; $gridLayout?: string }>` display: flex; ${(props) => css` cursor: ${props.onClick ? 'pointer' : 'default'}; @@ -54,10 +59,14 @@ const BaseTableBodyRow = styled.div<{ $highlightRowsOnHover?: boolean }>` background-color: ${props.theme.colors.selectedTheme.table.hover}; } `} + ${props.$gridLayout && + css` + ${props.$gridLayout} + `} `} ` -export const TableCell = styled(FlexDivCentered)` +export const TableCell = styled(FlexDivCentered)<{ $gridLayout?: string }>` box-sizing: border-box; &:first-child { padding-left: 18px; @@ -65,6 +74,12 @@ export const TableCell = styled(FlexDivCentered)` &:last-child { padding-right: 14px; } + + ${(props) => + props.$gridLayout && + css` + ${props.$gridLayout} + `} ` export default TableBodyRow diff --git a/packages/app/src/constants/links.ts b/packages/app/src/constants/links.ts index 8f40632c5d..091e220a5e 100644 --- a/packages/app/src/constants/links.ts +++ b/packages/app/src/constants/links.ts @@ -49,6 +49,7 @@ export const EXTERNAL_LINKS = { TradingRewardsV2: 'https://mirror.xyz/kwenta.eth/7k-5UYXXcCNJ_DRRWvYBsK5zDm5UA945My4QrInhxoI', RewardsGuide: 'https://mirror.xyz/kwenta.eth/8KyrISnjOcuAX_VW-GxVqxpcbWukB_RlP5XWWMz-UGk', StakingV2Migration: 'https://docs.kwenta.io/kwenta-token/v2-migration', + Referrals: 'https://docs.kwenta.io/using-kwenta/referral', }, Optimism: { Home: 'https://optimism.io/', @@ -65,4 +66,7 @@ export const EXTERNAL_LINKS = { Competition: { LearnMore: 'https://mirror.xyz/kwenta.eth/s_PO64SxvuwDHz9fdHebsYeQAOOc73D3bL2q4nC6LvU', }, + Referrals: { + BoostNFT: 'https://opensea.io/assets/optimism/0xD3B8876073949D790AB718CAD21d9326a3adA60f', + }, } diff --git a/packages/app/src/constants/routes.ts b/packages/app/src/constants/routes.ts index 5858d3e27b..d3f728933c 100644 --- a/packages/app/src/constants/routes.ts +++ b/packages/app/src/constants/routes.ts @@ -61,6 +61,10 @@ export const ROUTES = { Trader: (trader: string) => `/leaderboard/?trader=${trader}`, Competition: (round: string) => `/leaderboard/?competitionRound=${round}`, }, + Referrals: { + Home: '/referrals', + nftMint: (asset: FuturesMarketAsset, ref: string) => formatUrl('/market', { asset, ref }), + }, Earn: { Home: '/earn', }, diff --git a/packages/app/src/containers/Connector/config.ts b/packages/app/src/containers/Connector/config.ts index abb888e114..59caafa018 100644 --- a/packages/app/src/containers/Connector/config.ts +++ b/packages/app/src/containers/Connector/config.ts @@ -52,13 +52,14 @@ const { chains, provider } = configureChains(Object.values(chain), [ }), jsonRpcProvider({ rpc: (networkChain) => ({ - http: process.env.NEXT_PUBLIC_DEVNET_ENABLED - ? process.env.NEXT_PUBLIC_DEVNET_RPC_URL! - : !BLAST_NETWORK_LOOKUP[networkChain.id] - ? networkChain.rpcUrls.default.http[0] - : `https://${BLAST_NETWORK_LOOKUP[networkChain.id]}.blastapi.io/${ - process.env.NEXT_PUBLIC_BLASTAPI_PROJECT_ID - }`, + http: + process.env.NEXT_PUBLIC_DEVNET_ENABLED === 'true' + ? process.env.NEXT_PUBLIC_DEVNET_RPC_URL! + : !BLAST_NETWORK_LOOKUP[networkChain.id] + ? networkChain.rpcUrls.default.http[0] + : `https://${BLAST_NETWORK_LOOKUP[networkChain.id]}.blastapi.io/${ + process.env.NEXT_PUBLIC_BLASTAPI_PROJECT_ID + }`, }), stallTimeout: STALL_TIMEOUT, priority: process.env.NEXT_PUBLIC_DEVNET_ENABLED ? 0 : 2, diff --git a/packages/app/src/pages/_app.tsx b/packages/app/src/pages/_app.tsx index a9c40a94c3..95c7766fb9 100644 --- a/packages/app/src/pages/_app.tsx +++ b/packages/app/src/pages/_app.tsx @@ -94,7 +94,6 @@ const InnerApp: FC = ({ Component, pageProps }) => { const App: FC = (props) => { const { t } = useTranslation() - return ( <> @@ -103,12 +102,13 @@ const App: FC = (props) => { name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,user-scalable=0" /> + {/* open graph */} - - + + diff --git a/packages/app/src/pages/_document.tsx b/packages/app/src/pages/_document.tsx index 4001a4f5f3..ece3920a11 100644 --- a/packages/app/src/pages/_document.tsx +++ b/packages/app/src/pages/_document.tsx @@ -5,7 +5,6 @@ export default class MyDocument extends Document { static async getInitialProps(ctx: any) { const styledComponentsSheet = new ServerStyleSheet() const originalRenderPage = ctx.renderPage - try { ctx.renderPage = () => originalRenderPage({ diff --git a/packages/app/src/pages/market.tsx b/packages/app/src/pages/market.tsx index 580458f9e2..7be4e4fb44 100644 --- a/packages/app/src/pages/market.tsx +++ b/packages/app/src/pages/market.tsx @@ -1,7 +1,7 @@ import { FuturesMarginType, FuturesMarketAsset } from '@kwenta/sdk/types' import { MarketKeyByAsset } from '@kwenta/sdk/utils' import { useRouter } from 'next/router' -import { useEffect, FC, ReactNode, useMemo } from 'react' +import { useEffect, FC, ReactNode, useMemo, useCallback, useLayoutEffect } from 'react' import styled from 'styled-components' import Loader from 'components/Loader' @@ -28,6 +28,7 @@ import TradePanelSmartMargin from 'sections/futures/Trade/TradePanelSmartMargin' import TransferSmartMarginModal from 'sections/futures/Trade/TransferSmartMarginModal' import DelayedOrderConfirmationModal from 'sections/futures/TradeConfirmation/CrossMarginOrderConfirmation' import TradeConfirmationModalCrossMargin from 'sections/futures/TradeConfirmation/TradeConfirmationModalCrossMargin' +import BaseReferralModal from 'sections/referrals/ReferralModal/BaseReferralModal' import AppLayout from 'sections/shared/Layout/AppLayout' import { setOpenModal } from 'state/app/reducer' import { selectShowModal, selectShowPositionModal } from 'state/app/selectors' @@ -45,6 +46,8 @@ import { selectSmartMarginAccountQueryStatus, } from 'state/futures/smartMargin/selectors' import { useAppDispatch, useAppSelector } from 'state/hooks' +import { fetchUnmintedBoostNftForCode } from 'state/referrals/action' +import { selectIsReferralCodeValid } from 'state/referrals/selectors' import { FetchStatus } from 'state/types' import { PageContent } from 'styles/common' import media from 'styles/media' @@ -57,9 +60,7 @@ const Market: MarketComponent = () => { const dispatch = useAppDispatch() const { greaterThanWidth } = useWindowSize() usePollMarketFuturesData() - - const routerMarketAsset = router.query.asset as FuturesMarketAsset - + const routerMarketAsset = (router.query.asset || 'sETH') as FuturesMarketAsset const setCurrentMarket = useAppSelector(selectMarketAsset) const showOnboard = useAppSelector(selectShowSmartMarginOnboard) const showCrossMarginOnboard = useAppSelector(selectShowCrossMarginOnboard) @@ -68,6 +69,8 @@ const Market: MarketComponent = () => { const accountType = useAppSelector(selectFuturesType) const selectedMarketAsset = useAppSelector(selectMarketAsset) const crossMarginSupportedNetwork = useAppSelector(selectCrossMarginSupportedNetwork) + const routerReferralCode = (router.query.ref as string)?.toLowerCase() + const isReferralCodeValid = useAppSelector(selectIsReferralCodeValid) const routerAccountType = useMemo(() => { if ( @@ -104,6 +107,21 @@ const Market: MarketComponent = () => { } }, [router, setCurrentMarket, dispatch, routerMarketAsset, selectedMarketAsset]) + useLayoutEffect(() => { + if (router.isReady && routerReferralCode) { + dispatch(fetchUnmintedBoostNftForCode(routerReferralCode)) + } + }, [dispatch, router.isReady, routerReferralCode]) + + useLayoutEffect(() => { + if (isReferralCodeValid) { + dispatch(setOpenModal('referrals_mint_boost_nft')) + } + }, [dispatch, isReferralCodeValid]) + + const onDismiss = useCallback(() => { + dispatch(setOpenModal(null)) + }, [dispatch]) return ( <> @@ -141,20 +159,17 @@ const Market: MarketComponent = () => { {showPositionModal?.type === 'futures_edit_position_size' && } {showPositionModal?.type === 'futures_edit_position_margin' && } {openModal === 'futures_deposit_withdraw_cross_margin' && ( - dispatch(setOpenModal(null))} - /> + )} {openModal === 'futures_deposit_withdraw_smart_margin' && ( - dispatch(setOpenModal(null))} - /> + )} {openModal === 'futures_confirm_smart_margin_trade' && } {openModal === 'futures_confirm_cross_margin_trade' && } + {openModal === 'referrals_mint_boost_nft' && routerReferralCode && isReferralCodeValid && ( + + )} ) } diff --git a/packages/app/src/pages/referrals.tsx b/packages/app/src/pages/referrals.tsx new file mode 100644 index 0000000000..d4263930f4 --- /dev/null +++ b/packages/app/src/pages/referrals.tsx @@ -0,0 +1,57 @@ +import Head from 'next/head' +import { FC, ReactNode, useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { MobileHiddenView, MobileOnlyView } from 'components/Media' +import ReferralsTabs from 'sections/referrals/ReferralsTabs' +import { ReferralsTab } from 'sections/referrals/types' +import AppLayout from 'sections/shared/Layout/AppLayout' +import { useFetchReferralData } from 'state/futures/hooks' +import { FullHeightContainer, MainContent, PageContent } from 'styles/common' + +type ReferralsComponent = FC & { getLayout: (page: ReactNode) => JSX.Element } + +const Referrals: ReferralsComponent = () => { + const { t } = useTranslation() + const [currentTab, setCurrentTab] = useState(ReferralsTab.Traders) + const handleChangeTab = useCallback( + (tab: ReferralsTab) => () => { + setCurrentTab(tab) + }, + [] + ) + + useFetchReferralData() + + return ( + <> + + {t('referrals.page-title')} + + + + + + + + + + + + + + + + + ) +} + +Referrals.getLayout = (page) => {page} + +const MobileMainContent = styled.div` + width: 100%; + padding: 15px; +` + +export default Referrals diff --git a/packages/app/src/queries/rates/constants.ts b/packages/app/src/queries/rates/constants.ts index 53dc99d3a8..c9356acd18 100644 --- a/packages/app/src/queries/rates/constants.ts +++ b/packages/app/src/queries/rates/constants.ts @@ -5,8 +5,7 @@ export const COMMODITIES_BASE_API_URL = export const FOREX_BASE_API_URL = 'https://api.exchangerate.host/latest' -export const DEFAULT_PYTH_TV_ENDPOINT = - 'https://benchmarks.pyth.network/v1/shims/tradingview/history' +export const DEFAULT_PYTH_TV_ENDPOINT = `${process.env.NEXT_PUBLIC_SERVICES_PROXY}/price-history/v1/shims/tradingview/history` export const NON_CRYPTO_ASSET_TYPES: AssetTypes = { Metal: ['XAU', 'XAG'], diff --git a/packages/app/src/sections/futures/MarketInfo/MarketHead.tsx b/packages/app/src/sections/futures/MarketInfo/MarketHead.tsx index 701fe91056..61b17ef834 100644 --- a/packages/app/src/sections/futures/MarketInfo/MarketHead.tsx +++ b/packages/app/src/sections/futures/MarketInfo/MarketHead.tsx @@ -9,7 +9,6 @@ import { useAppSelector } from 'state/hooks' const MarketHead: FC = () => { const { t } = useTranslation() - const marketAsset = useAppSelector(selectMarketAsset) const latestPrice = useAppSelector(selectSkewAdjustedPrice) const marketName = getDisplayAsset(marketAsset) diff --git a/packages/app/src/sections/homepage/Assets.tsx b/packages/app/src/sections/homepage/Assets.tsx index b3d30dc6be..c660e8b29c 100644 --- a/packages/app/src/sections/homepage/Assets.tsx +++ b/packages/app/src/sections/homepage/Assets.tsx @@ -1,5 +1,5 @@ import { SECONDS_PER_DAY } from '@kwenta/sdk/constants' -import { getDisplayAsset, MarketKeyByAsset } from '@kwenta/sdk/utils' +import { getDisplayAsset, hoursToMilliseconds, MarketKeyByAsset } from '@kwenta/sdk/utils' import { wei } from '@synthetixio/wei' import { ColorType, createChart, UTCTimestamp } from 'lightweight-charts' import router from 'next/router' @@ -133,31 +133,35 @@ const Assets = () => { const pastRates = useAppSelector(selectPreviousDayPrices) const futuresVolumes = useAppSelector(selectMarketVolumes) usePollAction('fetchOptimismMarkets', () => fetchOptimismMarkets(l2Provider), { - intervalTime: 600, + intervalTime: hoursToMilliseconds(1), }) - const PERPS = useMemo(() => { - return futuresMarkets.map((market) => { - const marketPrice = prices[market.asset]?.offChain ?? prices[market.asset]?.onChain ?? wei(0) - const description = getSynthDescription(market.asset, t) - const volume = futuresVolumes[market.marketKey]?.volume?.toNumber() ?? 0 - const pastPrice = pastRates.find( - (price) => price.synth === market.asset || price.synth === market.asset.slice(1) - ) - return { - key: market.asset, - name: market.asset[0] === 's' ? market.asset.slice(1) : market.asset, - description: description.split(' ')[0], - price: marketPrice.toNumber(), - volume, - priceChange: - !!marketPrice && !marketPrice.eq(0) && !!pastPrice?.rate - ? marketPrice.sub(pastPrice.rate).div(marketPrice) - : 0, - image: , - icon: , - } - }) + const perps = useMemo(() => { + return futuresMarkets + .map((market) => { + const marketPrice = + prices[market.asset]?.offChain ?? prices[market.asset]?.onChain ?? wei(0) + const description = getSynthDescription(market.asset, t) + const volume = futuresVolumes[market.marketKey]?.volume?.toNumber() ?? 0 + const pastPrice = pastRates.find( + (price) => price.synth === market.asset || price.synth === market.asset.slice(1) + ) + return { + key: market.asset, + name: market.asset[0] === 's' ? market.asset.slice(1) : market.asset, + description: description.split(' ')[0], + price: marketPrice.toNumber(), + volume, + priceChange: + !!marketPrice && !marketPrice.eq(0) && !!pastPrice?.rate + ? marketPrice.sub(pastPrice.rate).div(marketPrice) + : 0, + image: , + icon: , + } + }) + .sort((a, b) => b.volume - a.volume) + .slice(0, 16) }, [futuresMarkets, pastRates, futuresVolumes, t, prices]) const title = ( @@ -188,7 +192,7 @@ const Assets = () => { - {PERPS.map(({ key, name, description, price, volume, priceChange, image, icon }) => ( + {perps.map(({ key, name, description, price, volume, priceChange, image, icon }) => ( { {title} - {PERPS.map(({ key, name, description, price, volume, priceChange, image, icon }) => ( + {perps.map(({ key, name, description, price, volume, priceChange, image, icon }) => ( { + const { t } = useTranslation() + const tier = useAppSelector(selectReferralNft) + const mockReferralsRewards = useAppSelector(selectReferralCodes) + const referredCount = mockReferralsRewards.reduce( + (acc, { referredCount }) => acc + Number(referredCount), + 0 + ) + const { lessThanWidth } = useWindowSize() + const { icon, title, boost, nftPreview } = REFFERAL_TIERS[tier === -1 ? 0 : tier] + const [nftPreviewModalOpen, setNftPreviewModalOpen] = useState(false) + const handleOpenModal = useCallback(() => setNftPreviewModalOpen(true), []) + const handleDismissModal = useCallback(() => setNftPreviewModalOpen(false), []) + + return ( + + + + + + + + + + {icon} + + {t(title)} NFT + + {`${formatPercent(boost, { maxDecimals: 0 })} ${t( + 'referrals.affiliates.nft.boost' + )}`} + + + + + + + + + + + {t('referrals.affiliates.dashboard.traders-referred')} + + + {referredCount} + + + + | + + + + {t('referrals.traders.dashboard.rewards-boost')} + + + {formatPercent(boost, { maxDecimals: 0 })} + + + + + + + {nftPreview} + + {t('referrals.affiliates.nft.preview')} + + + + + + + + + + {lessThanWidth('lg') ? ( + + ) : ( + + )} + + + + + {icon} + + {t(title)} NFT + + {`${formatPercent(boost, { maxDecimals: 0 })} ${t( + 'referrals.affiliates.nft.boost' + )}`} + + + + + + + + + + {t('referrals.affiliates.dashboard.traders-referred')} + + + {referredCount} + + + + | + + + + {t('referrals.traders.dashboard.rewards-boost')} + + + {formatPercent(boost, { maxDecimals: 0 })} + + + + + {nftPreview} + + {t('referrals.affiliates.nft.preview')} + + + + + + + + + + {nftPreviewModalOpen && } + + ) +}) + +const StyledFlexDivRowCentered = styled(FlexDivRowCentered)<{ + marginBottom?: string + zIndex?: string +}>` + width: 100%; + margin-bottom: ${(props) => props.marginBottom || 'initial'}; + z-index: ${(props) => props.zIndex || 'initial'}; +` + +const StyledFlexDivCol = styled(FlexDivCol)<{ + marginTop?: string + width?: string + justifyContent?: string +}>` + width: ${(props) => props.width || '100%'}; + margin-top: ${(props) => props.marginTop || 'initial'}; + justify-content: ${(props) => props.justifyContent || 'initial'}; +` + +const StyledFlexDivRow = styled(FlexDivRow)<{ + marginBottom?: string + width?: string +}>` + width: ${(props) => props.width || '100%'}; + margin-bottom: ${(props) => props.marginBottom || 'initial'}; +` + +const BarContainer = styled.div` + margin-bottom: 20px; +` + +const SvgContainer = styled.div` + position: absolute; + overflow: hidden; + width: 100%; + height: 88px; + z-index: 1; + svg { + width: 100%; + height: auto; + } +` +const InfoContainer = styled(FlexDivRow)` + justify-content: flex-start; + padding-right: 10px; + padding-top: 10px; + column-gap: 25px; + ${media.lessThan('sm')` + column-gap: 0px; + justify-content: space-between; + `} +` +const ImageContainer = styled.div` + position: relative; + overflow: hidden; + margin-top: 20px; + svg { + cursor: pointer; + } + + &:hover .overlay { + opacity: 1; + } + + ${media.lessThan('lg')` + margin-top: 0px; + display: flex; + justify-content: center; + align-items: center; + `} +` + +const PreviewText = styled.div` + color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.secondary}; + padding: 10px 15px; + border: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.border}; + border-radius: 100px; + font-size: 13px; + font-family: ${(props) => props.theme.fonts.black}; + background: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.background}; +` + +const Overlay = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin-bottom: 2px; + border-radius: 20px; + background: ${(props) => + props.theme.colors.selectedTheme.newTheme.containers.primary.overlay.background}; + opacity: 0; + transition: opacity 0.3s ease; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + ${media.lessThan('lg')` + width: 244px; + margin: auto + `} +` + +const Container = styled.div` + position: relative; + margin-top: 25px; + width: 100%; + height: 100%; + border-radius: 20px; + background: ${(props) => props.theme.colors.selectedTheme.newTheme.containers.cards.background}; + border: 1px solid ${(props) => props.theme.colors.selectedTheme.newTheme.border.color}; +` + +const CardsContainer = styled.div` + position: relative; + margin-top: 0; +` + +const ContentContainer = styled(FlexDivColCentered)` + padding: 24px 25px; + width: 100%; + justify-content: flex-start; + column-gap: 50px; + row-gap: 25px; + flex-flow: row wrap; +` + +export default AffiliatesProfiles diff --git a/packages/app/src/sections/referrals/CreateReferralCodeModal.tsx b/packages/app/src/sections/referrals/CreateReferralCodeModal.tsx new file mode 100644 index 0000000000..9298639a84 --- /dev/null +++ b/packages/app/src/sections/referrals/CreateReferralCodeModal.tsx @@ -0,0 +1,82 @@ +import { useChainModal, useConnectModal } from '@rainbow-me/rainbowkit' +import { ChangeEvent, FC, memo, useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import BaseModal from 'components/BaseModal' +import Button from 'components/Button' +import Input from 'components/Input/Input' +import Spacer from 'components/Spacer' +import { Body } from 'components/Text' +import useIsL2 from 'hooks/useIsL2' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { createNewReferralCode } from 'state/referrals/action' +import { selectIsCreatingReferralCode } from 'state/referrals/selectors' +import { selectWallet } from 'state/wallet/selectors' + +type Props = { + onDismiss(): void +} + +const CreateReferralCodeModal: FC = memo(({ onDismiss }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { openChainModal } = useChainModal() + const { openConnectModal } = useConnectModal() + const isL2 = useIsL2() + const wallet = useAppSelector(selectWallet) + const isCreatingCode = useAppSelector(selectIsCreatingReferralCode) + + const [value, setValue] = useState('') + + const onChange = useCallback((e: ChangeEvent) => setValue(e.target.value), []) + + const handleCreateReferralCode = useCallback(() => { + const lowerCaseCode = value.toLowerCase() + dispatch(createNewReferralCode(lowerCaseCode)) + }, [dispatch, value]) + + return ( + + + + {t('referrals.affiliates.modal.create-referral-code.label')} + + + + + + + ) +}) + +const StyledBaseModal = styled(BaseModal)` + [data-reach-dialog-content] { + width: 400px; + margin-top: 300px; + } +` + +export default CreateReferralCodeModal diff --git a/packages/app/src/sections/referrals/NftPreviewModal.tsx b/packages/app/src/sections/referrals/NftPreviewModal.tsx new file mode 100644 index 0000000000..33dc38c5b1 --- /dev/null +++ b/packages/app/src/sections/referrals/NftPreviewModal.tsx @@ -0,0 +1,144 @@ +import { formatPercent } from '@kwenta/sdk/utils' +import Image from 'next/image' +import { FC, memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import LinkIconLight from 'assets/svg/app/link-light.svg' +import Badge from 'components/Badge' +import BaseModal from 'components/BaseModal' +import Button from 'components/Button' +import { FlexDivCol, FlexDivRow, FlexDivRowCentered } from 'components/layout/flex' +import Spacer from 'components/Spacer' +import { Body } from 'components/Text' +import { EXTERNAL_LINKS } from 'constants/links' +import useWindowSize from 'hooks/useWindowSize' +import { useAppSelector } from 'state/hooks' +import { selectReferralNft } from 'state/referrals/selectors' + +import { REFFERAL_TIERS } from './constants' + +type NftPreviewModalProps = { + onDismiss(): void +} + +type NftHeaderProps = { + title: string + tier: number +} +export const NftHeader: FC = memo(({ title, tier }) => { + const { t } = useTranslation() + return ( + + {t(title)} + {`${t('referrals.affiliates.modal.nft-preview.tier-level')} ${ + tier + 1 + }`} + + ) +}) + +const NftPreviewModal: FC = memo(({ onDismiss }) => { + const { t } = useTranslation() + const nftTier = useAppSelector(selectReferralNft) + const { boost, title, icon, animationUrl, tier } = REFFERAL_TIERS[nftTier] + const { deviceType } = useWindowSize() + const imageSize = useMemo(() => (deviceType === 'mobile' ? 330 : 355), [deviceType]) + + return ( + } isOpen onDismiss={onDismiss}> + + + + + + {icon} + + + {t('referrals.affiliates.modal.nft-preview.nft-tier')} + + + {`${t('referrals.affiliates.modal.nft-preview.tier-level')} ${tier + 1}`} + + + + + | + + + + + {t('referrals.traders.dashboard.rewards-boost')} + + + {formatPercent(boost, { minDecimals: 0 })} + + + + + + + + ) +}) + +const StyledImage = styled(Image)` + border-radius: 20px; + margin: 0 auto; +` + +const LabelContainer = styled(FlexDivCol)<{ + justifyContent?: string + marginRight?: string + marginLeft?: string +}>` + justify-content: ${(props) => props.justifyContent || 'flex-start'}; + margin-right: ${(props) => props.marginRight || 'initial'}; + margin-left: ${(props) => props.marginLeft || 'initial'}; +` + +const IconContainer = styled.div` + svg { + width: 48px; + height: 48px; + } +` +const ProfileItemContainer = styled(FlexDivRowCentered)` + height: 56px; +` + +const ProfileContainer = styled(FlexDivRowCentered)` + margin-bottom: 0px; + border-radius: 20px; + border: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.border}; + padding: 20px; +` +const StyledBaseModal = styled(BaseModal)` + [data-reach-dialog-content] { + width: 400px; + } + .card-header { + margin-top: 15px; + } +` + +export default NftPreviewModal diff --git a/packages/app/src/sections/referrals/ReferralCodes.tsx b/packages/app/src/sections/referrals/ReferralCodes.tsx new file mode 100644 index 0000000000..5d88a530e1 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralCodes.tsx @@ -0,0 +1,332 @@ +import { formatDollars, formatNumber } from '@kwenta/sdk/utils' +import { FC, memo, useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import CopyCheckIcon from 'assets/svg/referrals/copy-check.svg' +import Button from 'components/Button' +import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' +import { DesktopOnlyView, MobileOrTabletView } from 'components/Media' +import Table from 'components/Table' +import { TableHeader } from 'components/Table' +import { Body } from 'components/Text' +import { PROD_HOSTNAME } from 'constants/links' +import useIsL2 from 'hooks/useIsL2' +import { setOpenModal } from 'state/app/reducer' +import { selectShowModal } from 'state/app/selectors' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { selectIsCreatingReferralCode } from 'state/referrals/selectors' +import { selectWallet } from 'state/wallet/selectors' +import media from 'styles/media' + +import { referralGridLayoutTable } from './constants' +import CreateReferralCodeModal from './CreateReferralCodeModal' +import { ReferralTableNoResults } from './ReferralTableNoResults' +import { ReferralsRewardsPerCode } from './types' + +type ReferralCodesProps = { + data: ReferralsRewardsPerCode[] +} +const ReferralCodes: FC = memo(({ data }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const wallet = useAppSelector(selectWallet) + const isL2 = useIsL2() + const isCreatingCode = useAppSelector(selectIsCreatingReferralCode) + const [copiedStatus, setCopiedStatus] = useState(false) + const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null) + const openModal = useAppSelector(selectShowModal) + + const referralCodesTableProps = useMemo( + () => ({ + data, + compactPagination: true, + pageSize: 4, + showPagination: true, + columnsDeps: [wallet, isCreatingCode, isL2], + noResultsMessage: , + }), + [data, isCreatingCode, isL2, wallet] + ) + + const handleOpenModal = useCallback(() => { + dispatch(setOpenModal('referrals_create_referral_code')) + }, [dispatch]) + + const handleDismissModal = useCallback(() => { + dispatch(setOpenModal(null)) + }, [dispatch]) + + const handleCopyToClipboard = useCallback((text: string, event: React.MouseEvent) => { + const x = event.clientX + const y = event.clientY + const { protocol, hostname, port } = window.location + const fullUrl = `${protocol}//${ + process.env.NEXT_PUBLIC_REF_URL_OVERRIDE === 'true' ? PROD_HOSTNAME : hostname + }${port ? `:${port}` : ``}/?ref=${text}` + + navigator.clipboard.writeText(fullUrl).then(() => { + setCopiedStatus(true) + setTooltipPosition({ x, y }) + + setTimeout(() => { + setCopiedStatus(false) + setTooltipPosition(null) + }, 1000) + }) + }, []) + + return ( + + + ( + + + {t('referrals.table.codes.title')} + + {t('referrals.table.codes.copy')} + + + + + ), + accessorKey: 'title', + enableSorting: false, + columns: [ + { + header: () => ( + {t('referrals.table.header.referral-code')} + ), + cell: (cellProps) => { + return ( + + + {cellProps.getValue()} + + handleCopyToClipboard(cellProps.getValue(), event) + } + /> + + + ) + }, + accessorKey: 'code', + }, + { + header: () => ( + {t('referrals.table.header.total-volume')} + ), + cell: (cellProps) => ( + {formatDollars(cellProps.getValue(), { maxDecimals: 2 })} + ), + accessorKey: 'referralVolume', + }, + { + header: () => ( + {t('referrals.table.header.traders-referred')} + ), + cell: (cellProps) => ( + {cellProps.getValue()} + ), + accessorKey: 'referredCount', + }, + { + header: () => ( + {t('referrals.table.header.kwenta-earned')} + ), + cell: (cellProps) => ( + + {formatNumber(cellProps.getValue(), { maxDecimals: 2 })} + + ), + accessorKey: 'earnedRewards', + }, + ], + }, + ]} + /> + + + ( + + + {t('referrals.table.codes.title')} + + + + ), + accessorKey: 'title', + enableSorting: false, + columns: [ + { + header: () => null, + cell: (cellProps) => ( + + {t('referrals.table.header.referral-code')} + + {cellProps.getValue()} + + handleCopyToClipboard(cellProps.getValue(), event) + } + /> + + + ), + accessorKey: 'code', + }, + { + header: () => null, + cell: (cellProps) => ( + + {t('referrals.table.header.total-volume')} + {formatDollars(cellProps.getValue(), { maxDecimals: 2 })} + + ), + accessorKey: 'referralVolume', + }, + { + header: () => null, + cell: (cellProps) => ( + + {t('referrals.table.header.traders-referred')} + {cellProps.getValue()} + + ), + accessorKey: 'referredCount', + }, + { + header: () => null, + cell: (cellProps) => ( + + {t('referrals.table.header.kwenta-earned')} + {formatNumber(cellProps.getValue(), { maxDecimals: 2 })} + + ), + accessorKey: 'earnedRewards', + }, + ], + }, + ]} + /> + + {openModal === 'referrals_create_referral_code' && ( + + )} + {copiedStatus && tooltipPosition && ( +
+ + Code Copied! + +
+ )} +
+ ) +}) + +const StyledCopyCheckIcon = styled(CopyCheckIcon)` + cursor: pointer; +` + +const StyledTooltip = styled.div<{ + top?: string + left?: string + padding?: string + borderRadius?: string +}>` + position: fixed; + top: ${(props) => props.top}; + left: ${(props) => props.left}; + padding: ${(props) => props.padding}; + border-radius: ${(props) => props.borderRadius}; + background: ${(props) => props.theme.colors.selectedTheme.button.fill}; + border: ${(props) => props.theme.colors.selectedTheme.border}; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + transition: opacity 0.3s; + opacity: 1; +` + +const TableContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + margin: 30px 0 60px; + background: transparent; + border: none; +` + +const StyledTable = styled(Table)` + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-radius: 15px; + .table-row:first-of-type div:first-child { + padding: 12.5px 9px; + height: 100%; + } + + ${media.lessThan('lg')` + .table-row > div:first-child { + width: auto !important; + } + .table-row:nth-child(2) { + display: none; + } + .table-row:first-of-type div:first-child { + padding-left: 12.5px; + } + `} +` as typeof Table + +const TableCell = styled.div<{ $regular?: boolean; paddingLeft?: string }>` + font-size: 13px; + font-family: ${(props) => props.theme.fonts[props.$regular ? 'regular' : 'mono']}; + color: ${(props) => props.color || props.theme.colors.selectedTheme.button.text.primary}; + display: flex; + flex-direction: column; + padding-left: ${(props) => props.paddingLeft || 'initial'}; +` + +const StyledTableHeader = styled(TableHeader)` + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding-right: 9px; + text-transform: none; +` + +export default ReferralCodes diff --git a/packages/app/src/sections/referrals/ReferralModal/BaseReferralModal.tsx b/packages/app/src/sections/referrals/ReferralModal/BaseReferralModal.tsx new file mode 100644 index 0000000000..7d803e9898 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralModal/BaseReferralModal.tsx @@ -0,0 +1,81 @@ +import React, { FC, memo, useEffect, useMemo } from 'react' +import styled from 'styled-components' + +import GridSvg from 'assets/svg/referrals/modal-background.svg' +import BaseModal from 'components/BaseModal' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { checkSelfReferredByCode } from 'state/referrals/action' +import { setStartOnboarding } from 'state/referrals/reducer' +import { + selectBoostNft, + selectMintedBoostNft, + selectStartOnboarding, + selectUnmintedBoostNft, +} from 'state/referrals/selectors' +import { selectWallet } from 'state/wallet/selectors' + +import { MintedNftModal } from './MintedNftModal' +import { MintingModal } from './MintingModal' +import { OnboardModal } from './OnboardModal' +import { ReferralHeader } from './ReferralHeader' + +type ReferalModalProps = { + referralCode: string + onDismiss(): void +} + +const BaseReferralModal: FC = memo(({ referralCode, onDismiss }) => { + const dispatch = useAppDispatch() + const wallet = useAppSelector(selectWallet) + const isStartOnboarding = useAppSelector(selectStartOnboarding) + const isBoostNftMinted = useAppSelector(selectMintedBoostNft) + const unmintedNftTier = useAppSelector(selectUnmintedBoostNft) + const mintedNftTier = useAppSelector(selectBoostNft) + + useEffect(() => { + dispatch(setStartOnboarding(false)) + }, [dispatch]) + + useEffect(() => { + if (wallet) { + dispatch(checkSelfReferredByCode(referralCode)) + } + }, [dispatch, referralCode, wallet]) + + const displayNftTier = useMemo( + () => (isStartOnboarding && isBoostNftMinted ? mintedNftTier : unmintedNftTier), + [isBoostNftMinted, isStartOnboarding, mintedNftTier, unmintedNftTier] + ) + + const displayReferralCode = useMemo( + () => (!isStartOnboarding || !isBoostNftMinted ? referralCode : ''), + [isBoostNftMinted, isStartOnboarding, referralCode] + ) + return ( + } + headerBackground={} + isOpen + onDismiss={onDismiss} + > + {!isStartOnboarding ? ( + + ) : isBoostNftMinted ? ( + + ) : ( + + )} + + ) +}) + +const StyledBaseModal = styled(BaseModal)` + [data-reach-dialog-content] { + width: 448px; + } + .card-header { + margin-top: 15px; + } +` + +export default BaseReferralModal diff --git a/packages/app/src/sections/referrals/ReferralModal/MintedNftModal.tsx b/packages/app/src/sections/referrals/ReferralModal/MintedNftModal.tsx new file mode 100644 index 0000000000..cf416975d6 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralModal/MintedNftModal.tsx @@ -0,0 +1,98 @@ +import { formatPercent } from '@kwenta/sdk/utils' +import { FC, memo } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import SuccessIcon from 'assets/svg/referrals/success.svg' +import Badge from 'components/Badge' +import Button from 'components/Button' +import { FlexDivColCentered, FlexDivRow } from 'components/layout/flex' +import Spacer from 'components/Spacer' +import { Body, Heading } from 'components/Text' + +import { REFFERAL_TIERS } from '../constants' +import { ReferralTiers } from '../types' + +type Props = { + onDismiss(): void + boostNftTier: ReferralTiers +} + +export const MintedNftModal: FC = memo(({ onDismiss, boostNftTier }) => { + const { t } = useTranslation() + const { tier, boost } = REFFERAL_TIERS[boostNftTier] + + return ( + + + + + {t('referrals.affiliates.modal.referrer.congratulations')} + + + ]} + values={{ + tier: ` ${t('referrals.affiliates.modal.nft-preview.tier-level')} ${tier + 1} `, + }} + /> + + + + {REFFERAL_TIERS[boostNftTier].nftPreview} + + + {`${t('referrals.affiliates.modal.nft-preview.tier-level')} ${tier + 1}`} + + + + + + ]} + values={{ boost: ` ${formatPercent(boost, { minDecimals: 0 })} ` }} + /> + + + ]} + /> + + + + + ) +}) + +const BadgeContainer = styled.div` + width: 50px; + margin-top: -10px; +` + +const CenteredBody = styled(Body)` + text-align: center; +` + +const StyledSuccessIcon = styled(SuccessIcon)` + margin-top: -2px; +` + +const Emphasis = styled.b` + color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.preview}; + white-space: pre-wrap; +` diff --git a/packages/app/src/sections/referrals/ReferralModal/MintingModal.tsx b/packages/app/src/sections/referrals/ReferralModal/MintingModal.tsx new file mode 100644 index 0000000000..33404ba385 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralModal/MintingModal.tsx @@ -0,0 +1,140 @@ +import { formatPercent } from '@kwenta/sdk/utils' +import { useChainModal, useConnectModal } from '@rainbow-me/rainbowkit' +import { FC, memo, useCallback } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import MintingIcon from 'assets/svg/referrals/minting.svg' +import ReferralsIcon from 'assets/svg/referrals/referrals.svg' +import Badge from 'components/Badge' +import Button from 'components/Button' +import { FlexDivColCentered, FlexDivRow } from 'components/layout/flex' +import Loader from 'components/Loader' +import Spacer from 'components/Spacer' +import { Body, Heading } from 'components/Text' +import { EXTERNAL_LINKS } from 'constants/links' +import useIsL2 from 'hooks/useIsL2' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { mintBoostNft } from 'state/referrals/action' +import { selectIsMintingBoostNft } from 'state/referrals/selectors' +import { selectWallet } from 'state/wallet/selectors' + +import { REFFERAL_TIERS } from '../constants' +import { ReferralTiers } from '../types' + +type Props = { + referralCode: string + boostNftTier: ReferralTiers +} + +export const MintingModal: FC = memo(({ referralCode, boostNftTier }) => { + const { t } = useTranslation() + const { openChainModal } = useChainModal() + const { openConnectModal } = useConnectModal() + const isL2 = useIsL2() + const wallet = useAppSelector(selectWallet) + const dispatch = useAppDispatch() + const isMinting = useAppSelector(selectIsMintingBoostNft) + + const boost = REFFERAL_TIERS[boostNftTier].boost + + const handleMintBoostNft = useCallback(() => { + dispatch(mintBoostNft(referralCode)) + }, [dispatch, referralCode]) + + return ( + + + + + {t('referrals.affiliates.modal.referrer.welcome-message')} + + + ]} + values={{ boost: ` ${formatPercent(boost, { minDecimals: 0 })} ` }} + /> + + + + {isMinting ? ( + <> + + + + + {`${t('referrals.affiliates.modal.nft-preview.tier-level')} ${ + REFFERAL_TIERS[boostNftTier].displayTier + }`} + + + + ) : ( + <> + {REFFERAL_TIERS[boostNftTier].nftPreview} + + + {`${t('referrals.affiliates.modal.nft-preview.tier-level')} ${ + REFFERAL_TIERS[boostNftTier].displayTier + }`} + + + + )} + + + + + + {t('referrals.affiliates.modal.referrer.learn-more')} + + window.open(EXTERNAL_LINKS.Docs.Referrals, '_blank')} + > + ]} + /> + + + ) +}) + +const BadgeContainer = styled.div` + width: 50px; + margin-top: -10px; +` + +const CenteredBody = styled(Body)<{ cursor?: string }>` + text-align: center; + cursor: ${(props) => props.cursor || 'default'}; +` + +const Emphasis = styled.b` + color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.preview}; + white-space: pre-wrap; +` + +const Underline = styled(Emphasis)` + text-decoration: underline; +` diff --git a/packages/app/src/sections/referrals/ReferralModal/OnboardModal.tsx b/packages/app/src/sections/referrals/ReferralModal/OnboardModal.tsx new file mode 100644 index 0000000000..8f7ce10adb --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralModal/OnboardModal.tsx @@ -0,0 +1,157 @@ +import { formatPercent } from '@kwenta/sdk/utils' +import { useConnectModal } from '@rainbow-me/rainbowkit' +import { FC, memo, useCallback } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import DeepLiquidityIcon from 'assets/svg/referrals/deep-liquidity.svg' +import LowFeesIcon from 'assets/svg/referrals/low-fees.svg' +import ReferralsIcon from 'assets/svg/referrals/referrals.svg' +import TradingRewardsIcon from 'assets/svg/referrals/trading-rewards.svg' +import Badge from 'components/Badge' +import Button from 'components/Button' +import { + FlexDivCol, + FlexDivColCentered, + FlexDivRow, + FlexDivRowCentered, +} from 'components/layout/flex' +import Spacer from 'components/Spacer' +import { Body, Heading } from 'components/Text' +import { EXTERNAL_LINKS } from 'constants/links' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { setStartOnboarding } from 'state/referrals/reducer' +import { selectCheckSelfReferred } from 'state/referrals/selectors' +import { selectWallet } from 'state/wallet/selectors' + +import { REFFERAL_TIERS } from '../constants' +import { ReferralTiers } from '../types' + +type Props = { + boostNftTier: ReferralTiers +} + +export const OnboardModal: FC = memo(({ boostNftTier }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const wallet = useAppSelector(selectWallet) + const { openConnectModal } = useConnectModal() + const boost = REFFERAL_TIERS[boostNftTier].boost + const isSelfReferred = useAppSelector(selectCheckSelfReferred) + + const handleStart = useCallback(() => { + if (wallet) { + dispatch(setStartOnboarding(true)) + } else { + openConnectModal?.() + } + }, [dispatch, openConnectModal, wallet]) + + return ( + + + + + {t('referrals.affiliates.modal.referrer.welcome-message')} + + + ]} + values={{ boost: ` ${formatPercent(boost, { minDecimals: 0 })} ` }} + /> + + + + + + + + + + {t('referrals.affiliates.modal.referrer.low-fees.title')} + + + {t('referrals.affiliates.modal.referrer.low-fees.copy')} + + + + + + + + {t('referrals.affiliates.modal.referrer.deep-liquidity.title')} + + + {t('referrals.affiliates.modal.referrer.deep-liquidity.copy')} + + + + + + + + {t('referrals.affiliates.modal.referrer.trading-rewards.title')} + + + {t('referrals.affiliates.modal.referrer.trading-rewards.copy')} + + + + + + + + + {t('referrals.affiliates.modal.referrer.learn-more')} + + window.open(EXTERNAL_LINKS.Docs.Referrals, '_blank')} + > + ]} + /> + + + ) +}) + +const CenteredBody = styled(Body)` + text-align: center; +` + +const FeatureItemContainer = styled(FlexDivCol)` + width: 70%; +` +const IconContainer = styled.div` + width: 60px; +` + +const Emphasis = styled.b` + color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.preview}; + white-space: pre-wrap; +` + +const Underline = styled(Emphasis)` + text-decoration: underline; +` diff --git a/packages/app/src/sections/referrals/ReferralModal/ReferralHeader.tsx b/packages/app/src/sections/referrals/ReferralModal/ReferralHeader.tsx new file mode 100644 index 0000000000..b15731fdae --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralModal/ReferralHeader.tsx @@ -0,0 +1,43 @@ +import { truncateAddress } from '@kwenta/sdk/utils' +import { FC, memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +import Badge from 'components/Badge' +import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' +import { Body } from 'components/Text' + +import { REFFERAL_TIERS } from '../constants' +import { ReferralTiers } from '../types' + +type Props = { + referralCode?: string + boostNftTier: ReferralTiers +} + +export const ReferralHeader: FC = memo(({ boostNftTier, referralCode }) => { + const { t } = useTranslation() + const displayReferralCode = useMemo(() => { + if (!referralCode) return null + if (referralCode.length < 13) return referralCode + return truncateAddress(referralCode) + }, [referralCode]) + + return ( + + {t(REFFERAL_TIERS[boostNftTier].title)} + + + {displayReferralCode} + + + {`${t('referrals.affiliates.modal.nft-preview.tier-level')} ${ + REFFERAL_TIERS[boostNftTier].displayTier + }`} + + + + ) +}) diff --git a/packages/app/src/sections/referrals/ReferralRewardsHistory.tsx b/packages/app/src/sections/referrals/ReferralRewardsHistory.tsx new file mode 100644 index 0000000000..a34739c766 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralRewardsHistory.tsx @@ -0,0 +1,198 @@ +import { formatDollars, formatNumber } from '@kwenta/sdk/utils' +import { FC, memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { DesktopOnlyView, MobileOrTabletView } from 'components/Media' +import Table from 'components/Table' +import { TableHeader } from 'components/Table' +import { Body } from 'components/Text' +import media from 'styles/media' + +import { referralGridLayoutTable } from './constants' +import { ReferralTableNoResults } from './ReferralTableNoResults' +import { ReferralsRewardsPerEpoch } from './types' + +type ReferralRewardsHistoryProps = { + data: ReferralsRewardsPerEpoch[] +} + +const ReferralRewardsHistory: FC = memo(({ data }) => { + const { t } = useTranslation() + + const rewardsHistoryTableProps = useMemo( + () => ({ + data, + compactPagination: true, + pageSize: 4, + showPagination: true, + noResultsMessage: , + }), + [data] + ) + + return ( + + + ( + + {t('referrals.table.history.title')} + + {t('referrals.table.history.copy')} + + + ), + accessorKey: 'title', + enableSorting: false, + columns: [ + { + header: () => {t('referrals.table.header.epoch')}, + cell: (cellProps) => {cellProps.getValue()}, + accessorKey: 'epoch', + }, + { + header: () => ( + {t('referrals.table.header.total-volume')} + ), + cell: (cellProps) => ( + {formatDollars(cellProps.getValue(), { maxDecimals: 2 })} + ), + accessorKey: 'referralVolume', + }, + { + header: () => ( + {t('referrals.table.header.traders-referred')} + ), + cell: (cellProps) => ( + {cellProps.getValue()} + ), + accessorKey: 'referredCount', + }, + { + header: () => ( + {t('referrals.table.header.kwenta-earned')} + ), + cell: (cellProps) => ( + + {formatNumber(cellProps.getValue(), { maxDecimals: 2 })} + + ), + accessorKey: 'earnedRewards', + }, + ], + }, + ]} + /> + + + ( + + {t('referrals.table.history.title')} + + ), + accessorKey: 'title', + enableSorting: false, + columns: [ + { + header: () => null, + cell: (cellProps) => ( + + {t('referrals.table.header.epoch')} + {cellProps.getValue()} + + ), + accessorKey: 'epoch', + }, + { + header: () => null, + cell: (cellProps) => ( + + {t('referrals.table.header.total-volume')} + {formatDollars(cellProps.getValue(), { maxDecimals: 2 })} + + ), + accessorKey: 'referralVolume', + }, + { + header: () => null, + cell: (cellProps) => ( + + {t('referrals.table.header.traders-referred')} + {cellProps.getValue()} + + ), + accessorKey: 'referredCount', + }, + { + header: () => null, + cell: (cellProps) => ( + + {t('referrals.table.header.kwenta-earned')} + {formatNumber(cellProps.getValue(), { maxDecimals: 2 })} + + ), + accessorKey: 'earnedRewards', + }, + ], + }, + ]} + /> + + + ) +}) + +const TableContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + margin: 30px 0 60px; + background: transparent; + border: none; +` + +const StyledTable = styled(Table)` + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-radius: 15px; + .table-row:first-of-type div:first-child { + padding: 25px 18px; + height: 100%; + } + + ${media.lessThan('lg')` + .table-row > div:first-child { + width: auto !important; + } + .table-row:nth-child(2) { + display: none; + } + .table-row:first-of-type div:first-child { + padding-left: 25px; + } + `} +` as typeof Table + +const TableCell = styled.div<{ $regular?: boolean; paddingLeft?: string }>` + font-size: 13px; + font-family: ${(props) => props.theme.fonts[props.$regular ? 'regular' : 'mono']}; + color: ${(props) => props.color || props.theme.colors.selectedTheme.button.text.primary}; + display: flex; + flex-direction: column; + padding-left: ${(props) => props.paddingLeft || 'auto'}; +` + +const StyledTableHeader = styled(TableHeader)` + text-transform: none; +` + +export default ReferralRewardsHistory diff --git a/packages/app/src/sections/referrals/ReferralTableNoResults.tsx b/packages/app/src/sections/referrals/ReferralTableNoResults.tsx new file mode 100644 index 0000000000..b3847d2cb8 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralTableNoResults.tsx @@ -0,0 +1,37 @@ +import { useConnectModal } from '@rainbow-me/rainbowkit' +import { memo } from 'react' +import { useTranslation } from 'react-i18next' + +import { TableNoResults } from 'components/Table' +import useIsL2 from 'hooks/useIsL2' +import useNetworkSwitcher from 'hooks/useNetworkSwitcher' +import { useAppSelector } from 'state/hooks' +import { selectWallet } from 'state/wallet/selectors' + +export const ReferralTableNoResults = memo(() => { + const { t } = useTranslation() + const wallet = useAppSelector(selectWallet) + const { switchToL2 } = useNetworkSwitcher() + const { openConnectModal } = useConnectModal() + const isL2 = useIsL2() + + return ( + + {wallet ? ( + isL2 ? ( + t('referrals.table.no-results-table') + ) : ( + <> + {t('referrals.table.switch-to-optimism-prompt')} +
{t('homepage.l2.cta-buttons.switch-l2')}
+ + ) + ) : ( + <> + {t('referrals.table.switch-to-optimism-prompt')} +
{t('common.wallet.connect-wallet')}
+ + )} +
+ ) +}) diff --git a/packages/app/src/sections/referrals/ReferralTiersProgressBar.tsx b/packages/app/src/sections/referrals/ReferralTiersProgressBar.tsx new file mode 100644 index 0000000000..fd89562327 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralTiersProgressBar.tsx @@ -0,0 +1,146 @@ +import { formatPercent } from '@kwenta/sdk/utils' +import { FC, memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import InactiveTierIcon from 'assets/svg/referrals/inactive-tier.svg' +import { FlexDivCol, FlexDivColCentered, FlexDivRowCentered } from 'components/layout/flex' +import { Body } from 'components/Text' +import { useAppSelector } from 'state/hooks' +import { selectReferralNft, selectReferralScore } from 'state/referrals/selectors' + +import { MAX_REFERRAL_SCORE, REFFERAL_TIERS } from './constants' +import { ReferralTierDetails, ReferralTiers } from './types' + +type ReferralTiersProgressBarProps = { + referralTiersDef: ReferralTierDetails[] +} + +const ReferralTiersProgressBar: FC = memo(({ referralTiersDef }) => { + const { t } = useTranslation() + const referralNftTier = useAppSelector(selectReferralNft) + const score = useAppSelector(selectReferralScore) + const ratio = useMemo(() => score / MAX_REFERRAL_SCORE, [score]) + + return ( + + + {referralTiersDef + .filter(({ tier }) => tier >= 0) + .map(({ title, tier, displayTier, icon }) => ( + + {referralNftTier === tier ? ( + icon + ) : ( + + + = tier ? 'primary' : 'secondary'}`} + > + {displayTier} + + + )} + + {t(title)} + + + ))} + + + + + + + 0} + $active={REFFERAL_TIERS[referralNftTier].tier === 0} + > + + {REFFERAL_TIERS[ReferralTiers.BRONZE].threshold} + + + 1} + $active={REFFERAL_TIERS[referralNftTier].tier === 1} + > + + {REFFERAL_TIERS[ReferralTiers.SILVER].threshold} + + + + + {REFFERAL_TIERS[ReferralTiers.GOLD].threshold} + + + + + + ) +}) + +const DotsContainer = styled(FlexDivRowCentered)` + width: 100%; +` + +const StyledBody = styled(Body)` + margin-top: 30px; +` + +const InactiveTierIconContainer = styled(FlexDivRowCentered)` + position: relative; +` + +const LineCotainer = styled.div` + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: 8px; + background-color: ${(props) => + props.theme.colors.selectedTheme.newTheme.button.default.background}; + transform: translateY(-50%); +` + +const FilledLine = styled.div<{ $ratio: string }>` + width: ${(props) => props.$ratio}; + height: 100%; + background-color: ${(props) => + props.theme.colors.selectedTheme.newTheme.checkBox.default.checked}; + border-radius: 100px; +` + +const ProgressBar = styled.div` + position: relative; + width: calc(100% - 45px); + margin: 15px 22.5px; + border-radius: 100px; +` + +const Dot = styled.div<{ $active: boolean; $pass: boolean; left?: string }>` + position: absolute; + top: 50%; + left: ${(props) => props.left || '0%'}; + width: 16px; + height: 16px; + background-color: ${(props) => + props.$pass + ? props.theme.colors.selectedTheme.newTheme.checkBox.default.checked + : props.$active + ? props.theme.colors.selectedTheme.newTheme.button.tab.active + : props.theme.colors.selectedTheme.newTheme.button.default.background}; + border-radius: 50%; + transform: translate(-50%, -50%); + display: flex; + justify-content: center; +` + +const CenteredNumber = styled(Body)` + position: absolute; + z-index: 2; + font-size: 16px; +` + +export default ReferralTiersProgressBar diff --git a/packages/app/src/sections/referrals/ReferralsHeading.tsx b/packages/app/src/sections/referrals/ReferralsHeading.tsx new file mode 100644 index 0000000000..6794bb6cd4 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralsHeading.tsx @@ -0,0 +1,50 @@ +import { FC, memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import Button from 'components/Button' +import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' +import { Body, Heading } from 'components/Text' +import { EXTERNAL_LINKS } from 'constants/links' +import useWindowSize from 'hooks/useWindowSize' + +type HeadingProps = { + title: string + copy: string +} + +export const ReferralsHeading: FC = memo(({ title, copy }) => { + const { t } = useTranslation() + const { deviceType } = useWindowSize() + const isMobile = useMemo(() => deviceType === 'mobile', [deviceType]) + + return ( + + + {title} + {!isMobile && {copy}} + + window.open(EXTERNAL_LINKS.Docs.Referrals, '_blank')} + > + {t('referrals.docs')} + + + ) +}) + +const TitleContainer = styled(FlexDivRowCentered)` + margin: 72px 0 30px; +` + +const StyledButton = styled(Button)` + border-width: 0px; + color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.secondary}; +` + +const StyledHeading = styled(Heading)` + font-weight: 400; +` diff --git a/packages/app/src/sections/referrals/ReferralsTabs.tsx b/packages/app/src/sections/referrals/ReferralsTabs.tsx new file mode 100644 index 0000000000..00993280b9 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferralsTabs.tsx @@ -0,0 +1,158 @@ +import { formatDollars, formatNumber, formatPercent } from '@kwenta/sdk/utils' +import { FC, memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import TabButton from 'components/Button/TabButton' +import { TabPanel } from 'components/Tab' +import { useAppSelector } from 'state/hooks' +import { + selectBoostNft, + selectCumulativeStatsByCode, + selectMintedBoostNft, + selectReferralCodes, + selectReferralEpoch, +} from 'state/referrals/selectors' +import media from 'styles/media' + +import AffiliatesProfiles from './AffiliatesProfiles' +import { REFFERAL_TIERS } from './constants' +import ReferralCodes from './ReferralCodes' +import ReferralRewardsHistory from './ReferralRewardsHistory' +import { ReferralsHeading } from './ReferralsHeading' +import ReferrersDashboard from './ReferrersDashboard' +import { ReferralsTab } from './types' + +type ReferralsTabsProp = { + currentTab: ReferralsTab + onChangeTab(tab: ReferralsTab): () => void +} + +const ReferralsTabs: FC = memo(({ currentTab, onChangeTab }) => { + const { t } = useTranslation() + const referralsCodes = useAppSelector(selectReferralCodes) + const referralsEpoch = useAppSelector(selectReferralEpoch) + const { totalVolume, totalRewards, totalTraders } = useAppSelector(selectCumulativeStatsByCode) + const hasMinted = useAppSelector(selectMintedBoostNft) + const boostNftTier = useAppSelector(selectBoostNft) + + const { boost, icon } = REFFERAL_TIERS[boostNftTier] + + const tradersMetrics = useMemo( + () => [ + { + key: 'rewards-boost', + label: t('referrals.traders.dashboard.rewards-boost'), + value: hasMinted + ? formatPercent(boost, { suggestDecimals: true, maxDecimals: 2 }) + : 'Boost NFT not minted', + icon: hasMinted ? icon : undefined, + }, + { + key: 'total-volume', + label: t('referrals.traders.dashboard.total-volume'), + value: formatDollars(totalVolume, { suggestDecimals: true }), + }, + { + key: 'kwenta-earned', + label: t('referrals.traders.dashboard.kwenta-earned'), + value: formatNumber(totalRewards, { suggestDecimals: true }), + buttonLabel: t('referrals.traders.claim'), + onClick: () => {}, + active: false, + loading: false, + }, + ], + [boost, hasMinted, icon, t, totalRewards, totalVolume] + ) + + const affiliatesMetrics = useMemo( + () => [ + { + key: 'traders-referred', + label: t('referrals.affiliates.dashboard.traders-referred'), + value: totalTraders.toString(), + }, + { + key: 'trading-volume', + label: t('referrals.affiliates.dashboard.trading-volume'), + value: formatDollars(totalVolume, { suggestDecimals: true }), + }, + { + key: 'kwenta-earned', + label: t('referrals.affiliates.dashboard.kwenta-earned'), + value: formatNumber(totalRewards, { suggestDecimals: true }), + }, + ], + [t, totalRewards, totalTraders, totalVolume] + ) + + return ( + + + {process.env.NEXT_PUBLIC_ENABLE_AFFILIATE === 'true' && ( + + + + + + + )} +
+ + + + + + + + + +
+
+ ) +}) + +const ReferralsTabsHeader = styled.div` + display: flex; + justify-content: space-between; + margin-top: 30px; + margin-bottom: 30px; + + ${media.lessThan('lg')` + flex-direction: column; + row-gap: 10px; + margin-bottom: 25px; + margin-top: 0px; + `} +` + +const ReferralsTabsContainer = styled.div` + ${media.lessThan('lg')` + padding: 15px; + `} +` + +const TabButtons = styled.div` + display: flex; + + & > button:not(:last-of-type) { + margin-right: 25px; + } + + ${media.lessThan('lg')` + justify-content: flex-start; + `} +` + +export default ReferralsTabs diff --git a/packages/app/src/sections/referrals/ReferrersDashboard.tsx b/packages/app/src/sections/referrals/ReferrersDashboard.tsx new file mode 100644 index 0000000000..1792b33565 --- /dev/null +++ b/packages/app/src/sections/referrals/ReferrersDashboard.tsx @@ -0,0 +1,72 @@ +import { FC, memo } from 'react' +import styled from 'styled-components' + +import Button from 'components/Button' +import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' +import Spacer from 'components/Spacer' +import { Body } from 'components/Text' +import { StakingCard } from 'sections/dashboard/Stake/card' +import media from 'styles/media' + +import { ReferralsMetrics } from './types' + +type ReferrersDashboardProps = { + data: ReferralsMetrics[] +} + +const ReferrersDashboard: FC = memo(({ data }) => { + return ( + + {data.map(({ key, label, value, buttonLabel, icon, active, onClick, loading }) => ( + + + {icon} + + {label} + + + {value} + + + + {buttonLabel && ( + + )} + + ))} + + ) +}) + +const Container = styled(FlexDivRowCentered)` + margin: 0; + ${media.lessThan('lg')` + flex-direction: column; + row-gap: 25px; + margin: 0; + margin-bottom: 25px; + `} +` + +const StyledCard = styled(StakingCard)` + width: 100%; + column-gap: 15px; + padding: 25px; + display: flex; + height: 100px; + flex-direction: row; + align-items: center; + justify-content: space-between; +` + +export default ReferrersDashboard diff --git a/packages/app/src/sections/referrals/constants.tsx b/packages/app/src/sections/referrals/constants.tsx new file mode 100644 index 0000000000..142068fac7 --- /dev/null +++ b/packages/app/src/sections/referrals/constants.tsx @@ -0,0 +1,75 @@ +import BronzeSmallIcon from 'assets/svg/referrals/bronze-small.svg' +import BronzeIcon from 'assets/svg/referrals/bronze.svg' +import GoldSmallIcon from 'assets/svg/referrals/gold-small.svg' +import GoldIcon from 'assets/svg/referrals/gold.svg' +import SilverSmallIcon from 'assets/svg/referrals/silver-small.svg' +import SilverIcon from 'assets/svg/referrals/silver.svg' +import { GridLayoutStyles } from 'components/Table/Table' + +import { ReferralTiers, ReferralTierDetails } from './types' + +export const MAX_REFERRAL_SCORE = 200 + +export const REFFERAL_TIERS: Record = { + [ReferralTiers.INVALID]: { + title: '', + tier: ReferralTiers.INVALID, + displayTier: -1, + icon: <>, + boost: 0, + nftPreview: <>, + threshold: -1, + animationUrl: '', + }, + [ReferralTiers.BRONZE]: { + title: 'referrals.affiliates.nft.bronze', + tier: ReferralTiers.BRONZE, + displayTier: ReferralTiers.BRONZE + 1, + icon: , + boost: 0.05, + nftPreview: , + threshold: 0, + animationUrl: '../images/referrals/bronze.gif', + }, + [ReferralTiers.SILVER]: { + title: 'referrals.affiliates.nft.silver', + tier: ReferralTiers.SILVER, + displayTier: ReferralTiers.SILVER + 1, + icon: , + boost: 0.1, + nftPreview: , + threshold: 100, + animationUrl: '../images/referrals/silver.gif', + }, + [ReferralTiers.GOLD]: { + title: 'referrals.affiliates.nft.gold', + tier: ReferralTiers.GOLD, + displayTier: ReferralTiers.GOLD + 1, + icon: , + boost: 0.15, + nftPreview: , + threshold: 200, + animationUrl: '../images/referrals/gold.gif', + }, +} + +export const REFERRAL_TIERS_ARRAY: ReferralTierDetails[] = Object.values(REFFERAL_TIERS) + +export const referralGridLayoutTable: GridLayoutStyles = { + row: ` + @media (max-width: 1150px) { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + grid-column-gap: 25px; + padding: 7.5px 25px; + } + `, + cell: ` + @media (max-width: 1150px) { + &:first-child { + padding-left: 0px; + } + } + `, +} diff --git a/packages/app/src/sections/referrals/types.ts b/packages/app/src/sections/referrals/types.ts new file mode 100644 index 0000000000..b888271676 --- /dev/null +++ b/packages/app/src/sections/referrals/types.ts @@ -0,0 +1,47 @@ +export type ReferralRewardsInfo = { + earnedRewards: string + referralVolume: string + referredCount: string +} + +export type ReferralsRewardsPerCode = ReferralRewardsInfo & { + code: string +} + +export type ReferralsRewardsPerEpoch = ReferralRewardsInfo & { + epoch: string +} + +export enum ReferralsTab { + Traders = 'traders', + Affiliates = 'affiliates', +} + +export type ReferralsMetrics = { + key: string + label: string + value: string + icon?: JSX.Element + active?: boolean + buttonLabel?: string + onClick?: () => void + loading?: boolean +} + +export enum ReferralTiers { + INVALID = -1, + BRONZE = 0, + SILVER = 1, + GOLD = 2, +} + +export type ReferralTierDetails = { + title: string + tier: ReferralTiers + displayTier: number + icon: JSX.Element + boost: number + nftPreview: JSX.Element + threshold: number + animationUrl: string +} diff --git a/packages/app/src/sections/referrals/utils.ts b/packages/app/src/sections/referrals/utils.ts new file mode 100644 index 0000000000..9fde630b10 --- /dev/null +++ b/packages/app/src/sections/referrals/utils.ts @@ -0,0 +1,6 @@ +import { ReferralRewardsInfo } from './types' + +export const calculateTotal = ( + arr: T[], + key: K +): number => arr.reduce((acc, obj) => acc + Number(obj[key]), 0) diff --git a/packages/app/src/sections/shared/Layout/AppLayout/Header/constants.tsx b/packages/app/src/sections/shared/Layout/AppLayout/Header/constants.tsx index 75ef9e3e5f..ba743d223b 100644 --- a/packages/app/src/sections/shared/Layout/AppLayout/Header/constants.tsx +++ b/packages/app/src/sections/shared/Layout/AppLayout/Header/constants.tsx @@ -99,6 +99,10 @@ export const getMenuLinks = (isMobile: boolean): MenuLinks => [ ] : null, }, + { + i18nLabel: 'header.nav.referrals', + link: ROUTES.Referrals.Home, + }, { i18nLabel: 'header.nav.options.title', link: EXTERNAL_LINKS.Options.Trade, diff --git a/packages/app/src/sections/shared/Layout/HomeLayout/Footer.tsx b/packages/app/src/sections/shared/Layout/HomeLayout/Footer.tsx index f034faab55..65b279377a 100644 --- a/packages/app/src/sections/shared/Layout/HomeLayout/Footer.tsx +++ b/packages/app/src/sections/shared/Layout/HomeLayout/Footer.tsx @@ -9,6 +9,7 @@ import { FlexDivCentered } from 'components/layout/flex' import PoweredBySynthetix from 'components/PoweredBySynthetix' import { Body } from 'components/Text' import { EXTERNAL_LINKS } from 'constants/links' +import ROUTES from 'constants/routes' import { GridContainer } from 'sections/homepage/section' import { ExternalLink } from 'styles/common' import media from 'styles/media' @@ -60,9 +61,9 @@ const Footer = memo(() => { link: EXTERNAL_LINKS.Trade.Spot, }, { - key: 'legacy', - title: t('homepage.footer.use-kwenta.legacy'), - link: EXTERNAL_LINKS.Trading.Legacy, + key: 'referrals', + title: t('homepage.footer.use-kwenta.referrals'), + link: ROUTES.Referrals.Home, }, { key: 'perps-v1', diff --git a/packages/app/src/state/app/types.ts b/packages/app/src/state/app/types.ts index 836f7e4413..20ea4a2a25 100644 --- a/packages/app/src/state/app/types.ts +++ b/packages/app/src/state/app/types.ts @@ -1,6 +1,7 @@ import { TransactionStatus, FuturesMarketKey, KwentaStatus, GasPrice } from '@kwenta/sdk/types' import { FuturesTransactionType } from 'state/futures/common/types' +import { ReferralTransactionType } from 'state/referrals/types' export type ModalType = | 'futures_deposit_withdraw_smart_margin' @@ -11,6 +12,8 @@ export type ModalType = | 'futures_smart_margin_onboard' | 'futures_cross_margin_onboard' | 'futures_smart_margin_socket' + | 'referrals_create_referral_code' + | 'referrals_mint_boost_nft' | null export type FuturesPositionModalType = @@ -22,7 +25,7 @@ export type FuturesPositionModalType = export type GasSpeed = 'average' | 'fast' | 'fastest' -export type TransactionType = FuturesTransactionType // TODO: Support all types +export type TransactionType = FuturesTransactionType | ReferralTransactionType // TODO: Support all types export type Transaction = { type: TransactionType diff --git a/packages/app/src/state/futures/hooks.ts b/packages/app/src/state/futures/hooks.ts index 08eb2408c7..ca63430f4a 100644 --- a/packages/app/src/state/futures/hooks.ts +++ b/packages/app/src/state/futures/hooks.ts @@ -12,8 +12,17 @@ import { selectCrossMarginSupportedNetwork, } from 'state/futures/crossMargin/selectors' import { useAppSelector, useFetchAction, usePollAction } from 'state/hooks' +import { + fetchAllReferralData, + fetchBoostNftForAccount, + fetchBoostNftMinted, +} from 'state/referrals/action' import { fetchStakeMigrateData } from 'state/staking/actions' -import { selectSelectedEpoch, selectStakingSupportedNetwork } from 'state/staking/selectors' +import { + selectSelectedEpoch, + selectStakingSupportedNetwork, + selectTradingRewardsSupportedNetwork, +} from 'state/staking/selectors' import { selectNetwork, selectWallet } from 'state/wallet/selectors' import { fetchFuturesPositionHistory, fetchMarginTransfers } from './actions' @@ -34,7 +43,6 @@ import { } from './smartMargin/selectors' // TODO: Optimise polling and queries - export const usePollMarketFuturesData = () => { const networkId = useAppSelector(selectNetwork) const markets = useAppSelector(selectMarkets) @@ -45,6 +53,17 @@ export const usePollMarketFuturesData = () => { const selectedAccountType = useAppSelector(selectFuturesType) const networkSupportsSmartMargin = useAppSelector(selectSmartMarginSupportedNetwork) const networkSupportsCrossMargin = useAppSelector(selectCrossMarginSupportedNetwork) + const networkSupportTradingRewards = useAppSelector(selectTradingRewardsSupportedNetwork) + + useFetchAction(fetchBoostNftMinted, { + dependencies: [wallet], + disabled: !networkSupportTradingRewards, + }) + + useFetchAction(fetchBoostNftForAccount, { + dependencies: [wallet], + disabled: !networkSupportTradingRewards, + }) useFetchAction(fetchSmartMarginAccount, { dependencies: [networkId, wallet, selectedAccountType], @@ -191,3 +210,14 @@ export const useFetchStakeMigrateData = () => { disabled: !wallet || !networkSupportsStaking, }) } + +export const useFetchReferralData = () => { + const networkId = useAppSelector(selectNetwork) + const wallet = useAppSelector(selectWallet) + const networkSupportTradingRewards = useAppSelector(selectTradingRewardsSupportedNetwork) + + useFetchAction(fetchAllReferralData, { + dependencies: [networkId, wallet], + disabled: !networkSupportTradingRewards, + }) +} diff --git a/packages/app/src/state/migrations.ts b/packages/app/src/state/migrations.ts index f9704454cf..30d4abbde4 100644 --- a/packages/app/src/state/migrations.ts +++ b/packages/app/src/state/migrations.ts @@ -6,6 +6,7 @@ import { FUTURES_INITIAL_STATE } from './futures/reducer' import { HOME_INITIAL_STATE } from './home/reducer' import { PREFERENCES_INITIAL_STATE } from './preferences/reducer' import { PRICES_INITIAL_STATE } from './prices/reducer' +import { REFERRALS_INITIAL_STATE } from './referrals/reducer' import { STAKING_INITIAL_STATE } from './staking/reducer' import { STATS_INITIAL_STATE } from './stats/reducer' import { WALLET_INITIAL_STATE } from './wallet/reducer' @@ -65,6 +66,12 @@ export const migrations = { stats: STATS_INITIAL_STATE, } }, + 38: (state: any) => { + return { + ...state, + referral: REFERRALS_INITIAL_STATE, + } + }, } export default migrations diff --git a/packages/app/src/state/referrals/action.ts b/packages/app/src/state/referrals/action.ts new file mode 100644 index 0000000000..3c6a3deca0 --- /dev/null +++ b/packages/app/src/state/referrals/action.ts @@ -0,0 +1,237 @@ +import { TransactionStatus } from '@kwenta/sdk/types' +import { createAsyncThunk } from '@reduxjs/toolkit' +import { wei } from '@synthetixio/wei' + +import { notifyError } from 'components/ErrorNotifier' +import { REFFERAL_TIERS } from 'sections/referrals/constants' +import { + ReferralTiers, + ReferralsRewardsPerCode, + ReferralsRewardsPerEpoch, +} from 'sections/referrals/types' +import { calculateTotal } from 'sections/referrals/utils' +import { monitorAndAwaitTransaction } from 'state/app/helpers' +import { handleTransactionError, setOpenModal, setTransaction } from 'state/app/reducer' +import { selectTradingRewardsSupportedNetwork } from 'state/staking/selectors' +import { ThunkConfig } from 'state/types' +import { selectWallet } from 'state/wallet/selectors' +import logError from 'utils/logError' + +import { setMintedBoostNft, setStartOnboarding } from './reducer' + +export const mintBoostNft = createAsyncThunk( + 'referrals/mintBoostNft', + async (referralCode, { dispatch, getState, extra: { sdk } }) => { + const wallet = selectWallet(getState()) + const supportedNetwork = selectTradingRewardsSupportedNetwork(getState()) + + try { + if (!wallet) throw new Error('Wallet not connected') + if (!supportedNetwork) + throw new Error( + 'Minting Boost NFT is unsupported on this network. Please switch to Optimism.' + ) + + dispatch( + setTransaction({ + status: TransactionStatus.AwaitingExecution, + type: 'mint_boost_nft', + hash: null, + }) + ) + + const tx = await sdk.referrals.mintBoostNft(referralCode) + await monitorAndAwaitTransaction(dispatch, tx) + dispatch(fetchBoostNftForAccount()) + dispatch(setMintedBoostNft(true)) + dispatch(setStartOnboarding(false)) + } catch (err) { + logError(err) + dispatch(handleTransactionError(err.message)) + throw err + } + } +) + +export const createNewReferralCode = createAsyncThunk( + 'referrals/createReferralCode', + async (referralCode, { dispatch, getState, extra: { sdk } }) => { + const wallet = selectWallet(getState()) + const supportedNetwork = selectTradingRewardsSupportedNetwork(getState()) + + try { + if (!wallet) throw new Error('Wallet not connected') + if (!supportedNetwork) + throw new Error( + 'Creating new code is unsupported on this network. Please switch to Optimism.' + ) + + dispatch( + setTransaction({ + status: TransactionStatus.AwaitingExecution, + type: 'register_referral_code', + hash: null, + }) + ) + const tx = await sdk.referrals.registerReferralCode(referralCode) + await monitorAndAwaitTransaction(dispatch, tx) + dispatch(fetchReferralCodes()) + dispatch(setOpenModal(null)) + } catch (err) { + logError(err) + dispatch(handleTransactionError(err.message)) + throw err + } + } +) + +export const fetchUnmintedBoostNftForCode = createAsyncThunk( + 'referrals/fetchUnmintedBoostNftForCode', + async (code, { getState, extra: { sdk } }) => { + const supportedNetwork = selectTradingRewardsSupportedNetwork(getState()) + + try { + if (!supportedNetwork) + throw new Error('Boost NFT is unsupported on this network. Please switch to Optimism.') + return await sdk.referrals.getTierByReferralCode(code) + } catch (err) { + logError(err) + notifyError('Failed to fetch Boost NFT for referral code.', err) + return ReferralTiers.INVALID + } + } +) + +export const checkSelfReferredByCode = createAsyncThunk( + 'referrals/fetchReferrerByCode', + async (code, { getState, extra: { sdk } }) => { + try { + const wallet = selectWallet(getState()) + const owner = await sdk.referrals.getReferrerByCode(code) + return wallet && owner ? wallet.toLowerCase() === owner.toLowerCase() : false + } catch (err) { + logError(err) + notifyError('Failed to fetch referrer by referral code.', err) + return false + } + } +) + +export const fetchBoostNftForAccount = createAsyncThunk( + 'referrals/fetchTraderNftForAccount', + async (_, { getState, extra: { sdk } }) => { + try { + const wallet = selectWallet(getState()) + if (!wallet) return ReferralTiers.INVALID + return await sdk.referrals.getBoostNftTierByHolder(wallet) + } catch (err) { + logError(err) + notifyError('Failed to fetch Boost NFT for account', err) + return ReferralTiers.INVALID + } + } +) + +export const fetchBoostNftMinted = createAsyncThunk( + 'referrals/fetchBoostNftMinted', + async (_, { getState, extra: { sdk } }) => { + try { + const wallet = selectWallet(getState()) + if (!wallet) return false + return await sdk.referrals.checkNftMintedForAccount(wallet) + } catch (err) { + logError(err) + notifyError('Failed to check Boost NFT for account', err) + return false + } + } +) + +//TODO: Need to calculate by epoch +export const fetchReferralEpoch = createAsyncThunk( + 'referrals/fetchReferralEpoch', + async (_, { getState, extra: { sdk } }) => { + try { + const wallet = selectWallet(getState()) + if (!wallet) return [] + const { epochPeriod: currentEpoch } = await sdk.kwentaToken.getStakingData() + const referralEpoch = await sdk.referrals.getCumulativeStatsByReferrer(wallet) + const referralVolume = calculateTotal(referralEpoch, 'referralVolume') + const referredCount = calculateTotal(referralEpoch, 'referredCount') + return referralEpoch.length > 0 + ? [ + { + epoch: currentEpoch.toString(), + referralVolume: referralVolume.toString(), + referredCount: referredCount.toString(), + earnedRewards: '0', + }, + ] + : [] + } catch (err) { + logError(err) + notifyError('Failed to fetch referral rewards per epoch', err) + return [] + } + } +) + +export const fetchReferralNftForAccount = createAsyncThunk( + 'referrals/fetchReferralNft', + async (_, { getState, extra: { sdk } }) => { + try { + const wallet = selectWallet(getState()) + if (!wallet) return ReferralTiers.BRONZE + const tier = await sdk.referrals.getReferralNftTierByReferrer(wallet) + return tier ?? ReferralTiers.BRONZE + } catch (err) { + logError(err) + notifyError('Failed to fetch Referral NFT for account', err) + return ReferralTiers.BRONZE + } + } +) + +export const fetchReferralScoreForAccount = createAsyncThunk( + 'referrals/fetchReferralScoreForAccount', + async (_, { getState, extra: { sdk } }) => { + try { + const wallet = selectWallet(getState()) + if (!wallet) return 0 + const score = await sdk.referrals.getReferralScoreByReferrer(wallet) + const maxScore = REFFERAL_TIERS[ReferralTiers.GOLD].threshold + return Math.min(maxScore, wei(score).toNumber()) ?? 0 + } catch (err) { + logError(err) + notifyError('Failed to fetch referral score for account', err) + return 0 + } + } +) + +export const fetchReferralCodes = createAsyncThunk( + 'referrals/fetchReferralCodes', + async (_, { getState, extra: { sdk } }) => { + try { + const wallet = selectWallet(getState()) + if (!wallet) return [] + return await sdk.referrals.getCumulativeStatsByReferrer(wallet) + } catch (err) { + logError(err) + notifyError('Failed to fetch cumulative stats by referral codes', err) + return [] + } + } +) + +export const fetchAllReferralData = createAsyncThunk( + 'referrals/fetchAllReferralData', + async (_, { dispatch }) => { + dispatch(fetchBoostNftMinted()) + dispatch(fetchBoostNftForAccount()) + dispatch(fetchReferralScoreForAccount()) + dispatch(fetchReferralNftForAccount()) + dispatch(fetchReferralCodes()) + dispatch(fetchReferralEpoch()) + } +) diff --git a/packages/app/src/state/referrals/reducer.ts b/packages/app/src/state/referrals/reducer.ts new file mode 100644 index 0000000000..5c2dbba56f --- /dev/null +++ b/packages/app/src/state/referrals/reducer.ts @@ -0,0 +1,187 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit' + +import { + ReferralTiers, + ReferralsRewardsPerCode, + ReferralsRewardsPerEpoch, +} from 'sections/referrals/types' +import { DEFAULT_QUERY_STATUS, LOADING_STATUS, SUCCESS_STATUS } from 'state/constants' +import { FetchStatus } from 'state/types' + +import { + checkSelfReferredByCode, + fetchBoostNftForAccount, + fetchBoostNftMinted, + fetchUnmintedBoostNftForCode, + fetchReferralCodes, + fetchReferralEpoch, + fetchReferralNftForAccount, + fetchReferralScoreForAccount, +} from './action' +import { ReferralsState } from './types' + +const ONBOARDING_INITIAL_STATE = { + mintedBoostNft: false, + startOnboarding: false, + isSelfReferred: false, + unmintedBoostNft: ReferralTiers.INVALID, + checkReferrerStatus: DEFAULT_QUERY_STATUS, +} + +const HOLDER_INITIAL_STATE = { + boostNft: ReferralTiers.INVALID, + referralEpoch: [], + fetchBoostNftStatus: DEFAULT_QUERY_STATUS, + fetchReferralEpochStatus: DEFAULT_QUERY_STATUS, +} + +const REFERRER_INITIAL_STATE = { + referralNft: ReferralTiers.BRONZE, + referralScore: 0, + referralCodes: [], + fetchReferralNftStatus: DEFAULT_QUERY_STATUS, + fetchReferralScoreStatus: DEFAULT_QUERY_STATUS, + fetchReferralCodeStatus: DEFAULT_QUERY_STATUS, +} + +export const REFERRALS_INITIAL_STATE: ReferralsState = { + onboarding: ONBOARDING_INITIAL_STATE, + holder: HOLDER_INITIAL_STATE, + referrer: REFERRER_INITIAL_STATE, +} + +const referralsSlice = createSlice({ + name: 'referrals', + initialState: REFERRALS_INITIAL_STATE, + reducers: { + setMintedBoostNft: (state, action: PayloadAction) => { + state.onboarding.mintedBoostNft = action.payload + }, + setStartOnboarding: (state, action: PayloadAction) => { + state.onboarding.startOnboarding = action.payload + }, + }, + extraReducers: (builder) => { + builder.addCase(checkSelfReferredByCode.pending, (state) => { + state.onboarding.checkReferrerStatus = LOADING_STATUS + }) + builder.addCase(checkSelfReferredByCode.fulfilled, (state, action: PayloadAction) => { + state.onboarding.checkReferrerStatus = SUCCESS_STATUS + state.onboarding.isSelfReferred = action.payload + }) + builder.addCase(checkSelfReferredByCode.rejected, (state) => { + state.onboarding.checkReferrerStatus = { + error: 'Failed to check if user is self-referred', + status: FetchStatus.Error, + } + }) + builder.addCase(fetchUnmintedBoostNftForCode.pending, (state) => { + state.holder.fetchBoostNftStatus = LOADING_STATUS + }) + builder.addCase( + fetchUnmintedBoostNftForCode.fulfilled, + (state, action: PayloadAction) => { + state.holder.fetchBoostNftStatus = SUCCESS_STATUS + state.onboarding.unmintedBoostNft = action.payload + } + ) + builder.addCase(fetchUnmintedBoostNftForCode.rejected, (state) => { + state.holder.fetchBoostNftStatus = { + error: 'Failed to fetch Boost NFT for referral code', + status: FetchStatus.Error, + } + }) + builder.addCase(fetchBoostNftForAccount.pending, (state) => { + state.holder.fetchBoostNftStatus = LOADING_STATUS + }) + builder.addCase( + fetchBoostNftForAccount.fulfilled, + (state, action: PayloadAction) => { + state.holder.fetchBoostNftStatus = SUCCESS_STATUS + state.holder.boostNft = action.payload + } + ) + builder.addCase(fetchBoostNftForAccount.rejected, (state) => { + state.holder.fetchBoostNftStatus = { + error: 'Failed to fetch Boost NFT for account', + status: FetchStatus.Error, + } + }) + builder.addCase(fetchBoostNftMinted.fulfilled, (state, action: PayloadAction) => { + state.onboarding.mintedBoostNft = action.payload + }) + builder.addCase(fetchBoostNftMinted.rejected, (state) => { + state.onboarding.mintedBoostNft = false + }) + builder.addCase(fetchBoostNftMinted.pending, (state) => { + state.onboarding.mintedBoostNft = false + }) + builder.addCase( + fetchReferralEpoch.fulfilled, + (state, action: PayloadAction) => { + state.holder.fetchReferralEpochStatus = SUCCESS_STATUS + state.holder.referralEpoch = action.payload + } + ) + builder.addCase(fetchReferralEpoch.pending, (state) => { + state.holder.fetchReferralEpochStatus = LOADING_STATUS + }) + builder.addCase(fetchReferralEpoch.rejected, (state) => { + state.holder.fetchReferralEpochStatus = { + error: 'Failed to fetch referral rewards per epoch', + status: FetchStatus.Error, + } + }) + builder.addCase(fetchReferralNftForAccount.pending, (state) => { + state.referrer.fetchReferralNftStatus = LOADING_STATUS + }) + builder.addCase( + fetchReferralNftForAccount.fulfilled, + (state, action: PayloadAction) => { + state.referrer.fetchReferralNftStatus = SUCCESS_STATUS + state.referrer.referralNft = action.payload + } + ) + builder.addCase(fetchReferralNftForAccount.rejected, (state) => { + state.referrer.fetchReferralNftStatus = { + error: 'Failed to fetch referral NFT for account', + status: FetchStatus.Error, + } + }) + builder.addCase( + fetchReferralScoreForAccount.fulfilled, + (state, action: PayloadAction) => { + state.referrer.fetchReferralScoreStatus = SUCCESS_STATUS + state.referrer.referralScore = action.payload + } + ) + builder.addCase(fetchReferralScoreForAccount.pending, (state) => { + state.referrer.fetchReferralScoreStatus = LOADING_STATUS + }) + builder.addCase(fetchReferralScoreForAccount.rejected, (state) => { + state.referrer.fetchReferralScoreStatus = { + error: 'Failed to fetch referral score for account', + status: FetchStatus.Error, + } + }) + builder.addCase( + fetchReferralCodes.fulfilled, + (state, action: PayloadAction) => { + state.referrer.fetchReferralCodeStatus = SUCCESS_STATUS + state.referrer.referralCodes = action.payload + } + ) + builder.addCase(fetchReferralCodes.pending, (state) => { + state.referrer.fetchReferralCodeStatus = LOADING_STATUS + }) + builder.addCase(fetchReferralCodes.rejected, (state) => { + state.referrer.fetchReferralCodeStatus = { + error: 'Failed to fetch referral codes', + status: FetchStatus.Error, + } + }) + }, +}) + +export default referralsSlice.reducer +export const { setStartOnboarding, setMintedBoostNft } = referralsSlice.actions diff --git a/packages/app/src/state/referrals/selectors.ts b/packages/app/src/state/referrals/selectors.ts new file mode 100644 index 0000000000..b9ebbc429d --- /dev/null +++ b/packages/app/src/state/referrals/selectors.ts @@ -0,0 +1,85 @@ +import { TransactionStatus } from '@kwenta/sdk/types' +import { createSelector } from '@reduxjs/toolkit' + +import { ReferralTiers } from 'sections/referrals/types' +import { calculateTotal } from 'sections/referrals/utils' +import { RootState } from 'state/store' +import { FetchStatus } from 'state/types' +import { selectWallet } from 'state/wallet/selectors' + +export const selectMintedBoostNft = (state: RootState) => state.referrals.onboarding.mintedBoostNft + +export const selectStartOnboarding = (state: RootState) => + state.referrals.onboarding.startOnboarding + +export const selectUnmintedBoostNft = (state: RootState) => + state.referrals.onboarding.unmintedBoostNft ?? ReferralTiers.INVALID + +export const selectCheckSelfReferred = (state: RootState) => + state.referrals.onboarding.isSelfReferred + +export const selectIsReferralCodeValid = createSelector( + selectUnmintedBoostNft, + (unmintedBoostNft) => unmintedBoostNft !== ReferralTiers.INVALID +) + +export const selectBoostNft = (state: RootState) => state.referrals.holder.boostNft + +export const selectIsBoostNftLoaded = (state: RootState): boolean => { + const { status } = state.referrals.holder.fetchBoostNftStatus + return status === FetchStatus.Success || status === FetchStatus.Error +} + +export const selectReferralEpoch = createSelector( + (state: RootState) => state.referrals.holder.referralEpoch, + selectWallet, + (referralStatsByEpoch, wallet) => { + if (!wallet) return [] + return referralStatsByEpoch + } +) + +export const selectReferralNft = (state: RootState) => state.referrals.referrer.referralNft + +export const selectReferralScore = (state: RootState) => state.referrals.referrer.referralScore + +export const selectReferralCodes = createSelector( + (state: RootState) => state.referrals.referrer.referralCodes, + selectWallet, + (referralStatsByCodes, wallet) => { + if (!wallet) return [] + return referralStatsByCodes + } +) + +export const selectSubmittingReferralTx = createSelector( + (state: RootState) => state.app, + (app) => { + return ( + app.transaction?.status === TransactionStatus.AwaitingExecution || + app.transaction?.status === TransactionStatus.Executed + ) + } +) + +export const selectIsMintingBoostNft = createSelector( + selectSubmittingReferralTx, + (state: RootState) => state.app, + (submitting, app) => { + return submitting && app.transaction?.type === 'mint_boost_nft' + } +) + +export const selectIsCreatingReferralCode = createSelector( + selectSubmittingReferralTx, + (state: RootState) => state.app, + (submitting, app) => { + return submitting && app.transaction?.type === 'register_referral_code' + } +) + +export const selectCumulativeStatsByCode = createSelector(selectReferralCodes, (referralCodes) => ({ + totalVolume: calculateTotal(referralCodes, 'referralVolume'), + totalRewards: calculateTotal(referralCodes, 'earnedRewards'), + totalTraders: calculateTotal(referralCodes, 'referredCount'), +})) diff --git a/packages/app/src/state/referrals/types.ts b/packages/app/src/state/referrals/types.ts new file mode 100644 index 0000000000..b378bcb707 --- /dev/null +++ b/packages/app/src/state/referrals/types.ts @@ -0,0 +1,47 @@ +import { TransactionStatus } from '@kwenta/sdk/types' + +import { + ReferralTiers, + ReferralsRewardsPerCode, + ReferralsRewardsPerEpoch, +} from 'sections/referrals/types' +import { QueryStatus } from 'state/types' + +type OnboardingState = { + mintedBoostNft: boolean + startOnboarding: boolean + isSelfReferred: boolean + unmintedBoostNft: ReferralTiers + checkReferrerStatus: QueryStatus +} + +type HolderState = { + boostNft: ReferralTiers + referralEpoch: ReferralsRewardsPerEpoch[] + fetchBoostNftStatus: QueryStatus + fetchReferralEpochStatus: QueryStatus +} + +type ReferrerState = { + referralNft: ReferralTiers + referralScore: number + referralCodes: ReferralsRewardsPerCode[] + fetchReferralNftStatus: QueryStatus + fetchReferralScoreStatus: QueryStatus + fetchReferralCodeStatus: QueryStatus +} + +export type ReferralsState = { + onboarding: OnboardingState + holder: HolderState + referrer: ReferrerState +} + +export type ReferralTransactionType = 'mint_boost_nft' | 'register_referral_code' + +export type ReferralTransaction = { + type: ReferralTransactionType + status: TransactionStatus + error?: string + hash: string | null +} diff --git a/packages/app/src/state/store.ts b/packages/app/src/state/store.ts index e09cecf46c..829e29e0af 100644 --- a/packages/app/src/state/store.ts +++ b/packages/app/src/state/store.ts @@ -26,6 +26,7 @@ import homeReducer from './home/reducer' import migrations from './migrations' import preferencesReducer from './preferences/reducer' import pricesReducer from './prices/reducer' +import referralsReducer from './referrals/reducer' import sdk from './sdk' import stakingReducer from './staking/reducer' import statsReducer from './stats/reducer' @@ -36,7 +37,7 @@ const LOG_REDUX = false const persistConfig = { key: 'root1', storage, - version: 37, + version: 38, blacklist: ['app', 'wallet'], migrate: createMigrate(migrations, { debug: true }), } @@ -55,6 +56,7 @@ const combinedReducers = combineReducers({ preferenes: preferencesReducer, prices: pricesReducer, stats: statsReducer, + referrals: referralsReducer, }) const persistedReducer = persistReducer(persistConfig, combinedReducers) diff --git a/packages/app/src/styles/common.tsx b/packages/app/src/styles/common.tsx index 62e4c146ae..73aadb407f 100644 --- a/packages/app/src/styles/common.tsx +++ b/packages/app/src/styles/common.tsx @@ -103,6 +103,7 @@ export const MobileScreenContainer = styled.div` export const FullHeightContainer = styled(FlexDiv)` justify-content: space-between; width: 100%; + overflow: auto; flex-grow: 1; gap: 10px; ` diff --git a/packages/app/src/styles/theme/colors/common.ts b/packages/app/src/styles/theme/colors/common.ts index 1ed53f2b0c..62b2792eaa 100644 --- a/packages/app/src/styles/theme/colors/common.ts +++ b/packages/app/src/styles/theme/colors/common.ts @@ -79,6 +79,8 @@ export const palette = { white5: 'rgb(255,255,255,0.5)', white10: 'rgb(255,255,255,0.1)', white25: 'rgba(255, 255, 255, 0.25)', + gray60: 'rgba(17, 14, 9, 0.6)', + white60: 'rgba(250, 250, 250, 0.6)', red10: 'rgb(241,43,43,0.1)', red15: 'rgb(241,43,43,0.15)', red5: 'rgb(241,43,43,0.05)', diff --git a/packages/app/src/styles/theme/colors/dark.ts b/packages/app/src/styles/theme/colors/dark.ts index 845e7b131e..6f46683024 100644 --- a/packages/app/src/styles/theme/colors/dark.ts +++ b/packages/app/src/styles/theme/colors/dark.ts @@ -4,6 +4,9 @@ const newTheme = { containers: { primary: { background: common.palette.neutral.n1000, + overlay: { + background: common.palette.alpha.gray60, + }, }, secondary: { background: common.palette.neutral.n900, @@ -91,6 +94,15 @@ const newTheme = { border: common.palette.alpha.white10, }, }, + primary: { + text: common.palette.neutral.n900, + background: common.palette.yellow.y500, + dark: { + background: common.palette.yellow.y1000, + text: common.palette.neutral.n0, + border: common.palette.alpha.white10, + }, + }, gray: { text: common.palette.neutral.n900, background: common.palette.neutral.n50, diff --git a/packages/app/src/styles/theme/colors/light.ts b/packages/app/src/styles/theme/colors/light.ts index 84240a5dfd..be2e410c22 100644 --- a/packages/app/src/styles/theme/colors/light.ts +++ b/packages/app/src/styles/theme/colors/light.ts @@ -4,6 +4,9 @@ const newTheme = { containers: { primary: { background: common.palette.neutral.n10, + overlay: { + background: common.palette.alpha.white60, + }, }, secondary: { background: common.palette.neutral.n0, @@ -154,6 +157,15 @@ const newTheme = { border: common.palette.alpha.white10, }, }, + primary: { + text: common.palette.neutral.n900, + background: common.palette.yellow.y500, + dark: { + background: common.palette.yellow.y1000, + text: common.palette.neutral.n0, + border: common.palette.alpha.white10, + }, + }, gray: { text: common.palette.neutral.n900, background: common.palette.neutral.n50, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 6bd147485c..cd0c8efb15 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -4,8 +4,10 @@ }, "meta": { "description": "Gain exposure to a variety of assets with up to 50x leverage and deep liquidity", + "ref-description": "Receive a 15% boost on trading rewards!", "og": { "title": "Kwenta", + "ref-title": "You are invited to Kwenta", "site-name": "Kwenta" } }, @@ -21,6 +23,7 @@ "vaults": "vaults", "rewards": "rewards" }, + "referrals": "referrals", "isolated-margin": "Legacy Futures", "cross-margin": "Smart Margin", "leaderboard-alltime": "All Time", @@ -199,6 +202,7 @@ "perps": "Perpetual Futures", "spot": "Spot Trading", "legacy": "Legacy Exchange", + "referrals": "Referral Program", "perps-v1": "Legacy Perpetuals" }, "community": { @@ -1167,6 +1171,104 @@ } } }, + "referrals": { + "page-title": "Referrals | Kwenta", + "title": "Referral Program", + "copy": "Earn boosted rewards from trading, or become an affiliate to earn even more.", + "docs": "Docs →", + "table": { + "no-results-table": "No rewards distribution history yet.", + "switch-to-optimism-prompt": "Switch to Optimism to view your rewards distribution history.", + "history": { + "title": "Boosted Rewards History", + "copy": "Track your weekly boosted trading rewards." + }, + "codes": { + "title": "Referral Codes", + "copy": "Earn a portion of the $KWENTA rewards earned by traders using your referral codes", + "create-code-button": "Create Code" + }, + "header": { + "referral-code": "Referral Code", + "total-volume": "Total Volume", + "traders-referred": "Traders Referred", + "kwenta-earned": "$KWENTA Earned", + "epoch": "Epoch" + } + }, + "traders": { + "title": "traders", + "rewards-boost": "Rewards Boost", + "total-volume": "Total Trading Volume", + "kwenta-earned": "Total $KWENTA Earned", + "claim": "Claim", + "level-up": "Level Up", + "dashboard": { + "rewards-boost": "Rewards Boost", + "total-volume": "Total Volume", + "kwenta-earned": "$KWENTA Earned" + } + }, + "affiliates": { + "title": "affiliates", + "dashboard": { + "traders-referred": "Total Traders Referred", + "trading-volume": "Total Trading Volume", + "kwenta-earned": "Total $KWENTA Earned", + "unlocked-nft": "You Have Unlocked" + }, + "nft": { + "bronze": "Bronze", + "silver": "Silver", + "gold": "Gold", + "boost": "Boost", + "preview": "Preview" + }, + "view-all-rates": "View Tier Details →", + "rates": "Rates →", + "modal": { + "create-referral-code": { + "title": "Create Referral Code", + "label": "Referral Code", + "input-placeholder": "Enter Code", + "create-referral-button": "Create Referral" + }, + "nft-preview": { + "tier-1": "Tier 1", + "tier-level": "Tier", + "nft-tier": "NFT Tier", + "view-button": "View on OpenSea" + }, + "referrer": { + "title": "Referrer", + "welcome-message": "You are invited to Kwenta", + "badge-copy": "Receive a <0>{{boost}} boost on trading rewards", + "boost-button": "Mint Boost", + "learn-more": "Learn more about the Kwenta", + "referral-program": " <0>Referral program", + "low-fees": { + "title": "Low fees", + "copy": "Trade with fees as low as 0.015% with up to 50x leverage on a variety assets." + }, + "deep-liquidity": { + "title": "Deep Liquidity", + "copy": "Enjoy deep liquidity powered by the Synthetix protocol." + }, + "trading-rewards": { + "title": "Trading Rewards", + "copy": "Earn $KWENTA rewards on every trade." + }, + "get-started-button": "Get Started", + "no-self-referred": "You cannot refer yourself.", + "congratulations": "Congratulations!", + "mint-message": "You minted a <0>{{tier}} Rewards Boost.", + "earn-boost": "You're earning a <0>{{boost}} boost on trading rewards.", + "stake-more": "Stake <0>$KWENTA to earn even more.", + "trade-now-button": "Trade Now" + } + } + } + }, "not-found": { "page-title": "Not Found | Kwenta", "title": "404", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index feb8de4623..6ae86f2ea2 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@kwenta/sdk", - "version": "1.0.9", + "version": "1.0.10", "description": "SDK for headless interaction with Kwenta", "main": "dist/index.js", "directories": { diff --git a/packages/sdk/src/constants/exchange.ts b/packages/sdk/src/constants/exchange.ts index f9eeeb9960..59df172db2 100644 --- a/packages/sdk/src/constants/exchange.ts +++ b/packages/sdk/src/constants/exchange.ts @@ -2,7 +2,7 @@ import keyBy from 'lodash/keyBy' import { FuturesMarketKey } from '../types/futures' -export const CG_BASE_API_URL = `${process.env.NEXT_PUBLIC_ONE_INCH_COINGECKO_PROXY}/coingecko/api/v3` +export const CG_BASE_API_URL = `${process.env.NEXT_PUBLIC_SERVICES_PROXY}/coingecko/api/v3` export const PROTOCOLS = 'OPTIMISM_UNISWAP_V3,OPTIMISM_SYNTHETIX,OPTIMISM_SYNTHETIX_WRAPPER,OPTIMISM_ONE_INCH_LIMIT_ORDER,OPTIMISM_ONE_INCH_LIMIT_ORDER_V2,OPTIMISM_CURVE,OPTIMISM_BALANCER_V2,OPTIMISM_VELODROME,OPTIMISM_KYBERSWAP_ELASTIC' diff --git a/packages/sdk/src/constants/futures.ts b/packages/sdk/src/constants/futures.ts index 49e2799d02..d2da1efd7f 100644 --- a/packages/sdk/src/constants/futures.ts +++ b/packages/sdk/src/constants/futures.ts @@ -648,7 +648,7 @@ export const MARKETS: Record = { [FuturesMarketKey.sUMAPERP]: { key: FuturesMarketKey.sUMAPERP, asset: FuturesMarketAsset.UMA, - supports: 'testnet', + supports: 'both', version: 2, pythIds: { mainnet: '0x4b78d251770732f6304b1f41e9bebaabc3b256985ef18988f6de8d6562dd254c', @@ -728,7 +728,7 @@ export const MARKETS: Record = { [FuturesMarketKey.sZRXPERP]: { key: FuturesMarketKey.sZRXPERP, asset: FuturesMarketAsset.ZRX, - supports: 'testnet', + supports: 'both', version: 2, pythIds: { mainnet: '0x7d17b9fe4ea7103be16b6836984fabbc889386d700ca5e5b3d34b7f92e449268', diff --git a/packages/sdk/src/constants/referrals.ts b/packages/sdk/src/constants/referrals.ts new file mode 100644 index 0000000000..22d7cf8bf6 --- /dev/null +++ b/packages/sdk/src/constants/referrals.ts @@ -0,0 +1,6 @@ +export const REFERRALS_ENDPOINT_OP_MAINNET = + 'https://subgraph.satsuma-prod.com/05943208e921/kwenta/referrals/api' + +export const REFERRALS_ENDPOINTS: Record = { + 10: REFERRALS_ENDPOINT_OP_MAINNET, +} diff --git a/packages/sdk/src/contracts/abis/BoostNFT.json b/packages/sdk/src/contracts/abis/BoostNFT.json new file mode 100644 index 0000000000..a8128fe43d --- /dev/null +++ b/packages/sdk/src/contracts/abis/BoostNFT.json @@ -0,0 +1,750 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "_uri", + "type": "string" + }, + { + "internalType": "address", + "name": "_stakingRewardsAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_affiliateAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AlreadyMinted", + "type": "error" + }, + { + "inputs": [], + "name": "CannotUseOwnCode", + "type": "error" + }, + { + "inputs": [], + "name": "CodeAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCode", + "type": "error" + }, + { + "inputs": [], + "name": "NotTransferrable", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "affiliateAddress", + "type": "address" + } + ], + "name": "AffiliateSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "issuer", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "code", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tier", + "type": "uint256" + } + ], + "name": "BoostMinted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "referrer", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "code", + "type": "bytes32" + } + ], + "name": "CodeRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "stakingRewardsAddress", + "type": "address" + } + ], + "name": "StakingRewardsSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [], + "name": "BRONZE_ID", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GOLD_ID", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GOLD_SCORE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SILVER_ID", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SILVER_SCORE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "affiliateAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "codeOwners", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_referrer", + "type": "address" + } + ], + "name": "getReferralScore", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_score", + "type": "uint256" + } + ], + "name": "getTierFromReferralScore", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "hasMinted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_code", + "type": "bytes32" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_code", + "type": "bytes32" + } + ], + "name": "registerCode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newAddress", + "type": "address" + } + ], + "name": "setAffiliateAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newAddress", + "type": "address" + } + ], + "name": "setStakingRewardsAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakingRewardsAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/sdk/src/contracts/constants.ts b/packages/sdk/src/contracts/constants.ts index 9aa2d125d6..f506fb4eb3 100644 --- a/packages/sdk/src/contracts/constants.ts +++ b/packages/sdk/src/contracts/constants.ts @@ -156,4 +156,7 @@ export const ADDRESSES: Record> = { PerpsV3AccountProxy: { 420: '0x01C2f64ABd46AF20950736f3C3e1a9cfc5c36c82', }, + BoostNft: { + 10: '0xD3B8876073949D790AB718CAD21d9326a3adA60f', + }, } diff --git a/packages/sdk/src/contracts/index.ts b/packages/sdk/src/contracts/index.ts index ab0e53794f..d607a1300e 100644 --- a/packages/sdk/src/contracts/index.ts +++ b/packages/sdk/src/contracts/index.ts @@ -26,6 +26,7 @@ import SynthRedeemerABI from './abis/SynthRedeemer.json' import SynthUtilABI from './abis/SynthUtil.json' import SystemStatusABI from './abis/SystemStatus.json' import PerpsV3AccountProxyABI from './abis/PerpsV3AccountProxy.json' +import BoostNftABI from './abis/BoostNFT.json' import { ADDRESSES } from './constants' import { SmartMarginAccountFactory__factory, @@ -56,6 +57,7 @@ import { RewardEscrowV2__factory, KwentaStakingRewardsV2__factory, PerpsV3AccountProxy__factory, + BoostNFT__factory, } from './types' import { PerpsV2MarketData__factory } from './types/factories/PerpsV2MarketData__factory' import { PerpsV2MarketSettings__factory } from './types/factories/PerpsV2MarketSettings__factory' @@ -192,6 +194,9 @@ export const getContractsByNetwork = ( perpsV3AccountProxy: ADDRESSES.PerpsV3AccountProxy[networkId] ? PerpsV3AccountProxy__factory.connect(ADDRESSES.PerpsV3AccountProxy[networkId], provider) : undefined, + BoostNft: ADDRESSES.BoostNft[networkId] + ? BoostNFT__factory.connect(ADDRESSES.BoostNft[networkId], provider) + : undefined, } } @@ -278,6 +283,9 @@ export const getMulticallContractsByNetwork = (networkId: NetworkId) => { perpsV3AccountProxy: ADDRESSES.PerpsV3AccountProxy[networkId] ? new EthCallContract(ADDRESSES.PerpsV3AccountProxy[networkId], PerpsV3AccountProxyABI) : undefined, + BoostNft: ADDRESSES.BoostNft[networkId] + ? new EthCallContract(ADDRESSES.BoostNft[networkId], BoostNftABI) + : undefined, } } diff --git a/packages/sdk/src/contracts/types/BoostNFT.ts b/packages/sdk/src/contracts/types/BoostNFT.ts new file mode 100644 index 0000000000..4365f37299 --- /dev/null +++ b/packages/sdk/src/contracts/types/BoostNFT.ts @@ -0,0 +1,1126 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { + FunctionFragment, + Result, + EventFragment, +} from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from "./common"; + +export interface BoostNFTInterface extends utils.Interface { + functions: { + "BRONZE_ID()": FunctionFragment; + "GOLD_ID()": FunctionFragment; + "GOLD_SCORE()": FunctionFragment; + "SILVER_ID()": FunctionFragment; + "SILVER_SCORE()": FunctionFragment; + "acceptOwnership()": FunctionFragment; + "affiliateAddress()": FunctionFragment; + "balanceOf(address,uint256)": FunctionFragment; + "balanceOfBatch(address[],uint256[])": FunctionFragment; + "codeOwners(address,bytes32)": FunctionFragment; + "getReferralScore(address)": FunctionFragment; + "getTierFromReferralScore(uint256)": FunctionFragment; + "hasMinted(address)": FunctionFragment; + "isApprovedForAll(address,address)": FunctionFragment; + "mint(bytes32)": FunctionFragment; + "owner()": FunctionFragment; + "pendingOwner()": FunctionFragment; + "registerCode(bytes32)": FunctionFragment; + "renounceOwnership()": FunctionFragment; + "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)": FunctionFragment; + "safeTransferFrom(address,address,uint256,uint256,bytes)": FunctionFragment; + "setAffiliateAddress(address)": FunctionFragment; + "setApprovalForAll(address,bool)": FunctionFragment; + "setStakingRewardsAddress(address)": FunctionFragment; + "stakingRewardsAddress()": FunctionFragment; + "supportsInterface(bytes4)": FunctionFragment; + "transferOwnership(address)": FunctionFragment; + "uri(uint256)": FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | "BRONZE_ID" + | "GOLD_ID" + | "GOLD_SCORE" + | "SILVER_ID" + | "SILVER_SCORE" + | "acceptOwnership" + | "affiliateAddress" + | "balanceOf" + | "balanceOfBatch" + | "codeOwners" + | "getReferralScore" + | "getTierFromReferralScore" + | "hasMinted" + | "isApprovedForAll" + | "mint" + | "owner" + | "pendingOwner" + | "registerCode" + | "renounceOwnership" + | "safeBatchTransferFrom" + | "safeTransferFrom" + | "setAffiliateAddress" + | "setApprovalForAll" + | "setStakingRewardsAddress" + | "stakingRewardsAddress" + | "supportsInterface" + | "transferOwnership" + | "uri" + ): FunctionFragment; + + encodeFunctionData(functionFragment: "BRONZE_ID", values?: undefined): string; + encodeFunctionData(functionFragment: "GOLD_ID", values?: undefined): string; + encodeFunctionData( + functionFragment: "GOLD_SCORE", + values?: undefined + ): string; + encodeFunctionData(functionFragment: "SILVER_ID", values?: undefined): string; + encodeFunctionData( + functionFragment: "SILVER_SCORE", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "acceptOwnership", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "affiliateAddress", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "balanceOf", + values: [string, BigNumberish] + ): string; + encodeFunctionData( + functionFragment: "balanceOfBatch", + values: [string[], BigNumberish[]] + ): string; + encodeFunctionData( + functionFragment: "codeOwners", + values: [string, BytesLike] + ): string; + encodeFunctionData( + functionFragment: "getReferralScore", + values: [string] + ): string; + encodeFunctionData( + functionFragment: "getTierFromReferralScore", + values: [BigNumberish] + ): string; + encodeFunctionData(functionFragment: "hasMinted", values: [string]): string; + encodeFunctionData( + functionFragment: "isApprovedForAll", + values: [string, string] + ): string; + encodeFunctionData(functionFragment: "mint", values: [BytesLike]): string; + encodeFunctionData(functionFragment: "owner", values?: undefined): string; + encodeFunctionData( + functionFragment: "pendingOwner", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "registerCode", + values: [BytesLike] + ): string; + encodeFunctionData( + functionFragment: "renounceOwnership", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "safeBatchTransferFrom", + values: [string, string, BigNumberish[], BigNumberish[], BytesLike] + ): string; + encodeFunctionData( + functionFragment: "safeTransferFrom", + values: [string, string, BigNumberish, BigNumberish, BytesLike] + ): string; + encodeFunctionData( + functionFragment: "setAffiliateAddress", + values: [string] + ): string; + encodeFunctionData( + functionFragment: "setApprovalForAll", + values: [string, boolean] + ): string; + encodeFunctionData( + functionFragment: "setStakingRewardsAddress", + values: [string] + ): string; + encodeFunctionData( + functionFragment: "stakingRewardsAddress", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "supportsInterface", + values: [BytesLike] + ): string; + encodeFunctionData( + functionFragment: "transferOwnership", + values: [string] + ): string; + encodeFunctionData(functionFragment: "uri", values: [BigNumberish]): string; + + decodeFunctionResult(functionFragment: "BRONZE_ID", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "GOLD_ID", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "GOLD_SCORE", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "SILVER_ID", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "SILVER_SCORE", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "acceptOwnership", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "affiliateAddress", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "balanceOf", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "balanceOfBatch", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "codeOwners", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "getReferralScore", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "getTierFromReferralScore", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "hasMinted", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "isApprovedForAll", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "mint", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "owner", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "pendingOwner", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "registerCode", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "renounceOwnership", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "safeBatchTransferFrom", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "safeTransferFrom", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "setAffiliateAddress", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "setApprovalForAll", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "setStakingRewardsAddress", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "stakingRewardsAddress", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "supportsInterface", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "transferOwnership", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "uri", data: BytesLike): Result; + + events: { + "AffiliateSet(address)": EventFragment; + "ApprovalForAll(address,address,bool)": EventFragment; + "BoostMinted(address,bytes32,uint256)": EventFragment; + "CodeRegistered(address,bytes32)": EventFragment; + "OwnershipTransferStarted(address,address)": EventFragment; + "OwnershipTransferred(address,address)": EventFragment; + "StakingRewardsSet(address)": EventFragment; + "TransferBatch(address,address,address,uint256[],uint256[])": EventFragment; + "TransferSingle(address,address,address,uint256,uint256)": EventFragment; + "URI(string,uint256)": EventFragment; + }; + + getEvent(nameOrSignatureOrTopic: "AffiliateSet"): EventFragment; + getEvent(nameOrSignatureOrTopic: "ApprovalForAll"): EventFragment; + getEvent(nameOrSignatureOrTopic: "BoostMinted"): EventFragment; + getEvent(nameOrSignatureOrTopic: "CodeRegistered"): EventFragment; + getEvent(nameOrSignatureOrTopic: "OwnershipTransferStarted"): EventFragment; + getEvent(nameOrSignatureOrTopic: "OwnershipTransferred"): EventFragment; + getEvent(nameOrSignatureOrTopic: "StakingRewardsSet"): EventFragment; + getEvent(nameOrSignatureOrTopic: "TransferBatch"): EventFragment; + getEvent(nameOrSignatureOrTopic: "TransferSingle"): EventFragment; + getEvent(nameOrSignatureOrTopic: "URI"): EventFragment; +} + +export interface AffiliateSetEventObject { + affiliateAddress: string; +} +export type AffiliateSetEvent = TypedEvent<[string], AffiliateSetEventObject>; + +export type AffiliateSetEventFilter = TypedEventFilter; + +export interface ApprovalForAllEventObject { + account: string; + operator: string; + approved: boolean; +} +export type ApprovalForAllEvent = TypedEvent< + [string, string, boolean], + ApprovalForAllEventObject +>; + +export type ApprovalForAllEventFilter = TypedEventFilter; + +export interface BoostMintedEventObject { + issuer: string; + code: string; + tier: BigNumber; +} +export type BoostMintedEvent = TypedEvent< + [string, string, BigNumber], + BoostMintedEventObject +>; + +export type BoostMintedEventFilter = TypedEventFilter; + +export interface CodeRegisteredEventObject { + referrer: string; + code: string; +} +export type CodeRegisteredEvent = TypedEvent< + [string, string], + CodeRegisteredEventObject +>; + +export type CodeRegisteredEventFilter = TypedEventFilter; + +export interface OwnershipTransferStartedEventObject { + previousOwner: string; + newOwner: string; +} +export type OwnershipTransferStartedEvent = TypedEvent< + [string, string], + OwnershipTransferStartedEventObject +>; + +export type OwnershipTransferStartedEventFilter = + TypedEventFilter; + +export interface OwnershipTransferredEventObject { + previousOwner: string; + newOwner: string; +} +export type OwnershipTransferredEvent = TypedEvent< + [string, string], + OwnershipTransferredEventObject +>; + +export type OwnershipTransferredEventFilter = + TypedEventFilter; + +export interface StakingRewardsSetEventObject { + stakingRewardsAddress: string; +} +export type StakingRewardsSetEvent = TypedEvent< + [string], + StakingRewardsSetEventObject +>; + +export type StakingRewardsSetEventFilter = + TypedEventFilter; + +export interface TransferBatchEventObject { + operator: string; + from: string; + to: string; + ids: BigNumber[]; + values: BigNumber[]; +} +export type TransferBatchEvent = TypedEvent< + [string, string, string, BigNumber[], BigNumber[]], + TransferBatchEventObject +>; + +export type TransferBatchEventFilter = TypedEventFilter; + +export interface TransferSingleEventObject { + operator: string; + from: string; + to: string; + id: BigNumber; + value: BigNumber; +} +export type TransferSingleEvent = TypedEvent< + [string, string, string, BigNumber, BigNumber], + TransferSingleEventObject +>; + +export type TransferSingleEventFilter = TypedEventFilter; + +export interface URIEventObject { + value: string; + id: BigNumber; +} +export type URIEvent = TypedEvent<[string, BigNumber], URIEventObject>; + +export type URIEventFilter = TypedEventFilter; + +export interface BoostNFT extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: BoostNFTInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + BRONZE_ID(overrides?: CallOverrides): Promise<[BigNumber]>; + + GOLD_ID(overrides?: CallOverrides): Promise<[BigNumber]>; + + GOLD_SCORE(overrides?: CallOverrides): Promise<[BigNumber]>; + + SILVER_ID(overrides?: CallOverrides): Promise<[BigNumber]>; + + SILVER_SCORE(overrides?: CallOverrides): Promise<[BigNumber]>; + + acceptOwnership( + overrides?: Overrides & { from?: string } + ): Promise; + + affiliateAddress(overrides?: CallOverrides): Promise<[string]>; + + balanceOf( + account: string, + id: BigNumberish, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + + balanceOfBatch( + accounts: string[], + ids: BigNumberish[], + overrides?: CallOverrides + ): Promise<[BigNumber[]]>; + + codeOwners( + arg0: string, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise<[boolean]>; + + getReferralScore( + _referrer: string, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + + getTierFromReferralScore( + _score: BigNumberish, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + + hasMinted(arg0: string, overrides?: CallOverrides): Promise<[boolean]>; + + isApprovedForAll( + account: string, + operator: string, + overrides?: CallOverrides + ): Promise<[boolean]>; + + mint( + _code: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + owner(overrides?: CallOverrides): Promise<[string]>; + + pendingOwner(overrides?: CallOverrides): Promise<[string]>; + + registerCode( + _code: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + renounceOwnership( + overrides?: Overrides & { from?: string } + ): Promise; + + safeBatchTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish[], + arg3: BigNumberish[], + arg4: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + safeTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish, + arg3: BigNumberish, + arg4: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + setAffiliateAddress( + _newAddress: string, + overrides?: Overrides & { from?: string } + ): Promise; + + setApprovalForAll( + operator: string, + approved: boolean, + overrides?: Overrides & { from?: string } + ): Promise; + + setStakingRewardsAddress( + _newAddress: string, + overrides?: Overrides & { from?: string } + ): Promise; + + stakingRewardsAddress(overrides?: CallOverrides): Promise<[string]>; + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise<[boolean]>; + + transferOwnership( + newOwner: string, + overrides?: Overrides & { from?: string } + ): Promise; + + uri(arg0: BigNumberish, overrides?: CallOverrides): Promise<[string]>; + }; + + BRONZE_ID(overrides?: CallOverrides): Promise; + + GOLD_ID(overrides?: CallOverrides): Promise; + + GOLD_SCORE(overrides?: CallOverrides): Promise; + + SILVER_ID(overrides?: CallOverrides): Promise; + + SILVER_SCORE(overrides?: CallOverrides): Promise; + + acceptOwnership( + overrides?: Overrides & { from?: string } + ): Promise; + + affiliateAddress(overrides?: CallOverrides): Promise; + + balanceOf( + account: string, + id: BigNumberish, + overrides?: CallOverrides + ): Promise; + + balanceOfBatch( + accounts: string[], + ids: BigNumberish[], + overrides?: CallOverrides + ): Promise; + + codeOwners( + arg0: string, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise; + + getReferralScore( + _referrer: string, + overrides?: CallOverrides + ): Promise; + + getTierFromReferralScore( + _score: BigNumberish, + overrides?: CallOverrides + ): Promise; + + hasMinted(arg0: string, overrides?: CallOverrides): Promise; + + isApprovedForAll( + account: string, + operator: string, + overrides?: CallOverrides + ): Promise; + + mint( + _code: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + pendingOwner(overrides?: CallOverrides): Promise; + + registerCode( + _code: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + renounceOwnership( + overrides?: Overrides & { from?: string } + ): Promise; + + safeBatchTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish[], + arg3: BigNumberish[], + arg4: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + safeTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish, + arg3: BigNumberish, + arg4: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + setAffiliateAddress( + _newAddress: string, + overrides?: Overrides & { from?: string } + ): Promise; + + setApprovalForAll( + operator: string, + approved: boolean, + overrides?: Overrides & { from?: string } + ): Promise; + + setStakingRewardsAddress( + _newAddress: string, + overrides?: Overrides & { from?: string } + ): Promise; + + stakingRewardsAddress(overrides?: CallOverrides): Promise; + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: Overrides & { from?: string } + ): Promise; + + uri(arg0: BigNumberish, overrides?: CallOverrides): Promise; + + callStatic: { + BRONZE_ID(overrides?: CallOverrides): Promise; + + GOLD_ID(overrides?: CallOverrides): Promise; + + GOLD_SCORE(overrides?: CallOverrides): Promise; + + SILVER_ID(overrides?: CallOverrides): Promise; + + SILVER_SCORE(overrides?: CallOverrides): Promise; + + acceptOwnership(overrides?: CallOverrides): Promise; + + affiliateAddress(overrides?: CallOverrides): Promise; + + balanceOf( + account: string, + id: BigNumberish, + overrides?: CallOverrides + ): Promise; + + balanceOfBatch( + accounts: string[], + ids: BigNumberish[], + overrides?: CallOverrides + ): Promise; + + codeOwners( + arg0: string, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise; + + getReferralScore( + _referrer: string, + overrides?: CallOverrides + ): Promise; + + getTierFromReferralScore( + _score: BigNumberish, + overrides?: CallOverrides + ): Promise; + + hasMinted(arg0: string, overrides?: CallOverrides): Promise; + + isApprovedForAll( + account: string, + operator: string, + overrides?: CallOverrides + ): Promise; + + mint(_code: BytesLike, overrides?: CallOverrides): Promise; + + owner(overrides?: CallOverrides): Promise; + + pendingOwner(overrides?: CallOverrides): Promise; + + registerCode(_code: BytesLike, overrides?: CallOverrides): Promise; + + renounceOwnership(overrides?: CallOverrides): Promise; + + safeBatchTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish[], + arg3: BigNumberish[], + arg4: BytesLike, + overrides?: CallOverrides + ): Promise; + + safeTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish, + arg3: BigNumberish, + arg4: BytesLike, + overrides?: CallOverrides + ): Promise; + + setAffiliateAddress( + _newAddress: string, + overrides?: CallOverrides + ): Promise; + + setApprovalForAll( + operator: string, + approved: boolean, + overrides?: CallOverrides + ): Promise; + + setStakingRewardsAddress( + _newAddress: string, + overrides?: CallOverrides + ): Promise; + + stakingRewardsAddress(overrides?: CallOverrides): Promise; + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: CallOverrides + ): Promise; + + uri(arg0: BigNumberish, overrides?: CallOverrides): Promise; + }; + + filters: { + "AffiliateSet(address)"( + affiliateAddress?: string | null + ): AffiliateSetEventFilter; + AffiliateSet(affiliateAddress?: string | null): AffiliateSetEventFilter; + + "ApprovalForAll(address,address,bool)"( + account?: string | null, + operator?: string | null, + approved?: null + ): ApprovalForAllEventFilter; + ApprovalForAll( + account?: string | null, + operator?: string | null, + approved?: null + ): ApprovalForAllEventFilter; + + "BoostMinted(address,bytes32,uint256)"( + issuer?: string | null, + code?: BytesLike | null, + tier?: null + ): BoostMintedEventFilter; + BoostMinted( + issuer?: string | null, + code?: BytesLike | null, + tier?: null + ): BoostMintedEventFilter; + + "CodeRegistered(address,bytes32)"( + referrer?: string | null, + code?: BytesLike | null + ): CodeRegisteredEventFilter; + CodeRegistered( + referrer?: string | null, + code?: BytesLike | null + ): CodeRegisteredEventFilter; + + "OwnershipTransferStarted(address,address)"( + previousOwner?: string | null, + newOwner?: string | null + ): OwnershipTransferStartedEventFilter; + OwnershipTransferStarted( + previousOwner?: string | null, + newOwner?: string | null + ): OwnershipTransferStartedEventFilter; + + "OwnershipTransferred(address,address)"( + previousOwner?: string | null, + newOwner?: string | null + ): OwnershipTransferredEventFilter; + OwnershipTransferred( + previousOwner?: string | null, + newOwner?: string | null + ): OwnershipTransferredEventFilter; + + "StakingRewardsSet(address)"( + stakingRewardsAddress?: string | null + ): StakingRewardsSetEventFilter; + StakingRewardsSet( + stakingRewardsAddress?: string | null + ): StakingRewardsSetEventFilter; + + "TransferBatch(address,address,address,uint256[],uint256[])"( + operator?: string | null, + from?: string | null, + to?: string | null, + ids?: null, + values?: null + ): TransferBatchEventFilter; + TransferBatch( + operator?: string | null, + from?: string | null, + to?: string | null, + ids?: null, + values?: null + ): TransferBatchEventFilter; + + "TransferSingle(address,address,address,uint256,uint256)"( + operator?: string | null, + from?: string | null, + to?: string | null, + id?: null, + value?: null + ): TransferSingleEventFilter; + TransferSingle( + operator?: string | null, + from?: string | null, + to?: string | null, + id?: null, + value?: null + ): TransferSingleEventFilter; + + "URI(string,uint256)"( + value?: null, + id?: BigNumberish | null + ): URIEventFilter; + URI(value?: null, id?: BigNumberish | null): URIEventFilter; + }; + + estimateGas: { + BRONZE_ID(overrides?: CallOverrides): Promise; + + GOLD_ID(overrides?: CallOverrides): Promise; + + GOLD_SCORE(overrides?: CallOverrides): Promise; + + SILVER_ID(overrides?: CallOverrides): Promise; + + SILVER_SCORE(overrides?: CallOverrides): Promise; + + acceptOwnership( + overrides?: Overrides & { from?: string } + ): Promise; + + affiliateAddress(overrides?: CallOverrides): Promise; + + balanceOf( + account: string, + id: BigNumberish, + overrides?: CallOverrides + ): Promise; + + balanceOfBatch( + accounts: string[], + ids: BigNumberish[], + overrides?: CallOverrides + ): Promise; + + codeOwners( + arg0: string, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise; + + getReferralScore( + _referrer: string, + overrides?: CallOverrides + ): Promise; + + getTierFromReferralScore( + _score: BigNumberish, + overrides?: CallOverrides + ): Promise; + + hasMinted(arg0: string, overrides?: CallOverrides): Promise; + + isApprovedForAll( + account: string, + operator: string, + overrides?: CallOverrides + ): Promise; + + mint( + _code: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + pendingOwner(overrides?: CallOverrides): Promise; + + registerCode( + _code: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + renounceOwnership( + overrides?: Overrides & { from?: string } + ): Promise; + + safeBatchTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish[], + arg3: BigNumberish[], + arg4: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + safeTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish, + arg3: BigNumberish, + arg4: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + setAffiliateAddress( + _newAddress: string, + overrides?: Overrides & { from?: string } + ): Promise; + + setApprovalForAll( + operator: string, + approved: boolean, + overrides?: Overrides & { from?: string } + ): Promise; + + setStakingRewardsAddress( + _newAddress: string, + overrides?: Overrides & { from?: string } + ): Promise; + + stakingRewardsAddress(overrides?: CallOverrides): Promise; + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: Overrides & { from?: string } + ): Promise; + + uri(arg0: BigNumberish, overrides?: CallOverrides): Promise; + }; + + populateTransaction: { + BRONZE_ID(overrides?: CallOverrides): Promise; + + GOLD_ID(overrides?: CallOverrides): Promise; + + GOLD_SCORE(overrides?: CallOverrides): Promise; + + SILVER_ID(overrides?: CallOverrides): Promise; + + SILVER_SCORE(overrides?: CallOverrides): Promise; + + acceptOwnership( + overrides?: Overrides & { from?: string } + ): Promise; + + affiliateAddress(overrides?: CallOverrides): Promise; + + balanceOf( + account: string, + id: BigNumberish, + overrides?: CallOverrides + ): Promise; + + balanceOfBatch( + accounts: string[], + ids: BigNumberish[], + overrides?: CallOverrides + ): Promise; + + codeOwners( + arg0: string, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise; + + getReferralScore( + _referrer: string, + overrides?: CallOverrides + ): Promise; + + getTierFromReferralScore( + _score: BigNumberish, + overrides?: CallOverrides + ): Promise; + + hasMinted( + arg0: string, + overrides?: CallOverrides + ): Promise; + + isApprovedForAll( + account: string, + operator: string, + overrides?: CallOverrides + ): Promise; + + mint( + _code: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + pendingOwner(overrides?: CallOverrides): Promise; + + registerCode( + _code: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + renounceOwnership( + overrides?: Overrides & { from?: string } + ): Promise; + + safeBatchTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish[], + arg3: BigNumberish[], + arg4: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + safeTransferFrom( + arg0: string, + arg1: string, + arg2: BigNumberish, + arg3: BigNumberish, + arg4: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + setAffiliateAddress( + _newAddress: string, + overrides?: Overrides & { from?: string } + ): Promise; + + setApprovalForAll( + operator: string, + approved: boolean, + overrides?: Overrides & { from?: string } + ): Promise; + + setStakingRewardsAddress( + _newAddress: string, + overrides?: Overrides & { from?: string } + ): Promise; + + stakingRewardsAddress( + overrides?: CallOverrides + ): Promise; + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise; + + transferOwnership( + newOwner: string, + overrides?: Overrides & { from?: string } + ): Promise; + + uri( + arg0: BigNumberish, + overrides?: CallOverrides + ): Promise; + }; +} diff --git a/packages/sdk/src/contracts/types/factories/BoostNFT__factory.ts b/packages/sdk/src/contracts/types/factories/BoostNFT__factory.ts new file mode 100644 index 0000000000..43eea8844a --- /dev/null +++ b/packages/sdk/src/contracts/types/factories/BoostNFT__factory.ts @@ -0,0 +1,771 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from "ethers"; +import type { Provider } from "@ethersproject/providers"; +import type { BoostNFT, BoostNFTInterface } from "../BoostNFT"; + +const _abi = [ + { + inputs: [ + { + internalType: "string", + name: "_uri", + type: "string", + }, + { + internalType: "address", + name: "_stakingRewardsAddress", + type: "address", + }, + { + internalType: "address", + name: "_affiliateAddress", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "AlreadyMinted", + type: "error", + }, + { + inputs: [], + name: "CannotUseOwnCode", + type: "error", + }, + { + inputs: [], + name: "CodeAlreadyExists", + type: "error", + }, + { + inputs: [], + name: "InvalidCode", + type: "error", + }, + { + inputs: [], + name: "NotTransferrable", + type: "error", + }, + { + inputs: [], + name: "ZeroAddress", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "affiliateAddress", + type: "address", + }, + ], + name: "AffiliateSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: false, + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "ApprovalForAll", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "issuer", + type: "address", + }, + { + indexed: true, + internalType: "bytes32", + name: "code", + type: "bytes32", + }, + { + indexed: false, + internalType: "uint256", + name: "tier", + type: "uint256", + }, + ], + name: "BoostMinted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "referrer", + type: "address", + }, + { + indexed: true, + internalType: "bytes32", + name: "code", + type: "bytes32", + }, + ], + name: "CodeRegistered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferStarted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "stakingRewardsAddress", + type: "address", + }, + ], + name: "StakingRewardsSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: false, + internalType: "uint256[]", + name: "ids", + type: "uint256[]", + }, + { + indexed: false, + internalType: "uint256[]", + name: "values", + type: "uint256[]", + }, + ], + name: "TransferBatch", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "id", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "TransferSingle", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "string", + name: "value", + type: "string", + }, + { + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", + }, + ], + name: "URI", + type: "event", + }, + { + inputs: [], + name: "BRONZE_ID", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "GOLD_ID", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "GOLD_SCORE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SILVER_ID", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SILVER_SCORE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "acceptOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "affiliateAddress", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "uint256", + name: "id", + type: "uint256", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "accounts", + type: "address[]", + }, + { + internalType: "uint256[]", + name: "ids", + type: "uint256[]", + }, + ], + name: "balanceOfBatch", + outputs: [ + { + internalType: "uint256[]", + name: "", + type: "uint256[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + name: "codeOwners", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_referrer", + type: "address", + }, + ], + name: "getReferralScore", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_score", + type: "uint256", + }, + ], + name: "getTierFromReferralScore", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "hasMinted", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "address", + name: "operator", + type: "address", + }, + ], + name: "isApprovedForAll", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "_code", + type: "bytes32", + }, + ], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pendingOwner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "_code", + type: "bytes32", + }, + ], + name: "registerCode", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "uint256[]", + name: "", + type: "uint256[]", + }, + { + internalType: "uint256[]", + name: "", + type: "uint256[]", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + name: "safeBatchTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "uint256", + name: "", + type: "uint256", + }, + { + internalType: "uint256", + name: "", + type: "uint256", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_newAddress", + type: "address", + }, + ], + name: "setAffiliateAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "operator", + type: "address", + }, + { + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_newAddress", + type: "address", + }, + ], + name: "setStakingRewardsAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "stakingRewardsAddress", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "uri", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +export class BoostNFT__factory { + static readonly abi = _abi; + static createInterface(): BoostNFTInterface { + return new utils.Interface(_abi) as BoostNFTInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): BoostNFT { + return new Contract(address, _abi, signerOrProvider) as BoostNFT; + } +} diff --git a/packages/sdk/src/contracts/types/factories/index.ts b/packages/sdk/src/contracts/types/factories/index.ts index fe646ad3ca..f349e5e737 100644 --- a/packages/sdk/src/contracts/types/factories/index.ts +++ b/packages/sdk/src/contracts/types/factories/index.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ export { BatchClaimer__factory } from "./BatchClaimer__factory"; +export { BoostNFT__factory } from "./BoostNFT__factory"; export { DappMaintenance__factory } from "./DappMaintenance__factory"; export { ERC20__factory } from "./ERC20__factory"; export { ExchangeRates__factory } from "./ExchangeRates__factory"; diff --git a/packages/sdk/src/contracts/types/index.ts b/packages/sdk/src/contracts/types/index.ts index ef9023e926..7c80292c94 100644 --- a/packages/sdk/src/contracts/types/index.ts +++ b/packages/sdk/src/contracts/types/index.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ export type { BatchClaimer } from "./BatchClaimer"; +export type { BoostNFT } from "./BoostNFT"; export type { DappMaintenance } from "./DappMaintenance"; export type { ERC20 } from "./ERC20"; export type { ExchangeRates } from "./ExchangeRates"; @@ -40,6 +41,7 @@ export type { VKwentaRedeemer } from "./VKwentaRedeemer"; export type { VeKwentaRedeemer } from "./VeKwentaRedeemer"; export * as factories from "./factories"; export { BatchClaimer__factory } from "./factories/BatchClaimer__factory"; +export { BoostNFT__factory } from "./factories/BoostNFT__factory"; export { DappMaintenance__factory } from "./factories/DappMaintenance__factory"; export { ERC20__factory } from "./factories/ERC20__factory"; export { Exchanger__factory } from "./factories/Exchanger__factory"; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index e39c83cf3c..4f9bad765e 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -10,6 +10,7 @@ import SynthsService from './services/synths' import SystemService from './services/system' import TransactionsService from './services/transactions' import StatsService from './services/stats' +import ReferralsService from './services/referrals' export default class KwentaSDK { public context: Context @@ -22,6 +23,7 @@ export default class KwentaSDK { public kwentaToken: KwentaTokenService public prices: PricesService public stats: StatsService + public referrals: ReferralsService public system: SystemService constructor(context: IContext) { @@ -35,6 +37,7 @@ export default class KwentaSDK { this.stats = new StatsService(this) this.system = new SystemService(this) this.perpsV3 = new PerpsV3(this) + this.referrals = new ReferralsService(this) } public setProvider(provider: ethers.providers.Provider) { diff --git a/packages/sdk/src/queries/futures.ts b/packages/sdk/src/queries/futures.ts index 2f642d81e1..ab9bf41a40 100644 --- a/packages/sdk/src/queries/futures.ts +++ b/packages/sdk/src/queries/futures.ts @@ -7,7 +7,7 @@ import { ISOLATED_MARGIN_FRAGMENT, DEFAULT_NUMBER_OF_TRADES, } from '../constants/futures' -import { FuturesMarketAsset, FuturesMarketKey } from '../types/futures' +import { FuturesMarketAsset, FuturesMarketKey, FuturesTradeByReferral } from '../types/futures' import { mapMarginTransfers, mapSmartMarginTransfers } from '../utils/futures' import { FuturesAccountType, getFuturesPositions, getFuturesTrades } from '../utils/subgraph' @@ -225,3 +225,53 @@ export const queryFundingRateHistory = async ( fundingRate: Number(x.fundingRate), })) } + +export const queryVolumeByTrader = async ( + sdk: KwentaSDK, + trader: string, + mintedTime: string +): Promise => { + let queryResponseCount = 0 + let lastMintedAtInSeconds = Math.floor(Number(mintedTime) / 1000) + const currentTimeInSeconds = Math.floor(new Date().getTime() / 1000) + const futuresTrades: FuturesTradeByReferral[] = [] + + do { + const response: { futuresTrades: FuturesTradeByReferral[] } = await request( + sdk.futures.futuresGqlEndpoint, + gql` + query futuresTrades($minTimestamp: BigInt!, $maxTimestamp: BigInt!, $account: String!) { + futuresTrades( + where: { + timestamp_gt: $minTimestamp + timestamp_lte: $maxTimestamp + trackingCode: "0x4b57454e54410000000000000000000000000000000000000000000000000000" + account: $account + } + orderBy: timestamp + orderDirection: asc + first: 1000 + ) { + timestamp + account + size + price + } + } + `, + { + minTimestamp: lastMintedAtInSeconds, + maxTimestamp: currentTimeInSeconds, + account: trader, + } + ) + + queryResponseCount = response.futuresTrades.length + if (queryResponseCount > 0) { + lastMintedAtInSeconds = Number(response.futuresTrades[queryResponseCount - 1].timestamp) + futuresTrades.push(...response.futuresTrades) + } + } while (queryResponseCount === 1000) + + return futuresTrades +} diff --git a/packages/sdk/src/queries/referrals.ts b/packages/sdk/src/queries/referrals.ts new file mode 100644 index 0000000000..d8ed94ad87 --- /dev/null +++ b/packages/sdk/src/queries/referrals.ts @@ -0,0 +1,74 @@ +import { request, gql } from 'graphql-request' +import KwentaSDK from '..' +import { formatBytes32String, parseBytes32String } from '@ethersproject/strings' +import { BoostHolder, BoostReferrer } from '../types/referrals' + +export const queryReferrerByCode = async (sdk: KwentaSDK, code: string): Promise => { + const response: any = await request( + sdk.referrals.referralsGqlEndpoint, + gql` + query boostReferrers($code: String!) { + boostReferrers(where: { id: $code }) { + account + } + } + `, + { code: formatBytes32String(code) } + ) + return response?.boostReferrers[0]?.account || null +} + +export const queryBoostNftTierByHolder = async ( + sdk: KwentaSDK, + walletAddress: string +): Promise => { + if (!walletAddress) return -1 + const response: any = await request( + sdk.referrals.referralsGqlEndpoint, + gql` + query boostHolders($walletAddress: String!) { + boostHolders(where: { id: $walletAddress }) { + tier + } + } + `, + { walletAddress } + ) + + return response?.boostHolders[0]?.tier || -1 +} + +export const queryCodesByReferrer = async ( + sdk: KwentaSDK, + walletAddress: string +): Promise => { + if (!walletAddress) return [] + const response: { boostReferrers: BoostReferrer[] } = await request( + sdk.referrals.referralsGqlEndpoint, + gql` + query boostReferrers($walletAddress: String!) { + boostReferrers(where: { account: $walletAddress }) { + id + } + } + `, + { walletAddress } + ) + return response?.boostReferrers.map(({ id }) => parseBytes32String(id)) || [] +} + +export const queryTradersByCode = async (sdk: KwentaSDK, code: string): Promise => { + const response: { boostHolders: BoostHolder[] } = await request( + sdk.referrals.referralsGqlEndpoint, + gql` + query boostHolders($code: String!) { + boostHolders(where: { code: $code }) { + id + lastMintedAt + } + } + `, + { code: formatBytes32String(code) } + ) + return response?.boostHolders || [] +} diff --git a/packages/sdk/src/services/exchange.ts b/packages/sdk/src/services/exchange.ts index 7c700682f9..a4e1d9fe4a 100644 --- a/packages/sdk/src/services/exchange.ts +++ b/packages/sdk/src/services/exchange.ts @@ -930,7 +930,7 @@ export default class ExchangeService { } private get oneInchApiUrl() { - return `${process.env.NEXT_PUBLIC_ONE_INCH_COINGECKO_PROXY}/1inch/swap/v5.2/${ + return `${process.env.NEXT_PUBLIC_SERVICES_PROXY}/1inch/swap/v5.2/${ this.sdk.context.isL2 ? 10 : 1 }/` } diff --git a/packages/sdk/src/services/referrals.ts b/packages/sdk/src/services/referrals.ts new file mode 100644 index 0000000000..26a5e82bac --- /dev/null +++ b/packages/sdk/src/services/referrals.ts @@ -0,0 +1,171 @@ +import { formatBytes32String } from '@ethersproject/strings' +import KwentaSDK from '..' +import * as sdkErrors from '../common/errors' +import { getReferralStatisticsByAccount, getReferralsGqlEndpoint } from '../utils/referrals' +import { + queryBoostNftTierByHolder, + queryCodesByReferrer, + queryReferrerByCode, +} from '../queries/referrals' + +export default class ReferralsService { + private sdk: KwentaSDK + + constructor(sdk: KwentaSDK) { + this.sdk = sdk + } + + get referralsGqlEndpoint() { + const { networkId } = this.sdk.context + return getReferralsGqlEndpoint(networkId) + } + + /** + * Mint a Boost NFT using the given code. + * @param code - The referral code. + * @returns ethers.js TransactionResponse object + */ + public mintBoostNft(code: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + return this.sdk.transactions.createContractTxn(this.sdk.context.contracts.BoostNft, 'mint', [ + formatBytes32String(code), + ]) + } + + /** + * Register a new referral code. + * @param code - The referral code to register. + * @returns ethers.js TransactionResponse object + */ + public registerReferralCode(code: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + return this.sdk.transactions.createContractTxn( + this.sdk.context.contracts.BoostNft, + 'registerCode', + [formatBytes32String(code)] + ) + } + + /** + * Fetches the referral score by a given account. + * @param account - The account of the referral code creator. + * @returns The referral score of the account. + */ + public getReferralScoreByReferrer(account: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + return this.sdk.context.contracts.BoostNft.getReferralScore(account) + } + + /** + * Fetch the tier of a boost NFT determined by the issuer's referral score. + * @param account - The account of the boost NFT issuer. + * @returns The tier level of the boost NFT. + */ + public async getReferralNftTierByReferrer(account: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + try { + const score = await this.getReferralScoreByReferrer(account) + const tier = await this.sdk.context.contracts.BoostNft.getTierFromReferralScore(score) + return tier.toNumber() + } catch (err) { + this.sdk.context.logError(err) + throw err + } + } + + /** + * Retrieve the tier level associated with a given referral code. + * @param code - The referral code. + * @returns The tier level of the referral code, or -1 if the code is invalid. + */ + public async getTierByReferralCode(code: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + try { + const account = await this.getReferrerByCode(code) + if (!account) { + return -1 + } + return this.getReferralNftTierByReferrer(account) + } catch (err) { + this.sdk.context.logError(err) + throw err + } + } + + /** + * Check whether a Boost NFT has been minted by a given account. + * @param account - The account to check. + * @returns Boolean indicating whether the NFT has been minted. + */ + public checkNftMintedForAccount(account: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + return this.sdk.context.contracts.BoostNft.hasMinted(account) + } + + /** + * Retrieve the owner's address associated with a given referral code. + * @param code - The referral code. + * @returns The code owner's address, or null if the code is not found. + */ + public getReferrerByCode(code: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + return queryReferrerByCode(this.sdk, code) + } + + /** + * Fetch the tier of a Boost NFT by the owner's account. + * @param account - The account of the Boost NFT owner. + * @returns The tier level of a Boost NFT. + */ + public getBoostNftTierByHolder(account: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + return queryBoostNftTierByHolder(this.sdk, account) + } + + /** + * Retrieve all the referral codes created by a given referrer. + * @param account - The account of the referrer. + * @returns All the referral codes created by the referrer. + */ + public getCodesByReferrer(account: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + return queryCodesByReferrer(this.sdk, account) + } + + /** + * Retrieve the cumulative statistics for a given referrer. + * @param account - The account of the referrer. + * @returns Object containing total referrerd account and total referral volumes per code + */ + public getCumulativeStatsByReferrer(account: string) { + if (!this.sdk.context.contracts.BoostNft) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + return getReferralStatisticsByAccount(this.sdk, account) + } +} diff --git a/packages/sdk/src/types/futures.ts b/packages/sdk/src/types/futures.ts index 16c4139ced..3e25e81c46 100644 --- a/packages/sdk/src/types/futures.ts +++ b/packages/sdk/src/types/futures.ts @@ -630,6 +630,13 @@ export type PerpsV3SubgraphMarket = { takerFee: string } +export interface FuturesTradeByReferral { + timestamp: string + account: string + size: string + price: string +} + export type PrepareTxParams = { isPrepareOnly?: T } diff --git a/packages/sdk/src/types/referrals.ts b/packages/sdk/src/types/referrals.ts new file mode 100644 index 0000000000..aa97587999 --- /dev/null +++ b/packages/sdk/src/types/referrals.ts @@ -0,0 +1,22 @@ +export type ReferralNftDetail = { + code: string + tier: number +} + +export interface BoostReferrer { + id: string + account: string +} + +export interface BoostHolder { + id: string + code: string + lastMintedAt: string +} + +export type ReferralCumulativeStats = { + code: string + referralVolume: string + referredCount: string + earnedRewards: string +} diff --git a/packages/sdk/src/utils/date.ts b/packages/sdk/src/utils/date.ts index 8409a1da1e..a428fc88f1 100644 --- a/packages/sdk/src/utils/date.ts +++ b/packages/sdk/src/utils/date.ts @@ -75,3 +75,5 @@ export const getNextSunday = (date: Date) => { nextSunday.setHours(0, 0, 0, 0) return nextSunday } + +export const hoursToMilliseconds = (hours: number) => hours * 60 * 60 * 1000 diff --git a/packages/sdk/src/utils/referrals.ts b/packages/sdk/src/utils/referrals.ts new file mode 100644 index 0000000000..c809103e91 --- /dev/null +++ b/packages/sdk/src/utils/referrals.ts @@ -0,0 +1,41 @@ +import KwentaSDK from '..' +import { REFERRALS_ENDPOINTS } from '../constants/referrals' +import { queryVolumeByTrader } from '../queries/futures' +import { queryCodesByReferrer, queryTradersByCode } from '../queries/referrals' +import { FuturesTradeByReferral } from '../types/futures' +import { ReferralCumulativeStats } from '../types/referrals' + +const calculateTraderVolume = (futuresTrades: FuturesTradeByReferral[]) => { + return futuresTrades.reduce((acc, trade) => { + return acc + (Math.abs(+trade.size) / 1e18) * (+trade.price / 1e18) + }, 0) +} + +export const getReferralsGqlEndpoint = (networkId: number) => { + return REFERRALS_ENDPOINTS[networkId] || REFERRALS_ENDPOINTS[10] +} + +export const getReferralStatisticsByAccount = async ( + sdk: KwentaSDK, + account: string +): Promise => { + const codes = await queryCodesByReferrer(sdk, account) + + return await Promise.all( + codes.map(async (code) => { + const traders = await queryTradersByCode(sdk, code) + + const totalVolume = await traders.reduce(async (accVolume, trader) => { + const volume = await queryVolumeByTrader(sdk, trader.id, trader.lastMintedAt) + return (await accVolume) + calculateTraderVolume(volume) + }, Promise.resolve(0)) + + return { + code, + referredCount: traders.length.toString(), + referralVolume: totalVolume.toString(), + earnedRewards: '0', + } + }) + ) +}