diff --git a/.tool-versions b/.tool-versions index d064b3ea..dfe63496 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 18.19.0 +nodejs 20.13.1 diff --git a/README.md b/README.md index 17261b7b..dacfb864 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ CHAINLINK_BASEURL=http://localhost:6688 yarn start Now navigate to http://localhost:3000. -If sign-in doesn't work, check your network console, it's probably a CORS issue. You may need to run your chainlink node with `ALLOW_ORIGINS=http://localhost:3000` set. +If sign-in doesn't work, check your network console, it's probably a CORS issue. You may need to run your chainlink node with `[WebServer] AllowOrigins=http://localhost:3000` set in TOML config. ## Running Tests diff --git a/package.json b/package.json index 38d1a2e7..c1fa6974 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "artifacts" ], "engines": { - "node": "^18.0.0" + "node": "^20.0.0" }, "scripts": { "lint": "eslint --ext js,jsx,ts,tsx .", diff --git a/src/Private.tsx b/src/Private.tsx index 8f89756a..16ae852a 100644 --- a/src/Private.tsx +++ b/src/Private.tsx @@ -62,7 +62,7 @@ const Private = ({ classes }: { classes: { content: string } }) => { - + diff --git a/src/actionCreators.ts b/src/actionCreators.ts index e2361cd4..707349c2 100644 --- a/src/actionCreators.ts +++ b/src/actionCreators.ts @@ -333,12 +333,6 @@ const RECEIVE_CREATE_SUCCESS_ACTION = { type: ResourceActionType.RECEIVE_CREATE_SUCCESS, } -const receiveDeleteSuccess = (id: string) => - ({ - type: ResourceActionType.RECEIVE_DELETE_SUCCESS, - id, - } as const) - export const submitSignIn = (data: Parameter) => sendSignIn(data) @@ -346,32 +340,6 @@ export const submitSignOut = () => sendSignOut export const beginRegistration = () => sendBeginRegistration() -export const deleteChain = ( - id: string, - successCallback: React.ReactNode, - errorCallback: React.ReactNode, -) => { - return (dispatch: Dispatch) => { - dispatch({ type: ResourceActionType.REQUEST_DELETE }) - - const endpoint = api.v2.chains - - return endpoint - .destroyChain(id) - .then((doc) => { - dispatch(receiveDeleteSuccess(id)) - dispatch(notifySuccess(successCallback, doc)) - }) - .catch((error: Errors) => { - curryErrorHandler( - dispatch, - ResourceActionType.RECEIVE_DELETE_ERROR, - )(error) - dispatch(notifyError(errorCallback, error)) - }) - } -} - export const createJobRunV2 = ( id: string, pipelineInput: string, diff --git a/src/api/v2/chains.ts b/src/api/v2/chains.ts index c8968b1e..d056f075 100644 --- a/src/api/v2/chains.ts +++ b/src/api/v2/chains.ts @@ -1,52 +1,20 @@ import * as jsonapi from 'utils/json-api-client' import * as models from 'core/store/models' -export const ENDPOINT = '/v2/chains/evm' -const UPDATE_ENDPOINT = `${ENDPOINT}/:id` +export const ENDPOINT = '/v2/chains/:network' + export class Chains { constructor(private api: jsonapi.Api) {} - public getChains = (): Promise> => { - return this.index() - } - - public createChain = ( - request: models.CreateChainRequest, - ): Promise> => { - return this.create(request) - } - - public destroyChain = (id: string): Promise> => { - return this.destroy(undefined, { id }) + public getChains = ( + network: string, + ): Promise> => { + return this.index(undefined, { network }) } - public updateChain = ( - id: string, - req: models.UpdateChainRequest, - ): Promise> => { - return this.update(req, { id }) - } - - private index = this.api.fetchResource(ENDPOINT) - - private create = this.api.createResource< - models.CreateChainRequest, - models.Chain + private index = this.api.fetchResource< + object, + models.Chain[], + { network: string } >(ENDPOINT) - - private destroy = this.api.deleteResource< - undefined, - null, - { - id: string - } - >(UPDATE_ENDPOINT) - - private update = this.api.updateResource< - models.UpdateChainRequest, - models.Chain, - { - id: string - } - >(UPDATE_ENDPOINT) } diff --git a/src/components/Form/ChainConfigurationForm.test.tsx b/src/components/Form/ChainConfigurationForm.test.tsx index d42751da..42dfe12e 100644 --- a/src/components/Form/ChainConfigurationForm.test.tsx +++ b/src/components/Form/ChainConfigurationForm.test.tsx @@ -232,6 +232,9 @@ describe('ChainConfigurationForm', () => { solanaKeys: { results: [], }, + starknetKeys: { + results: [], + }, }) const chainType = getByRole('button', { name: 'EVM' }) @@ -350,6 +353,9 @@ test('should be able to select OCR2 Job Type with Key Bundle ID', async () => { solanaKeys: { results: [], }, + starknetKeys: { + results: [], + }, }, ) @@ -418,6 +424,9 @@ function renderChainConfigurationForm( solanaKeys: { results: [{ id: 'solana_xxxx' }], }, + starknetKeys: { + results: [{ id: 'starknet_xxxx' }], + }, }, ) { return render( diff --git a/src/components/Form/ChainConfigurationForm.tsx b/src/components/Form/ChainConfigurationForm.tsx index 1fb71c19..73aec663 100644 --- a/src/components/Form/ChainConfigurationForm.tsx +++ b/src/components/Form/ChainConfigurationForm.tsx @@ -48,8 +48,8 @@ export type FormValues = { ocr2RebalancerPluginEnabled: boolean } -const isStarknet = (chainID: string): boolean => { - return chainID === 'SN_MAIN' || chainID === 'SN_SEPOLIA' +const isStarknet = (chainType: string): boolean => { + return chainType === 'STARKNET' } const ValidationSchema = Yup.object().shape({ @@ -117,7 +117,7 @@ interface AccountAddrFieldProps extends FieldAttributes { const AccountAddrField = ({ addresses, ...props }: AccountAddrFieldProps) => { const { - values: { chainID, accountAddr }, + values: { chainID, chainType, accountAddr }, setFieldValue, } = useFormikContext() @@ -148,7 +148,7 @@ const AccountAddrField = ({ addresses, ...props }: AccountAddrFieldProps) => { return ( <> - {!isStarknet(chainID) && ( + {!isStarknet(chainType) && ( { ))} )} - {isStarknet(chainID) && ( + {isStarknet(chainType) && ( { fullWidth /> )} - {isStarknet(chainID) && ( + {isStarknet(chainType) && (
createStyles({ @@ -101,9 +96,6 @@ const styles = (theme: Theme) => modalContent: { width: 'inherit', }, - deleteButton: { - marginTop: theme.spacing.unit * 4, - }, runJobButton: { marginBottom: theme.spacing.unit * 3, }, @@ -116,100 +108,18 @@ export type ChainResource = Resource interface Props extends WithStyles { chainId: string + network: string chain?: ChainResource - deleteChain: (...args: any[]) => any } -const DeleteSuccessNotification = ({ id }: any) => ( - Successfully deleted chain {id} -) - -const RegionalNavComponent = ({ - classes, - chainId, - chain, - deleteChain, -}: Props) => { - const [modalOpen, setModalOpen] = useState(false) - const [deleted, setDeleted] = useState(false) +const RegionalNavComponent = ({ classes, chainId, network, chain }: Props) => { const location = useLocation() const navOverridesActive = location.pathname.endsWith('/config-overrides') const editActive = location.pathname.endsWith('/edit') const navNodesActive = !navOverridesActive && !editActive - const handleDelete = (id: string) => { - deleteChain(id, () => DeleteSuccessNotification({ id }), ErrorMessage) - setDeleted(true) - } - return ( <> - setModalOpen(false)} - > - - - - - - Warning: This Action Cannot Be Undone - - - - setModalOpen(false)} - /> - - - - - - - - Disabling the chain may be a safer option - - - - All associated RPC Nodes will be permanently deleted - - - - Access to this page will be lost - - - - - - - - - - - - - @@ -245,7 +155,7 @@ const RegionalNavComponent = ({ interface RouteParams { + network: string chainId: string } export const ChainsShow = () => { - const { chainId } = useParams() + const { chainId, network } = useParams() const { path } = useRouteMatch() const [chain, setChain] = React.useState() const [nodes, setNodes] = React.useState([]) @@ -28,16 +29,16 @@ export const ChainsShow = () => { }, []) React.useEffect(() => { - Promise.all([v2.chains.getChains()]) + Promise.all([v2.chains.getChains(network)]) .then(([v2Chains]) => v2Chains.data.find((chain: ChainResource) => chain.id === chainId), ) .then(setChain) - }, [chainId]) + }, [chainId, network]) return ( <> - + {chain && } diff --git a/src/screens/Chains/ChainRow.test.tsx b/src/screens/Chains/ChainRow.test.tsx index f227cf13..2dd07708 100644 --- a/src/screens/Chains/ChainRow.test.tsx +++ b/src/screens/Chains/ChainRow.test.tsx @@ -19,7 +19,7 @@ function renderComponent(chain: ChainsPayload_ResultsFields) { - + Link Success , @@ -32,6 +32,7 @@ describe('ChainRow', () => { renderComponent(chain) + expect(queryByText('EVM')).toBeInTheDocument() expect(queryByText('5')).toBeInTheDocument() expect(queryByText('true')).toBeInTheDocument() }) @@ -42,7 +43,7 @@ describe('ChainRow', () => { renderComponent(chain) const link = getByRole('link', { name: /5/i }) - expect(link).toHaveAttribute('href', '/chains/5') + expect(link).toHaveAttribute('href', '/chains/evm/5') userEvent.click(link) diff --git a/src/screens/Chains/ChainRow.tsx b/src/screens/Chains/ChainRow.tsx index b7010b7b..20f49b27 100644 --- a/src/screens/Chains/ChainRow.tsx +++ b/src/screens/Chains/ChainRow.tsx @@ -14,8 +14,12 @@ interface Props extends WithStyles { export const ChainRow = withStyles(tableStyles)(({ chain, classes }: Props) => { return ( + {chain.network} - + {chain.id} diff --git a/src/screens/Chains/ChainsView.test.tsx b/src/screens/Chains/ChainsView.test.tsx index 5209f878..daf69dcf 100644 --- a/src/screens/Chains/ChainsView.test.tsx +++ b/src/screens/Chains/ChainsView.test.tsx @@ -6,7 +6,7 @@ import { buildChains } from 'support/factories/gql/fetchChains' import { ChainsView, Props as ChainsViewProps } from './ChainsView' import userEvent from '@testing-library/user-event' -const { getAllByRole, getByRole, queryByText } = screen +const { getAllByRole, getByRole, queryByText, queryAllByText } = screen function renderComponent(viewProps: ChainsViewProps) { renderWithRouter() @@ -25,9 +25,11 @@ describe('ChainsView', () => { expect(getAllByRole('row')).toHaveLength(3) + expect(queryByText('Network')).toBeInTheDocument() expect(queryByText('Chain ID')).toBeInTheDocument() expect(queryByText('Enabled')).toBeInTheDocument() + expect(queryAllByText('EVM')).toHaveLength(2) expect(queryByText('5')).toBeInTheDocument() expect(queryByText('42')).toBeInTheDocument() diff --git a/src/screens/Chains/ChainsView.tsx b/src/screens/Chains/ChainsView.tsx index 06127a8a..d2e67b1f 100644 --- a/src/screens/Chains/ChainsView.tsx +++ b/src/screens/Chains/ChainsView.tsx @@ -86,6 +86,7 @@ export const ChainsView: React.FC = ({ + Network Chain ID Enabled @@ -93,7 +94,7 @@ export const ChainsView: React.FC = ({ {filteredChains.length === 0 && ( - + No chains found diff --git a/src/screens/Job/JobView.tsx b/src/screens/Job/JobView.tsx index 51b700b4..c2485188 100644 --- a/src/screens/Job/JobView.tsx +++ b/src/screens/Job/JobView.tsx @@ -68,6 +68,7 @@ const JOB_PAYLOAD__SPEC = gql` p2pv2Bootstrappers relay relayConfig + onchainSigningStrategy transmitterID pluginType pluginConfig diff --git a/src/screens/Job/generateJobDefinition.test.ts b/src/screens/Job/generateJobDefinition.test.ts index 6462a10f..ff9bfc05 100644 --- a/src/screens/Job/generateJobDefinition.test.ts +++ b/src/screens/Job/generateJobDefinition.test.ts @@ -356,6 +356,7 @@ observationSource = """ pluginConfig: { juelsPerFeeCoinSource: '1000000000', }, + onchainSigningStrategy: {}, transmitterID: '0x01010CaB43e77116c95745D219af1069fE050d7A', feedID: 'feed-id', }, @@ -386,6 +387,7 @@ p2pv2Bootstrappers = [ ] relay = "evm" pluginType = "median" +onchainSigningStrategy = { } feedID = "feed-id" transmitterID = "0x01010CaB43e77116c95745D219af1069fE050d7A" observationSource = """ diff --git a/src/screens/Job/generateJobDefinition.ts b/src/screens/Job/generateJobDefinition.ts index 9262acd9..aababa93 100644 --- a/src/screens/Job/generateJobDefinition.ts +++ b/src/screens/Job/generateJobDefinition.ts @@ -147,6 +147,7 @@ export const generateJobDefinition = ( 'relayConfig', 'pluginType', 'pluginConfig', + 'onchainSigningStrategy', 'feedID', ), // We need to call 'extractSpecFields' again here so we get the spec 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.tsx b/src/screens/KeyManagement/NonEVMKeyRow.tsx new file mode 100644 index 00000000..0a3c87ba --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeyRow.tsx @@ -0,0 +1,27 @@ +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..b0ad06b5 --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeys.test.tsx @@ -0,0 +1,54 @@ +import * as React from 'react' + +import { renderWithRouter, screen } from 'support/test-utils' +import { MockedProvider, MockedResponse } from '@apollo/client/testing' + +import { NonEVMKeys } from './NonEVMKeys' +import { NON_EVM_KEYS_QUERY } from 'hooks/queries/useNonEvmAccountsQuery' +import { buildAptosKeys } from 'support/factories/gql/fetchNonEVMKeys' +import Notifications from 'pages/Notifications' +import { waitForLoading } from 'support/test-helpers/wait' + +const { findByText } = screen + +function renderComponent(mocks: MockedResponse[]) { + renderWithRouter( + <> + + + + + , + ) +} + +function fetchNonEVMKeysQuery( + aptosKeys: ReadonlyArray, +) { + return { + request: { + query: NON_EVM_KEYS_QUERY, + }, + result: { + data: { + aptosKeys: { + results: aptosKeys, + }, + }, + }, + } +} + +describe('NonEVMKeys', () => { + it('renders the page', async () => { + const payload = buildAptosKeys() + const mocks: MockedResponse[] = [fetchNonEVMKeysQuery(payload)] + + renderComponent(mocks) + + await waitForLoading() + + expect(await findByText(payload[0].id)).toBeInTheDocument() + expect(await findByText(payload[0].account)).toBeInTheDocument() + }) +}) diff --git a/src/screens/KeyManagement/NonEVMKeys.tsx b/src/screens/KeyManagement/NonEVMKeys.tsx new file mode 100644 index 00000000..74221309 --- /dev/null +++ b/src/screens/KeyManagement/NonEVMKeys.tsx @@ -0,0 +1,89 @@ +import React from 'react' + +import Grid from '@material-ui/core/Grid' +import CircularProgress from '@material-ui/core/CircularProgress' + +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 ( + <> + {loading && ( + + + + )} + + {data && + Object.entries(data).map( + ([key, chain]) => + typeof chain === 'object' && + 'results' in chain && + chain.results?.length > 0 && ( + + ), + )} + + ) +} diff --git a/src/screens/KeyManagement/NonEVMKeysCard.tsx b/src/screens/KeyManagement/NonEVMKeysCard.tsx new file mode 100644 index 00000000..a456e7de --- /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) => ( + + ))} + +
+
+ ) +} diff --git a/src/screens/Node/NodeScreen.test.tsx b/src/screens/Node/NodeScreen.test.tsx index d80f3b7d..56df9fdf 100644 --- a/src/screens/Node/NodeScreen.test.tsx +++ b/src/screens/Node/NodeScreen.test.tsx @@ -22,7 +22,7 @@ function renderComponent(mocks: MockedResponse[]) { - + Redirect Success , diff --git a/support/factories/gql/fetchNonEVMKeys.ts b/support/factories/gql/fetchNonEVMKeys.ts new file mode 100644 index 00000000..7363d0a2 --- /dev/null +++ b/support/factories/gql/fetchNonEVMKeys.ts @@ -0,0 +1,27 @@ +// buildAptosKey builds a Aptos Key for the FetchNonEVMKeys query. +export function buildAptosKey( + overrides?: Partial, +): AptosKeysPayload_ResultsFields { + return { + __typename: 'AptosKey', + id: 'aa67b61969793d51a3008cffba147bf57f1c89c423e32ce93ec9471d21e4231d', + account: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ...overrides, + } +} + +// buildAptosKeys builds a list of aptos keys. +export function buildAptosKeys(): ReadonlyArray { + return [ + buildAptosKey({ + id: 'aa67b61969793d51a3008cffba147bf57f1c89c423e32ce93ec9471d21e4231d', + account: + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + }), + buildAptosKey({ + id: 'e09c2e1444322d91cfb9b8576ce5895e54dc5caef37c5aff4accca9272412f5b', + account: + 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + }), + ] +}