From 32af22a0609380eb13e79fb7f1f114e26ffed83d Mon Sep 17 00:00:00 2001 From: leifu Date: Thu, 5 Oct 2023 22:00:58 +0300 Subject: [PATCH 1/6] fix(app): referral trades count and sort epoch desc (#1019) --- .../referrals/ReferralRewardsHistory.tsx | 12 ++--- packages/app/src/sections/referrals/types.ts | 1 + packages/app/src/state/referrals/action.ts | 47 ++++++++++--------- packages/app/src/translations/en.json | 1 + packages/sdk/src/types/referrals.ts | 1 + packages/sdk/src/utils/referrals.ts | 2 + 6 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/app/src/sections/referrals/ReferralRewardsHistory.tsx b/packages/app/src/sections/referrals/ReferralRewardsHistory.tsx index 8167d0483..dbf99df0b 100644 --- a/packages/app/src/sections/referrals/ReferralRewardsHistory.tsx +++ b/packages/app/src/sections/referrals/ReferralRewardsHistory.tsx @@ -22,7 +22,7 @@ const ReferralRewardsHistory: FC = memo(({ data }) const rewardsHistoryTableProps = useMemo( () => ({ - data, + data: data, pageSize: 4, showPagination: true, noResultsMessage: , @@ -67,12 +67,12 @@ const ReferralRewardsHistory: FC = memo(({ data }) }, { header: () => ( - {t('referrals.table.header.traders-referred')} + {t('referrals.table.header.trades-referred')} ), cell: (cellProps) => ( - {cellProps.getValue()} + {cellProps.getValue()} ), - accessorKey: 'referredCount', + accessorKey: 'tradesCount', }, { header: () => ( @@ -129,11 +129,11 @@ const ReferralRewardsHistory: FC = memo(({ data }) header: () => null, cell: (cellProps) => ( - {t('referrals.table.header.traders-referred')} + {t('referrals.table.header.trades-referred')} {cellProps.getValue()} ), - accessorKey: 'referredCount', + accessorKey: 'tradesCount', }, { header: () => null, diff --git a/packages/app/src/sections/referrals/types.ts b/packages/app/src/sections/referrals/types.ts index b88827167..b6ce2f4cd 100644 --- a/packages/app/src/sections/referrals/types.ts +++ b/packages/app/src/sections/referrals/types.ts @@ -2,6 +2,7 @@ export type ReferralRewardsInfo = { earnedRewards: string referralVolume: string referredCount: string + tradesCount: string } export type ReferralsRewardsPerCode = ReferralRewardsInfo & { diff --git a/packages/app/src/state/referrals/action.ts b/packages/app/src/state/referrals/action.ts index 5a896853d..abf4bef05 100644 --- a/packages/app/src/state/referrals/action.ts +++ b/packages/app/src/state/referrals/action.ts @@ -161,27 +161,32 @@ export const fetchReferralEpoch = createAsyncThunk { - const referralEpoch = await sdk.referrals.getCumulativeStatsByReferrerAndEpochTime( - wallet, - start, - end - ) - - const kwentaRewards = - period !== Number(epochPeriod) - ? await sdk.kwentaToken.getKwentaRewardsByEpoch(period) - : wei(0) - const referralVolume = calculateTotal(referralEpoch, 'referralVolume') - const referredCount = calculateTotal(referralEpoch, 'referredCount') - - return { - epoch: period.toString(), - referralVolume: referralVolume.toString(), - referredCount: referredCount.toString(), - earnedRewards: kwentaRewards.toString(), - } - }) + epochData + .slice(REFERRAL_PROGRAM_START_EPOCH) + .sort((a, b) => Number(b.period) - Number(a.period)) + .map(async ({ period, start, end }) => { + const referralEpoch = await sdk.referrals.getCumulativeStatsByReferrerAndEpochTime( + wallet, + start, + end + ) + + const kwentaRewards = + period !== Number(epochPeriod) + ? await sdk.kwentaToken.getKwentaRewardsByEpoch(period) + : wei(0) + const referralVolume = calculateTotal(referralEpoch, 'referralVolume') + const referredCount = calculateTotal(referralEpoch, 'referredCount') + const tradesCount = calculateTotal(referralEpoch, 'tradesCount') + + return { + epoch: period.toString(), + referralVolume: referralVolume.toString(), + referredCount: referredCount.toString(), + earnedRewards: kwentaRewards.toString(), + tradesCount: tradesCount.toString(), + } + }) ) return statsPerEpoch diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index f4b221bbd..49144a2a4 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -1272,6 +1272,7 @@ "referral-code": "Referral Code", "total-volume": "Total Volume", "traders-referred": "Traders Referred", + "trades-referred": "Trades Referred", "kwenta-earned": "$KWENTA Earned", "epoch": "Epoch" } diff --git a/packages/sdk/src/types/referrals.ts b/packages/sdk/src/types/referrals.ts index aa9758799..ea0a02ab7 100644 --- a/packages/sdk/src/types/referrals.ts +++ b/packages/sdk/src/types/referrals.ts @@ -19,4 +19,5 @@ export type ReferralCumulativeStats = { referralVolume: string referredCount: string earnedRewards: string + tradesCount: string } diff --git a/packages/sdk/src/utils/referrals.ts b/packages/sdk/src/utils/referrals.ts index 9cbc801cd..a7bf00641 100644 --- a/packages/sdk/src/utils/referrals.ts +++ b/packages/sdk/src/utils/referrals.ts @@ -78,6 +78,7 @@ const getCumulativeStatsByCode = async ( referredCount: traders.length.toString(), referralVolume: totalVolume.toString(), earnedRewards: totalRewards.toString(), + tradesCount: totalTrades.length.toString(), } } else { return { @@ -85,6 +86,7 @@ const getCumulativeStatsByCode = async ( referredCount: '0', referralVolume: '0', earnedRewards: totalRewards.toString(), + tradesCount: '0', } } }) From 4345e46bbc29142c119b1a266db9c9ff344cb314 Mon Sep 17 00:00:00 2001 From: leifu Date: Fri, 6 Oct 2023 14:09:25 +0300 Subject: [PATCH 2/6] feat(app): transfer escrow staking v2 (#1020) * Added transferFrom and bulkTransferFrom functionality * Updated the UI and added check for unstaked escrow entries * Updated the copy * Fixed the total transfer amount when the length of the entries over the maximum limit * Fixed the typo --- .../sections/dashboard/Stake/EscrowTable.tsx | 127 ++++++++++++---- .../dashboard/Stake/TransferInputModal.tsx | 137 ++++++++++++++++++ .../dashboard/Stake/VestConfirmationModal.tsx | 18 ++- packages/app/src/state/app/types.ts | 2 + packages/app/src/state/staking/actions.ts | 80 +++++++++- packages/app/src/state/staking/selectors.ts | 9 ++ packages/app/src/state/staking/types.ts | 15 +- .../app/src/state/stakingMigration/actions.ts | 8 +- packages/app/src/translations/en.json | 10 +- packages/sdk/src/services/kwentaToken.ts | 24 +++ 10 files changed, 384 insertions(+), 46 deletions(-) create mode 100644 packages/app/src/sections/dashboard/Stake/TransferInputModal.tsx diff --git a/packages/app/src/sections/dashboard/Stake/EscrowTable.tsx b/packages/app/src/sections/dashboard/Stake/EscrowTable.tsx index 8fc0d2733..8d974afc6 100644 --- a/packages/app/src/sections/dashboard/Stake/EscrowTable.tsx +++ b/packages/app/src/sections/dashboard/Stake/EscrowTable.tsx @@ -1,7 +1,7 @@ import { ZERO_WEI } from '@kwenta/sdk/constants' import { formatNumber, formatPercent } from '@kwenta/sdk/utils' import { wei } from '@synthetixio/wei' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -16,33 +16,46 @@ import { TableCellHead, TableHeader } from 'components/Table' import StakingPagination from 'components/Table/StakingPagination' import { Body } from 'components/Text' import { STAKING_DISABLED } from 'constants/ui' +import { setOpenModal } from 'state/app/reducer' +import { selectShowModal } from 'state/app/selectors' import { useAppDispatch, useAppSelector } from 'state/hooks' import { vestEscrowedRewards, vestEscrowedRewardsV2 } from 'state/staking/actions' import { selectCanVestBeforeMigration, selectEscrowEntries, selectStakingV1, + selectUnstakedEscrowedKwentaBalance, } from 'state/staking/selectors' import media from 'styles/media' import common from 'styles/theme/colors/common' +import TransferInputModal from './TransferInputModal' import VestConfirmationModal from './VestConfirmationModal' +const TRANSFER_BATCH_SIZE = 200 + const EscrowTable = () => { const { t } = useTranslation() const dispatch = useAppDispatch() const stakingV1 = useAppSelector(selectStakingV1) const canVestBeforeMigration = useAppSelector(selectCanVestBeforeMigration) const escrowData = useAppSelector(selectEscrowEntries) + const unstakedEscrowedKwentaBalance = useAppSelector(selectUnstakedEscrowedKwentaBalance) + const openModal = useAppSelector(selectShowModal) const [checkedState, setCheckedState] = useState(escrowData.map((_) => false)) const [checkAllState, setCheckAllState] = useState(false) - const [isConfirmModalOpen, setConfirmModalOpen] = useState(false) + + useEffect(() => { + setCheckedState(escrowData.map((_) => false)) + setCheckAllState(false) + }, [escrowData]) const handleOnChange = useCallback( (position: number) => { - checkedState[position] = !checkedState[position] - setCheckedState([...checkedState]) + const newCheckedState = [...checkedState] + newCheckedState[position] = !newCheckedState[position] + setCheckedState(newCheckedState) }, [checkedState] ) @@ -63,7 +76,7 @@ const EscrowTable = () => { () => checkedState.reduce( (acc, current, index) => { - if (current) { + if (current && escrowData[index]) { acc.totalVestable = acc.totalVestable.add(escrowData[index].vestable) acc.totalFee = acc.totalFee.add(escrowData[index].fee) } @@ -75,12 +88,48 @@ const EscrowTable = () => { [checkedState, escrowData] ) - const { ids, vestEnabled } = useMemo(() => { + const { totalTransferAmount } = useMemo( + () => + checkedState.reduce( + (acc, current, index) => { + if (acc.totalCount >= TRANSFER_BATCH_SIZE) { + return acc + } + + if (current && escrowData[index]) { + acc.totalTransferAmount = acc.totalTransferAmount.add(escrowData[index].amount) + acc.totalCount++ + } + + return acc + }, + { totalTransferAmount: ZERO_WEI, totalCount: 0 } + ), + [checkedState, escrowData] + ) + const { ids, vestEnabled, transferEnabled } = useMemo(() => { const ids = escrowData.filter((_, i) => !!checkedState[i]).map((d) => d.id) const vestEnabled = ids.length > 0 && !STAKING_DISABLED + const transferEnabled = + ids.length > 0 && + !STAKING_DISABLED && + !stakingV1 && + unstakedEscrowedKwentaBalance.gte(totalTransferAmount) + + return { ids, vestEnabled, transferEnabled } + }, [escrowData, stakingV1, unstakedEscrowedKwentaBalance, totalTransferAmount, checkedState]) + + const handleOpenVestModal = useCallback(() => { + dispatch(setOpenModal('vest_escrow_entries')) + }, [dispatch]) - return { ids, vestEnabled } - }, [escrowData, checkedState]) + const handleOpenTransferModal = useCallback(() => { + dispatch(setOpenModal('transfer_escrow_entries')) + }, [dispatch]) + + const handleDismissModal = useCallback(() => { + dispatch(setOpenModal(null)) + }, [dispatch]) const handleVest = useCallback(async () => { if (vestEnabled) { @@ -93,15 +142,8 @@ const EscrowTable = () => { } else { await dispatch(vestEscrowedRewardsV2(ids)) } - setCheckedState(escrowData.map((_) => false)) - setCheckAllState(false) } - - setConfirmModalOpen(false) - }, [canVestBeforeMigration, dispatch, escrowData, ids, stakingV1, vestEnabled]) - - const openConfirmModal = useCallback(() => setConfirmModalOpen(true), []) - const closeConfirmModal = useCallback(() => setConfirmModalOpen(false), []) + }, [canVestBeforeMigration, dispatch, ids, stakingV1, vestEnabled]) const EscrowStatsContainer = () => ( @@ -117,15 +159,28 @@ const EscrowTable = () => { - - {t('dashboard.stake.tabs.escrow.vest')} - + + {!stakingV1 && ( + + {t('dashboard.stake.tabs.escrow.transfer')} + + )} + + {t('dashboard.stake.tabs.escrow.vest')} + + ) @@ -337,17 +392,32 @@ const EscrowTable = () => { ]} /> - {isConfirmModalOpen && ( + {openModal === 'vest_escrow_entries' && ( )} + {openModal === 'transfer_escrow_entries' && ( + + )} ) } +const ButtonContainer = styled(FlexDivRowCentered)` + column-gap: 25px; + ${media.lessThan('lg')` + width: 100%; + column-gap: 15px; + `} +` + const StyledButton = styled(Button)` padding: 10px 20px; ${media.lessThan('lg')` @@ -387,9 +457,6 @@ const LabelContainers = styled(FlexDivRow)` const StatsContainer = styled(FlexDivRowCentered)` ${media.lessThan('lg')` - padding: 15px 15px; - `} - ${media.lessThan('md')` flex-direction: column; row-gap: 25px; padding: 15px 15px; diff --git a/packages/app/src/sections/dashboard/Stake/TransferInputModal.tsx b/packages/app/src/sections/dashboard/Stake/TransferInputModal.tsx new file mode 100644 index 000000000..d80f73050 --- /dev/null +++ b/packages/app/src/sections/dashboard/Stake/TransferInputModal.tsx @@ -0,0 +1,137 @@ +import { formatNumber } from '@kwenta/sdk/utils' +import { useChainModal, useConnectModal } from '@rainbow-me/rainbowkit' +import Wei from '@synthetixio/wei' +import { isAddress } from 'ethers/lib/utils.js' +import { ChangeEvent, FC, memo, useCallback, useMemo, 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 { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' +import Spacer from 'components/Spacer' +import { Body } from 'components/Text' +import useENS from 'hooks/useENS' +import useIsL2 from 'hooks/useIsL2' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { bulkTransferEscrowEntries, transferEscrowEntry } from 'state/staking/actions' +import { selectIsTransferring } from 'state/staking/selectors' +import { selectWallet } from 'state/wallet/selectors' +import media from 'styles/media' + +type Props = { + onDismiss(): void + totalEntries: number[] + totalAmount: Wei +} + +const TransferInputModal: FC = memo(({ onDismiss, totalEntries, totalAmount }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { openChainModal } = useChainModal() + const { openConnectModal } = useConnectModal() + const isL2 = useIsL2() + const wallet = useAppSelector(selectWallet) + const isTransferring = useAppSelector(selectIsTransferring) + + const [addressOrName, setAddressOrName] = useState('') + + const onChange = useCallback( + (e: ChangeEvent) => setAddressOrName(e.target.value), + [] + ) + const { ensAddress } = useENS(addressOrName) + const recipient = ensAddress || addressOrName + + const isRecipientValid = useMemo(() => isAddress(recipient), [recipient]) + + const handleTransfer = useCallback(() => { + if (isRecipientValid) { + if (totalEntries.length > 1) { + dispatch(bulkTransferEscrowEntries({ recipient, entries: totalEntries })) + } else if (totalEntries.length === 1) { + dispatch(transferEscrowEntry({ recipient, entry: totalEntries[0] })) + } + } + }, [isRecipientValid, totalEntries, dispatch, recipient]) + + return ( + + {t('dashboard.stake.tabs.escrow.transfer-modal.copy')} + + + {t('dashboard.stake.tabs.escrow.transfer-modal.recipient-address')} + + + + + + + + {t('dashboard.stake.tabs.escrow.transfer-modal.entries-total')} + + + {totalEntries.length} + + + + + {t('dashboard.stake.tabs.escrow.transfer-modal.amount-total')} + + + {formatNumber(totalAmount, { suggestDecimals: true })} + + + + + + {wallet + ? isL2 + ? t('dashboard.stake.tabs.escrow.transfer-modal.transfer-escrowed-kwenta') + : t('homepage.l2.cta-buttons.switch-l2') + : t('common.wallet.connect-wallet')} + + + ) +}) + +const StyledInput = styled(Input)` + font-size: 14px; + font-family: ${(props) => props.theme.fonts.mono}; + + ${media.lessThan('md')` + font-size: 13px; + `} +` + +const TransferButton = styled(Button)` + height: 50px; +` + +const StyledBaseModal = styled(BaseModal)` + [data-reach-dialog-content] { + width: 440px; + margin-top: 300px; + } + + ${media.lessThan('md')` + [data-reach-dialog-content] { + width: 300px; + margin-top: 200px; + } + `} +` + +export default TransferInputModal diff --git a/packages/app/src/sections/dashboard/Stake/VestConfirmationModal.tsx b/packages/app/src/sections/dashboard/Stake/VestConfirmationModal.tsx index 24206c8b2..bc0cbf202 100644 --- a/packages/app/src/sections/dashboard/Stake/VestConfirmationModal.tsx +++ b/packages/app/src/sections/dashboard/Stake/VestConfirmationModal.tsx @@ -1,6 +1,6 @@ import { formatNumber } from '@kwenta/sdk/utils' import Wei from '@synthetixio/wei' -import React from 'react' +import { FC } from 'react' import { Trans, useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' @@ -25,23 +25,26 @@ const LinkText = () => { return ( - {t('dashboard.stake.tabs.escrow.modal.more-info')} + {t('dashboard.stake.tabs.escrow.vest-modal.more-info')} ) } -const VestConfirmationModal: React.FC = ({ onDismiss, totalFee, handleVest }) => { +const VestConfirmationModal: FC = ({ onDismiss, totalFee, handleVest }) => { const { t } = useTranslation() const isVestingEscrowedRewards = useAppSelector(selectIsVestingEscrowedRewards) return ( - ]} /> + ]} + /> @@ -49,7 +52,7 @@ const VestConfirmationModal: React.FC = ({ onDismiss, totalFee, handleVes ]} /> @@ -59,14 +62,13 @@ const VestConfirmationModal: React.FC = ({ onDismiss, totalFee, handleVes - {t('dashboard.stake.tabs.escrow.modal.confirm-button')} + {t('dashboard.stake.tabs.escrow.vest-modal.confirm-button')} ) diff --git a/packages/app/src/state/app/types.ts b/packages/app/src/state/app/types.ts index 5be3d7e0d..202c01e76 100644 --- a/packages/app/src/state/app/types.ts +++ b/packages/app/src/state/app/types.ts @@ -16,6 +16,8 @@ export type ModalType = | 'futures_smart_margin_socket' | 'referrals_create_referral_code' | 'referrals_mint_boost_nft' + | 'transfer_escrow_entries' + | 'vest_escrow_entries' | null export type FuturesPositionModalType = diff --git a/packages/app/src/state/staking/actions.ts b/packages/app/src/state/staking/actions.ts index 4e709ac85..f1ccc2b88 100644 --- a/packages/app/src/state/staking/actions.ts +++ b/packages/app/src/state/staking/actions.ts @@ -5,7 +5,7 @@ import { BigNumber } from 'ethers' import { notifyError } from 'components/ErrorNotifier' import { monitorTransaction } from 'contexts/RelayerContext' import { monitorAndAwaitTransaction } from 'state/app/helpers' -import { handleTransactionError, setTransaction } from 'state/app/reducer' +import { handleTransactionError, setOpenModal, setTransaction } from 'state/app/reducer' import { selectStakingSupportedNetwork, selectTradingRewardsSupportedNetwork, @@ -37,6 +37,8 @@ import { EstimatedRewards, StakingAction, StakingActionV2, + TransferEscrowEntriesInput, + TransferEscrowEntryInput, } from './types' export const fetchStakingData = createAsyncThunk( @@ -267,6 +269,7 @@ export const vestEscrowedRewards = createAsyncThunk onTxConfirmed: () => { dispatch({ type: 'staking/setVestEscrowedRewardsStatus', payload: FetchStatus.Success }) dispatch(fetchStakeMigrateData()) + dispatch(setOpenModal(null)) }, onTxFailed: () => { dispatch({ type: 'staking/setVestEscrowedRewardsStatus', payload: FetchStatus.Error }) @@ -286,7 +289,9 @@ export const vestEscrowedRewardsV2 = createAsyncThunk { dispatch({ type: 'staking/setVestEscrowedRewardsStatus', payload: FetchStatus.Success }) - dispatch(fetchStakeMigrateData()) + dispatch(fetchStakingV2Data()) + dispatch(fetchEscrowV2Data()) + dispatch(setOpenModal(null)) }, onTxFailed: () => { dispatch({ type: 'staking/setVestEscrowedRewardsStatus', payload: FetchStatus.Error }) @@ -350,6 +355,77 @@ export const compoundRewards = createAsyncThunk( } ) +export const bulkTransferEscrowEntries = createAsyncThunk< + void, + TransferEscrowEntriesInput, + ThunkConfig +>( + 'staking/bulkTransferEscrowEntries', + async ({ entries, recipient }, { dispatch, getState, extra: { sdk } }) => { + const wallet = selectWallet(getState()) + const supportedNetwork = selectStakingSupportedNetwork(getState()) + + try { + if (!wallet) throw new Error('Wallet not connected') + if (!supportedNetwork) + throw new Error( + 'Transferring entries is unsupported on this network. Please switch to Optimism.' + ) + + dispatch( + setTransaction({ + status: TransactionStatus.AwaitingExecution, + type: 'transfer_escrow_entries', + hash: null, + }) + ) + const tx = await sdk.kwentaToken.bulkTransferFrom(wallet, recipient, entries) + await monitorAndAwaitTransaction(dispatch, tx) + dispatch(fetchStakingV2Data()) + dispatch(fetchEscrowV2Data()) + dispatch(setOpenModal(null)) + } catch (err) { + logError(err) + dispatch(handleTransactionError(err.message)) + throw err + } + } +) + +export const transferEscrowEntry = createAsyncThunk( + 'staking/transferEscrowEntry', + async ({ entry, recipient }, { dispatch, getState, extra: { sdk } }) => { + const wallet = selectWallet(getState()) + const supportedNetwork = selectStakingSupportedNetwork(getState()) + + try { + if (!wallet) throw new Error('Wallet not connected') + if (!supportedNetwork) + throw new Error( + 'Transferring entry is unsupported on this network. Please switch to Optimism.' + ) + + dispatch( + setTransaction({ + status: TransactionStatus.AwaitingExecution, + type: 'transfer_escrow_entry', + hash: null, + }) + ) + + const tx = await sdk.kwentaToken.transferFrom(wallet, recipient, entry) + await monitorAndAwaitTransaction(dispatch, tx) + dispatch(fetchStakingV2Data()) + dispatch(fetchEscrowV2Data()) + dispatch(setOpenModal(null)) + } catch (err) { + logError(err) + dispatch(handleTransactionError(err.message)) + throw err + } + } +) + export const fetchClaimableRewards = createAsyncThunk( 'staking/fetchClaimableRewards', async (_, { getState, extra: { sdk } }) => { diff --git a/packages/app/src/state/staking/selectors.ts b/packages/app/src/state/staking/selectors.ts index fb500e705..2a36c4e11 100644 --- a/packages/app/src/state/staking/selectors.ts +++ b/packages/app/src/state/staking/selectors.ts @@ -492,3 +492,12 @@ export const selectIsApprovingOperator = createSelector( (state: RootState) => state.app, (submitting, app) => submitting && app.transaction?.type === 'approve_operator' ) + +export const selectIsTransferring = createSelector( + selectSubmittingStakingTx, + (state: RootState) => state.app, + (submitting, app) => + submitting && + (app.transaction?.type === 'transfer_escrow_entries' || + app.transaction?.type === 'transfer_escrow_entry') +) diff --git a/packages/app/src/state/staking/types.ts b/packages/app/src/state/staking/types.ts index 4783a8bec..2fd56adff 100644 --- a/packages/app/src/state/staking/types.ts +++ b/packages/app/src/state/staking/types.ts @@ -72,7 +72,10 @@ export type StakingAction = StakeBalance & StakingMiscInfo export type StakingActionV2 = StakeBalance & StakingMiscInfoV2 -export type StakingTransactionType = 'approve_operator' +export type StakingTransactionType = + | 'approve_operator' + | 'transfer_escrow_entries' + | 'transfer_escrow_entry' export type StakingTransaction = { type: StakingTransactionType @@ -80,3 +83,13 @@ export type StakingTransaction = { error?: string hash: string | null } + +export type TransferEscrowEntriesInput = { + recipient: string + entries: number[] +} + +export type TransferEscrowEntryInput = { + recipient: string + entry: number +} diff --git a/packages/app/src/state/stakingMigration/actions.ts b/packages/app/src/state/stakingMigration/actions.ts index bca5e8471..c0f5b4a43 100644 --- a/packages/app/src/state/stakingMigration/actions.ts +++ b/packages/app/src/state/stakingMigration/actions.ts @@ -127,7 +127,7 @@ export const fetchUnregisteredVestingEntryIDs = createAsyncThunk< } } catch (err) { logError(err) - notifyError('Failed to fetch registered vesting entry ids', err) + notifyError('Failed to fetch unregistered vesting entry ids', err) throw err } }) @@ -174,7 +174,7 @@ export const fetchUnvestedRegisteredEntryIDs = createAsyncThunk< } } catch (err) { logError(err) - notifyError('Failed to fetch registered vesting entry ids', err) + notifyError('Failed to fetch unvested registered entry ids', err) throw err } }) @@ -198,7 +198,7 @@ export const fetchUnmigratedRegisteredEntryIDs = createAsyncThunk< } } catch (err) { logError(err) - notifyError('Failed to fetch registered vesting entry ids', err) + notifyError('Failed to fetch unmigrated registered vesting entry ids', err) throw err } }) @@ -221,7 +221,7 @@ export const fetchMigrationDeadline = createAsyncThunk< } } catch (err) { logError(err) - notifyError('Failed to fetch registered vesting entry ids', err) + notifyError('Failed to fetch migration deadline', err) throw err } }) diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 49144a2a4..6dec51669 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -579,12 +579,20 @@ "no-entries": "You have no escrowed entries.", "v1": "V1", "v2": "V2", - "modal": { + "vest-modal": { "title": "Early Vesting Fee", "confirm-button": "Confirm", "warning": "Vesting tokens will result in the loss of locked funds as they will be returned to the Kwenta treasury. ", "more-info": "<0>More information here.", "confirm-text": "You will currently forfeit <0>{{totalFee}} in KWENTA by vesting. Do you still wish to proceed?" + }, + "transfer-modal": { + "title": "Transfer", + "copy": "Indicate the wallet address to which you would like to transfer your selected unstaked escrow entries.", + "recipient-address": "Recipient address", + "entries-total": "Entries Total", + "amount-total": "Amount Total", + "transfer-escrowed-kwenta": "Transfer Escrowed $KWENTA" } }, "migrate": { diff --git a/packages/sdk/src/services/kwentaToken.ts b/packages/sdk/src/services/kwentaToken.ts index a961b9438..2c5619067 100644 --- a/packages/sdk/src/services/kwentaToken.ts +++ b/packages/sdk/src/services/kwentaToken.ts @@ -900,4 +900,28 @@ export default class KwentaTokenService { return queryOperatorsByOwner(this.sdk, walletAddress) } + + public transferFrom(from: string, to: string, id: number) { + const { RewardEscrowV2 } = this.sdk.context.contracts + + if (!RewardEscrowV2) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + return this.sdk.transactions.createContractTxn(RewardEscrowV2, 'transferFrom', [from, to, id]) + } + + public bulkTransferFrom(from: string, to: string, ids: number[]) { + const { RewardEscrowV2 } = this.sdk.context.contracts + + if (!RewardEscrowV2) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK) + } + + return this.sdk.transactions.createContractTxn(RewardEscrowV2, 'bulkTransferFrom', [ + from, + to, + ids, + ]) + } } From c73ab06c72f4e7282b3833d3f4137a8121cfaf9f Mon Sep 17 00:00:00 2001 From: leifu Date: Mon, 9 Oct 2023 17:33:50 +0300 Subject: [PATCH 3/6] fix(app): handle undefined selectedUserMigrationInfo (#1022) --- .../app/src/state/stakingMigration/reducer.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/app/src/state/stakingMigration/reducer.ts b/packages/app/src/state/stakingMigration/reducer.ts index 1ccd751f8..6d12a0a23 100644 --- a/packages/app/src/state/stakingMigration/reducer.ts +++ b/packages/app/src/state/stakingMigration/reducer.ts @@ -55,6 +55,9 @@ export const stakingMigrationSlice = createSlice({ }) builder.addCase(fetchRegisteredVestingEntryIDs.fulfilled, (state, { payload }) => { state.fetchRegisteredVestingEntryIDsStatus = SUCCESS_STATUS + if (!state.selectedUserMigrationInfo[payload.wallet]) { + state.selectedUserMigrationInfo[payload.wallet] = DEFAULT_MIGRATION_INFO + } state.selectedUserMigrationInfo[payload.wallet].registeredVestingEntryIDs = payload.registeredVestingEntryIDs }) @@ -69,6 +72,9 @@ export const stakingMigrationSlice = createSlice({ }) builder.addCase(fetchUnregisteredVestingEntryIDs.fulfilled, (state, { payload }) => { state.fetchUnregisteredVestingEntryIDsStatus = SUCCESS_STATUS + if (!state.selectedUserMigrationInfo[payload.wallet]) { + state.selectedUserMigrationInfo[payload.wallet] = DEFAULT_MIGRATION_INFO + } state.selectedUserMigrationInfo[payload.wallet].unregisteredVestingEntryIDs = payload.unregisteredVestingEntryIDs }) @@ -83,6 +89,9 @@ export const stakingMigrationSlice = createSlice({ }) builder.addCase(fetchUnvestedRegisteredEntryIDs.fulfilled, (state, { payload }) => { state.fetchUnvestedRegisteredEntryIDsStatus = SUCCESS_STATUS + if (!state.selectedUserMigrationInfo[payload.wallet]) { + state.selectedUserMigrationInfo[payload.wallet] = DEFAULT_MIGRATION_INFO + } state.selectedUserMigrationInfo[payload.wallet].unvestedRegisteredEntryIDs = payload.unvestedRegisteredEntryIDs }) @@ -97,6 +106,9 @@ export const stakingMigrationSlice = createSlice({ }) builder.addCase(fetchUnmigratedRegisteredEntryIDs.fulfilled, (state, { payload }) => { state.fetchUnmigratedRegisteredEntryIDsStatus = SUCCESS_STATUS + if (!state.selectedUserMigrationInfo[payload.wallet]) { + state.selectedUserMigrationInfo[payload.wallet] = DEFAULT_MIGRATION_INFO + } state.selectedUserMigrationInfo[payload.wallet].unmigratedRegisteredEntryIDs = payload.unmigratedRegisteredEntryIDs }) @@ -111,6 +123,9 @@ export const stakingMigrationSlice = createSlice({ }) builder.addCase(fetchToPay.fulfilled, (state, { payload }) => { state.fetchToPayStatus = SUCCESS_STATUS + if (!state.selectedUserMigrationInfo[payload.wallet]) { + state.selectedUserMigrationInfo[payload.wallet] = DEFAULT_MIGRATION_INFO + } state.selectedUserMigrationInfo[payload.wallet].toPay = payload.toPay }) builder.addCase(fetchToPay.rejected, (state) => { @@ -124,6 +139,9 @@ export const stakingMigrationSlice = createSlice({ }) builder.addCase(fetchEscrowMigratorAllowance.fulfilled, (state, { payload }) => { state.fetchEscrowMigratorAllowanceStatus = SUCCESS_STATUS + if (!state.selectedUserMigrationInfo[payload.wallet]) { + state.selectedUserMigrationInfo[payload.wallet] = DEFAULT_MIGRATION_INFO + } state.selectedUserMigrationInfo[payload.wallet].escrowMigratorAllowance = payload.escrowMigratorAllowance }) @@ -138,6 +156,9 @@ export const stakingMigrationSlice = createSlice({ }) builder.addCase(fetchMigrationDeadline.fulfilled, (state, { payload }) => { state.fetchMigrationDeadlineStatus = SUCCESS_STATUS + if (!state.selectedUserMigrationInfo[payload.wallet]) { + state.selectedUserMigrationInfo[payload.wallet] = DEFAULT_MIGRATION_INFO + } state.selectedUserMigrationInfo[payload.wallet].migrationPeriod = payload.migrationPeriod }) builder.addCase(fetchMigrationDeadline.rejected, (state) => { @@ -151,6 +172,9 @@ export const stakingMigrationSlice = createSlice({ }) builder.addCase(fetchTotalEscrowUnmigrated.fulfilled, (state, { payload }) => { state.fetchTotalEscrowUnmigratedStatus = SUCCESS_STATUS + if (!state.selectedUserMigrationInfo[payload.wallet]) { + state.selectedUserMigrationInfo[payload.wallet] = DEFAULT_MIGRATION_INFO + } state.selectedUserMigrationInfo[payload.wallet].totalEscrowUnmigrated = payload.totalEscrowUnmigrated }) From 4b526d6c62a86a611d6b7fa517b3902bc23de2b1 Mon Sep 17 00:00:00 2001 From: pldespaigne <5858657+pldespaigne@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:32:12 +0200 Subject: [PATCH 4/6] feat(app): redesign mobile trades & position history (#2795) * ui requests & mobile view * design request changes * chore(app): fix lint errors --------- Co-authored-by: platschi --- packages/app/src/pages/dashboard/history.tsx | 49 +- .../src/sections/dashboard/HistoryTabs.tsx | 64 +- .../TradeConfirmationModal.tsx | 1 - .../src/sections/futures/TraderHistory.tsx | 503 ++++++++------- .../src/sections/futures/Trades/Trades.tsx | 596 +++++++++--------- packages/app/src/state/futures/selectors.ts | 4 +- packages/app/src/translations/en.json | 18 + 7 files changed, 615 insertions(+), 620 deletions(-) diff --git a/packages/app/src/pages/dashboard/history.tsx b/packages/app/src/pages/dashboard/history.tsx index 01bc45ef1..d48fe164c 100644 --- a/packages/app/src/pages/dashboard/history.tsx +++ b/packages/app/src/pages/dashboard/history.tsx @@ -5,15 +5,13 @@ import styled from 'styled-components' import UploadIcon from 'assets/svg/futures/upload-icon.svg' import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' -import { MobileHiddenView, MobileOnlyView } from 'components/Media' -import Spacer from 'components/Spacer' import { Body, Heading } from 'components/Text' import DashboardLayout from 'sections/dashboard/DashboardLayout' import HistoryTabs, { HistoryTab } from 'sections/dashboard/HistoryTabs' -import TradesTab from 'sections/futures/MobileTrade/UserTabs/TradesTab' import { usePollDashboardFuturesData } from 'state/futures/hooks' import { selectPositionsCsvData, selectTradesCsvData } from 'state/futures/selectors' import { useAppSelector } from 'state/hooks' +import media from 'styles/media' type HistoryPageProps = React.FC & { getLayout: (page: ReactNode) => JSX.Element } @@ -46,33 +44,36 @@ const HistoryPage: HistoryPageProps = () => { ) return ( - <> + {t('dashboard-history.page-title')} - - - - - {t('dashboard-history.main-title')} - {t('dashboard-history.subtitle')} - - - {t('dashboard-history.export-btn')} - - - - - - - - - - - + + + + {t('dashboard-history.main-title')} + {t('dashboard-history.subtitle')} + + + {t('dashboard-history.export-btn')} + + + + + + ) } +const TitleContainer = styled.div` + ${media.lessThan('lg')` + margin: 15px 15px 0 15px; + `} + ${media.lessThan('sm')` + margin: 15px 15px 0 15px; + `} +` + const ExportButton = styled.a` gap: 8px; height: 36px; diff --git a/packages/app/src/sections/dashboard/HistoryTabs.tsx b/packages/app/src/sections/dashboard/HistoryTabs.tsx index eeed3b00e..6d65d2bcd 100644 --- a/packages/app/src/sections/dashboard/HistoryTabs.tsx +++ b/packages/app/src/sections/dashboard/HistoryTabs.tsx @@ -10,7 +10,6 @@ import { fetchPositionHistoryForTrader } from 'state/futures/actions' import { selectPositionsHistoryTableData } from 'state/futures/selectors' import { useAppDispatch, useAppSelector } from 'state/hooks' import { selectWallet } from 'state/wallet/selectors' -import media from 'styles/media' export enum HistoryTab { Positions = 'positions', @@ -33,23 +32,21 @@ const HistoryTabs: React.FC = ({ currentTab, onChangeTab }) => }, [dispatch, walletAddress]) return ( - - - - - - - + <> + + + +
= ({ currentTab, onChangeTab }) =>
-
+ ) } -const HistoryTabsHeader = styled.div` - display: flex; - justify-content: space-between; - margin-bottom: 15px; - - ${media.lessThan('md')` - flex-direction: column; - row-gap: 10px; - margin-bottom: 25px; - margin-top: 0px; - `} -` - -const HistoryTabsContainer = styled.div` - ${media.lessThan('md')` - padding: 15px; - `} -` - const TabButtons = styled.div` display: flex; - - & > button:not(:last-of-type) { - margin-right: 25px; - } - - ${media.lessThan('md')` - justify-content: flex-start; - `} + gap: 25px; + margin-bottom: 25px; ` export default HistoryTabs diff --git a/packages/app/src/sections/futures/TradeConfirmation/TradeConfirmationModal.tsx b/packages/app/src/sections/futures/TradeConfirmation/TradeConfirmationModal.tsx index a837865a4..9861221b8 100644 --- a/packages/app/src/sections/futures/TradeConfirmation/TradeConfirmationModal.tsx +++ b/packages/app/src/sections/futures/TradeConfirmation/TradeConfirmationModal.tsx @@ -36,7 +36,6 @@ import { selectNewTradeHasSlTp, selectOrderType, selectSlTpTradeInputs, - selectSelectedSwapDepositToken, selectSmartMarginOrderPrice, selectTradePreview, } from 'state/futures/smartMargin/selectors' diff --git a/packages/app/src/sections/futures/TraderHistory.tsx b/packages/app/src/sections/futures/TraderHistory.tsx index 8c5e6c213..f0d7aca6b 100644 --- a/packages/app/src/sections/futures/TraderHistory.tsx +++ b/packages/app/src/sections/futures/TraderHistory.tsx @@ -1,22 +1,20 @@ import { FuturesMarketKey, FuturesPositionHistory } from '@kwenta/sdk/types' import Wei, { wei, WeiSource } from '@synthetixio/wei' -import router from 'next/router' import { FC, memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import Currency from 'components/Currency' import CurrencyIcon from 'components/Currency/CurrencyIcon' -import { FlexDiv } from 'components/layout/flex' +import { FlexDiv, FlexDivCol } from 'components/layout/flex' import { DesktopOnlyView, MobileOrTabletView } from 'components/Media' import Table, { TableHeader, TableNoResults } from 'components/Table' +import { TableCell } from 'components/Table/TableBodyRow' import { Body } from 'components/Text' -import ROUTES from 'constants/routes' import TimeDisplay from 'sections/futures/Trades/TimeDisplay' import { selectQueryStatuses } from 'state/futures/selectors' import { useAppSelector } from 'state/hooks' import { FetchStatus } from 'state/types' -import { ExternalLink } from 'styles/common' import media from 'styles/media' type PositionData = FuturesPositionHistory & { @@ -38,240 +36,227 @@ type TraderHistoryProps = { searchTerm?: string | undefined } -const TraderHistory: FC = memo( - ({ trader, traderEns, positionHistory, resetSelection, compact, searchTerm }) => { - const { t } = useTranslation() - const { selectedTraderPositionHistory: queryStatus } = useAppSelector(selectQueryStatuses) +const TraderHistory: FC = memo(({ positionHistory, compact, searchTerm }) => { + const { t } = useTranslation() + const { selectedTraderPositionHistory: queryStatus } = useAppSelector(selectQueryStatuses) - let data = useMemo(() => { - return positionHistory.filter((i) => - searchTerm?.length - ? i.marketShortName.toLowerCase().includes(searchTerm) || - i.status.toLowerCase().includes(searchTerm) - : true - ) - }, [positionHistory, searchTerm]) + let data = useMemo(() => { + return positionHistory.filter((i) => + searchTerm?.length + ? i.marketShortName.toLowerCase().includes(searchTerm) || + i.status.toLowerCase().includes(searchTerm) + : true + ) + }, [positionHistory, searchTerm]) - return ( - <> - - ( - - - {t('leaderboard.trader-history.table.back')} - - > - - {traderEns ?? trader} - - - ), - accessorKey: 'title', - enableSorting: false, - columns: [ - { - header: () => ( - {t('leaderboard.trader-history.table.timestamp')} - ), - accessorKey: 'openTimestamp', - cell: (cellProps) => { - return ( - - - - ) - }, - size: 100, - }, - { - header: () => ( - {t('leaderboard.trader-history.table.market')} - ), - accessorKey: 'asset', - cell: (cellProps) => ( - - - {cellProps.row.original.marketShortName} - - ), - size: 150, - }, - { - header: () => ( - {t('leaderboard.trader-history.table.status')} - ), - accessorKey: 'status', - cell: (cellProps) => { - return {cellProps.row.original.status} - }, - size: 30, - }, - { - header: () => ( - - {t('leaderboard.trader-history.table.total-trades')} - - ), - accessorKey: 'trades', - cell: (cellProps) => ( - {cellProps.getValue()} - ), - size: 130, - }, - { - header: () => ( - - {t('leaderboard.trader-history.table.total-volume')} - - ), - accessorKey: 'totalVolume', - cell: (cellProps) => ( - - - - ), - size: 130, - }, - { - header: () => ( - - {t('leaderboard.trader-history.table.total-pnl')} - - ), - accessorKey: 'pnl', - cell: (cellProps) => ( - - - - {cellProps.row.original.pnlPct} - - - ), - size: 130, - }, - { - header: () => ( - - {t('leaderboard.trader-history.table.funding')} - - ), - accessorKey: 'funding', - cell: (cellProps) => ( - - - - ), - size: 130, - }, - ], + return ( + <> + + ( + {t('leaderboard.trader-history.table.timestamp')} + ), + accessorKey: 'openTimestamp', + cell: (cellProps) => { + return ( + + + + ) }, - ]} - noResultsMessage={ - queryStatus.status !== FetchStatus.Loading && - data?.length === 0 && ( - - {t('dashboard.history.positions-history-table.no-result')} - - ) - } - /> - - - ( - - { - resetSelection() - router.push(ROUTES.Leaderboard.Home) - }} - > - {t('leaderboard.leaderboard.table.title')} - - > - - {traderEns ?? trader} - - - ), - accessorKey: 'title', - enableSorting: false, - columns: [ - { - header: () => ( - {t('leaderboard.trader-history.table.market')} - ), - accessorKey: 'asset', - cell: (cellProps) => ( - - - {cellProps.row.original.marketShortName} - - ), - size: 50, - }, - { - header: () => ( - {t('leaderboard.trader-history.table.status')} - ), - accessorKey: 'status', - cell: (cellProps) => { - return {cellProps.row.original.status} - }, - size: 30, - }, - { - header: () => ( - {t('leaderboard.trader-history.table.total-pnl')} - ), - accessorKey: 'pnl', - cell: (cellProps) => ( - - - - {cellProps.row.original.pnlPct} - - - ), - size: 40, - }, - ], + size: 100, + }, + { + header: () => ( + {t('leaderboard.trader-history.table.market')} + ), + accessorKey: 'asset', + cell: (cellProps) => ( + + + {cellProps.row.original.marketShortName} + + ), + size: 150, + }, + { + header: () => ( + {t('leaderboard.trader-history.table.status')} + ), + accessorKey: 'status', + cell: (cellProps) => { + return {cellProps.row.original.status} }, - ]} - /> - - - ) - } -) + size: 30, + }, + { + header: () => ( + + {t('leaderboard.trader-history.table.total-trades')} + + ), + accessorKey: 'trades', + cell: (cellProps) => ( + {cellProps.getValue()} + ), + size: 130, + }, + { + header: () => ( + + {t('leaderboard.trader-history.table.total-volume')} + + ), + accessorKey: 'totalVolume', + cell: (cellProps) => ( + + + + ), + size: 130, + }, + { + header: () => ( + + {t('leaderboard.trader-history.table.total-pnl')} + + ), + accessorKey: 'pnl', + cell: (cellProps) => ( + + + + {cellProps.row.original.pnlPct} + + + ), + size: 130, + }, + { + header: () => ( + + {t('leaderboard.trader-history.table.funding')} + + ), + accessorKey: 'funding', + cell: (cellProps) => ( + + + + ), + size: 130, + }, + ]} + noResultsMessage={ + queryStatus.status !== FetchStatus.Loading && + data?.length === 0 && ( + + {t('dashboard.history.positions-history-table.no-result')} + + ) + } + /> + + + ( + <> + {t('leaderboard.trader-history.mobile-table.market')} + + + {cellProps.row.original.marketShortName} + + + ), + }, + { + accessorKey: 'totalVolume', + cell: (cellProps) => ( + <> + {t('leaderboard.trader-history.mobile-table.volume')} + + + + + ), + }, + { + accessorKey: 'status', + cell: (cellProps) => ( + <> + {t('leaderboard.trader-history.mobile-table.status')} + {cellProps.row.original.status} + + ), + }, + { + accessorKey: 'funding', + cell: (cellProps) => ( + <> + {t('leaderboard.trader-history.mobile-table.funding')} + + + + + ), + }, + { + accessorKey: 'pnl', + cell: (cellProps) => ( + <> + {t('leaderboard.trader-history.mobile-table.pnl')} + + + + {cellProps.row.original.pnlPct} + + + + ), + }, + { + accessorKey: 'openTimestamp', + cell: (cellProps) => { + return ( + <> + + {t('leaderboard.trader-history.mobile-table.timestamp')} + + + + + + ) + }, + }, + ]} + /> + + + ) +}) const RightAlignedTableHeader = styled(TableHeader)` width: 90%; @@ -292,24 +277,36 @@ const StyledTable = styled(Table)<{ compact?: boolean; height?: number }>` `} ${media.lessThan('md')` - margin-bottom: 150px; + margin-bottom: 15px; `} ` as typeof Table -const TableTitle = styled.div` - width: 100%; - display: flex; - justify-content: flex-start; -` +const MobileTable = styled(Table)` + .table-row:first-child { + display: none; + } -const TitleText = styled.a` - font-family: ${(props) => props.theme.fonts.regular}; - color: ${(props) => props.theme.colors.selectedTheme.gray}; + .table-body-row { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + grid-column-gap: 15px; + grid-row-gap: 15px; + padding: 15px; + } - &:hover { - text-decoration: underline; + ${TableCell}:first-child, + ${TableCell}:last-child { + padding: 0; } -` + ${TableCell} { + height: 100%; + width: 100% !important; + display: flex; + align-items: start; + justify-content: space-between; + } +` as typeof Table const StyledCell = styled.div` color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; @@ -318,19 +315,19 @@ const StyledCell = styled.div` padding-bottom: 5px; ` -const TitleSeparator = styled.div` - margin-left: 10px; - margin-right: 10px; - font-family: ${(props) => props.theme.fonts.regular}; - color: ${(props) => props.theme.colors.selectedTheme.gray}; -` - const StyledCurrencyIcon = styled(CurrencyIcon)` width: 30px; height: 30px; margin-right: 5px; ` +const MobileCurrencyIcon = styled(CurrencyIcon)` + & > img { + width: 20px; + height: 20px; + } +` + const CurrencyInfo = styled(FlexDiv)` align-items: center; ` diff --git a/packages/app/src/sections/futures/Trades/Trades.tsx b/packages/app/src/sections/futures/Trades/Trades.tsx index 6d7fe26b8..e86f1bb58 100644 --- a/packages/app/src/sections/futures/Trades/Trades.tsx +++ b/packages/app/src/sections/futures/Trades/Trades.tsx @@ -6,14 +6,16 @@ import styled from 'styled-components' import ColoredPrice from 'components/ColoredPrice' import Currency from 'components/Currency' -import { FlexDivCol } from 'components/layout/flex' +import CurrencyIcon from 'components/Currency/CurrencyIcon' +import { FlexDiv, FlexDivCol } from 'components/layout/flex' +import { DesktopOnlyView, MobileOrTabletView } from 'components/Media' import Table, { TableHeader, TableNoResults } from 'components/Table' +import { TableCell } from 'components/Table/TableBodyRow' import { Body } from 'components/Text' import ROUTES from 'constants/routes' import { blockExplorer } from 'containers/Connector/Connector' import useIsL2 from 'hooks/useIsL2' import useNetworkSwitcher from 'hooks/useNetworkSwitcher' -import useWindowSize from 'hooks/useWindowSize' import { selectFuturesType } from 'state/futures/common/selectors' import { selectAllTradesForAccountType, @@ -23,6 +25,7 @@ import { selectSmartMarginQueryStatuses } from 'state/futures/smartMargin/select import { useAppSelector } from 'state/hooks' import { FetchStatus } from 'state/types' +import PositionType from '../PositionType' import TableMarketDetails from '../UserInfo/TableMarketDetails' import TimeDisplay from './TimeDisplay' @@ -35,7 +38,6 @@ const Trades: FC = memo(({ rounded = false, noBottom = true }) => { const { t } = useTranslation() const { switchToL2 } = useNetworkSwitcher() const router = useRouter() - const { lessThanWidth } = useWindowSize() const accountType = useAppSelector(selectFuturesType) const history = useAppSelector(selectAllTradesForAccountType) const historyData = useAppSelector(selectTradesHistoryTableData) @@ -48,300 +50,262 @@ const Trades: FC = memo(({ rounded = false, noBottom = true }) => { const columnsDeps = useMemo(() => [historyData], [historyData]) - return lessThanWidth('xl') ? ( - ( - {t('futures.market.user.trades.table.market-side')} - ), - accessorKey: 'market', - cell: (cellProps) => { - return ( - { - cellProps.row.original.market && - router.push( - ROUTES.Markets.MarketPair(cellProps.row.original.market.asset, accountType) - ) - e.stopPropagation() - }} - > - {cellProps.row.original.market ? ( - - ) : ( + return ( + <> + +
( + {t('futures.market.user.trades.table.market-side')} + ), + accessorKey: 'market', + cell: (cellProps) => { + return ( + { + cellProps.row.original.market && + router.push( + ROUTES.Markets.MarketPair( + cellProps.row.original.market.asset, + accountType + ) + ) + e.stopPropagation() + }} + > + {cellProps.row.original.market ? ( + + ) : ( + '-' + )} + + ) + }, + }, + { + header: () => {t('futures.market.user.trades.table.date')}, + accessorKey: 'time', + cell: (cellProps) => , + enableSorting: true, + }, + { + header: () => ( + + {t('futures.market.user.trades.table.price-type')} + + ), + accessorKey: 'value', + sortingFn: 'basic', + cell: (cellProps) => { + return ( + + + {cellProps.row.original.type} + + ) + }, + enableSorting: true, + }, + { + header: () => ( + + {t('futures.market.user.trades.table.size')} + + ), + accessorKey: 'amount', + sortingFn: 'basic', + cell: (cellProps) => { + return ( + + {formatNumber(cellProps.getValue(), { suggestDecimals: true })} + + + ) + }, + enableSorting: true, + }, + { + header: () => ( + + {t('futures.market.user.trades.table.pnl')} + + ), + accessorKey: 'netPnl', + sortingFn: 'basic', + cell: (cellProps) => { + return cellProps.getValue().eq(0) ? ( '-' - )} - - ) - }, - }, - { - header: () => {t('futures.market.user.trades.table.date')}, - accessorKey: 'time', - cell: (cellProps) => , - enableSorting: true, - }, - { - header: () => ( - - {t('futures.market.user.trades.table.price-type')} - - ), - accessorKey: 'value', - sortingFn: 'basic', - cell: (cellProps) => { - return ( - - - {cellProps.row.original.type} - - ) - }, - enableSorting: true, - }, - { - header: () => ( - - {t('futures.market.user.trades.table.size')} - - ), - accessorKey: 'amount', - sortingFn: 'basic', - cell: (cellProps) => { - return ( - - {formatNumber(cellProps.getValue(), { suggestDecimals: true })} - - - ) - }, - enableSorting: true, - }, - { - header: () => ( - - {t('futures.market.user.trades.table.pnl')} - - ), - accessorKey: 'netPnl', - sortingFn: 'basic', - cell: (cellProps) => { - return cellProps.getValue().eq(0) ? ( - '--' - ) : ( - - {formatDollars(cellProps.getValue(), { maxDecimals: 2 })} - - ) - }, - enableSorting: true, - }, - { - header: () => ( - - {t('futures.market.user.trades.table.fees')} - - ), - sortingFn: 'basic', - accessorKey: 'feesPaid', - cell: (cellProps) => ( -
- -
- ), - enableSorting: true, - }, - ]} - columnsDeps={columnsDeps} - data={historyData} - isLoading={isLoading && isLoaded} - onTableRowClick={(row) => - window.open(blockExplorer.txLink(row.original.txnHash), '_blank', 'noopener noreferrer') - } - noResultsMessage={ - !isL2 ? ( - - {t('common.l2-cta')} -
{t('homepage.l2.cta-buttons.switch-l2')}
-
- ) : isLoaded && historyData?.length === 0 ? ( - {t('futures.market.user.trades.table.no-results')} - ) : undefined - } - /> - ) : ( -
( - {t('futures.market.user.trades.table.market-side')} - ), - accessorKey: 'market', - cell: (cellProps) => { - return ( - { - cellProps.row.original.market && - router.push( - ROUTES.Markets.MarketPair(cellProps.row.original.market.asset, accountType) - ) - e.stopPropagation() - }} - > - {cellProps.row.original.market ? ( - ) : ( - '-' - )} - - ) - }, - size: 90, - }, - { - header: () => {t('futures.market.user.trades.table.date')}, - accessorKey: 'time', - cell: (cellProps) => , - enableSorting: true, - size: 90, - }, - { - header: () => ( - - {t('futures.market.user.trades.table.price')} - - ), - accessorKey: 'value', - sortingFn: 'basic', - cell: (cellProps) => { - return ( -
- - {formatDollars(cellProps.getValue(), { suggestDecimals: true })} - -
- ) - }, - enableSorting: true, - size: 125, - }, - { - header: () => ( - - {t('futures.market.user.trades.table.size')} - - ), - accessorKey: 'amount', - sortingFn: 'basic', - cell: (cellProps) => { - return ( - - {formatNumber(cellProps.getValue(), { suggestDecimals: true })} - - - ) - }, - enableSorting: true, - size: 100, - }, - { - header: () => ( - - {t('futures.market.user.trades.table.pnl')} - - ), - accessorKey: 'netPnl', - sortingFn: 'basic', - cell: (cellProps) => { - return cellProps.getValue().eq(0) ? ( - '--' - ) : ( - - {formatDollars(cellProps.getValue(), { maxDecimals: 2 })} - - ) - }, - enableSorting: true, - size: 100, - }, - { - header: () => ( - - {t('futures.market.user.trades.table.fees')} - - ), - sortingFn: 'basic', - accessorKey: 'feesPaid', - cell: (cellProps) => { - return cellProps.getValue().eq(0) ? ( - '--' - ) : ( - - - - ) - }, - enableSorting: true, - size: 100, - }, - { - header: () => ( - {t('futures.market.user.trades.table.order-type')} - ), - accessorKey: 'type', - sortingFn: 'basic', - cell: (cellProps) => <>{cellProps.getValue()}, - size: 60, - }, - ]} - columnsDeps={columnsDeps} - data={historyData} - isLoading={isLoading && isLoaded} - onTableRowClick={(row) => - window.open(blockExplorer.txLink(row.original.txnHash), '_blank', 'noopener noreferrer') - } - noResultsMessage={ - !isL2 ? ( - - {t('common.l2-cta')} -
{t('homepage.l2.cta-buttons.switch-l2')}
-
- ) : isLoaded && historyData?.length === 0 ? ( - {t('futures.market.user.trades.table.no-results')} - ) : undefined - } - /> + + {formatDollars(cellProps.getValue(), { maxDecimals: 2 })} + + ) + }, + enableSorting: true, + }, + { + header: () => ( + + {t('futures.market.user.trades.table.fees')} + + ), + sortingFn: 'basic', + accessorKey: 'feesPaid', + cell: (cellProps) => ( +
+ +
+ ), + enableSorting: true, + }, + ]} + columnsDeps={columnsDeps} + data={historyData} + isLoading={isLoading && isLoaded} + onTableRowClick={(row) => + window.open(blockExplorer.txLink(row.original.txnHash), '_blank', 'noopener noreferrer') + } + noResultsMessage={ + !isL2 ? ( + + {t('common.l2-cta')} +
{t('homepage.l2.cta-buttons.switch-l2')}
+
+ ) : isLoaded && historyData?.length === 0 ? ( + {t('futures.market.user.trades.table.no-results')} + ) : undefined + } + /> + + + + ( + <> + {t('futures.market.user.trades.mobile-table.market')} + + + {cellProps.row.original.market!.marketName} + + + ), + }, + { + accessorKey: 'price', + cell: (cellProps) => ( + <> + {t('futures.market.user.trades.mobile-table.price')} + + + ), + }, + { + accessorKey: 'side', + cell: (cellProps) => ( + <> + {t('futures.market.user.trades.mobile-table.side')} + + + ), + }, + { + accessorKey: 'fees', + cell: (cellProps) => ( + <> + {t('futures.market.user.trades.mobile-table.fees')} + + + ), + }, + { + accessorKey: 'date', + cell: (cellProps) => ( + <> + {t('futures.market.user.trades.mobile-table.date')} + + + ), + }, + { + accessorKey: 'type', + cell: (cellProps) => ( + <> + {t('futures.market.user.trades.mobile-table.type')} +
{cellProps.row.original.orderType}
+ + ), + }, + { + accessorKey: 'size', + cell: (cellProps) => ( + <> + {t('futures.market.user.trades.mobile-table.size')} + +
+ {formatNumber(cellProps.row.original.amount, { suggestDecimals: true })} + {' ETH'} +
+ +
+ + ), + }, + { + accessorKey: 'pnl', + cell: (cellProps) => ( + <> + {t('futures.market.user.trades.mobile-table.pnl')} + + + + + ), + }, + ]} + columnsDeps={columnsDeps} + data={historyData} + isLoading={isLoading && isLoaded} + onTableRowClick={(row) => + window.open(blockExplorer.txLink(row.original.txnHash), '_blank', 'noopener noreferrer') + } + noResultsMessage={ + !isL2 ? ( + + {t('common.l2-cta')} +
{t('homepage.l2.cta-buttons.switch-l2')}
+
+ ) : isLoaded && historyData?.length === 0 ? ( + {t('futures.market.user.trades.table.no-results')} + ) : undefined + } + /> +
+ ) }) @@ -350,3 +314,45 @@ export default Trades const MarketDetailsContainer = styled.div` cursor: pointer; ` +const MobileTable = styled(Table)` + .table-row:first-child { + display: none; + } + + .table-body-row { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + grid-column-gap: 15px; + grid-row-gap: 15px; + padding: 15px; + } + + ${TableCell}:first-child, + ${TableCell}:last-child { + padding: 0; + } + ${TableCell} { + height: 100%; + width: 100% !important; + display: flex; + align-items: start; + justify-content: space-between; + } +` as typeof Table + +const CurrencyInfo = styled(FlexDiv)` + align-items: center; +` + +const MobileCurrencyIcon = styled(CurrencyIcon)` + & > img { + width: 20px; + height: 20px; + } +` + +const StyledSubtitle = styled(Body)` + color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; + text-transform: capitalize; +` diff --git a/packages/app/src/state/futures/selectors.ts b/packages/app/src/state/futures/selectors.ts index c591a38b7..cd4c25baa 100644 --- a/packages/app/src/state/futures/selectors.ts +++ b/packages/app/src/state/futures/selectors.ts @@ -913,13 +913,15 @@ export const selectTradesHistoryTableData = createSelector( const pnl = trade?.pnl const feesPaid = trade?.feesPaid const netPnl = pnl.sub(feesPaid) + const notionalValue = trade?.price.mul(trade?.size.abs()) return { ...trade, pnl, + pnlPct: netPnl.div(notionalValue), feesPaid, netPnl, - notionalValue: trade?.price.mul(trade?.size.abs()), + notionalValue, value: Number(trade?.price), funding: Number(trade?.fundingAccrued), amount: trade?.size.abs(), diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 6dec51669..28cce5720 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -1155,6 +1155,16 @@ "liquidated": "liquidated", "entry": "entry" } + }, + "mobile-table": { + "market": "Market", + "price": "Price", + "side": "Side", + "fees": "Fees", + "date": "Date", + "type": "Order Type", + "size": "Size", + "pnl": "PnL" } }, "transfers": { @@ -1249,6 +1259,14 @@ "total-volume": "Total Volume", "total-pnl": "Realized P&L", "funding": "Funding" + }, + "mobile-table": { + "market": "Market", + "volume": "Volume", + "status": "Status", + "funding": "Funding", + "pnl": "PnL", + "timestamp": "Date" } }, "competition": { From 2ef93986fec3613abac25aeff47e99249e9ba555 Mon Sep 17 00:00:00 2001 From: Ralf Date: Mon, 9 Oct 2023 16:12:40 -0300 Subject: [PATCH 5/6] chore(app): remove cond order warning (#1024) --- .../UserTabs/ConditionalOrdersTab.tsx | 2 -- .../UserInfo/ConditionalOrdersTable.tsx | 2 -- .../UserInfo/ConditionalOrdersWarning.tsx | 31 ------------------- 3 files changed, 35 deletions(-) delete mode 100644 packages/app/src/sections/futures/UserInfo/ConditionalOrdersWarning.tsx diff --git a/packages/app/src/sections/futures/MobileTrade/UserTabs/ConditionalOrdersTab.tsx b/packages/app/src/sections/futures/MobileTrade/UserTabs/ConditionalOrdersTab.tsx index f12970b86..9f635d7f0 100644 --- a/packages/app/src/sections/futures/MobileTrade/UserTabs/ConditionalOrdersTab.tsx +++ b/packages/app/src/sections/futures/MobileTrade/UserTabs/ConditionalOrdersTab.tsx @@ -12,7 +12,6 @@ import { TableNoResults } from 'components/Table' import { Body } from 'components/Text' import { NO_VALUE } from 'constants/placeholder' import PositionType from 'sections/futures/PositionType' -import ConditionalOrdersWarning from 'sections/futures/UserInfo/ConditionalOrdersWarning' import { selectMarketAsset } from 'state/futures/common/selectors' import { cancelConditionalOrder } from 'state/futures/smartMargin/actions' import { @@ -53,7 +52,6 @@ const ConditionalOrdersTab: React.FC = () => { return (
- {rows.length === 0 ? ( You have no open orders diff --git a/packages/app/src/sections/futures/UserInfo/ConditionalOrdersTable.tsx b/packages/app/src/sections/futures/UserInfo/ConditionalOrdersTable.tsx index ca30a7581..b467c13e7 100644 --- a/packages/app/src/sections/futures/UserInfo/ConditionalOrdersTable.tsx +++ b/packages/app/src/sections/futures/UserInfo/ConditionalOrdersTable.tsx @@ -24,7 +24,6 @@ import { useAppDispatch, useAppSelector } from 'state/hooks' import PositionType from '../PositionType' -import ConditionalOrdersWarning from './ConditionalOrdersWarning' import TableMarketDetails from './TableMarketDetails' export default function ConditionalOrdersTable() { @@ -57,7 +56,6 @@ export default function ConditionalOrdersTable() { return ( -
- Conditional orders are executed based on the onchain Pyth or Chainlink price. See the{' '} - - blog post - {' '} - for more details. - - ) -} - -const OrdersWarning = styled.div<{ mobile?: boolean }>` - padding: 10px; - color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.warning}; - border-top: ${(props) => - props.mobile ? 'none' : props.theme.colors.selectedTheme.newTheme.border.style}; - border-bottom: ${(props) => - props.mobile ? props.theme.colors.selectedTheme.newTheme.border.style : 'none'}; - text-align: center; -` From 875a133add6b70020dd8ef3155bd9e66aff42a8a Mon Sep 17 00:00:00 2001 From: platschi Date: Mon, 9 Oct 2023 16:14:55 -0300 Subject: [PATCH 6/6] chore(*): bump version --- package.json | 2 +- packages/app/package.json | 2 +- packages/sdk/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e5753d6b9..bbefdfcf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kwenta", - "version": "7.9.6", + "version": "7.9.7", "description": "Kwenta", "main": "index.js", "scripts": { diff --git a/packages/app/package.json b/packages/app/package.json index 41d959bad..9f961a0c5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@kwenta/app", - "version": "7.9.6", + "version": "7.9.7", "scripts": { "dev": "next", "build": "next build", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e6d05dab4..c5803cac3 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@kwenta/sdk", - "version": "1.0.11", + "version": "1.0.12", "description": "SDK for headless interaction with Kwenta", "main": "dist/index.js", "directories": {