From e623f73d1401aa6a39687f7e7e5ffde33680150c Mon Sep 17 00:00:00 2001 From: Graham Goh Date: Sat, 12 Oct 2024 10:51:53 +1100 Subject: [PATCH] feat(chainconfig): support aptos Allow APTOS to be selected in the chainconfig modal instead of just EVM JIRA: https://smartcontract-it.atlassian.net/browse/DPA-1155 --- .changeset/serious-turtles-fold.md | 5 + .../Form/ChainConfigurationForm.test.tsx | 216 +++++++++++------- .../Form/ChainConfigurationForm.tsx | 62 +++-- src/components/Form/ChainTypes.ts | 7 + .../queries/useAptosAccountsQuery.test.tsx | 63 +++++ src/hooks/queries/useAptosAccountsQuery.ts | 24 ++ src/hooks/queries/useChainsQuery.ts | 1 + .../FeedsManager/EditSupportedChainDialog.tsx | 24 +- .../FeedsManager/NewSupportedChainDialog.tsx | 24 +- support/factories/gql/fetchChains.ts | 1 + 10 files changed, 310 insertions(+), 117 deletions(-) create mode 100644 .changeset/serious-turtles-fold.md create mode 100644 src/components/Form/ChainTypes.ts create mode 100644 src/hooks/queries/useAptosAccountsQuery.test.tsx create mode 100644 src/hooks/queries/useAptosAccountsQuery.ts diff --git a/.changeset/serious-turtles-fold.md b/.changeset/serious-turtles-fold.md new file mode 100644 index 00000000..4e0f3f05 --- /dev/null +++ b/.changeset/serious-turtles-fold.md @@ -0,0 +1,5 @@ +--- +'@smartcontractkit/operator-ui': minor +--- + +Support APTOS in chain config diff --git a/src/components/Form/ChainConfigurationForm.test.tsx b/src/components/Form/ChainConfigurationForm.test.tsx index 6a94b41a..225189e6 100644 --- a/src/components/Form/ChainConfigurationForm.test.tsx +++ b/src/components/Form/ChainConfigurationForm.test.tsx @@ -1,45 +1,25 @@ import * as React from 'react' -import { render, screen } from 'support/test-utils' import userEvent from '@testing-library/user-event' +import { render, screen, waitFor } from 'support/test-utils' import { ChainConfigurationForm, FormValues } from './ChainConfigurationForm' +import { ChainTypes } from './ChainTypes' const { getByRole, findByTestId } = screen describe('ChainConfigurationForm', () => { it('validates top level input', async () => { const handleSubmit = jest.fn() - const initialValues: FormValues = { - chainID: '', - chainType: '', - accountAddr: '', - adminAddr: '', - fluxMonitorEnabled: false, - ocr1Enabled: false, - ocr1IsBootstrap: false, - ocr1Multiaddr: '', - ocr1P2PPeerID: '', - ocr1KeyBundleID: '', - ocr2Enabled: false, - ocr2IsBootstrap: false, - ocr2Multiaddr: '', - ocr2P2PPeerID: '', - ocr2KeyBundleID: '', - ocr2CommitPluginEnabled: false, - ocr2ExecutePluginEnabled: false, - ocr2MedianPluginEnabled: false, - ocr2MercuryPluginEnabled: false, - ocr2RebalancerPluginEnabled: false, - ocr2ForwarderAddress: '', - } + const initialValues = emptyFormValues() render( { it('validates OCR input', async () => { const handleSubmit = jest.fn() - const initialValues: FormValues = { - chainID: '', - chainType: '', - accountAddr: '', - accountAddrPubKey: '', - adminAddr: '', - fluxMonitorEnabled: false, - ocr1Enabled: false, - ocr1IsBootstrap: false, - ocr1Multiaddr: '', - ocr1P2PPeerID: '', - ocr1KeyBundleID: '', - ocr2Enabled: false, - ocr2IsBootstrap: false, - ocr2Multiaddr: '', - ocr2P2PPeerID: '', - ocr2KeyBundleID: '', - ocr2CommitPluginEnabled: false, - ocr2ExecutePluginEnabled: false, - ocr2MedianPluginEnabled: false, - ocr2MercuryPluginEnabled: false, - ocr2RebalancerPluginEnabled: false, - ocr2ForwarderAddress: '', - } + const initialValues = emptyFormValues() render( { it('validates OCR2 input', async () => { const handleSubmit = jest.fn() - const initialValues: FormValues = { - chainID: '', - chainType: '', - accountAddr: '', - accountAddrPubKey: '', - adminAddr: '', - fluxMonitorEnabled: false, - ocr1Enabled: false, - ocr1IsBootstrap: false, - ocr1Multiaddr: '', - ocr1P2PPeerID: '', - ocr1KeyBundleID: '', - ocr2Enabled: false, - ocr2IsBootstrap: false, - ocr2Multiaddr: '', - ocr2P2PPeerID: '', - ocr2KeyBundleID: '', - ocr2CommitPluginEnabled: false, - ocr2ExecutePluginEnabled: false, - ocr2MedianPluginEnabled: false, - ocr2MercuryPluginEnabled: false, - ocr2RebalancerPluginEnabled: false, - ocr2ForwarderAddress: '', - } + const initialValues = emptyFormValues() render( { await findByTestId('ocr2P2PPeerID-helper-text'), ).not.toHaveTextContent('Required') }) + + test('should able to create APTOS chain config', async () => { + const handleSubmit = jest.fn() + const initialValues = emptyFormValues() + initialValues.chainType = ChainTypes.EVM + initialValues.adminAddr = '0x1234567' + + const { container } = render( + handleSubmit(x)} + accountsEVM={[ + { + address: '0x1111', + chain: { + id: '1111', + }, + createdAt: '2021-10-06T00:00:00Z', + isDisabled: false, + }, + ]} + accountsAptos={[ + { + account: '0x123', + id: '2222', + }, + ]} + chains={[ + { + id: '1111', + enabled: true, + network: 'evm', + }, + { + id: '2222', + enabled: true, + network: 'aptos', + }, + ]} + p2pKeys={[]} + ocrKeys={[]} + ocr2Keys={[]} + showSubmit + />, + ) + + const chainType = getByRole('button', { name: 'EVM' }) + userEvent.click(chainType) + userEvent.click(getByRole('option', { name: 'APTOS' })) + await screen.findByRole('button', { name: 'APTOS' }) + + // no easy way to use react testing framework to do what i want, + // had to resort to using #id and querySelector + // formik does not seem to work well with react testing framework + const chainId = container.querySelector('#select-chainID') + expect(chainId).toBeInTheDocument() + // workaround ts lint warning - unable to use chainId! + chainId && userEvent.click(chainId) + userEvent.click(getByRole('option', { name: '2222' })) + await screen.findByRole('button', { name: '2222' }) + + const address = container.querySelector('#select-accountAddr') + expect(address).toBeInTheDocument() + address && userEvent.click(address) + userEvent.click(getByRole('option', { name: '0x123' })) + await screen.findByRole('button', { name: '0x123' }) + + await userEvent.click(getByRole('button', { name: /submit/i })) + + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalledWith({ + accountAddr: '0x123', + accountAddrPubKey: '', + adminAddr: '0x1234567', + chainID: '2222', + chainType: 'APTOS', + fluxMonitorEnabled: false, + ocr1Enabled: false, + ocr1IsBootstrap: false, + ocr1KeyBundleID: '', + ocr1Multiaddr: '', + ocr1P2PPeerID: '', + ocr2CommitPluginEnabled: false, + ocr2Enabled: false, + ocr2ExecutePluginEnabled: false, + ocr2ForwarderAddress: '', + ocr2IsBootstrap: false, + ocr2KeyBundleID: '', + ocr2MedianPluginEnabled: false, + ocr2MercuryPluginEnabled: false, + ocr2Multiaddr: '', + ocr2P2PPeerID: '', + ocr2RebalancerPluginEnabled: false, + }) + expect(handleSubmit).toHaveBeenCalledTimes(1) + }) + }) }) + +function emptyFormValues(): FormValues { + return { + chainID: '', + chainType: '', + accountAddr: '', + accountAddrPubKey: '', + adminAddr: '', + fluxMonitorEnabled: false, + ocr1Enabled: false, + ocr1IsBootstrap: false, + ocr1Multiaddr: '', + ocr1P2PPeerID: '', + ocr1KeyBundleID: '', + ocr2Enabled: false, + ocr2IsBootstrap: false, + ocr2Multiaddr: '', + ocr2P2PPeerID: '', + ocr2KeyBundleID: '', + ocr2CommitPluginEnabled: false, + ocr2ExecutePluginEnabled: false, + ocr2MedianPluginEnabled: false, + ocr2MercuryPluginEnabled: false, + ocr2RebalancerPluginEnabled: false, + ocr2ForwarderAddress: '', + } +} diff --git a/src/components/Form/ChainConfigurationForm.tsx b/src/components/Form/ChainConfigurationForm.tsx index b9956ab9..39b1410d 100644 --- a/src/components/Form/ChainConfigurationForm.tsx +++ b/src/components/Form/ChainConfigurationForm.tsx @@ -21,6 +21,7 @@ import { withStyles, } from '@material-ui/core/styles' import Typography from '@material-ui/core/Typography' +import { ChainTypes } from './ChainTypes' export type FormValues = { accountAddr: string @@ -111,13 +112,10 @@ const styles = (theme: Theme) => { // A custom account address field which clears the input based on the chain id // value changing, and also allows user to input their own value if none is available in the list. interface AccountAddrFieldProps extends FieldAttributes { - chainAccounts: { address: string }[] + addresses: string[] } -const AccountAddrField = ({ - chainAccounts, - ...props -}: AccountAddrFieldProps) => { +const AccountAddrField = ({ addresses, ...props }: AccountAddrFieldProps) => { const { values: { chainID, accountAddr }, setFieldValue, @@ -157,9 +155,9 @@ const AccountAddrField = ({ value={isCustom ? 'custom' : accountAddr} onChange={handleSelectChange} > - {chainAccounts.map((account) => ( - - {account.address} + {addresses.map((address) => ( + + {address} ))} @@ -204,8 +202,9 @@ export interface Props extends WithStyles { values: FormValues, formikHelpers: FormikHelpers, ) => void | Promise - chainIDs: string[] - accounts: ReadonlyArray + chains: ReadonlyArray + accountsEVM: ReadonlyArray + accountsAptos: ReadonlyArray p2pKeys: ReadonlyArray ocrKeys: ReadonlyArray ocr2Keys: ReadonlyArray @@ -221,8 +220,9 @@ export const ChainConfigurationForm = withStyles(styles)( innerRef, initialValues, onSubmit, - chainIDs = [], - accounts = [], + chains = [], + accountsEVM = [], + accountsAptos = [], p2pKeys = [], ocrKeys = [], ocr2Keys = [], @@ -236,9 +236,17 @@ export const ChainConfigurationForm = withStyles(styles)( onSubmit={onSubmit} > {({ values }) => { - const chainAccounts = accounts.filter( - (acc) => acc.chain.id == values.chainID && !acc.isDisabled, - ) + let chainAccountAddresses: string[] = [] + if (values.chainType === ChainTypes.EVM) { + chainAccountAddresses = accountsEVM + .filter( + (acc) => acc.chain.id == values.chainID && !acc.isDisabled, + ) + .map((acc) => acc.address) + } + if (values.chainType === ChainTypes.APTOS) { + chainAccountAddresses = accountsAptos.map((acc) => acc.account) + } return (
- + {/* todo: in future use chains query to retrieve list of supported chains */} + EVM + + APTOS + @@ -279,11 +291,15 @@ export const ChainConfigurationForm = withStyles(styles)( 'data-testid': 'chainID-helper-text', }} > - {chainIDs.map((chainID) => ( - - {chainID} - - ))} + {chains + .filter( + (x) => x.network.toUpperCase() === values.chainType, + ) + .map((x) => ( + + {x.id} + + ))} @@ -298,7 +314,7 @@ export const ChainConfigurationForm = withStyles(styles)( fullWidth select helperText="The account address used for this chain" - chainAccounts={chainAccounts} + addresses={chainAccountAddresses} FormHelperTextProps={{ 'data-testid': 'accountAddr-helper-text', }} diff --git a/src/components/Form/ChainTypes.ts b/src/components/Form/ChainTypes.ts new file mode 100644 index 00000000..387bc7b8 --- /dev/null +++ b/src/components/Form/ChainTypes.ts @@ -0,0 +1,7 @@ +export const ChainTypes = { + EVM: 'EVM', + APTOS: 'APTOS', + SOLANA: 'SOLANA', + STARKNET: 'STARKNET', + COSMOS: 'COSMOS', +} diff --git a/src/hooks/queries/useAptosAccountsQuery.test.tsx b/src/hooks/queries/useAptosAccountsQuery.test.tsx new file mode 100644 index 00000000..4ee6c28d --- /dev/null +++ b/src/hooks/queries/useAptosAccountsQuery.test.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { MockedProvider } from '@apollo/client/testing' +import { + useAptosAccountsQuery, + APTOS_KEYS_QUERY, +} from './useAptosAccountsQuery' + +const mockData = { + data: { + aptosKeys: { + __typename: 'AptosKeys', + results: [ + { __typename: 'AptosKey', account: 'account1', id: '1' }, + { __typename: 'AptosKey', account: 'account2', id: '2' }, + ], + }, + }, +} + +const mocks = [ + { + request: { + query: APTOS_KEYS_QUERY, + }, + result: mockData, + }, +] + +const TestComponent: React.FC = () => { + const { data, loading, error } = useAptosAccountsQuery() + + if (loading) return

Loading...

+ if (error) return

Error: {error.message}

+ + return ( +
+ {data?.aptosKeys.results.map((key, i) => ( +
+

Account: {key.account}

+

ID: {key.id}

+
+ ))} +
+ ) +} + +describe('useAptosAccountsQuery', () => { + test('renders data with correct graphql query', async () => { + render( + + + , + ) + + await waitFor(() => { + expect(screen.getByText('Account: account1')).toBeInTheDocument() + expect(screen.getByText('ID: 1')).toBeInTheDocument() + expect(screen.getByText('Account: account2')).toBeInTheDocument() + expect(screen.getByText('ID: 2')).toBeInTheDocument() + }) + }) +}) diff --git a/src/hooks/queries/useAptosAccountsQuery.ts b/src/hooks/queries/useAptosAccountsQuery.ts new file mode 100644 index 00000000..780412d3 --- /dev/null +++ b/src/hooks/queries/useAptosAccountsQuery.ts @@ -0,0 +1,24 @@ +import { gql, QueryHookOptions, useQuery } from '@apollo/client' + +export const APTOS_KEYS_PAYLOAD__RESULTS_FIELDS = gql` + fragment AptosKeysPayload_ResultsFields on AptosKey { + account + id + } +` + +export const APTOS_KEYS_QUERY = gql` + ${APTOS_KEYS_PAYLOAD__RESULTS_FIELDS} + query FetchAptosKeys { + aptosKeys { + results { + ...AptosKeysPayload_ResultsFields + } + } + } +` + +// useAptosAccountsQuery fetches the Aptos accounts. +export const useAptosAccountsQuery = (opts: QueryHookOptions = {}) => { + return useQuery(APTOS_KEYS_QUERY, opts) +} diff --git a/src/hooks/queries/useChainsQuery.ts b/src/hooks/queries/useChainsQuery.ts index 42c0f430..74c902e7 100644 --- a/src/hooks/queries/useChainsQuery.ts +++ b/src/hooks/queries/useChainsQuery.ts @@ -4,6 +4,7 @@ export const CHAINS_PAYLOAD__RESULTS_FIELDS = gql` fragment ChainsPayload_ResultsFields on Chain { id enabled + network } ` diff --git a/src/screens/FeedsManager/EditSupportedChainDialog.tsx b/src/screens/FeedsManager/EditSupportedChainDialog.tsx index 2638c0d7..e9624c44 100644 --- a/src/screens/FeedsManager/EditSupportedChainDialog.tsx +++ b/src/screens/FeedsManager/EditSupportedChainDialog.tsx @@ -13,9 +13,11 @@ import { } from 'src/components/Form/ChainConfigurationForm' import { useChainsQuery } from 'src/hooks/queries/useChainsQuery' import { useEVMAccountsQuery } from 'src/hooks/queries/useEVMAccountsQuery' +import { useAptosAccountsQuery } from 'src/hooks/queries/useAptosAccountsQuery' import { useP2PKeysQuery } from 'src/hooks/queries/useP2PKeysQuery' import { useOCRKeysQuery } from 'src/hooks/queries/useOCRKeysQuery' import { useOCR2KeysQuery } from 'src/hooks/queries/useOCR2KeysQuery' +import { ChainTypes } from 'src/components/Form/ChainTypes' type Props = { cfg: FeedsManager_ChainConfigFields | null @@ -35,7 +37,11 @@ export const EditSupportedChainDialog = ({ fetchPolicy: 'network-only', }) - const { data: accountData } = useEVMAccountsQuery({ + const { data: accountDataEVM } = useEVMAccountsQuery({ + fetchPolicy: 'cache-and-network', + }) + + const { data: accountDataAptos } = useAptosAccountsQuery({ fetchPolicy: 'cache-and-network', }) @@ -57,7 +63,7 @@ export const EditSupportedChainDialog = ({ const initialValues = { chainID: cfg.chainID, - chainType: 'EVM', + chainType: ChainTypes.EVM, accountAddr: cfg.accountAddr, adminAddr: cfg.adminAddr, accountAddrPubKey: cfg.accountAddrPubKey, @@ -80,11 +86,12 @@ export const EditSupportedChainDialog = ({ ocr2ForwarderAddress: cfg.ocr2JobConfig.forwarderAddress, } - const chainIDs: string[] = chainData - ? chainData.chains.results.map((c) => c.id) - : [] + const chains = chainData ? chainData.chains.results : [] - const accounts = accountData ? accountData.ethKeys.results : [] + const accountsEVM = accountDataEVM ? accountDataEVM.ethKeys.results : [] + const accountsAptos = accountDataAptos + ? accountDataAptos.aptosKeys.results + : [] const p2pKeys = p2pKeysData ? p2pKeysData.p2pKeys.results : [] const ocrKeys = ocrKeysData ? ocrKeysData.ocrKeyBundles.results : [] const ocr2Keys = ocr2KeysData ? ocr2KeysData.ocr2KeyBundles.results : [] @@ -100,8 +107,9 @@ export const EditSupportedChainDialog = ({ innerRef={formRef} initialValues={initialValues} onSubmit={onSubmit} - chainIDs={chainIDs} - accounts={accounts} + chains={chains} + accountsEVM={accountsEVM} + accountsAptos={accountsAptos} p2pKeys={p2pKeys} ocrKeys={ocrKeys} ocr2Keys={ocr2Keys} diff --git a/src/screens/FeedsManager/NewSupportedChainDialog.tsx b/src/screens/FeedsManager/NewSupportedChainDialog.tsx index 0f1701ef..ef590f96 100644 --- a/src/screens/FeedsManager/NewSupportedChainDialog.tsx +++ b/src/screens/FeedsManager/NewSupportedChainDialog.tsx @@ -13,9 +13,11 @@ import { } from 'src/components/Form/ChainConfigurationForm' import { useChainsQuery } from 'src/hooks/queries/useChainsQuery' import { useEVMAccountsQuery } from 'src/hooks/queries/useEVMAccountsQuery' +import { useAptosAccountsQuery } from 'src/hooks/queries/useAptosAccountsQuery' import { useP2PKeysQuery } from 'src/hooks/queries/useP2PKeysQuery' import { useOCRKeysQuery } from 'src/hooks/queries/useOCRKeysQuery' import { useOCR2KeysQuery } from 'src/hooks/queries/useOCR2KeysQuery' +import { ChainTypes } from 'src/components/Form/ChainTypes' type Props = { open: boolean @@ -29,7 +31,11 @@ export const NewSupportedChainDialog = ({ onClose, open, onSubmit }: Props) => { fetchPolicy: 'network-only', }) - const { data: accountData } = useEVMAccountsQuery({ + const { data: accountDataEVM } = useEVMAccountsQuery({ + fetchPolicy: 'cache-and-network', + }) + + const { data: accountDataAptos } = useAptosAccountsQuery({ fetchPolicy: 'cache-and-network', }) @@ -47,7 +53,7 @@ export const NewSupportedChainDialog = ({ onClose, open, onSubmit }: Props) => { const initialValues = { chainID: '', - chainType: 'EVM', + chainType: ChainTypes.EVM, accountAddr: '', adminAddr: '', accountAddrPubKey: '', @@ -70,11 +76,12 @@ export const NewSupportedChainDialog = ({ onClose, open, onSubmit }: Props) => { ocr2ForwarderAddress: '', } - const chainIDs: string[] = chainData - ? chainData.chains.results.map((c) => c.id) - : [] + const chains = chainData ? chainData.chains.results : [] - const accounts = accountData ? accountData.ethKeys.results : [] + const accountsEVM = accountDataEVM ? accountDataEVM.ethKeys.results : [] + const accountsAptos = accountDataAptos + ? accountDataAptos.aptosKeys.results + : [] const p2pKeys = p2pKeysData ? p2pKeysData.p2pKeys.results : [] const ocrKeys = ocrKeysData ? ocrKeysData.ocrKeyBundles.results : [] const ocr2Keys = ocr2KeysData ? ocr2KeysData.ocr2KeyBundles.results : [] @@ -90,8 +97,9 @@ export const NewSupportedChainDialog = ({ onClose, open, onSubmit }: Props) => { innerRef={formRef} initialValues={initialValues} onSubmit={onSubmit} - chainIDs={chainIDs} - accounts={accounts} + chains={chains} + accountsEVM={accountsEVM} + accountsAptos={accountsAptos} p2pKeys={p2pKeys} ocrKeys={ocrKeys} ocr2Keys={ocr2Keys} diff --git a/support/factories/gql/fetchChains.ts b/support/factories/gql/fetchChains.ts index 6185bfd4..7121851b 100644 --- a/support/factories/gql/fetchChains.ts +++ b/support/factories/gql/fetchChains.ts @@ -6,6 +6,7 @@ export function buildChain( __typename: 'Chain', id: '5', enabled: true, + network: 'EVM', ...overrides, } }