Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/lucky-pandas-wander.md
Original file line number Diff line number Diff line change
@@ -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.
95 changes: 95 additions & 0 deletions src/components/Form/ChainConfigurationForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ describe('ChainConfigurationForm', () => {
suiKeys: {
results: [],
},
stellarKeys: {
results: [],
},
})

await selectOptionOnUI(/chain type/i, 'APTOS')
Expand Down Expand Up @@ -326,6 +329,9 @@ describe('ChainConfigurationForm', () => {
suiKeys: {
results: [],
},
stellarKeys: {
results: [],
},
})

await selectOptionOnUI(/chain type/i, 'SUI')
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -618,6 +705,14 @@ function renderChainConfigurationForm(
suiKeys: {
results: [{ account: '0x123', id: '6666' }],
},
stellarKeys: {
results: [
{
account: 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H',
id: '7777',
},
],
},
},
) {
return render(
Expand Down
8 changes: 8 additions & 0 deletions src/components/Form/ChainConfigurationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
}
Expand Down Expand Up @@ -404,6 +409,9 @@ export const ChainConfigurationForm = withStyles(styles)(({
<MenuItem key={ChainTypes.SUI} value={ChainTypes.SUI}>
SUI
</MenuItem>
<MenuItem key={ChainTypes.STELLAR} value={ChainTypes.STELLAR}>
STELLAR
</MenuItem>
</Field>
</Grid>

Expand Down
1 change: 1 addition & 0 deletions src/components/Form/ChainTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const ChainTypes = {
TRON: 'TRON',
TON: 'TON',
SUI: 'SUI',
STELLAR: 'STELLAR',
}
14 changes: 14 additions & 0 deletions src/hooks/queries/useNonEvmAccountsQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const mockData = {
{ __typename: 'SuiKey', account: 'account7', id: '7' },
],
},
stellarKeys: {
__typename: 'StellarKeys',
results: [{ __typename: 'StellarKey', account: 'account8', id: '8' }],
},
},
}

Expand Down Expand Up @@ -85,6 +89,13 @@ const TestComponent: React.FC = () => {
<p>Sui ID: {key.id}</p>
</div>
))}

{data?.stellarKeys.results.map((key, i) => (
<div key={i}>
<p>Account: {key.account}</p>
<p>Stellar ID: {key.id}</p>
</div>
))}
</div>
)
}
Expand All @@ -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()
})
})
})
13 changes: 13 additions & 0 deletions src/hooks/queries/useNonEvmAccountsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,21 @@ 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}
${STARKNET_KEYS_PAYLOAD__RESULTS_FIELDS}
${TRON_KEYS_PAYLOAD__RESULTS_FIELDS}
${TON_KEYS_PAYLOAD__RESULTS_FIELDS}
${SUI_KEYS_PAYLOAD__RESULTS_FIELDS}
${STELLAR_KEYS_PAYLOAD__RESULTS_FIELDS}
query FetchNonEvmKeys {
aptosKeys {
results {
Expand Down Expand Up @@ -78,6 +86,11 @@ export const NON_EVM_KEYS_QUERY = gql`
...SuiKeysPayload_ResultsFields
}
}
stellarKeys {
results {
...StellarKeysPayload_ResultsFields
}
}
}
`

Expand Down
34 changes: 34 additions & 0 deletions src/screens/KeyManagement/NonEVMKeys.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -59,6 +60,23 @@ function fetchNonEVMKeysQuerySui(
}
}

function fetchNonEVMKeysQueryStellar(
stellarKeys: ReadonlyArray<StellarKeysPayload_ResultsFields>,
) {
return {
request: {
query: NON_EVM_KEYS_QUERY,
},
result: {
data: {
stellarKeys: {
results: stellarKeys,
},
},
},
}
}

describe('NonEVMKeys', () => {
it('renders the page', async () => {
const payload = buildAptosKeys()
Expand Down Expand Up @@ -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()
})
})
6 changes: 6 additions & 0 deletions src/screens/KeyManagement/NonEVMKeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
29 changes: 29 additions & 0 deletions support/factories/gql/fetchNonEVMKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,32 @@ export function buildSuiKeys(): ReadonlyArray<SuiKeysPayload_ResultsFields> {
}),
]
}

// 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>,
): StellarKeysPayload_ResultsFields {
const account = 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H'

return {
__typename: 'StellarKey',
id: account,
account,
...overrides,
}
}

// buildStellarKeys builds a list of stellar keys.
export function buildStellarKeys(): ReadonlyArray<StellarKeysPayload_ResultsFields> {
return [
buildStellarKey(),
buildStellarKey({
id: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
account: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
}),
]
}
Loading