diff --git a/packages/extension-base/src/background/handlers/Extension.test.ts b/packages/extension-base/src/background/handlers/Extension.test.ts index e6dbf1da..0991f15a 100644 --- a/packages/extension-base/src/background/handlers/Extension.test.ts +++ b/packages/extension-base/src/background/handlers/Extension.test.ts @@ -69,8 +69,8 @@ describe('Extension', () => { password }, {} as chrome.runtime.Port); - expect(result.exportedJson).toContain(address); - expect(result.exportedJson).toContain('"encoded"'); + expect(result.exportedJson.address).toBe(address); + expect(result.exportedJson.encoded).toBeDefined(); }); describe('account derivation', () => { diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index ec30c184..b586918e 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -5,7 +5,7 @@ import type { MetadataDef } from '@polkadot/extension-inject/types'; import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types'; import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import type { SubjectInfo } from '@polkadot/ui-keyring/observable/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 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, ResponseAccountsExport, 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'; @@ -97,14 +97,12 @@ export default class Extension { } private accountsExport ({ address, password }: RequestAccountExport): ResponseAccountExport { - return { exportedJson: JSON.stringify(keyring.backupAccount(keyring.getPair(address), password)) }; + return { exportedJson: keyring.backupAccount(keyring.getPair(address), password) }; } - private async accountsBatchExport ({ addresses, password }: RequestAccountBatchExport): Promise { + private async accountsBatchExport ({ addresses, password }: RequestAccountBatchExport): Promise { return { - exportedJson: JSON.stringify( - await keyring.backupAccounts(addresses, password) - ) + exportedJson: await keyring.backupAccounts(addresses, password) }; } diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index f04b6009..cf9fbc82 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -79,7 +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.batchExport)': [RequestAccountBatchExport, ResponseAccountsExport] 'pri(accounts.forget)': [RequestAccountForget, boolean]; 'pri(accounts.show)': [RequestAccountShow, boolean]; 'pri(accounts.tie)': [RequestAccountTie, boolean]; @@ -352,7 +352,11 @@ export interface ResponseSeedValidate { } export interface ResponseAccountExport { - exportedJson: string; + exportedJson: KeyringPair$Json; +} + +export interface ResponseAccountsExport { + exportedJson: KeyringPairs$Json; } export type ResponseRpcListProviders = ProviderList; diff --git a/packages/extension-ui/package.json b/packages/extension-ui/package.json index 82d95603..1f531d82 100644 --- a/packages/extension-ui/package.json +++ b/packages/extension-ui/package.json @@ -21,6 +21,7 @@ "@polkadot/react-qr": "^0.72.1", "@polkadot/util": "^6.0.5", "@polkadot/util-crypto": "^6.0.5", + "file-saver": "^2.0.5", "i18next": "^19.9.2", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -35,6 +36,7 @@ "@types/bn.js": "^4.11.6", "@types/enzyme": "^3.10.8", "@types/enzyme-adapter-react-16": "^1.0.6", + "@types/file-saver": "^2.0.2", "@types/react-copy-to-clipboard": "^5.0.0", "@types/react-dom": "^17.0.3", "@types/react-router": "^5.1.13", diff --git a/packages/extension-ui/src/Popup/Export.test.tsx b/packages/extension-ui/src/Popup/Export.test.tsx index 03ae49b5..5c2305f0 100644 --- a/packages/extension-ui/src/Popup/Export.test.tsx +++ b/packages/extension-ui/src/Popup/Export.test.tsx @@ -10,6 +10,8 @@ import { act } from 'react-dom/test-utils'; import { MemoryRouter, Route } from 'react-router'; import { ThemeProvider } from 'styled-components'; +import { KeyringPair$Json } from '@polkadot/keyring/types'; + import { Button, themes } from '../components'; import * as messaging from '../messaging'; import { flushAllPromises } from '../testHelpers'; @@ -27,7 +29,7 @@ describe('Export component', () => { }; beforeEach(() => { - jest.spyOn(messaging, 'exportAccount').mockResolvedValue({ exportedJson: '{ "meta": { "name": "account_name" } }' }); + jest.spyOn(messaging, 'exportAccount').mockResolvedValue({ exportedJson: { meta: { name: 'account_name' } } as unknown as KeyringPair$Json }); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment wrapper = mount( diff --git a/packages/extension-ui/src/Popup/Export.tsx b/packages/extension-ui/src/Popup/Export.tsx index 01b43cfa..2e1b028d 100644 --- a/packages/extension-ui/src/Popup/Export.tsx +++ b/packages/extension-ui/src/Popup/Export.tsx @@ -3,6 +3,7 @@ import type { ThemeProps } from '../types'; +import { saveAs } from 'file-saver'; import React, { useCallback, useContext, useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router'; import styled from 'styled-components'; @@ -43,12 +44,9 @@ function Export ({ className, match: { params: { address } } }: Props): React.Re exportAccount(address, pass) .then(({ exportedJson }) => { - const element = document.createElement('a'); - const { meta } = JSON.parse(exportedJson) as { meta: { name: string } }; + const blob = new Blob([JSON.stringify(exportedJson)], { type: 'application/json; charset=utf-8' }); - element.href = `data:text/plain;charset=utf-8,${exportedJson}`; - element.download = `${meta.name}_exported_account_${Date.now()}.json`; - element.click(); + saveAs(blob, `${address}.json`); onAction('/'); }) diff --git a/packages/extension-ui/src/Popup/ExportAll.tsx b/packages/extension-ui/src/Popup/ExportAll.tsx index 0cd1ef39..caf47a92 100644 --- a/packages/extension-ui/src/Popup/ExportAll.tsx +++ b/packages/extension-ui/src/Popup/ExportAll.tsx @@ -3,6 +3,7 @@ import type { ThemeProps } from '../types'; +import { saveAs } from 'file-saver'; import React, { useCallback, useContext, useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router'; import styled from 'styled-components'; @@ -44,11 +45,9 @@ function ExportAll ({ className }: Props): React.ReactElement { exportAccounts(accounts.map((account) => account.address), pass) .then(({ exportedJson }) => { - const element = document.createElement('a'); + const blob = new Blob([JSON.stringify(exportedJson)], { type: 'application/json; charset=utf-8' }); - element.href = `data:text/plain;charset=utf-8,${exportedJson}`; - element.download = `batch_exported_account_${Date.now()}.json`; - element.click(); + saveAs(blob, `batch_exported_account_${Date.now()}.json`); onAction('/'); }) diff --git a/packages/extension-ui/src/messaging.ts b/packages/extension-ui/src/messaging.ts index 61f6a1ac..95d56e9a 100644 --- a/packages/extension-ui/src/messaging.ts +++ b/packages/extension-ui/src/messaging.ts @@ -79,11 +79,11 @@ export async function tieAccount (address: string, genesisHash: string | null): return sendMessage('pri(accounts.tie)', { address, genesisHash }); } -export async function exportAccount (address: string, password: string): Promise<{ exportedJson: string }> { +export async function exportAccount (address: string, password: string): Promise<{ exportedJson: KeyringPair$Json }> { return sendMessage('pri(accounts.export)', { address, password }); } -export async function exportAccounts (addresses: string[], password: string): Promise<{ exportedJson: string }> { +export async function exportAccounts (addresses: string[], password: string): Promise<{ exportedJson: KeyringPairs$Json }> { return sendMessage('pri(accounts.batchExport)', { addresses, password }); } diff --git a/yarn.lock b/yarn.lock index f35dc07b..3f9bbc83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2150,6 +2150,7 @@ __metadata: "@types/bn.js": ^4.11.6 "@types/enzyme": ^3.10.8 "@types/enzyme-adapter-react-16": ^1.0.6 + "@types/file-saver": ^2.0.2 "@types/react-copy-to-clipboard": ^5.0.0 "@types/react-dom": ^17.0.3 "@types/react-router": ^5.1.13 @@ -2159,6 +2160,7 @@ __metadata: "@wojtekmaj/enzyme-adapter-react-17": ^0.6.0 enzyme: ^3.11.0 enzyme-adapter-react-16: ^1.15.6 + file-saver: ^2.0.5 i18next: ^19.9.2 react: ^17.0.2 react-dom: ^17.0.2 @@ -2787,6 +2789,13 @@ __metadata: languageName: node linkType: hard +"@types/file-saver@npm:^2.0.2": + version: 2.0.2 + resolution: "@types/file-saver@npm:2.0.2" + checksum: ccdfbbe45c68b54192d1a9fb9c5f7594c7eabe64ad55982839f0e758b72a62ae0e2edcee16af2b7c197d53f57047b5e11dac5d7805326718d0cb5a5e2f4dc382 + languageName: node + linkType: hard + "@types/filesystem@npm:*": version: 0.0.29 resolution: "@types/filesystem@npm:0.0.29" @@ -7611,6 +7620,13 @@ __metadata: languageName: node linkType: hard +"file-saver@npm:^2.0.5": + version: 2.0.5 + resolution: "file-saver@npm:2.0.5" + checksum: 0f835236534377c9b7591b995d94ae5061166452fb1f784548b706ff48984930e45a7947cfe393ce86b84ef2ec89064120c5d5ab5186146c508bb022c71ced3d + languageName: node + linkType: hard + "file-selector@npm:^0.2.2": version: 0.2.4 resolution: "file-selector@npm:0.2.4"