From 8798f916dca16c441c42fe778cbe0b8aba9eac67 Mon Sep 17 00:00:00 2001 From: ilija42 Date: Thu, 6 Aug 2026 13:13:33 +0200 Subject: [PATCH] Add Stellar --- .changeset/lucky-pandas-wander.md | 9 ++ .../Form/ChainConfigurationForm.test.tsx | 95 +++++++++++++++++++ .../Form/ChainConfigurationForm.tsx | 8 ++ src/components/Form/ChainTypes.ts | 1 + .../queries/useNonEvmAccountsQuery.test.tsx | 14 +++ src/hooks/queries/useNonEvmAccountsQuery.ts | 13 +++ src/screens/KeyManagement/NonEVMKeys.test.tsx | 34 +++++++ src/screens/KeyManagement/NonEVMKeys.tsx | 6 ++ support/factories/gql/fetchNonEVMKeys.ts | 29 ++++++ 9 files changed, 209 insertions(+) create mode 100644 .changeset/lucky-pandas-wander.md diff --git a/.changeset/lucky-pandas-wander.md b/.changeset/lucky-pandas-wander.md new file mode 100644 index 00000000..5ed08926 --- /dev/null +++ b/.changeset/lucky-pandas-wander.md @@ -0,0 +1,9 @@ +--- +'@smartcontractkit/operator-ui': minor +--- + +feat: Add stellar + +Requires a node exposing the `stellarKeys` GraphQL query; against older nodes the +account address dropdown will be empty. No account public key field, as a Stellar +account address already encodes the ed25519 public key. diff --git a/src/components/Form/ChainConfigurationForm.test.tsx b/src/components/Form/ChainConfigurationForm.test.tsx index 5d999b82..3cec611f 100644 --- a/src/components/Form/ChainConfigurationForm.test.tsx +++ b/src/components/Form/ChainConfigurationForm.test.tsx @@ -214,6 +214,9 @@ describe('ChainConfigurationForm', () => { suiKeys: { results: [], }, + stellarKeys: { + results: [], + }, }) await selectOptionOnUI(/chain type/i, 'APTOS') @@ -326,6 +329,9 @@ describe('ChainConfigurationForm', () => { suiKeys: { results: [], }, + stellarKeys: { + results: [], + }, }) await selectOptionOnUI(/chain type/i, 'SUI') @@ -369,6 +375,84 @@ describe('ChainConfigurationForm', () => { }) }) + test('should able to create STELLAR chain config (with manual input)', async () => { + const handleSubmit = jest.fn() + const initialValues = emptyFormValues() + initialValues.chainType = ChainTypes.EVM + + renderChainConfigurationForm(initialValues, handleSubmit, [], { + aptosKeys: { + results: [], + }, + solanaKeys: { + results: [], + }, + starknetKeys: { + results: [], + }, + tronKeys: { + results: [], + }, + tonKeys: { + results: [], + }, + suiKeys: { + results: [], + }, + stellarKeys: { + results: [], + }, + }) + + await selectOptionOnUI(/chain type/i, 'STELLAR') + + // A Stellar chain ID is the SHA-256 of the network passphrase. + const chainIdTextBox = getByRole('textbox', { name: /chain id/i }) + userEvent.type( + chainIdTextBox, + 'cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472', + ) + + const accountAddrTextBox = getByRole('textbox', { + name: /account address/i, + }) + userEvent.type( + accountAddrTextBox, + 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', + ) + + await userEvent.click(getByRole('button', { name: /submit/i })) + + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalledWith({ + accountAddr: 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', + accountAddrPubKey: '', + adminAddr: '', + chainID: + 'cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472', + chainType: 'STELLAR', + 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) + }) + }) + test('should able to create Solana chain config', async () => { const handleSubmit = jest.fn() const initialValues = emptyFormValues() @@ -529,6 +613,9 @@ test('should be able to select OCR2 Job Type with Key Bundle ID', async () => { suiKeys: { results: [], }, + stellarKeys: { + results: [], + }, }) const ocr2CheckBox = screen.getByText(/ocr2/i) @@ -618,6 +705,14 @@ function renderChainConfigurationForm( suiKeys: { results: [{ account: '0x123', id: '6666' }], }, + stellarKeys: { + results: [ + { + account: 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', + id: '7777', + }, + ], + }, }, ) { return render( diff --git a/src/components/Form/ChainConfigurationForm.tsx b/src/components/Form/ChainConfigurationForm.tsx index 09ea90f1..5ad356dd 100644 --- a/src/components/Form/ChainConfigurationForm.tsx +++ b/src/components/Form/ChainConfigurationForm.tsx @@ -360,6 +360,11 @@ export const ChainConfigurationForm = withStyles(styles)(({ chainPublicKeys = accountsNonEvm?.suiKeys.results.map((acc) => acc.id) ?? [] break + case ChainTypes.STELLAR: + chainAccountAddresses = + accountsNonEvm?.stellarKeys.results.map((acc) => acc.account) ?? + [] + break default: chainAccountAddresses = [] } @@ -404,6 +409,9 @@ export const ChainConfigurationForm = withStyles(styles)(({ SUI + + STELLAR + diff --git a/src/components/Form/ChainTypes.ts b/src/components/Form/ChainTypes.ts index 1d98292c..626588a7 100644 --- a/src/components/Form/ChainTypes.ts +++ b/src/components/Form/ChainTypes.ts @@ -7,4 +7,5 @@ export const ChainTypes = { TRON: 'TRON', TON: 'TON', SUI: 'SUI', + STELLAR: 'STELLAR', } diff --git a/src/hooks/queries/useNonEvmAccountsQuery.test.tsx b/src/hooks/queries/useNonEvmAccountsQuery.test.tsx index 6ef92bbb..466d4ab8 100644 --- a/src/hooks/queries/useNonEvmAccountsQuery.test.tsx +++ b/src/hooks/queries/useNonEvmAccountsQuery.test.tsx @@ -34,6 +34,10 @@ const mockData = { { __typename: 'SuiKey', account: 'account7', id: '7' }, ], }, + stellarKeys: { + __typename: 'StellarKeys', + results: [{ __typename: 'StellarKey', account: 'account8', id: '8' }], + }, }, } @@ -85,6 +89,13 @@ const TestComponent: React.FC = () => {

Sui ID: {key.id}

))} + + {data?.stellarKeys.results.map((key, i) => ( +
+

Account: {key.account}

+

Stellar ID: {key.id}

+
+ ))} ) } @@ -111,6 +122,9 @@ describe('useNonEvmAccountsQuery', () => { expect(screen.getByText('Sui ID: 6')).toBeInTheDocument() expect(screen.getByText('Account: account7')).toBeInTheDocument() expect(screen.getByText('Sui ID: 7')).toBeInTheDocument() + + expect(screen.getByText('Account: account8')).toBeInTheDocument() + expect(screen.getByText('Stellar ID: 8')).toBeInTheDocument() }) }) }) diff --git a/src/hooks/queries/useNonEvmAccountsQuery.ts b/src/hooks/queries/useNonEvmAccountsQuery.ts index 94d6ae40..f8ebd9a4 100644 --- a/src/hooks/queries/useNonEvmAccountsQuery.ts +++ b/src/hooks/queries/useNonEvmAccountsQuery.ts @@ -40,6 +40,13 @@ export const SUI_KEYS_PAYLOAD__RESULTS_FIELDS = gql` } ` +export const STELLAR_KEYS_PAYLOAD__RESULTS_FIELDS = gql` + fragment StellarKeysPayload_ResultsFields on StellarKey { + account + id + } +` + export const NON_EVM_KEYS_QUERY = gql` ${APTOS_KEYS_PAYLOAD__RESULTS_FIELDS} ${SOLANA_KEYS_PAYLOAD__RESULTS_FIELDS} @@ -47,6 +54,7 @@ export const NON_EVM_KEYS_QUERY = gql` ${TRON_KEYS_PAYLOAD__RESULTS_FIELDS} ${TON_KEYS_PAYLOAD__RESULTS_FIELDS} ${SUI_KEYS_PAYLOAD__RESULTS_FIELDS} + ${STELLAR_KEYS_PAYLOAD__RESULTS_FIELDS} query FetchNonEvmKeys { aptosKeys { results { @@ -78,6 +86,11 @@ export const NON_EVM_KEYS_QUERY = gql` ...SuiKeysPayload_ResultsFields } } + stellarKeys { + results { + ...StellarKeysPayload_ResultsFields + } + } } ` diff --git a/src/screens/KeyManagement/NonEVMKeys.test.tsx b/src/screens/KeyManagement/NonEVMKeys.test.tsx index 39706b4d..de968041 100644 --- a/src/screens/KeyManagement/NonEVMKeys.test.tsx +++ b/src/screens/KeyManagement/NonEVMKeys.test.tsx @@ -7,6 +7,7 @@ import { NonEVMKeys } from './NonEVMKeys' import { NON_EVM_KEYS_QUERY } from 'hooks/queries/useNonEvmAccountsQuery' import { buildAptosKeys, + buildStellarKeys, buildSuiKeys, } from 'support/factories/gql/fetchNonEVMKeys' import Notifications from 'pages/Notifications' @@ -59,6 +60,23 @@ function fetchNonEVMKeysQuerySui( } } +function fetchNonEVMKeysQueryStellar( + stellarKeys: ReadonlyArray, +) { + return { + request: { + query: NON_EVM_KEYS_QUERY, + }, + result: { + data: { + stellarKeys: { + results: stellarKeys, + }, + }, + }, + } +} + describe('NonEVMKeys', () => { it('renders the page', async () => { const payload = buildAptosKeys() @@ -86,3 +104,19 @@ describe('NonEVMKeys_Sui', () => { expect(await findByText(payload[0].account)).toBeInTheDocument() }) }) + +describe('NonEVMKeys_Stellar', () => { + it('renders the page', async () => { + const payload = buildStellarKeys() + const mocks: MockedResponse[] = [fetchNonEVMKeysQueryStellar(payload)] + + renderComponent(mocks) + + await waitForLoading() + + // Only the account is asserted: the Stellar card renders a single field, + // since a key's ID is its "G..." account address. + expect(await findByText(payload[0].account)).toBeInTheDocument() + expect(await findByText(payload[1].account)).toBeInTheDocument() + }) +}) diff --git a/src/screens/KeyManagement/NonEVMKeys.tsx b/src/screens/KeyManagement/NonEVMKeys.tsx index 951d1d7e..2f0b9cd0 100644 --- a/src/screens/KeyManagement/NonEVMKeys.tsx +++ b/src/screens/KeyManagement/NonEVMKeys.tsx @@ -41,6 +41,12 @@ const SCHEMAS = { { label: 'Account', key: 'account', copy: true }, ], }, + // Only one field: a Stellar account address is the StrKey ("G...") encoding + // of the ed25519 public key, so `id` and `account` are the same string. + stellarKeys: { + title: 'Stellar', + fields: [{ label: 'Account', key: 'account', copy: true }], + }, } export const NonEVMKeys = () => { diff --git a/support/factories/gql/fetchNonEVMKeys.ts b/support/factories/gql/fetchNonEVMKeys.ts index ee4d81f1..1d17dc04 100644 --- a/support/factories/gql/fetchNonEVMKeys.ts +++ b/support/factories/gql/fetchNonEVMKeys.ts @@ -53,3 +53,32 @@ export function buildSuiKeys(): ReadonlyArray { }), ] } + +// buildStellarKey builds a Stellar Key for the FetchNonEVMKeys query. +// +// `id` and `account` are deliberately the same value: a Stellar account is the +// StrKey ("G...") encoding of the ed25519 public key, and the node's key ID is +// that same address. +export function buildStellarKey( + overrides?: Partial, +): StellarKeysPayload_ResultsFields { + const account = 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H' + + return { + __typename: 'StellarKey', + id: account, + account, + ...overrides, + } +} + +// buildStellarKeys builds a list of stellar keys. +export function buildStellarKeys(): ReadonlyArray { + return [ + buildStellarKey(), + buildStellarKey({ + id: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN', + account: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN', + }), + ] +}