From bd97651dd3c0b664e35fa9d34fe89e4d4cf53d1f Mon Sep 17 00:00:00 2001 From: Art Date: Thu, 1 Feb 2024 13:04:36 +0100 Subject: [PATCH 01/10] fix(trading): game cards are not showing (#5713) --- apps/trading/lib/hooks/use-games.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/trading/lib/hooks/use-games.ts b/apps/trading/lib/hooks/use-games.ts index 71058812b..aec13a2e5 100644 --- a/apps/trading/lib/hooks/use-games.ts +++ b/apps/trading/lib/hooks/use-games.ts @@ -1,12 +1,23 @@ import compact from 'lodash/compact'; import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards'; import { isActiveReward } from '../../components/rewards-container/active-rewards'; -import { EntityScope, type TransferNode } from '@vegaprotocol/types'; +import { + EntityScope, + IndividualScope, + type TransferNode, +} from '@vegaprotocol/types'; const isScopedToTeams = (node: TransferNode) => node.transfer.kind.__typename === 'RecurringTransfer' && - node.transfer.kind.dispatchStrategy?.entityScope === - EntityScope.ENTITY_SCOPE_TEAMS; + // scoped to teams + (node.transfer.kind.dispatchStrategy?.entityScope === + EntityScope.ENTITY_SCOPE_TEAMS || + // or to individuals + (node.transfer.kind.dispatchStrategy?.entityScope === + EntityScope.ENTITY_SCOPE_INDIVIDUALS && + // but they have to be in a team + node.transfer.kind.dispatchStrategy.individualScope === + IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM)); export const useGames = ({ currentEpoch, From 02bd031bee81fbc3ce60083b1fe3d0c3012c3985 Mon Sep 17 00:00:00 2001 From: Art Date: Thu, 1 Feb 2024 14:27:21 +0100 Subject: [PATCH 02/10] fix(trading): mark as updated when tx confirmed (#5715) --- .../client-pages/competitions/team-form.tsx | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/apps/trading/client-pages/competitions/team-form.tsx b/apps/trading/client-pages/competitions/team-form.tsx index 9ed0393e0..01593a12a 100644 --- a/apps/trading/client-pages/competitions/team-form.tsx +++ b/apps/trading/client-pages/competitions/team-form.tsx @@ -6,6 +6,8 @@ import { TextArea, TradingButton, Intent, + VegaIcon, + VegaIconNames, } from '@vegaprotocol/ui-toolkit'; import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils'; @@ -17,6 +19,8 @@ import type { UpdateReferralSet, Status, } from '@vegaprotocol/wallet'; +import classNames from 'classnames'; +import { useLayoutEffect, useState } from 'react'; export type FormFields = { id: string; @@ -239,16 +243,52 @@ const SubmitButton = ({ text = t('Update'); } + let confirmedText = t('Created'); + if (type === TransactionType.UpdateReferralSet) { + confirmedText = t('Updated'); + } + if (status === 'requested') { text = t('Confirm in wallet...'); } else if (status === 'pending') { text = t('Confirming transaction...'); } + const [showConfirmed, setShowConfirmed] = useState(false); + useLayoutEffect(() => { + let to: ReturnType; + if (status === 'confirmed' && !showConfirmed) { + to = setTimeout(() => { + setShowConfirmed(true); + }, 100); + } + return () => { + clearTimeout(to); + }; + }, [showConfirmed, status]); + + const confirmed = ( + + {' '} + {confirmedText} + + ); + return ( - - {text} - +
+ + {text} + + {status === 'confirmed' && confirmed} +
); }; From 0f311611bad009c0c7fc578cd2fb02876b8ca9c0 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 1 Feb 2024 19:33:25 +0000 Subject: [PATCH 03/10] chore(trading): team e2e tests (#5716) --- .../competitions/competitions-create-team.tsx | 13 +- .../competitions/competitions-home.tsx | 3 + .../client-pages/competitions/join-team.tsx | 12 +- .../client-pages/competitions/team-form.tsx | 30 ++- .../rewards-container/active-rewards.tsx | 35 ++-- .../e2e/tests/rewards/test_filtered_cards.py | 4 +- apps/trading/e2e/tests/teams/test_teams.py | 192 ++++++++++++++---- 7 files changed, 224 insertions(+), 65 deletions(-) diff --git a/apps/trading/client-pages/competitions/competitions-create-team.tsx b/apps/trading/client-pages/competitions/competitions-create-team.tsx index 31906568d..21661fef3 100644 --- a/apps/trading/client-pages/competitions/competitions-create-team.tsx +++ b/apps/trading/client-pages/competitions/competitions-create-team.tsx @@ -71,18 +71,27 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => { if (status === 'confirmed') { return ( -
+

{t('Team creation transaction successful')}

{code && ( <>

Your team ID is:{' '} - {code} + + {code} +

{t('View team')} diff --git a/apps/trading/client-pages/competitions/competitions-home.tsx b/apps/trading/client-pages/competitions/competitions-home.tsx index 4b933f716..24217c02e 100644 --- a/apps/trading/client-pages/competitions/competitions-home.tsx +++ b/apps/trading/client-pages/competitions/competitions-home.tsx @@ -60,6 +60,7 @@ export const CompetitionsHome = () => { e.preventDefault(); navigate(Links.COMPETITIONS_CREATE_TEAM()); }} + data-testid="create-public-team-button" > {t('Create a public team')} @@ -78,6 +79,7 @@ export const CompetitionsHome = () => { e.preventDefault(); navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO()); }} + data-testid="create-private-team-button" > {t('Create a private team')} @@ -96,6 +98,7 @@ export const CompetitionsHome = () => { e.preventDefault(); navigate(Links.COMPETITIONS_TEAMS()); }} + data-testid="choose-team-button" > {t('Choose a team')} diff --git a/apps/trading/client-pages/competitions/join-team.tsx b/apps/trading/client-pages/competitions/join-team.tsx index 0d583f5a0..47d54db0f 100644 --- a/apps/trading/client-pages/competitions/join-team.tsx +++ b/apps/trading/client-pages/competitions/join-team.tsx @@ -105,7 +105,11 @@ export const JoinButton = ({ // Party is in a team, but not this one else if (partyTeam && partyTeam.teamId !== team.teamId) { return ( - ); @@ -215,7 +219,11 @@ const DialogContent = ({ )}
- + + ))} + + + +
+ ); +}; + +/** + * Sets the english ordinal for given rank only if the current language is set + * to english. + */ +const RankLabel = ({ rank }: { rank: number }) => { + const t = useT(); + return t('place', { count: rank, ordinal: true }); +}; diff --git a/apps/trading/components/competitions/team-stats.tsx b/apps/trading/components/competitions/team-stats.tsx index d875e85e8..ced4ad18c 100644 --- a/apps/trading/components/competitions/team-stats.tsx +++ b/apps/trading/components/competitions/team-stats.tsx @@ -15,6 +15,7 @@ import { } from '../../lib/hooks/use-team'; import { useT } from '../../lib/use-t'; import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types'; +import classNames from 'classnames'; export const TeamStats = ({ stats, @@ -102,7 +103,13 @@ const LatestResults = ({ games }: { games: TeamGame[] }) => { ); }; -const FavoriteGame = ({ games }: { games: TeamGame[] }) => { +export const FavoriteGame = ({ + games, + noLabel = false, +}: { + games: TeamGame[]; + noLabel?: boolean; +}) => { const t = useT(); const rewardMetrics = games.map( @@ -128,7 +135,13 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => { return (
-
{t('Favorite game')}
+
+ {t('Favorite game')} +
{ ); }; -const StatSection = ({ children }: { children: ReactNode }) => { +export const StatSection = ({ children }: { children: ReactNode }) => { return (
{children} @@ -150,11 +163,11 @@ const StatSection = ({ children }: { children: ReactNode }) => { ); }; -const StatSectionSeparator = () => { +export const StatSectionSeparator = () => { return
; }; -const StatList = ({ children }: { children: ReactNode }) => { +export const StatList = ({ children }: { children: ReactNode }) => { return (
{children} @@ -162,19 +175,21 @@ const StatList = ({ children }: { children: ReactNode }) => { ); }; -const Stat = ({ +export const Stat = ({ value, label, tooltip, valueTestId, + className, }: { value: ReactNode; label: ReactNode; tooltip?: string; valueTestId?: string; + className?: classNames.Argument; }) => { return ( -
+
{value}
diff --git a/apps/trading/lib/hooks/Teams.graphql b/apps/trading/lib/hooks/Teams.graphql index be308f3bf..b256c6a3c 100644 --- a/apps/trading/lib/hooks/Teams.graphql +++ b/apps/trading/lib/hooks/Teams.graphql @@ -1,15 +1,20 @@ +fragment TeamsFields on Team { + teamId + referrer + name + teamUrl + avatarUrl + createdAt + createdAtEpoch + closed + totalMembers +} + query Teams($teamId: ID, $partyId: ID) { teams(teamId: $teamId, partyId: $partyId) { edges { node { - teamId - referrer - name - teamUrl - avatarUrl - createdAt - createdAtEpoch - closed + ...TeamsFields } } } diff --git a/apps/trading/lib/hooks/__generated__/Teams.ts b/apps/trading/lib/hooks/__generated__/Teams.ts index 08b2f1432..ea610632b 100644 --- a/apps/trading/lib/hooks/__generated__/Teams.ts +++ b/apps/trading/lib/hooks/__generated__/Teams.ts @@ -3,33 +3,40 @@ import * as Types from '@vegaprotocol/types'; import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; const defaultOptions = {} as const; +export type TeamsFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number }; + export type TeamsQueryVariables = Types.Exact<{ teamId?: Types.InputMaybe; partyId?: Types.InputMaybe; }>; -export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null }; - +export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number } }> } | null }; +export const TeamsFieldsFragmentDoc = gql` + fragment TeamsFields on Team { + teamId + referrer + name + teamUrl + avatarUrl + createdAt + createdAtEpoch + closed + totalMembers +} + `; export const TeamsDocument = gql` query Teams($teamId: ID, $partyId: ID) { teams(teamId: $teamId, partyId: $partyId) { edges { node { - teamId - referrer - name - teamUrl - avatarUrl - createdAt - createdAtEpoch - closed + ...TeamsFields } } } } - `; + ${TeamsFieldsFragmentDoc}`; /** * __useTeamsQuery__ diff --git a/apps/trading/lib/hooks/use-my-team.ts b/apps/trading/lib/hooks/use-my-team.ts new file mode 100644 index 000000000..dac0ab6f2 --- /dev/null +++ b/apps/trading/lib/hooks/use-my-team.ts @@ -0,0 +1,25 @@ +import { useVegaWallet } from '@vegaprotocol/wallet'; +import compact from 'lodash/compact'; +import first from 'lodash/first'; +import { useTeamsQuery } from './__generated__/Teams'; +import { useTeam } from './use-team'; +import { useTeams } from './use-teams'; + +export const useMyTeam = () => { + const { pubKey } = useVegaWallet(); + const { data: teams } = useTeams(); + + const { data: maybeMyTeam } = useTeamsQuery({ + variables: { + partyId: pubKey, + }, + skip: !pubKey, + fetchPolicy: 'cache-and-network', + }); + + const team = first(compact(maybeMyTeam?.teams?.edges.map((n) => n.node))); + const rank = teams.findIndex((t) => t.teamId === team?.teamId) + 1; + const { games, stats } = useTeam(team?.teamId); + + return { team, stats, games, rank }; +}; diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json index ed36da973..82a2e0aea 100644 --- a/libs/i18n/src/locales/en/trading.json +++ b/libs/i18n/src/locales/en/trading.json @@ -417,6 +417,7 @@ "myVolume_other": "My volume (last {{count}} epochs)", "numberEpochs": "{{count}} epochs", "numberEpochs_one": "{{count}} epoch", + "Rewards earned": "Rewards earned", "Rewards paid out": "Rewards paid out", "{{reward}}x": "{{reward}}x", "userActive": "{{active}} trader: {{count}} epochs so far", @@ -431,5 +432,19 @@ "{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot", "{{checkedAssets}} Assets": "{{checkedAssets}} Assets", "{{distance}} ago": "{{distance}} ago", - "{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision" + "{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision", + "My team": "My team", + "Profile": "Profile", + "Last {{games}} games result_one": "Last game result", + "Last {{games}} games result_other": "Last {{games}} games result", + "Leaderboard": "Leaderboard", + "View all teams": "View all teams", + "Competitions": "Competitions", + "Be a team player! Participate in games and work together to rake in as much profit to win.": "Be a team player! Participate in games and work together to rake in as much profit to win.", + "Create a public team": "Create a public team", + "Create a private team": "Create a private team", + "Choose a team": "Choose a team", + "Join a team": "Join a team", + "Solo team / lone wolf": "Solo team / lone wolf", + "Choose a team to get involved": "Choose a team to get involved" } From 94e7ad489f371263d2445cdde5d3ebe1c773ccd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Mon, 5 Feb 2024 16:06:33 +0100 Subject: [PATCH 06/10] feat(trading): main to develop back merge (#5739) Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com> Co-authored-by: Matthew Russell Co-authored-by: Edd --- .../rewards-container/active-rewards.tsx | 37 ++------ apps/trading/e2e/tests/teams/test_teams.py | 23 +++-- libs/accounts/src/lib/transfer-form.spec.tsx | 87 +------------------ libs/accounts/src/lib/transfer-form.tsx | 66 +++----------- libs/i18n/src/locales/en/accounts.json | 2 - specs/1003-TRAN-transfer.md | 10 --- 6 files changed, 37 insertions(+), 188 deletions(-) diff --git a/apps/trading/components/rewards-container/active-rewards.tsx b/apps/trading/components/rewards-container/active-rewards.tsx index 941f49df6..232efa97f 100644 --- a/apps/trading/components/rewards-container/active-rewards.tsx +++ b/apps/trading/components/rewards-container/active-rewards.tsx @@ -316,49 +316,30 @@ export const ActiveRewardCard = ({ MarketState.STATE_CLOSED, ].includes(m.state) ); + if (marketSettled) { return null; } - const assetInSettledMarket = + const assetInActiveMarket = allMarkets && Object.values(allMarkets).some((m: MarketFieldsFragment | null) => { if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) { - return ( - m?.state && - [ - MarketState.STATE_TRADING_TERMINATED, - MarketState.STATE_SETTLED, - MarketState.STATE_CANCELLED, - MarketState.STATE_CLOSED, - ].includes(m.state) - ); + return m?.state && MarketState.STATE_ACTIVE === m.state; } return false; }); - // Gray out the cards that are related to suspended markets - const suspended = transferNode.markets?.some( + const marketSuspended = transferNode.markets?.some( (m) => m?.state === MarketState.STATE_SUSPENDED || m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE ); - const assetInSuspendedMarket = - allMarkets && - Object.values(allMarkets).some((m: MarketFieldsFragment | null) => { - if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) { - return ( - m?.state === MarketState.STATE_SUSPENDED || - m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE - ); - } - return false; - }); - // Gray out the cards that are related to suspended markets + // Or settlement assets in markets that are not active and eligible for rewards const { gradientClassName, mainClassName } = - suspended || assetInSuspendedMarket || assetInSettledMarket + marketSuspended || !assetInActiveMarket ? { gradientClassName: 'from-vega-cdark-500 to-vega-clight-400', mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%', @@ -449,12 +430,12 @@ export const ActiveRewardCard = ({ {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '} { }); it.each([ - { - targetText: 'Include transfer fee', - tooltipText: - 'The fee will be taken from the amount you are transferring.', - }, { targetText: 'Transfer fee', tooltipText: /transfer\.fee\.factor/, @@ -276,9 +271,6 @@ describe('TransferForm', () => { const amountInput = screen.getByLabelText('Amount'); - const checkbox = screen.getByTestId('include-transfer-fee'); - expect(checkbox).not.toBeChecked(); - await userEvent.clear(amountInput); await userEvent.type(amountInput, '50'); @@ -288,10 +280,7 @@ describe('TransferForm', () => { await userEvent.click(screen.getByRole('button', { name: 'Use max' })); expect(amountInput).toHaveValue('100.00'); - // If transfering from a vested account 'include fees' checkbox should - // be disabled and fees should be 0 - expect(checkbox).not.toBeChecked(); - expect(checkbox).toBeDisabled(); + // If transfering from a vested account fees should be 0 const expectedFee = '0'; const total = new BigNumber(amount).plus(expectedFee).toFixed(); @@ -396,78 +385,7 @@ describe('TransferForm', () => { }); }); }); - describe('IncludeFeesCheckbox', () => { - it('validates fields and submits when checkbox is checked', async () => { - const mockSubmit = jest.fn(); - renderComponent({ ...props, submitTransfer: mockSubmit }); - - // check current pubkey not shown - const keySelect = screen.getByLabelText('To Vega key'); - const pubKeyOptions = ['', pubKey, props.pubKeys[1]]; - expect(keySelect.children).toHaveLength(pubKeyOptions.length); - expect(Array.from(keySelect.options).map((o) => o.value)).toEqual( - pubKeyOptions - ); - - await submit(); - expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value - - // Select a pubkey - await userEvent.selectOptions( - screen.getByLabelText('To Vega key'), - props.pubKeys[1] - ); - - // Select asset - await selectAsset(asset); - - await userEvent.selectOptions( - screen.getByLabelText('From account'), - `${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}` - ); - - const amountInput = screen.getByLabelText('Amount'); - const checkbox = screen.getByTestId('include-transfer-fee'); - - // 1003-TRAN-022 - expect(checkbox).not.toBeChecked(); - - await userEvent.clear(amountInput); - await userEvent.type(amountInput, amount); - await userEvent.click(checkbox); - - expect(checkbox).toBeChecked(); - const expectedFee = new BigNumber(amount) - .times(props.feeFactor) - .toFixed(); - const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed(); - - // 1003-TRAN-020 - expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee); - expect(screen.getByTestId('transfer-amount')).toHaveTextContent( - expectedAmount - ); - expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent( - amount - ); - - await submit(); - - await waitFor(() => { - // 1003-TRAN-023 - expect(mockSubmit).toHaveBeenCalledTimes(1); - expect(mockSubmit).toHaveBeenCalledWith({ - fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL, - toAccountType: AccountType.ACCOUNT_TYPE_GENERAL, - to: props.pubKeys[1], - asset: asset.id, - amount: removeDecimal(expectedAmount, asset.decimals), - oneOff: {}, - }); - }); - }); - it('validates fields when checkbox is not checked', async () => { renderComponent(props); @@ -497,11 +415,8 @@ describe('TransferForm', () => { ); const amountInput = screen.getByLabelText('Amount'); - const checkbox = screen.getByTestId('include-transfer-fee'); - expect(checkbox).not.toBeChecked(); await userEvent.type(amountInput, amount); - expect(checkbox).not.toBeChecked(); const expectedFee = new BigNumber(amount) .times(props.feeFactor) .toFixed(); diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx index dc1adf77a..d69f8dfde 100644 --- a/libs/accounts/src/lib/transfer-form.tsx +++ b/libs/accounts/src/lib/transfer-form.tsx @@ -15,7 +15,6 @@ import { TradingRichSelect, TradingSelect, Tooltip, - TradingCheckbox, TradingButton, } from '@vegaprotocol/ui-toolkit'; import type { Transfer } from '@vegaprotocol/wallet'; @@ -135,32 +134,17 @@ export const TransferForm = ({ const accountBalance = account && addDecimal(account.balance, account.asset.decimals); - const [includeFee, setIncludeFee] = useState(false); - // Max amount given selected asset and from account const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0); - const transferAmount = useMemo(() => { - if (!amount) return undefined; - if (includeFee && feeFactor) { - return new BigNumber(1).minus(feeFactor).times(amount).toString(); - } - return amount; - }, [amount, includeFee, feeFactor]); - - const fee = useMemo(() => { - if (!transferAmount) return undefined; - if (includeFee) { - return new BigNumber(amount).minus(transferAmount).toString(); - } - return ( - feeFactor && new BigNumber(feeFactor).times(transferAmount).toString() - ); - }, [amount, includeFee, transferAmount, feeFactor]); + const fee = useMemo( + () => feeFactor && new BigNumber(feeFactor).times(amount).toString(), + [amount, feeFactor] + ); const onSubmit = useCallback( (fields: FormFields) => { - if (!transferAmount) { + if (!amount) { throw new Error('Submitted transfer with no amount selected'); } @@ -173,7 +157,7 @@ export const TransferForm = ({ const transfer = normalizeTransfer( fields.toVegaKey, - transferAmount, + amount, type, AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form { @@ -183,7 +167,7 @@ export const TransferForm = ({ ); submitTransfer(transfer); }, - [submitTransfer, transferAmount, assets] + [submitTransfer, amount, assets] ); // reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569 @@ -279,7 +263,6 @@ export const TransferForm = ({ ) { setValue('toVegaKey', pubKey); setToVegaKeyMode('select'); - setIncludeFee(false); } }} > @@ -449,27 +432,9 @@ export const TransferForm = ({ )} -
- -
- setIncludeFee((x) => !x)} - /> -
-
-
- {transferAmount && fee && ( + {amount && fee && ( { const t = useT(); - if (!feeFactor || !amount || !transferAmount || !fee) return null; - if ( - isNaN(Number(feeFactor)) || - isNaN(Number(amount)) || - isNaN(Number(transferAmount)) || - isNaN(Number(fee)) - ) { + if (!feeFactor || !amount || !fee) return null; + if (isNaN(Number(feeFactor)) || isNaN(Number(amount)) || isNaN(Number(fee))) { return null; } - const totalValue = new BigNumber(transferAmount).plus(fee).toString(); + const totalValue = new BigNumber(amount).plus(fee).toString(); return (
diff --git a/libs/i18n/src/locales/en/accounts.json b/libs/i18n/src/locales/en/accounts.json index e581189c3..cface53cb 100644 --- a/libs/i18n/src/locales/en/accounts.json +++ b/libs/i18n/src/locales/en/accounts.json @@ -21,7 +21,6 @@ "Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.": "Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.", "Enter manually": "Enter manually", "From account": "From account", - "Include transfer fee": "Include transfer fee", "initial level": "initial level", "maintenance level": "maintenance level", "Margin health": "Margin health", @@ -33,7 +32,6 @@ "release level": "release level", "search level": "search level", "Select from wallet": "Select from wallet", - "The fee will be taken from the amount you are transferring.": "The fee will be taken from the amount you are transferring.", "The total amount of each asset on this key. Includes used and available collateral.": "The total amount of each asset on this key. Includes used and available collateral.", "The total amount taken from your account. The amount to be transferred plus the fee.": "The total amount taken from your account. The amount to be transferred plus the fee.", "The total amount to be transferred (without the fee)": "The total amount to be transferred (without the fee)", diff --git a/specs/1003-TRAN-transfer.md b/specs/1003-TRAN-transfer.md index 4e8b1eee5..b6d08f33f 100644 --- a/specs/1003-TRAN-transfer.md +++ b/specs/1003-TRAN-transfer.md @@ -40,22 +40,12 @@ ## Transfer -- **Must** can select include transfer fee (1003-TRAN-015) - -- **Must** display tooltip for "Include transfer fee" when hovered over.(1003-TRAN-016) - - **Must** display tooltip for "Transfer fee when hovered over.(1003-TRAN-017) - **Must** display tooltip for "Amount to be transferred" when hovered over.(1003-TRAN-018) - **Must** display tooltip for "Total amount (with fee)" when hovered over.(1003-TRAN-019) -- **Must** amount to be transferred and transfer fee update correctly when include transfer fee is selected (1003-TRAN-020) - -- **Must** total amount with fee is correct with and without "Include transfer fee" selected (1003-TRAN-021) - -- **Must** i cannot select include transfer fee unless amount is entered (1003-TRAN-022) - - **Must** With all fields entered correctly, clicking "confirm transfer" button will start transaction(1003-TRAN-023) ### Transfer page From f62e29c67f0e6cbf4b0fedcae563846195f0a044 Mon Sep 17 00:00:00 2001 From: Edd Date: Mon, 5 Feb 2024 17:35:12 +0000 Subject: [PATCH 07/10] feat(explorer): batch proposal support (#5711) --- .../src/app/components/links/hash.tsx | 2 +- .../src/app/components/links/index.ts | 1 + .../network-parameter-link.tsx | 30 ++ .../links/proposal-link/proposal-link.tsx | 2 +- .../txs/details/proposal/Proposal.graphql | 5 + .../proposal/__generated__/Proposal.ts | 7 +- .../txs/details/proposal/batch-item.spec.tsx | 257 ++++++++++++++++++ .../txs/details/proposal/batch-item.tsx | 87 ++++++ .../txs/details/proposal/summary.tsx | 21 +- .../txs/details/tx-batch-proposal.tsx | 75 +++++ .../txs/details/tx-details-wrapper.tsx | 3 + .../src/app/components/txs/tx-filter.tsx | 9 +- 12 files changed, 493 insertions(+), 6 deletions(-) create mode 100644 apps/explorer/src/app/components/links/network-parameter-link/network-parameter-link.tsx create mode 100644 apps/explorer/src/app/components/txs/details/proposal/batch-item.spec.tsx create mode 100644 apps/explorer/src/app/components/txs/details/proposal/batch-item.tsx create mode 100644 apps/explorer/src/app/components/txs/details/tx-batch-proposal.tsx diff --git a/apps/explorer/src/app/components/links/hash.tsx b/apps/explorer/src/app/components/links/hash.tsx index 3fd073aa7..f2f96c900 100644 --- a/apps/explorer/src/app/components/links/hash.tsx +++ b/apps/explorer/src/app/components/links/hash.tsx @@ -1,4 +1,4 @@ -export type HashProps = { +export type HashProps = React.HTMLProps & { text: string; truncate?: boolean; }; diff --git a/apps/explorer/src/app/components/links/index.ts b/apps/explorer/src/app/components/links/index.ts index ec0211d43..e835eba87 100644 --- a/apps/explorer/src/app/components/links/index.ts +++ b/apps/explorer/src/app/components/links/index.ts @@ -2,4 +2,5 @@ export { default as BlockLink } from './block-link/block-link'; export { default as PartyLink } from './party-link/party-link'; export { default as NodeLink } from './node-link/node-link'; export { default as MarketLink } from './market-link/market-link'; +export { default as NetworkParameterLink } from './network-parameter-link/network-parameter-link'; export * from './asset-link/asset-link'; diff --git a/apps/explorer/src/app/components/links/network-parameter-link/network-parameter-link.tsx b/apps/explorer/src/app/components/links/network-parameter-link/network-parameter-link.tsx new file mode 100644 index 000000000..2b8bbc54c --- /dev/null +++ b/apps/explorer/src/app/components/links/network-parameter-link/network-parameter-link.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Routes } from '../../../routes/route-names'; +import { Link } from 'react-router-dom'; + +import type { ComponentProps } from 'react'; +import Hash from '../hash'; + +export type NetworkParameterLinkProps = Partial> & { + parameter: string; +}; + +/** + * Links a given network parameter to the relevant page and anchor on the page + */ +const NetworkParameterLink = ({ + parameter, + ...props +}: NetworkParameterLinkProps) => { + return ( + + + + ); +}; + +export default NetworkParameterLink; diff --git a/apps/explorer/src/app/components/links/proposal-link/proposal-link.tsx b/apps/explorer/src/app/components/links/proposal-link/proposal-link.tsx index c7bd14820..ee6b35c3f 100644 --- a/apps/explorer/src/app/components/links/proposal-link/proposal-link.tsx +++ b/apps/explorer/src/app/components/links/proposal-link/proposal-link.tsx @@ -26,7 +26,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => { >; const base = ENV.dataSources.governanceUrl; - const label = proposal?.rationale.title || id; + const label = proposal?.rationale?.title || id; return ( diff --git a/apps/explorer/src/app/components/txs/details/proposal/Proposal.graphql b/apps/explorer/src/app/components/txs/details/proposal/Proposal.graphql index db4b2ea66..3c51a6d9f 100644 --- a/apps/explorer/src/app/components/txs/details/proposal/Proposal.graphql +++ b/apps/explorer/src/app/components/txs/details/proposal/Proposal.graphql @@ -5,5 +5,10 @@ query ExplorerProposalStatus($id: ID!) { state rejectionReason } + ... on BatchProposal { + id + state + rejectionReason + } } } diff --git a/apps/explorer/src/app/components/txs/details/proposal/__generated__/Proposal.ts b/apps/explorer/src/app/components/txs/details/proposal/__generated__/Proposal.ts index 0034cd2e2..f2f4c2488 100644 --- a/apps/explorer/src/app/components/txs/details/proposal/__generated__/Proposal.ts +++ b/apps/explorer/src/app/components/txs/details/proposal/__generated__/Proposal.ts @@ -8,7 +8,7 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{ }>; -export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null }; +export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null }; export const ExplorerProposalStatusDocument = gql` @@ -19,6 +19,11 @@ export const ExplorerProposalStatusDocument = gql` state rejectionReason } + ... on BatchProposal { + id + state + rejectionReason + } } } `; diff --git a/apps/explorer/src/app/components/txs/details/proposal/batch-item.spec.tsx b/apps/explorer/src/app/components/txs/details/proposal/batch-item.spec.tsx new file mode 100644 index 000000000..39557e557 --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/proposal/batch-item.spec.tsx @@ -0,0 +1,257 @@ +import { render, screen } from '@testing-library/react'; +import { BatchItem } from './batch-item'; +import { MemoryRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import type { components } from '../../../../../types/explorer'; +type Item = components['schemas']['vegaBatchProposalTermsChange']; + +describe('BatchItem', () => { + it('Renders "Unknown proposal type" by default', () => { + const item = {}; + render(); + expect(screen.getByText('Unknown proposal type')).toBeInTheDocument(); + }); + + it('Renders "Unknown proposal type" for unknown items', () => { + const item = { + newLochNessMonster: { + location: 'Loch Ness', + }, + } as unknown as Item; + render(); + expect(screen.getByText('Unknown proposal type')).toBeInTheDocument(); + }); + + it('Renders "New spot market"', () => { + const item = { + newSpotMarket: {}, + }; + render(); + expect(screen.getByText('New spot market')).toBeInTheDocument(); + }); + + it('Renders "Cancel transfer"', () => { + const item = { + cancelTransfer: { + changes: { + transferId: 'transfer123', + }, + }, + }; + render(); + expect(screen.getByText('Cancel transfer')).toBeInTheDocument(); + expect(screen.getByText('transf')).toBeInTheDocument(); + }); + + it('Renders "Cancel transfer" without an id', () => { + const item = { + cancelTransfer: { + changes: {}, + }, + }; + render(); + expect(screen.getByText('Cancel transfer')).toBeInTheDocument(); + }); + + it('Renders "New freeform"', () => { + const item = { + newFreeform: {}, + }; + render(); + expect(screen.getByText('New freeform proposal')).toBeInTheDocument(); + }); + + it('Renders "New market"', () => { + const item = { + newMarket: {}, + }; + render(); + expect(screen.getByText('New market')).toBeInTheDocument(); + }); + + it('Renders "New transfer"', () => { + const item = { + newTransfer: {}, + }; + render(); + expect(screen.getByText('New transfer')).toBeInTheDocument(); + }); + + it('Renders "Update asset" with assetId', () => { + const item = { + updateAsset: { + assetId: 'asset123', + }, + }; + render( + + + + + + ); + expect(screen.getByText('Update asset')).toBeInTheDocument(); + expect(screen.getByText('asset123')).toBeInTheDocument(); + }); + + it('Renders "Update asset" even if assetId is not set', () => { + const item = { + updateAsset: { + assetId: undefined, + }, + }; + render(); + expect(screen.getByText('Update asset')).toBeInTheDocument(); + }); + + it('Renders "Update market state" with marketId', () => { + const item = { + updateMarketState: { + changes: { + marketId: 'market123', + }, + }, + }; + render( + + + + + + ); + expect(screen.getByText('Update market state')).toBeInTheDocument(); + expect(screen.getByText('market123')).toBeInTheDocument(); + }); + + it('Renders "Update market state" even if marketId is not set', () => { + const item = { + updateMarketState: { + changes: { + marketId: undefined, + }, + }, + }; + render( + + + + + + ); + expect(screen.getByText('Update market state')).toBeInTheDocument(); + }); + + it('Renders "Update network parameter" with parameter', () => { + const item = { + updateNetworkParameter: { + changes: { + key: 'parameter123', + }, + }, + }; + render( + + + + + + ); + expect(screen.getByText('Update network parameter')).toBeInTheDocument(); + expect(screen.getByText('parameter123')).toBeInTheDocument(); + }); + + it('Renders "Update network parameter" even if parameter is not set', () => { + const item = { + updateNetworkParameter: { + changes: { + key: undefined, + }, + }, + }; + render(); + expect(screen.getByText('Update network parameter')).toBeInTheDocument(); + }); + + it('Renders "Update referral program"', () => { + const item = { + updateReferralProgram: {}, + }; + render(); + expect(screen.getByText('Update referral program')).toBeInTheDocument(); + }); + + it('Renders "Update spot market" with marketId', () => { + const item = { + updateSpotMarket: { + marketId: 'market123', + }, + }; + render( + + + + + + ); + expect(screen.getByText('Update spot market')).toBeInTheDocument(); + expect(screen.getByText('market123')).toBeInTheDocument(); + }); + + it('Renders "Update spot market" even if marketId is not set', () => { + const item = { + updateSpotMarket: { + marketId: undefined, + }, + }; + render( + + + + + + ); + expect(screen.getByText('Update spot market')).toBeInTheDocument(); + }); + it('Renders "Update market" with marketId', () => { + const item = { + updateMarket: { + marketId: 'market123', + }, + }; + render( + + + + + + ); + expect(screen.getByText('Update market')).toBeInTheDocument(); + expect(screen.getByText('market123')).toBeInTheDocument(); + }); + + it('Renders "Update market" even if marketId is not set', () => { + const item = { + updateMarket: { + marketId: undefined, + }, + }; + render( + + + + + + ); + expect(screen.getByText('Update market')).toBeInTheDocument(); + }); + + it('Renders "Update volume discount program"', () => { + const item = { + updateVolumeDiscountProgram: {}, + }; + render(); + expect( + screen.getByText('Update volume discount program') + ).toBeInTheDocument(); + }); +}); diff --git a/apps/explorer/src/app/components/txs/details/proposal/batch-item.tsx b/apps/explorer/src/app/components/txs/details/proposal/batch-item.tsx new file mode 100644 index 000000000..8fc80e92c --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/proposal/batch-item.tsx @@ -0,0 +1,87 @@ +import { t } from '@vegaprotocol/i18n'; +import { AssetLink, MarketLink, NetworkParameterLink } from '../../../links'; +import type { components } from '../../../../../types/explorer'; +import Hash from '../../../links/hash'; + +type Item = components['schemas']['vegaBatchProposalTermsChange']; + +export interface BatchItemProps { + item: Item; +} + +/** + * Produces a one line summary for an item in a batch proposal. Could + * easily be adapted to summarise individual proposals, but there is no + * place for that yet. + * + * Details (like IDs) should be shown and linked if available, but handled + * if not available. This is adequate as the ProposalSummary component contains + * a JSON viewer for the full proposal. + */ +export const BatchItem = ({ item }: BatchItemProps) => { + if (item.cancelTransfer) { + const transferId = item?.cancelTransfer?.changes?.transferId || false; + return ( + + {t('Cancel transfer')}  + {transferId && ( + + )} + + ); + } else if (item.newFreeform) { + return {t('New freeform proposal')}; + } else if (item.newMarket) { + return {t('New market')}; + } else if (item.newSpotMarket) { + return {t('New spot market')}; + } else if (item.newTransfer) { + return {t('New transfer')}; + } else if (item.updateAsset) { + const assetId = item?.updateAsset?.assetId || false; + return ( + + {t('Update asset')} + {assetId && } + + ); + } else if (item.updateMarket) { + const marketId = item?.updateMarket?.marketId || false; + return ( + + {t('Update market')}{' '} + {marketId && } + + ); + } else if (item.updateMarketState) { + const marketId = item?.updateMarketState?.changes?.marketId || false; + return ( + + {t('Update market state')} + {marketId && } + + ); + } else if (item.updateNetworkParameter) { + const param = item?.updateNetworkParameter?.changes?.key || false; + return ( + + {t('Update network parameter')} + {param && } + + ); + } else if (item.updateReferralProgram) { + return {t('Update referral program')}; + } else if (item.updateSpotMarket) { + const marketId = item?.updateSpotMarket?.marketId || ''; + return ( + + {t('Update spot market')} + + + ); + } else if (item.updateVolumeDiscountProgram) { + return {t('Update volume discount program')}; + } + + return {t('Unknown proposal type')}; +}; diff --git a/apps/explorer/src/app/components/txs/details/proposal/summary.tsx b/apps/explorer/src/app/components/txs/details/proposal/summary.tsx index 48c71cc6c..0972e3d07 100644 --- a/apps/explorer/src/app/components/txs/details/proposal/summary.tsx +++ b/apps/explorer/src/app/components/txs/details/proposal/summary.tsx @@ -1,6 +1,4 @@ -import type { ProposalTerms } from '../tx-proposal'; import { useState } from 'react'; -import type { components } from '../../../../../types/explorer'; import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog'; import ProposalLink from '../../../links/proposal-link/proposal-link'; import truncate from 'lodash/truncate'; @@ -9,7 +7,12 @@ import ReactMarkdown from 'react-markdown'; import { ProposalDate } from './proposal-date'; import { t } from '@vegaprotocol/i18n'; +import type { ProposalTerms } from '../tx-proposal'; +import type { components } from '../../../../../types/explorer'; +import { BatchItem } from './batch-item'; + type Rationale = components['schemas']['vegaProposalRationale']; +type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes']; type ProposalTermsDialog = { open: boolean; @@ -21,6 +24,7 @@ interface ProposalSummaryProps { id: string; rationale?: Rationale; terms?: ProposalTerms; + batch?: Batch; } /** @@ -31,6 +35,7 @@ export const ProposalSummary = ({ id, rationale, terms, + batch, }: ProposalSummaryProps) => { const [dialog, setDialog] = useState({ open: false, @@ -72,6 +77,18 @@ export const ProposalSummary = ({
)} + {batch && ( +
+

{t('Changes')}

+
    + {batch.map((change, index) => ( +
  1. + +
  2. + ))} +
+
+ )}