Skip to content

Commit

Permalink
Generic chain key component, render Solana, Aptos and Starknet
Browse files Browse the repository at this point in the history
  • Loading branch information
archseer committed Dec 17, 2024
1 parent 6b5010c commit a3b1b92
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 0 deletions.
12 changes: 12 additions & 0 deletions src/hooks/queries/useNonEvmAccountsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,6 +34,11 @@ export const NON_EVM_KEYS_QUERY = gql`
...SolanaKeysPayload_ResultsFields
}
}
starknetKeys {
results {
...StarknetKeysPayload_ResultsFields
}
}
}
`

Expand Down
5 changes: 5 additions & 0 deletions src/screens/KeyManagement/KeyManagementView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -35,6 +36,10 @@ export const KeyManagementView: React.FC<Props> = ({
<EVMAccounts />
</Grid>

<Grid item xs={12}>
<NonEVMKeys />
</Grid>

<Grid item xs={12}>
{isCSAKeysFeatureEnabled && <CSAKeys />}
</Grid>
Expand Down
28 changes: 28 additions & 0 deletions src/screens/KeyManagement/NonEVMKeyRow.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<table>
<tbody>
<CSAKeyRow csaKey={csaKey} />
</tbody>
</table>,
)
}

it('renders a row', () => {
const csaKey = buildCSAKey()

renderComponent(csaKey)

expect(queryByText(csaKey.publicKey)).toBeInTheDocument()
})
})
26 changes: 26 additions & 0 deletions src/screens/KeyManagement/NonEVMKeyRow.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ chainKey, fields }) => {
return (
<TableRow hover>
{fields.map((field, idx) => (
<TableCell key={idx}>
<Typography variant="body1">
{chainKey[field.key]} {field.copy && (<CopyIconButton data={chainKey[field.key]} />)}
</Typography>
</TableCell>
))}
</TableRow>
)
}
70 changes: 70 additions & 0 deletions src/screens/KeyManagement/NonEVMKeys.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<>
<Notifications />
<MockedProvider mocks={mocks} addTypename={false}>
<CSAKeys />
</MockedProvider>
</>,
)
}

function fetchCSAKeysQuery(
csaKeys: ReadonlyArray<CsaKeysPayload_ResultsFields>,
) {
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()
})
})
62 changes: 62 additions & 0 deletions src/screens/KeyManagement/NonEVMKeys.tsx
Original file line number Diff line number Diff line change
@@ -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<CreateCsaKey, CreateCsaKeyVariables>(
// 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 && (<NonEVMKeysCard
loading={loading}
schema={SCHEMAS[key as keyof typeof SCHEMAS] || {title: key, fields: [{label: "Public Key", key: "id", copy: true}]}}
data={chain}
errorMsg={error?.message}
onCreate={handleCreate}
key={key}
/>)
)}
</>
)
}
93 changes: 93 additions & 0 deletions src/screens/KeyManagement/NonEVMKeysCard.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<CSAKeysCard {...cardProps} />)
}

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()
})
})
62 changes: 62 additions & 0 deletions src/screens/KeyManagement/NonEVMKeysCard.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({
schema,
data,
errorMsg,
loading,
onCreate,
}) => {
return (
<Card>
<CardHeader
action={
data?.results?.length === 0 && (
<Button variant="outlined" color="primary" onClick={onCreate}>
New Key
</Button>
)
}
title={`${schema.title} Keys`}
subheader={`Manage your ${schema.title} Keys`}
/>
<Table>
<TableHead>
{schema.fields.map((field, idx) => (
<TableCell key={idx}>{field.label}</TableCell>
))}
</TableHead>
<TableBody>
<LoadingRow visible={loading} />
<NoContentRow visible={data?.results?.length === 0} />
<ErrorRow msg={errorMsg} />

{data?.results?.map((key: string, idx: number) => (
<NonEVMKeyRow chainKey={key} fields={schema.fields} key={idx} />
))}
</TableBody>
</Table>
</Card>
)
}

0 comments on commit a3b1b92

Please sign in to comment.