Defer keyring init until chain type is available (#8980)

* Defer keyring init until chain type is available

* Ensure optionsSubject operation

* Attempt to fix mocked hook

* Adjust bounties spec context order

* Fix is* flags on mock
This commit is contained in:
Jaco 2023-02-08 18:36:04 +02:00 committed by GitHub
parent a8b60d3afd
commit 3fc3b1edbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 99 additions and 49 deletions

View File

@ -43,13 +43,13 @@ function Root ({ isElectron, store }: Props): React.ReactElement<Props> {
return (
<Suspense fallback='...'>
<ThemeProvider theme={theme}>
<KeyringCtxRoot>
<QueueCtxRoot>
<ApiCtxRoot
apiUrl={settings.apiUrl}
isElectron={isElectron}
store={store}
>
<QueueCtxRoot>
<ApiCtxRoot
apiUrl={settings.apiUrl}
isElectron={isElectron}
store={store}
>
<KeyringCtxRoot>
<ApiStatsCtxRoot>
<BlockAuthorsCtxRoot>
<BlockEventsCtxRoot>
@ -61,9 +61,9 @@ function Root ({ isElectron, store }: Props): React.ReactElement<Props> {
</BlockEventsCtxRoot>
</BlockAuthorsCtxRoot>
</ApiStatsCtxRoot>
</ApiCtxRoot>
</QueueCtxRoot>
</KeyringCtxRoot>
</KeyringCtxRoot>
</ApiCtxRoot>
</QueueCtxRoot>
</ThemeProvider>
</Suspense>
);

View File

@ -111,6 +111,10 @@ export class BountiesPage {
}
}
},
isApiConnected: true,
isApiInitialized: true,
isApiReady: true,
isEthereum: false,
systemName: 'substrate'
} as unknown as ApiProps;
@ -124,15 +128,15 @@ export class BountiesPage {
<div id='tooltips' />
<Suspense fallback='...'>
<QueueCtx.Provider value={queue}>
<KeyringCtxRoot>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiCtx.Provider value={mockApi}>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiCtx.Provider value={mockApi}>
<KeyringCtxRoot>
<Bounties />
</ApiCtx.Provider>
</ThemeProvider>
</MemoryRouter>
</KeyringCtxRoot>
</KeyringCtxRoot>
</ApiCtx.Provider>
</ThemeProvider>
</MemoryRouter>
</QueueCtx.Provider>
</Suspense>
</>

View File

@ -6,9 +6,22 @@ import type { Option } from './types';
import React from 'react';
import { keyring } from '@polkadot/ui-keyring';
import { decodeAddress } from '@polkadot/util-crypto';
import KeyPair from './KeyPair';
export default function createItem (option: KeyringSectionOption, isUppercase = true): Option {
export default function createItem (option: KeyringSectionOption, isUppercase = true): Option | null {
try {
const u8a = decodeAddress(option.key);
if (u8a.length !== (keyring.keyring.type === 'ethereum' ? 20 : 32)) {
return null;
}
} catch {
return null;
}
return {
...option,
text: (

View File

@ -45,7 +45,7 @@ interface Props {
}
type ExportedType = React.ComponentType<Props> & {
createOption: (option: KeyringSectionOption, isUppercase?: boolean) => Option;
createOption: (option: KeyringSectionOption, isUppercase?: boolean) => Option | null;
setLastValue: (type: KeyringOption$Type, value: string) => void;
};
@ -80,7 +80,7 @@ function transformToAccountId (value: string): string | null {
: accountId;
}
function createOption (address: string): Option {
function createOption (address: string): Option | null {
let isRecent: boolean | undefined;
const pair = keyring.getAccount(address);
let name: string | undefined;
@ -177,9 +177,13 @@ class InputAddress extends React.PureComponent<Props, State> {
: (lastOption && lastOption.value)
);
const actualOptions: Option[] = options
? dedupe(options.map((o) => createItem(o)))
? dedupe(
options
.map((o) => createItem(o))
.filter((o): o is Option => !!o)
)
: isDisabled && actualValue
? [createOption(actualValue)]
? [createOption(actualValue)].filter((o): o is Option => !!o)
: actualValue
? this.addActual(actualValue)
: this.getFiltered();
@ -225,7 +229,7 @@ class InputAddress extends React.PureComponent<Props, State> {
return this.hasValue(actualValue)
? base
: base.concat(createOption(actualValue));
: base.concat(...[createOption(actualValue)].filter((o): o is Option => !!o));
}
private renderLabel = ({ value }: KeyringSectionOption): React.ReactNode => {
@ -377,11 +381,13 @@ const ExportedComponent = withMulti(
propName: 'optionsAll',
transform: (optionsAll: KeyringOptions): Record<string, (Option | React.ReactNode)[]> =>
Object.entries(optionsAll).reduce((result: Record<string, (Option | React.ReactNode)[]>, [type, options]): Record<string, (Option | React.ReactNode)[]> => {
result[type] = options.map((option): Option | React.ReactNode =>
option.value === null
? createHeader(option)
: createItem(option)
);
result[type] = options
.map((option): Option | React.ReactNode | null =>
option.value === null
? createHeader(option)
: createItem(option)
)
.filter((o): o is Option | React.ReactNode => !!o);
return result;
}, {})

View File

@ -11,6 +11,8 @@ import { keyring } from '@polkadot/ui-keyring';
import { u8aToHex } from '@polkadot/util';
import { decodeAddress } from '@polkadot/util-crypto';
import { useApi } from '../useApi';
interface Props {
children?: React.ReactNode;
}
@ -38,10 +40,24 @@ export const KeyringCtx = React.createContext<State>(EMPTY);
* The first check ensures that we never have dupes in the original. The second
* ensures that e.g. an address is not also available as an account
**/
function filter (items: string[], others: string[] = []): string[] {
function filter (isEthereum: boolean, items: string[], others: string[] = []): string[] {
const allowedLength = isEthereum
? 20
: 32;
return items.reduce<string[]>((result, a) => {
if (!result.includes(a) && !others.includes(a)) {
result.push(a);
try {
const u8a = decodeAddress(a);
if (u8a.length === allowedLength) {
result.push(a);
} else {
console.warn(`Not adding address ${a}, not in correct format for chain (requires ${allowedLength}-byte publickey)`);
}
} catch {
console.error(a, allowedLength);
}
}
return result;
@ -75,8 +91,8 @@ function createCheck (items: string[]): Accounts['isAccount'] {
!!a && items.includes(a.toString());
}
function extractAccounts (accounts: SubjectInfo = {}): Accounts {
const allAccounts = filter(Object.keys(accounts));
function extractAccounts (isEthereum: boolean, accounts: SubjectInfo = {}): Accounts {
const allAccounts = filter(isEthereum, Object.keys(accounts));
return {
allAccounts,
@ -87,8 +103,8 @@ function extractAccounts (accounts: SubjectInfo = {}): Accounts {
};
}
function extractAddresses (addresses: SubjectInfo = {}, accounts: string[]): Addresses {
const allAddresses = filter(Object.keys(addresses), accounts);
function extractAddresses (isEthereum: boolean, addresses: SubjectInfo = {}, accounts: string[]): Addresses {
const allAddresses = filter(isEthereum, Object.keys(addresses), accounts);
return {
allAddresses,
@ -100,27 +116,34 @@ function extractAddresses (addresses: SubjectInfo = {}, accounts: string[]): Add
}
export function KeyringCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { isApiReady, isEthereum } = useApi();
const [state, setState] = useState(EMPTY);
useEffect((): () => void => {
const sub = combineLatest([
keyring.accounts.subject.pipe(
map((accInfo) => extractAccounts(accInfo))
),
keyring.addresses.subject
])
.pipe(
map(([accounts, addrInfo]): State => ({
accounts,
addresses: extractAddresses(addrInfo, accounts.allAccounts)
}))
)
.subscribe((state) => setState(state));
let sub: null | { unsubscribe: () => void } = null;
// Defer keyring injection until the API is ready - we need to have the chain
// info to determine which type of addresses we can use (before subscribing)
if (isApiReady) {
sub = combineLatest([
keyring.accounts.subject.pipe(
map((accInfo) => extractAccounts(isEthereum, accInfo))
),
keyring.addresses.subject
])
.pipe(
map(([accounts, addrInfo]): State => ({
accounts,
addresses: extractAddresses(isEthereum, addrInfo, accounts.allAccounts)
}))
)
.subscribe((state) => setState(state));
}
return (): void => {
sub && sub.unsubscribe();
};
}, []);
}, [isApiReady, isEthereum]);
return (
<KeyringCtx.Provider value={state}>

View File

@ -197,6 +197,10 @@ export abstract class Page {
...api,
isReady: Promise.resolve(api)
},
isApiConnected: true,
isApiInitialized: true,
isApiReady: true,
isEthereum: false,
systemName: 'substrate'
} as unknown as ApiProps;