From ad6f0c5798bafd6cb7027e006e612cfe4d279442 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Wed, 6 Mar 2024 10:47:16 +0000 Subject: [PATCH] feat(trading): add and show key aliases (#5819) --- .../components/profile-dialog/index.ts | 1 + .../profile-dialog/profile-dialog.tsx | 150 ++++++++++++++++++ .../PartyProfiles.graphql | 14 ++ .../__generated__/PartyProfiles.ts | 57 +++++++ .../vega-wallet-connect-button.spec.tsx | 64 ++++++-- .../vega-wallet-connect-button.tsx | 90 +++++++---- apps/trading/pages/dialogs-container.tsx | 2 + apps/trading/stores/profile-dialog-store.ts | 19 +++ libs/i18n/src/locales/en/trading.json | 10 ++ .../src/hooks/use-simple-transaction.ts | 7 + libs/wallet/src/transaction-types.ts | 11 +- 11 files changed, 381 insertions(+), 44 deletions(-) create mode 100644 apps/trading/components/profile-dialog/index.ts create mode 100644 apps/trading/components/profile-dialog/profile-dialog.tsx create mode 100644 apps/trading/components/vega-wallet-connect-button/PartyProfiles.graphql create mode 100644 apps/trading/components/vega-wallet-connect-button/__generated__/PartyProfiles.ts create mode 100644 apps/trading/stores/profile-dialog-store.ts diff --git a/apps/trading/components/profile-dialog/index.ts b/apps/trading/components/profile-dialog/index.ts new file mode 100644 index 000000000..209d23d12 --- /dev/null +++ b/apps/trading/components/profile-dialog/index.ts @@ -0,0 +1 @@ +export { ProfileDialog } from './profile-dialog'; diff --git a/apps/trading/components/profile-dialog/profile-dialog.tsx b/apps/trading/components/profile-dialog/profile-dialog.tsx new file mode 100644 index 000000000..1e5662229 --- /dev/null +++ b/apps/trading/components/profile-dialog/profile-dialog.tsx @@ -0,0 +1,150 @@ +import { + Dialog, + FormGroup, + Input, + InputError, + Intent, + TradingButton, +} from '@vegaprotocol/ui-toolkit'; +import { useProfileDialogStore } from '../../stores/profile-dialog-store'; +import { useForm } from 'react-hook-form'; +import { useT } from '../../lib/use-t'; +import { useRequired } from '@vegaprotocol/utils'; +import { + useSimpleTransaction, + type Status, + useVegaWallet, +} from '@vegaprotocol/wallet-react'; +import { + usePartyProfilesQuery, + type PartyProfilesQuery, +} from '../vega-wallet-connect-button/__generated__/PartyProfiles'; + +export const ProfileDialog = () => { + const t = useT(); + const { pubKeys } = useVegaWallet(); + const { data, refetch } = usePartyProfilesQuery({ + variables: { partyIds: pubKeys.map((pk) => pk.publicKey) }, + skip: pubKeys.length <= 0, + }); + const open = useProfileDialogStore((store) => store.open); + const pubKey = useProfileDialogStore((store) => store.pubKey); + const setOpen = useProfileDialogStore((store) => store.setOpen); + + const { send, status, error, reset } = useSimpleTransaction({ + onSuccess: () => { + refetch(); + }, + }); + + const profileEdge = data?.partiesProfilesConnection?.edges.find( + (e) => e.node.partyId === pubKey + ); + + const sendTx = (field: FormFields) => { + send({ + updatePartyProfile: { + alias: field.alias, + metadata: [], + }, + }); + }; + + return ( + { + setOpen(undefined); + reset(); + }} + title={t('Edit profile')} + > + + + ); +}; + +interface FormFields { + alias: string; +} + +type Profile = NonNullable< + PartyProfilesQuery['partiesProfilesConnection'] +>['edges'][number]['node']; + +const ProfileForm = ({ + profile, + onSubmit, + status, + error, +}: { + profile: Profile | undefined; + onSubmit: (fields: FormFields) => void; + status: Status; + error: string | undefined; +}) => { + const t = useT(); + const required = useRequired(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + alias: profile?.alias, + }, + }); + + const renderButtonText = () => { + if (status === 'requested') { + return t('Confirm in wallet...'); + } + + if (status === 'pending') { + return t('Confirming transaction...'); + } + + return t('Submit'); + }; + + const errorMessage = errors.alias?.message || error; + + return ( +
+ + + {errorMessage && ( + +

+ {errorMessage} +

+
+ )} + + {status === 'confirmed' && ( +

+ {t('Profile updated')} +

+ )} +
+ + {renderButtonText()} + +
+ ); +}; diff --git a/apps/trading/components/vega-wallet-connect-button/PartyProfiles.graphql b/apps/trading/components/vega-wallet-connect-button/PartyProfiles.graphql new file mode 100644 index 000000000..6bfa72001 --- /dev/null +++ b/apps/trading/components/vega-wallet-connect-button/PartyProfiles.graphql @@ -0,0 +1,14 @@ +query PartyProfiles($partyIds: [ID!]) { + partiesProfilesConnection(ids: $partyIds) { + edges { + node { + partyId + alias + metadata { + key + value + } + } + } + } +} diff --git a/apps/trading/components/vega-wallet-connect-button/__generated__/PartyProfiles.ts b/apps/trading/components/vega-wallet-connect-button/__generated__/PartyProfiles.ts new file mode 100644 index 000000000..ef039cb93 --- /dev/null +++ b/apps/trading/components/vega-wallet-connect-button/__generated__/PartyProfiles.ts @@ -0,0 +1,57 @@ +import * as Types from '@vegaprotocol/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type PartyProfilesQueryVariables = Types.Exact<{ + partyIds?: Types.InputMaybe | Types.Scalars['ID']>; +}>; + + +export type PartyProfilesQuery = { __typename?: 'Query', partiesProfilesConnection?: { __typename?: 'PartiesProfilesConnection', edges: Array<{ __typename?: 'PartyProfileEdge', node: { __typename?: 'PartyProfile', partyId: string, alias: string, metadata: Array<{ __typename?: 'Metadata', key: string, value: string }> } }> } | null }; + + +export const PartyProfilesDocument = gql` + query PartyProfiles($partyIds: [ID!]) { + partiesProfilesConnection(ids: $partyIds) { + edges { + node { + partyId + alias + metadata { + key + value + } + } + } + } +} + `; + +/** + * __usePartyProfilesQuery__ + * + * To run a query within a React component, call `usePartyProfilesQuery` and pass it any options that fit your needs. + * When your component renders, `usePartyProfilesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = usePartyProfilesQuery({ + * variables: { + * partyIds: // value for 'partyIds' + * }, + * }); + */ +export function usePartyProfilesQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(PartyProfilesDocument, options); + } +export function usePartyProfilesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(PartyProfilesDocument, options); + } +export type PartyProfilesQueryHookResult = ReturnType; +export type PartyProfilesLazyQueryHookResult = ReturnType; +export type PartyProfilesQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.spec.tsx b/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.spec.tsx index 876c34091..a3e235b87 100644 --- a/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.spec.tsx +++ b/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.spec.tsx @@ -1,21 +1,57 @@ -import { act, fireEvent, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen, within } from '@testing-library/react'; import { VegaWalletConnectButton } from './vega-wallet-connect-button'; -import { truncateByChars } from '@vegaprotocol/utils'; import userEvent from '@testing-library/user-event'; import { mockConfig, MockedWalletProvider, } from '@vegaprotocol/wallet-react/testing'; +import { MockedProvider, type MockedResponse } from '@apollo/react-testing'; +import { + PartyProfilesDocument, + type PartyProfilesQuery, +} from './__generated__/PartyProfiles'; jest.mock('../../lib/hooks/use-get-current-route-id', () => ({ useGetCurrentRouteId: jest.fn().mockReturnValue('current-route-id'), })); +const key = { publicKey: '123456__123456', name: 'test' }; +const key2 = { publicKey: 'abcdef__abcdef', name: 'test2' }; +const keys = [key, key2]; +const keyProfile = { + __typename: 'PartyProfile' as const, + partyId: key.publicKey, + alias: `${key.name} alias`, + metadata: [], +}; + const renderComponent = (mockOnClick = jest.fn()) => { + const partyProfilesMock: MockedResponse = { + request: { + query: PartyProfilesDocument, + variables: { partyIds: keys.map((k) => k.publicKey) }, + }, + result: { + data: { + partiesProfilesConnection: { + __typename: 'PartiesProfilesConnection', + edges: [ + { + __typename: 'PartyProfileEdge', + node: keyProfile, + }, + ], + }, + }, + }, + }; + return ( - - - + + + + + ); }; @@ -43,10 +79,6 @@ describe('VegaWalletConnectButton', () => { }); it('should open dropdown and refresh keys when connected', async () => { - const key = { publicKey: '123456__123456', name: 'test' }; - const key2 = { publicKey: 'abcdef__abcdef', name: 'test2' }; - const keys = [key, key2]; - mockConfig.store.setState({ status: 'connected', keys, @@ -61,14 +93,22 @@ describe('VegaWalletConnectButton', () => { expect(screen.queryByTestId('connect-vega-wallet')).not.toBeInTheDocument(); const button = screen.getByTestId('manage-vega-wallet'); - expect(button).toHaveTextContent(truncateByChars(key.publicKey)); + expect(button).toHaveTextContent(key.name); fireEvent.click(button); expect(await screen.findByRole('menu')).toBeInTheDocument(); - expect(await screen.findAllByRole('menuitemradio')).toHaveLength( - keys.length + const menuItems = await screen.findAllByRole('menuitemradio'); + expect(menuItems).toHaveLength(keys.length); + + expect(within(menuItems[0]).getByTestId('alias')).toHaveTextContent( + keyProfile.alias ); + + expect(within(menuItems[1]).getByTestId('alias')).toHaveTextContent( + 'No alias' + ); + expect(refreshKeys).toHaveBeenCalled(); fireEvent.click(screen.getByTestId(`key-${key2.publicKey}`)); diff --git a/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx b/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx index 1f610a784..31f8b0358 100644 --- a/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx +++ b/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx @@ -14,6 +14,7 @@ import { TradingDropdownItem, TradingDropdownRadioItem, TradingDropdownItemIndicator, + Tooltip, } from '@vegaprotocol/ui-toolkit'; import { isBrowserWalletInstalled, type Key } from '@vegaprotocol/wallet'; import { useDialogStore, useVegaWallet } from '@vegaprotocol/wallet-react'; @@ -22,6 +23,8 @@ import classNames from 'classnames'; import { ViewType, useSidebar } from '../sidebar'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useT } from '../../lib/use-t'; +import { usePartyProfilesQuery } from './__generated__/PartyProfiles'; +import { useProfileDialogStore } from '../../stores/profile-dialog-store'; export const VegaWalletConnectButton = ({ intent = Intent.None, @@ -68,10 +71,10 @@ export const VegaWalletConnectButton = ({ {activeKey ? ( <> {activeKey && ( - {activeKey.name} + + {activeKey.name ? activeKey.name : t('Unnamed key')} + )} - {' | '} - {truncateByChars(activeKey.publicKey)} ) : ( <>{'Select key'} @@ -88,20 +91,11 @@ export const VegaWalletConnectButton = ({ onEscapeKeyDown={() => setDropdownOpen(false)} >
- { - selectPubKey(value); - }} - > - {pubKeys.map((pk) => ( - - ))} - + {!isReadOnly && ( { +const KeypairRadioGroup = ({ + pubKey, + pubKeys, + onSelect, +}: { + pubKey: string | undefined; + pubKeys: Key[]; + onSelect: (pubKey: string) => void; +}) => { + const { data } = usePartyProfilesQuery({ + variables: { partyIds: pubKeys.map((pk) => pk.publicKey) }, + skip: pubKeys.length <= 0, + }); + + return ( + + {pubKeys.map((pk) => { + const profile = data?.partiesProfilesConnection?.edges.find( + (e) => e.node.partyId === pk.publicKey + ); + return ( + + ); + })} + + ); +}; + +const KeypairItem = ({ pk, alias }: { pk: Key; alias: string | undefined }) => { const t = useT(); const [copied, setCopied] = useCopyTimeout(); + const setOpen = useProfileDialogStore((store) => store.setOpen); return ( -
- - {pk.name} +
+
+ {pk.name ? pk.name : t('Unnamed key')} {' | '} - {truncateByChars(pk.publicKey)} - - + + {truncateByChars(pk.publicKey, 3, 3)} + setCopied(true)}> {copied && {t('Copied')}} - +
+
+ + + +
diff --git a/apps/trading/pages/dialogs-container.tsx b/apps/trading/pages/dialogs-container.tsx index d59499164..f15a3bc03 100644 --- a/apps/trading/pages/dialogs-container.tsx +++ b/apps/trading/pages/dialogs-container.tsx @@ -8,6 +8,7 @@ import { } from '@vegaprotocol/web3'; import { WelcomeDialog } from '../components/welcome-dialog'; import { VegaWalletConnectDialog } from '../components/vega-wallet-connect-dialog'; +import { ProfileDialog } from '../components/profile-dialog'; const DialogsContainer = () => { const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore(); @@ -24,6 +25,7 @@ const DialogsContainer = () => { + ); }; diff --git a/apps/trading/stores/profile-dialog-store.ts b/apps/trading/stores/profile-dialog-store.ts new file mode 100644 index 000000000..de6982fc9 --- /dev/null +++ b/apps/trading/stores/profile-dialog-store.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; + +interface ProfileDialogStore { + open: boolean; + pubKey: string | undefined; + setOpen: (pubKey: string | undefined) => void; +} + +export const useProfileDialogStore = create((set) => ({ + open: false, + pubKey: undefined, + setOpen: (pubKey) => { + if (pubKey) { + set({ open: true, pubKey }); + } else { + set({ open: false, pubKey: undefined }); + } + }, +})); diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json index b2dc1a8d5..f62789226 100644 --- a/libs/i18n/src/locales/en/trading.json +++ b/libs/i18n/src/locales/en/trading.json @@ -86,6 +86,7 @@ "Docs": "Docs", "Earn commission & stake rewards": "Earn commission & stake rewards", "Earned by me": "Earned by me", + "Edit alias": "Edit alias", "Eligible teams": "Eligible teams", "Enactment date reached and usual auction exit checks pass": "Enactment date reached and usual auction exit checks pass", "[empty]": "[empty]", @@ -162,6 +163,7 @@ "Joined": "Joined", "Joined at": "Joined at", "Joined epoch": "Joined epoch", + "Key name": "Key name", "gameCount_one": "Last game result", "gameCount_other": "Last {{count}} game results", "Learn about providing liquidity": "Learn about providing liquidity", @@ -194,6 +196,7 @@ "My liquidity provision": "My liquidity provision", "My trading fees": "My trading fees", "Name": "Name", + "No alias": "No alias", "No closed orders": "No closed orders", "No data": "No data", "No deposits": "No deposits", @@ -227,6 +230,7 @@ "Not connected": "Not connected", "Number of epochs after distribution to delay vesting of rewards by": "Number of epochs after distribution to delay vesting of rewards by", "Number of traders": "Number of traders", + "On-change alias": "On-change alias", "Open": "Open", "Open a position": "Open a position", "Open markets": "Open markets", @@ -249,6 +253,7 @@ "Portfolio": "Portfolio", "Positions": "Positions", "Price": "Price", + "Profile updated": "Profile updated", "Program ends:": "Program ends:", "Propose a new market": "Propose a new market", "Proposed final price is {{price}} {{assetSymbol}}.": "Proposed final price is {{price}} {{assetSymbol}}.", @@ -289,6 +294,7 @@ "Search": "Search", "See all markets": "See all markets", "Select market": "Select market", + "Set party alias": "Set party alias", "Settings": "Settings", "Settlement asset": "Settlement asset", "Settlement date": "Settlement date", @@ -311,6 +317,7 @@ "Stop": "Stop", "Stop orders": "Stop orders", "Streak reward multiplier": "Streak reward multiplier", + "Submit": "Submit", "Successor of a market": "Successor of a market", "Successors to this market have been proposed": "Successors to this market have been proposed", "Supplied stake": "Supplied stake", @@ -371,6 +378,7 @@ "Staking rewards": "Staking rewards", "Unknown": "Unknown", "Unknown settlement date": "Unknown settlement date", + "Unnamed key": "Unnamed key", "Update team": "Update team", "URL": "URL", "Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team", @@ -407,8 +415,10 @@ "You will no longer be able to hold a position on this market when it closes in {{duration}}.": "You will no longer be able to hold a position on this market when it closes in {{duration}}.", "Your code has been rejected": "Your code has been rejected", "Your identity is always anonymous on Vega": "Your identity is always anonymous on Vega", + "Your key's private name, can be changed in your wallet": "Your key's private name, can be changed in your wallet", "Your referral code": "Your referral code", "Your tier": "Your tier", + "Your public alias, stored on chain": "Your public alias, stored on chain", "checkOutProposalsAndVote": "Check out the terms of the proposals and vote:", "checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:", "checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:", diff --git a/libs/wallet-react/src/hooks/use-simple-transaction.ts b/libs/wallet-react/src/hooks/use-simple-transaction.ts index 299571cb5..1e9a55383 100644 --- a/libs/wallet-react/src/hooks/use-simple-transaction.ts +++ b/libs/wallet-react/src/hooks/use-simple-transaction.ts @@ -33,6 +33,12 @@ export const useSimpleTransaction = (opts?: Options) => { const [result, setResult] = useState(); const [error, setError] = useState(); + const reset = () => { + setStatus('idle'); + setResult(undefined); + setError(undefined); + }; + const send = async (tx: Transaction) => { if (!pubKey) { throw new Error('no pubKey'); @@ -114,5 +120,6 @@ export const useSimpleTransaction = (opts?: Options) => { error, status, send, + reset, }; }; diff --git a/libs/wallet/src/transaction-types.ts b/libs/wallet/src/transaction-types.ts index b07f16e11..11ade2045 100644 --- a/libs/wallet/src/transaction-types.ts +++ b/libs/wallet/src/transaction-types.ts @@ -492,6 +492,14 @@ export interface UpdateMarginMode { export interface UpdateMarginModeBody { updateMarginMode: UpdateMarginMode; } + +export interface UpdatePartyProfile { + updatePartyProfile: { + alias: string; + metadata: Array<{ key: string; value: string }>; + }; +} + export type Transaction = | UpdateMarginModeBody | StopOrdersSubmissionBody @@ -510,7 +518,8 @@ export type Transaction = | ApplyReferralCode | JoinTeam | CreateReferralSet - | UpdateReferralSet; + | UpdateReferralSet + | UpdatePartyProfile; export interface TransactionResponse { transactionHash: string;