Batch export (#666)
* . * finish export all * finish export all * . * fix linting errors (warning is pending on password decision) * . * resolve conflicts * . * .
This commit is contained in:
parent
235c683476
commit
e37fdcf6f9
@ -4,7 +4,7 @@
|
||||
import type { MetadataDef } from '@polkadot/extension-inject/types';
|
||||
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types';
|
||||
import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
|
||||
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';
|
||||
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';
|
||||
|
||||
import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults';
|
||||
import chrome from '@polkadot/extension-inject/chrome';
|
||||
@ -95,6 +95,14 @@ export default class Extension {
|
||||
return { exportedJson: JSON.stringify(keyring.backupAccount(keyring.getPair(address), password)) };
|
||||
}
|
||||
|
||||
private async accountsBatchExport ({ addresses, password }: RequestAccountBatchExport): Promise<ResponseAccountExport> {
|
||||
return {
|
||||
exportedJson: JSON.stringify(
|
||||
await keyring.backupAccounts(addresses, password)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
private accountsForget ({ address }: RequestAccountForget): boolean {
|
||||
keyring.forgetAccount(address);
|
||||
|
||||
@ -261,6 +269,14 @@ export default class Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private batchRestore ({ file, password }: RequestBatchRestore): void {
|
||||
try {
|
||||
keyring.restoreAccounts(file, password);
|
||||
} catch (error) {
|
||||
throw new Error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
private jsonGetAccountInfo (json: KeyringPair$Json): ResponseJsonGetAccountInfo {
|
||||
try {
|
||||
const { address, meta: { genesisHash, name }, type } = keyring.createFromJson(json);
|
||||
@ -500,6 +516,9 @@ export default class Extension {
|
||||
case 'pri(accounts.export)':
|
||||
return this.accountsExport(request as RequestAccountExport);
|
||||
|
||||
case 'pri(accounts.batchExport)':
|
||||
return this.accountsBatchExport(request as RequestAccountBatchExport);
|
||||
|
||||
case 'pri(accounts.forget)':
|
||||
return this.accountsForget(request as RequestAccountForget);
|
||||
|
||||
@ -539,6 +558,9 @@ export default class Extension {
|
||||
case 'pri(json.restore)':
|
||||
return this.jsonRestore(request as RequestJsonRestore);
|
||||
|
||||
case 'pri(json.batchRestore)':
|
||||
return this.batchRestore(request as RequestBatchRestore);
|
||||
|
||||
case 'pri(json.account.info)':
|
||||
return this.jsonGetAccountInfo(request as KeyringPair$Json);
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import type { InjectedAccount, InjectedMetadataKnown, MetadataDef, ProviderList,
|
||||
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types';
|
||||
import type { JsonRpcResponse } from '@polkadot/rpc-provider/types';
|
||||
import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
|
||||
import type { KeyringPairs$Json } from '@polkadot/ui-keyring/types';
|
||||
import type { KeypairType } from '@polkadot/util-crypto/types';
|
||||
|
||||
import { TypeRegistry } from '@polkadot/types';
|
||||
@ -78,6 +79,7 @@ export interface RequestSignatures {
|
||||
'pri(accounts.create.suri)': [RequestAccountCreateSuri, boolean];
|
||||
'pri(accounts.edit)': [RequestAccountEdit, boolean];
|
||||
'pri(accounts.export)': [RequestAccountExport, ResponseAccountExport];
|
||||
'pri(accounts.batchExport)': [RequestAccountBatchExport, ResponseAccountExport]
|
||||
'pri(accounts.forget)': [RequestAccountForget, boolean];
|
||||
'pri(accounts.show)': [RequestAccountShow, boolean];
|
||||
'pri(accounts.tie)': [RequestAccountTie, boolean];
|
||||
@ -92,6 +94,7 @@ export interface RequestSignatures {
|
||||
'pri(derivation.create)': [RequestDeriveCreate, boolean];
|
||||
'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate];
|
||||
'pri(json.restore)': [RequestJsonRestore, void];
|
||||
'pri(json.batchRestore)': [RequestBatchRestore, void];
|
||||
'pri(json.account.info)': [KeyringPair$Json, ResponseJsonGetAccountInfo];
|
||||
'pri(metadata.approve)': [RequestMetadataApprove, boolean];
|
||||
'pri(metadata.get)': [string | null, MetadataDef | null];
|
||||
@ -238,6 +241,11 @@ export interface RequestAccountExport {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RequestAccountBatchExport {
|
||||
addresses: string[];
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RequestAccountList {
|
||||
anyType?: boolean;
|
||||
}
|
||||
@ -369,6 +377,11 @@ export interface RequestJsonRestore {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RequestBatchRestore {
|
||||
file: KeyringPairs$Json;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ResponseJsonRestore {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
131
packages/extension-ui/src/Popup/ExportAll.tsx
Normal file
131
packages/extension-ui/src/Popup/ExportAll.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AccountContext, ActionBar, ActionContext, ActionText, Button, InputWithLabel, Warning } from '../components';
|
||||
import useTranslation from '../hooks/useTranslation';
|
||||
import { exportAccounts } from '../messaging';
|
||||
import { Header } from '../partials';
|
||||
|
||||
const MIN_LENGTH = 6;
|
||||
|
||||
interface Props extends RouteComponentProps, ThemeProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ExportAll ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { accounts } = useContext(AccountContext);
|
||||
const onAction = useContext(ActionContext);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [pass, setPass] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const _goHome = useCallback(
|
||||
() => onAction('/'),
|
||||
[onAction]
|
||||
);
|
||||
|
||||
const onPassChange = useCallback(
|
||||
(password: string) => {
|
||||
setPass(password);
|
||||
setError('');
|
||||
}
|
||||
, []);
|
||||
|
||||
const _onExportAllButtonClick = useCallback(
|
||||
(): void => {
|
||||
setIsBusy(true);
|
||||
|
||||
exportAccounts(accounts.map((account) => account.address), pass)
|
||||
.then(({ exportedJson }) => {
|
||||
const element = document.createElement('a');
|
||||
|
||||
element.href = `data:text/plain;charset=utf-8,${exportedJson}`;
|
||||
element.download = `batch_exported_account_${Date.now()}.json`;
|
||||
element.click();
|
||||
|
||||
onAction('/');
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error(error);
|
||||
setError(error.message);
|
||||
setIsBusy(false);
|
||||
});
|
||||
},
|
||||
[accounts, onAction, pass]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
showBackArrow
|
||||
text={t<string>('All account')}
|
||||
/>
|
||||
<div className={className}>
|
||||
<div className='actionArea'>
|
||||
<InputWithLabel
|
||||
data-export-all-password
|
||||
disabled={isBusy}
|
||||
isError={pass.length < MIN_LENGTH || !!error}
|
||||
label={t<string>('password for encrypting all accounts')}
|
||||
onChange={onPassChange}
|
||||
type='password'
|
||||
/>
|
||||
{error && (
|
||||
<Warning
|
||||
isBelowInput
|
||||
isDanger
|
||||
>
|
||||
{error}
|
||||
</Warning>
|
||||
)}
|
||||
<Button
|
||||
className='export-button'
|
||||
data-export-button
|
||||
isBusy={isBusy}
|
||||
isDanger
|
||||
isDisabled={pass.length === 0 || !!error}
|
||||
onClick={_onExportAllButtonClick}
|
||||
>
|
||||
{t<string>('I want to export all my accounts')}
|
||||
</Button>
|
||||
<ActionBar className='withMarginTop'>
|
||||
<ActionText
|
||||
className='center'
|
||||
onClick={_goHome}
|
||||
text={t<string>('Cancel')}
|
||||
/>
|
||||
</ActionBar>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(styled(ExportAll)`
|
||||
.actionArea {
|
||||
padding: 10px 24px;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.export-button {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.movedWarning {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.withMarginTop {
|
||||
margin-top: 4px;
|
||||
}
|
||||
`);
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
import type { ResponseJsonGetAccountInfo } from '@polkadot/extension-base/background/types';
|
||||
import type { KeyringPair$Json } from '@polkadot/keyring/types';
|
||||
import type { KeyringPairs$Json } from '@polkadot/ui-keyring/types';
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@ -11,9 +12,10 @@ import { u8aToString } from '@polkadot/util';
|
||||
|
||||
import { AccountContext, ActionContext, Address, Button, InputFileWithLabel, InputWithLabel, Warning } from '../components';
|
||||
import useTranslation from '../hooks/useTranslation';
|
||||
import { jsonGetAccountInfo, jsonRestore } from '../messaging';
|
||||
import { batchRestore, jsonGetAccountInfo, jsonRestore } from '../messaging';
|
||||
import { Header } from '../partials';
|
||||
import { DEFAULT_TYPE } from '../util/defaultType';
|
||||
import { isKeyringPairs$Json } from '../util/typeGuards';
|
||||
|
||||
const acceptedFormats = ['application/json', 'text/plain'].join(', ');
|
||||
|
||||
@ -26,13 +28,14 @@ function Upload ({ className }: Props): React.ReactElement {
|
||||
const { accounts } = useContext(AccountContext);
|
||||
const onAction = useContext(ActionContext);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [{ address, genesisHash, name, type }, setAccountInfo] = useState<ResponseJsonGetAccountInfo>({ address: '', genesisHash: '', name: '', type: DEFAULT_TYPE });
|
||||
const [accountsInfo, setAccountsInfo] = useState<ResponseJsonGetAccountInfo[]>([]);
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [isFileError, setFileError] = useState(false);
|
||||
const [requirePassword, setRequirePassword] = useState(false);
|
||||
const [isPasswordError, setIsPasswordError] = useState(false);
|
||||
// don't use the info from the file directly
|
||||
// rather use what comes from the background from jsonGetAccountInfo
|
||||
const [file, setFile] = useState<KeyringPair$Json|undefined>(undefined);
|
||||
const [file, setFile] = useState<KeyringPair$Json | KeyringPairs$Json | undefined>(undefined);
|
||||
|
||||
useEffect((): void => {
|
||||
!accounts.length && onAction();
|
||||
@ -47,34 +50,56 @@ function Upload ({ className }: Props): React.ReactElement {
|
||||
|
||||
const _onChangeFile = useCallback(
|
||||
(file: Uint8Array): void => {
|
||||
let json: KeyringPair$Json | undefined;
|
||||
setAccountsInfo(() => []);
|
||||
|
||||
let json: KeyringPair$Json | KeyringPairs$Json | undefined;
|
||||
|
||||
try {
|
||||
json = JSON.parse(u8aToString(file)) as KeyringPair$Json;
|
||||
json = JSON.parse(u8aToString(file)) as KeyringPair$Json | KeyringPairs$Json;
|
||||
setFile(json);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setFileError(true);
|
||||
}
|
||||
|
||||
json && jsonGetAccountInfo(json)
|
||||
.then((accountInfo: ResponseJsonGetAccountInfo) => setAccountInfo(accountInfo))
|
||||
.catch((e) => {
|
||||
setFileError(true);
|
||||
console.error(e);
|
||||
if (json === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isKeyringPairs$Json(json)) {
|
||||
setRequirePassword(true);
|
||||
json.accounts.forEach((account) => {
|
||||
setAccountsInfo((old) => [...old, {
|
||||
address: account.address,
|
||||
genesisHash: account.meta.genesisHash,
|
||||
name: account.meta.name
|
||||
} as ResponseJsonGetAccountInfo]);
|
||||
});
|
||||
} else {
|
||||
setRequirePassword(true);
|
||||
jsonGetAccountInfo(json)
|
||||
.then((accountInfo) => setAccountsInfo((old) => [...old, accountInfo]))
|
||||
.catch((e) => {
|
||||
setFileError(true);
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
}, []
|
||||
);
|
||||
|
||||
const _onRestore = useCallback(
|
||||
(): void => {
|
||||
if (!file || !password) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (requirePassword && !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBusy(true);
|
||||
|
||||
jsonRestore(file, password)
|
||||
(isKeyringPairs$Json(file) ? batchRestore(file, password) : jsonRestore(file, password))
|
||||
.then(() => { onAction('/'); })
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
@ -82,7 +107,7 @@ function Upload ({ className }: Props): React.ReactElement {
|
||||
setIsPasswordError(true);
|
||||
});
|
||||
},
|
||||
[file, onAction, password]
|
||||
[file, onAction, password, requirePassword]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -93,14 +118,15 @@ function Upload ({ className }: Props): React.ReactElement {
|
||||
text={t<string>('Restore from JSON')}
|
||||
/>
|
||||
<div className={className}>
|
||||
<div>
|
||||
{accountsInfo.map(({ address, genesisHash, name, type = DEFAULT_TYPE }, index) => (
|
||||
<Address
|
||||
address={address}
|
||||
genesisHash={genesisHash}
|
||||
key={`${index}:${address}`}
|
||||
name={name}
|
||||
type={type}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<InputFileWithLabel
|
||||
accept={acceptedFormats}
|
||||
isError={isFileError}
|
||||
@ -115,19 +141,23 @@ function Upload ({ className }: Props): React.ReactElement {
|
||||
{t<string>('Invalid Json file')}
|
||||
</Warning>
|
||||
)}
|
||||
<InputWithLabel
|
||||
isError={isPasswordError}
|
||||
label={t<string>('Password for this file')}
|
||||
onChange={_onChangePass}
|
||||
type='password'
|
||||
/>
|
||||
{isPasswordError && (
|
||||
<Warning
|
||||
isBelowInput
|
||||
isDanger
|
||||
>
|
||||
{t<string>('Unable to decode using the supplied passphrase')}
|
||||
</Warning>
|
||||
{requirePassword && (
|
||||
<div>
|
||||
<InputWithLabel
|
||||
isError={isPasswordError}
|
||||
label={t<string>('Password for this file')}
|
||||
onChange={_onChangePass}
|
||||
type='password'
|
||||
/>
|
||||
{isPasswordError && (
|
||||
<Warning
|
||||
isBelowInput
|
||||
isDanger
|
||||
>
|
||||
{t<string>('Unable to decode using the supplied passphrase')}
|
||||
</Warning>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className='restoreButton'
|
||||
@ -144,6 +174,6 @@ function Upload ({ className }: Props): React.ReactElement {
|
||||
|
||||
export default styled(Upload)`
|
||||
.restoreButton {
|
||||
margin-top: 6px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -22,6 +22,7 @@ import Authorize from './Authorize';
|
||||
import CreateAccount from './CreateAccount';
|
||||
import Derive from './Derive';
|
||||
import Export from './Export';
|
||||
import ExportAll from './ExportAll';
|
||||
import Forget from './Forget';
|
||||
import ImportLedger from './ImportLedger';
|
||||
import ImportQr from './ImportQr';
|
||||
@ -136,6 +137,7 @@ export default function Popup (): React.ReactElement {
|
||||
<Route path='/account/create'>{wrapWithErrorBoundary(<CreateAccount />, 'account-creation')}</Route>
|
||||
<Route path='/account/forget/:address'>{wrapWithErrorBoundary(<Forget />, 'forget-address')}</Route>
|
||||
<Route path='/account/export/:address'>{wrapWithErrorBoundary(<Export />, 'export-address')}</Route>
|
||||
<Route path='/account/export-all'>{wrapWithErrorBoundary(<ExportAll />, 'export-all-address')}</Route>
|
||||
<Route path='/account/import-ledger'>{wrapWithErrorBoundary(<ImportLedger />, 'import-ledger')}</Route>
|
||||
<Route path='/account/import-qr'>{wrapWithErrorBoundary(<ImportQr />, 'import-qr')}</Route>
|
||||
<Route path='/account/import-seed'>{wrapWithErrorBoundary(<ImportSeed />, 'import-seed')}</Route>
|
||||
|
||||
@ -5,6 +5,7 @@ import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MessageT
|
||||
import type { Message } from '@polkadot/extension-base/types';
|
||||
import type { Chain } from '@polkadot/extension-chains/types';
|
||||
import type { KeyringPair$Json } from '@polkadot/keyring/types';
|
||||
import type { KeyringPairs$Json } from '@polkadot/ui-keyring/types';
|
||||
import type { KeypairType } from '@polkadot/util-crypto/types';
|
||||
|
||||
import { PORT_EXTENSION } from '@polkadot/extension-base/defaults';
|
||||
@ -82,6 +83,10 @@ export async function exportAccount (address: string, password: string): Promise
|
||||
return sendMessage('pri(accounts.export)', { address, password });
|
||||
}
|
||||
|
||||
export async function exportAccounts (addresses: string[], password: string): Promise<{ exportedJson: string }> {
|
||||
return sendMessage('pri(accounts.batchExport)', { addresses, password });
|
||||
}
|
||||
|
||||
export async function validateAccount (address: string, password: string): Promise<boolean> {
|
||||
return sendMessage('pri(accounts.validate)', { address, password });
|
||||
}
|
||||
@ -222,3 +227,7 @@ export async function jsonGetAccountInfo (json: KeyringPair$Json): Promise<Respo
|
||||
export async function jsonRestore (file: KeyringPair$Json, password: string): Promise<void> {
|
||||
return sendMessage('pri(json.restore)', { file, password });
|
||||
}
|
||||
|
||||
export async function batchRestore (file: KeyringPairs$Json, password: string): Promise<void> {
|
||||
return sendMessage('pri(json.batchRestore)', { file, password });
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
import { faUsb } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faCodeBranch, faFileUpload, faKey, faPlusCircle, faQrcode } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCodeBranch, faFileExport, faFileUpload, faKey, faPlusCircle, faQrcode } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@ -62,6 +62,12 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
|
||||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
<MenuItem className='menuItem'>
|
||||
<Link to={'/account/export-all'}>
|
||||
<FontAwesomeIcon icon={faFileExport} />
|
||||
<span>{t<string>('Export all accounts')}</span>
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem className='menuItem'>
|
||||
<Link to='/account/import-seed'>
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
|
||||
9
packages/extension-ui/src/util/typeGuards.ts
Normal file
9
packages/extension-ui/src/util/typeGuards.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KeyringPair$Json } from '@polkadot/keyring/types';
|
||||
import type { KeyringPairs$Json } from '@polkadot/ui-keyring/types';
|
||||
|
||||
export function isKeyringPairs$Json (json: KeyringPair$Json | KeyringPairs$Json): json is KeyringPairs$Json {
|
||||
return (json.encoding.content).includes('batch-pkcs8');
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user