From a3b1b927dcf20c5d558cbfa952b256f8d347e3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 16 Dec 2024 19:42:08 +0900 Subject: [PATCH] Generic chain key component, render Solana, Aptos and Starknet --- src/hooks/queries/useNonEvmAccountsQuery.ts | 12 +++ .../KeyManagement/KeyManagementView.tsx | 5 + .../KeyManagement/NonEVMKeyRow.test.tsx | 28 ++++++ src/screens/KeyManagement/NonEVMKeyRow.tsx | 26 ++++++ src/screens/KeyManagement/NonEVMKeys.test.tsx | 70 ++++++++++++++ src/screens/KeyManagement/NonEVMKeys.tsx | 62 +++++++++++++ .../KeyManagement/NonEVMKeysCard.test.tsx | 93 +++++++++++++++++++ src/screens/KeyManagement/NonEVMKeysCard.tsx | 62 +++++++++++++ 8 files changed, 358 insertions(+) create mode 100644 src/screens/KeyManagement/NonEVMKeyRow.test.tsx create mode 100644 src/screens/KeyManagement/NonEVMKeyRow.tsx create mode 100644 src/screens/KeyManagement/NonEVMKeys.test.tsx create mode 100644 src/screens/KeyManagement/NonEVMKeys.tsx create mode 100644 src/screens/KeyManagement/NonEVMKeysCard.test.tsx create mode 100644 src/screens/KeyManagement/NonEVMKeysCard.tsx diff --git a/src/hooks/queries/useNonEvmAccountsQuery.ts b/src/hooks/queries/useNonEvmAccountsQuery.ts index d9c16731..e349388d 100644 --- a/src/hooks/queries/useNonEvmAccountsQuery.ts +++ b/src/hooks/queries/useNonEvmAccountsQuery.ts @@ -13,9 +13,16 @@ export const SOLANA_KEYS_PAYLOAD__RESULTS_FIELDS = gql` } ` +export const STARKNET_KEYS_PAYLOAD__RESULTS_FIELDS = gql` + fragment StarknetKeysPayload_ResultsFields on StarkNetKey { + id + } +` + export const NON_EVM_KEYS_QUERY = gql` ${APTOS_KEYS_PAYLOAD__RESULTS_FIELDS} ${SOLANA_KEYS_PAYLOAD__RESULTS_FIELDS} + ${STARKNET_KEYS_PAYLOAD__RESULTS_FIELDS} query FetchNonEvmKeys { aptosKeys { results { @@ -27,6 +34,11 @@ export const NON_EVM_KEYS_QUERY = gql` ...SolanaKeysPayload_ResultsFields } } + starknetKeys { + results { + ...StarknetKeysPayload_ResultsFields + } + } } ` diff --git a/src/screens/KeyManagement/KeyManagementView.tsx b/src/screens/KeyManagement/KeyManagementView.tsx index fea29997..12e38e83 100644 --- a/src/screens/KeyManagement/KeyManagementView.tsx +++ b/src/screens/KeyManagement/KeyManagementView.tsx @@ -4,6 +4,7 @@ import Grid from '@material-ui/core/Grid' import Content from 'components/Content' import { EVMAccounts } from './EVMAccounts' +import { NonEVMKeys } from './NonEVMKeys' import { CSAKeys } from './CSAKeys' import { OCRKeys } from './OCRKeys' import { OCR2Keys } from './OCR2Keys' @@ -35,6 +36,10 @@ export const KeyManagementView: React.FC = ({ + + + + {isCSAKeysFeatureEnabled && } diff --git a/src/screens/KeyManagement/NonEVMKeyRow.test.tsx b/src/screens/KeyManagement/NonEVMKeyRow.test.tsx new file mode 100644 index 00000000..d3aa0f7a --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeyRow.test.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' + +import { render, screen } from 'support/test-utils' + +import { CSAKeyRow } from './CSAKeyRow' +import { buildCSAKey } from 'support/factories/gql/fetchCSAKeys' + +const { queryByText } = screen + +describe('CSAKeyRow', () => { + function renderComponent(csaKey: CsaKeysPayload_ResultsFields) { + render( + + + + +
, + ) + } + + it('renders a row', () => { + const csaKey = buildCSAKey() + + renderComponent(csaKey) + + expect(queryByText(csaKey.publicKey)).toBeInTheDocument() + }) +}) diff --git a/src/screens/KeyManagement/NonEVMKeyRow.tsx b/src/screens/KeyManagement/NonEVMKeyRow.tsx new file mode 100644 index 00000000..8fcd655b --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeyRow.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +import TableCell from '@material-ui/core/TableCell' +import TableRow from '@material-ui/core/TableRow' +import Typography from '@material-ui/core/Typography' + +import { CopyIconButton } from 'src/components/Copy/CopyIconButton' + +interface Props { + chainKey: any + fields: any[] +} + +export const NonEVMKeyRow: React.FC = ({ chainKey, fields }) => { + return ( + + {fields.map((field, idx) => ( + + + {chainKey[field.key]} {field.copy && ()} + + + ))} + + ) +} diff --git a/src/screens/KeyManagement/NonEVMKeys.test.tsx b/src/screens/KeyManagement/NonEVMKeys.test.tsx new file mode 100644 index 00000000..821263c8 --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeys.test.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' + +import { GraphQLError } from 'graphql' +import { renderWithRouter, screen } from 'support/test-utils' +import { MockedProvider, MockedResponse } from '@apollo/client/testing' + +import { CSAKeys, CSA_KEYS_QUERY } from './CSAKeys' +import { buildCSAKeys } from 'support/factories/gql/fetchCSAKeys' +import Notifications from 'pages/Notifications' +import { waitForLoading } from 'support/test-helpers/wait' + +const { findByText } = screen + +function renderComponent(mocks: MockedResponse[]) { + renderWithRouter( + <> + + + + + , + ) +} + +function fetchCSAKeysQuery( + csaKeys: ReadonlyArray, +) { + return { + request: { + query: CSA_KEYS_QUERY, + }, + result: { + data: { + csaKeys: { + results: csaKeys, + }, + }, + }, + } +} + +describe('CSAKeys', () => { + it('renders the page', async () => { + const payload = buildCSAKeys() + const mocks: MockedResponse[] = [fetchCSAKeysQuery(payload)] + + renderComponent(mocks) + + await waitForLoading() + + expect(await findByText(payload[0].publicKey)).toBeInTheDocument() + }) + + it('renders GQL query errors', async () => { + const mocks: MockedResponse[] = [ + { + request: { + query: CSA_KEYS_QUERY, + }, + result: { + errors: [new GraphQLError('Error!')], + }, + }, + ] + + renderComponent(mocks) + + expect(await findByText('Error!')).toBeInTheDocument() + }) +}) diff --git a/src/screens/KeyManagement/NonEVMKeys.tsx b/src/screens/KeyManagement/NonEVMKeys.tsx new file mode 100644 index 00000000..70fa0403 --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeys.tsx @@ -0,0 +1,62 @@ +import React from 'react' + +import { NonEVMKeysCard } from './NonEVMKeysCard' +import { useNonEvmAccountsQuery } from 'src/hooks/queries/useNonEvmAccountsQuery' + +const SCHEMAS = { + "aptosKeys": { + title: "Aptos", + fields: [{label: "Public Key", key: "id", copy: true}, {label: "Account", key: "account", copy: true}], + }, + "solanaKeys": { + title: "Solana", + fields: [{label: "Public Key", key: "id", copy: true}] + }, + "starknetKeys": { + title: "Starknet", + fields: [{label: "Public Key", key: "id", copy: true}] + } +} + +export const NonEVMKeys = () => { + const { data, loading, error } = useNonEvmAccountsQuery({ + fetchPolicy: 'cache-and-network', + }) + // TODO: + // const [createNonEVMKey] = useMutation( + // CREATE_NONEVM_KEY_MUTATION, + // ) + + const handleCreate = async () => { + // try { + // const result = await createNonEVMKey() + // + // const payload = result.data?.createNonEVMKey + // switch (payload?.__typename) { + // case 'CreateNonEVMKeySuccess': + // dispatch(notifySuccessMsg('NonEVM Key created')) + // + // refetch() + // + // break + // } + // } catch (e) { + // handleMutationError(e) + // } + } + + return ( + <> + {data && Object.entries(data).map( + ([key, chain]) => (typeof chain === 'object' && "results" in chain) && chain.results?.length > 0 && () + )} + + ) +} diff --git a/src/screens/KeyManagement/NonEVMKeysCard.test.tsx b/src/screens/KeyManagement/NonEVMKeysCard.test.tsx new file mode 100644 index 00000000..06c42a0c --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeysCard.test.tsx @@ -0,0 +1,93 @@ +import * as React from 'react' + +import { render, screen } from 'support/test-utils' + +import { buildCSAKeys } from 'support/factories/gql/fetchCSAKeys' +import { CSAKeysCard, Props as CSAKeysCardProps } from './CSAKeysCard' +import userEvent from '@testing-library/user-event' + +const { getByRole, queryByRole, queryByText } = screen + +function renderComponent(cardProps: CSAKeysCardProps) { + render() +} + +describe('CSAKeysCard', () => { + let handleCreate: jest.Mock + + beforeEach(() => { + handleCreate = jest.fn() + }) + + it('renders the keys', () => { + const csaKeys = buildCSAKeys() + + renderComponent({ + loading: false, + data: { + csaKeys: { + results: csaKeys, + }, + }, + onCreate: handleCreate, + }) + + expect(queryByText(csaKeys[0].publicKey)).toBeInTheDocument() + expect(queryByText(csaKeys[1].publicKey)).toBeInTheDocument() + + // Button should not appear when there are keys present + expect(queryByRole('button', { name: /new csa key/i })).toBeNull() + }) + + it('renders no content', () => { + renderComponent({ + loading: false, + data: { + csaKeys: { + results: [], + }, + }, + onCreate: handleCreate, + }) + + expect(queryByText('No entries to show')).toBeInTheDocument() + + // Button should appear when there are no keys + expect(queryByRole('button', { name: /new csa key/i })).toBeInTheDocument() + }) + + it('renders a loading spinner', () => { + renderComponent({ + loading: true, + onCreate: handleCreate, + }) + + expect(queryByRole('progressbar')).toBeInTheDocument() + }) + + it('renders an error message', () => { + renderComponent({ + loading: false, + errorMsg: 'error message', + onCreate: handleCreate, + }) + + expect(queryByText('error message')).toBeInTheDocument() + }) + + it('calls onCreate', () => { + renderComponent({ + loading: false, + data: { + csaKeys: { + results: [], + }, + }, + onCreate: handleCreate, + }) + + userEvent.click(getByRole('button', { name: /new csa key/i })) + + expect(handleCreate).toHaveBeenCalled() + }) +}) diff --git a/src/screens/KeyManagement/NonEVMKeysCard.tsx b/src/screens/KeyManagement/NonEVMKeysCard.tsx new file mode 100644 index 00000000..1547fcb1 --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeysCard.tsx @@ -0,0 +1,62 @@ +import React from 'react' + +import Button from '@material-ui/core/Button' +import Card from '@material-ui/core/Card' +import CardHeader from '@material-ui/core/CardHeader' +import Table from '@material-ui/core/Table' +import TableBody from '@material-ui/core/TableBody' +import TableCell from '@material-ui/core/TableCell' +import TableHead from '@material-ui/core/TableHead' + +import { NonEVMKeyRow } from './NonEVMKeyRow' +import { ErrorRow } from 'src/components/TableRow/ErrorRow' +import { LoadingRow } from 'src/components/TableRow/LoadingRow' +import { NoContentRow } from 'src/components/TableRow/NoContentRow' + +export interface Props { + loading: boolean + schema: {title: string, fields: any[]} + data?: any + errorMsg?: string + onCreate: () => void +} + +export const NonEVMKeysCard: React.FC = ({ + schema, + data, + errorMsg, + loading, + onCreate, +}) => { + return ( + + + New Key + + ) + } + title={`${schema.title} Keys`} + subheader={`Manage your ${schema.title} Keys`} + /> + + + {schema.fields.map((field, idx) => ( + {field.label} + ))} + + + + + + + {data?.results?.map((key: string, idx: number) => ( + + ))} + +
+
+ ) +}