From 3ba7f383fd5af4816f55584cf192267754dde6f1 Mon Sep 17 00:00:00 2001 From: irubido Date: Fri, 14 Jun 2024 14:09:16 +0200 Subject: [PATCH 1/3] fix: export seed coin type --- packages/snap/snap.manifest.json | 2 +- packages/snap/src/polkadot/account.ts | 2 +- packages/snap/src/rpc/exportSeed.ts | 9 ++++++--- packages/snap/test/unit/rpc/exportSeed.test.ts | 6 +++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 1babcfa1..11e7bd33 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "git+https://github.com/chainsafe/metamask-snap-polkadot.git" }, "source": { - "shasum": "tetP/U2cSxYl1CqUrLT+d1HD74GQyUy/MDUQ7h7qlEQ=", + "shasum": "VLA9zcJiR3GjrTia+9M5lgWjqwSZ8pCrAPrH39BUTrU=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/polkadot/account.ts b/packages/snap/src/polkadot/account.ts index 2966329c..3f4c0c56 100644 --- a/packages/snap/src/polkadot/account.ts +++ b/packages/snap/src/polkadot/account.ts @@ -26,7 +26,7 @@ export async function getKeyPair(): Promise { return keyring.addFromSeed(stringToU8a(seed)); } -export const getCoinTypeByNetwork = (network: SnapNetworks): number => { +export const getCoinTypeByNetwork = (network: SnapNetworks): 434 | 354 => { switch (network) { case 'kusama': case 'westend': diff --git a/packages/snap/src/rpc/exportSeed.ts b/packages/snap/src/rpc/exportSeed.ts index 86a9d900..ea432a3a 100644 --- a/packages/snap/src/rpc/exportSeed.ts +++ b/packages/snap/src/rpc/exportSeed.ts @@ -1,9 +1,12 @@ import type { JsonBIP44CoinTypeNode } from '@metamask/key-tree'; import { showConfirmationDialog } from '../util/confirmation'; - -const kusamaCoinType = 434; +import { getConfiguration } from '../configuration'; +import { getCoinTypeByNetwork } from '../polkadot/account'; export async function exportSeed(): Promise { + const configuration = await getConfiguration(); + const coinType = getCoinTypeByNetwork(configuration.networkName); + // ask for confirmation const confirmation = await showConfirmationDialog({ prompt: 'Do you want to export your seed?' @@ -12,7 +15,7 @@ export async function exportSeed(): Promise { if (confirmation) { const bip44Node = (await snap.request({ method: 'snap_getBip44Entropy', - params: { coinType: kusamaCoinType } + params: { coinType: coinType } })) as JsonBIP44CoinTypeNode; return bip44Node.privateKey.slice(0, 32); } diff --git a/packages/snap/test/unit/rpc/exportSeed.test.ts b/packages/snap/test/unit/rpc/exportSeed.test.ts index bcfd9cf1..4650cbd3 100644 --- a/packages/snap/test/unit/rpc/exportSeed.test.ts +++ b/packages/snap/test/unit/rpc/exportSeed.test.ts @@ -13,9 +13,9 @@ describe('Test rpc handler function: exportSeed', function () { }); it('should return seed on positive prompt confirmation and keyring saved in state', async function () { - walletStub.request.onFirstCall().returns(true); + walletStub.request.onSecondCall().returns(true); walletStub.request - .onSecondCall() + .onThirdCall() .returns({ privateKey: 'aba2dd1a12eeafda3fda62aa6dfa21ca2aa6dfaba13fda6a22ea2dd1eafda1ca' }); const result = await exportSeed(); expect(result).to.be.eq('aba2dd1a12eeafda3fda62aa6dfa21ca'); @@ -24,7 +24,7 @@ describe('Test rpc handler function: exportSeed', function () { it('should not return seed on negative prompt confirmation', async function () { walletStub.request.returns(false); const result = await exportSeed(); - expect(walletStub.request).to.have.been.calledOnce; + expect(walletStub.request).to.have.been.calledTwice; expect(result).to.be.eq(null); }); }); From 68da8a1ac60431dab426a2477d3b6d784874df05 Mon Sep 17 00:00:00 2001 From: irubido Date: Mon, 17 Jun 2024 16:04:24 +0200 Subject: [PATCH 2/3] feat: export account json rpc --- packages/adapter/src/methods.ts | 10 +++++ packages/adapter/src/snap.ts | 2 + packages/adapter/src/types.ts | 2 + packages/example/package.json | 2 + .../src/components/Account/Account.tsx | 42 ++++++++++++++++--- packages/snap/snap.manifest.json | 2 +- packages/snap/src/index.ts | 5 +++ packages/snap/src/rpc/exportAccount.ts | 14 +++++++ packages/snap/src/util/validation.ts | 6 +++ .../snap/test/unit/rpc/exportAccount.test.ts | 41 ++++++++++++++++++ packages/types/index.d.ts | 8 ++++ yarn.lock | 16 +++++++ 12 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 packages/snap/src/rpc/exportAccount.ts create mode 100644 packages/snap/test/unit/rpc/exportAccount.test.ts diff --git a/packages/adapter/src/methods.ts b/packages/adapter/src/methods.ts index 3cddbadb..82d39ad8 100644 --- a/packages/adapter/src/methods.ts +++ b/packages/adapter/src/methods.ts @@ -70,6 +70,16 @@ export async function exportSeed(this: MetamaskPolkadotSnap): Promise { return (await sendSnapMethod({ method: 'exportSeed' }, this.snapId)) as string; } +export async function exportAccount( + this: MetamaskPolkadotSnap, + jsonPassphrase?: string +): Promise { + return (await sendSnapMethod( + { method: 'exportAccount', params: { jsonPassphrase } }, + this.snapId + )) as string; +} + export async function setConfiguration( this: MetamaskPolkadotSnap, config: SnapConfig diff --git a/packages/adapter/src/snap.ts b/packages/adapter/src/snap.ts index 1dd12de1..10087cb9 100644 --- a/packages/adapter/src/snap.ts +++ b/packages/adapter/src/snap.ts @@ -1,5 +1,6 @@ import type { SnapConfig } from '@chainsafe/metamask-polkadot-types'; import { + exportAccount, exportSeed, generateTransactionPayload, getAddress, @@ -30,6 +31,7 @@ export class MetamaskPolkadotSnap { public getMetamaskSnapApi = (): MetamaskSnapApi => { return { exportSeed: exportSeed.bind(this), + exportAccount: exportAccount.bind(this), generateTransactionPayload: generateTransactionPayload.bind(this), getAddress: getAddress.bind(this), getAllTransactions: getAllTransactions.bind(this), diff --git a/packages/adapter/src/types.ts b/packages/adapter/src/types.ts index 0260d812..81d42e8c 100644 --- a/packages/adapter/src/types.ts +++ b/packages/adapter/src/types.ts @@ -18,6 +18,8 @@ export interface MetamaskSnapApi { exportSeed(): Promise; + exportAccount(jsonPassphrase?: string): Promise; + getLatestBlock(): Promise; setConfiguration(configuration: SnapConfig): Promise; diff --git a/packages/example/package.json b/packages/example/package.json index 72034539..1f010eb9 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -20,6 +20,7 @@ "@types/node": "^16.11.39", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", + "file-saver": "^2.0.5", "react": "^18.1.0", "react-dom": "^18.1.0", "react-scripts": "5.0.1", @@ -28,6 +29,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@types/file-saver": "^2", "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "@typescript-eslint/eslint-plugin": "^5.27.1", diff --git a/packages/example/src/components/Account/Account.tsx b/packages/example/src/components/Account/Account.tsx index 649f6afc..ab8f9219 100644 --- a/packages/example/src/components/Account/Account.tsx +++ b/packages/example/src/components/Account/Account.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { Box, Button, @@ -7,9 +7,11 @@ import { CardHeader, Divider, Grid, + TextField, Typography } from '@material-ui/core'; import { formatBalance } from '@polkadot/util/format/formatBalance'; +import FileSaver from 'file-saver'; import { MetaMaskContext } from '../../context/metamask'; import { getCurrency } from '../../services/format'; @@ -22,14 +24,27 @@ export interface AccountProps { export const Account = (props: AccountProps): React.JSX.Element => { const [state] = useContext(MetaMaskContext); + const [exportJsonPassphrase, setExportJsonPassphrase] = useState(''); - const handleExport = async (): Promise => { + const handleExportSeed = async (): Promise => { if (!state.polkadotSnap.snap) return; const api = state.polkadotSnap.snap.getMetamaskSnapApi(); const privateKey = await api.exportSeed(); alert(privateKey); }; + const handleExportAccount = async (): Promise => { + if (!state.polkadotSnap.snap) return; + const api = state.polkadotSnap.snap.getMetamaskSnapApi(); + const privateKey = await api.exportAccount(exportJsonPassphrase); + const blob = new Blob([privateKey]); + FileSaver.saveAs(blob, 'account.json'); + }; + + const handleChange = (event: React.ChangeEvent): void => { + setExportJsonPassphrase(event.target.value); + }; + return ( @@ -54,10 +69,25 @@ export const Account = (props: AccountProps): React.JSX.Element => { - - + + + + + + + + diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 11e7bd33..165ea544 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "git+https://github.com/chainsafe/metamask-snap-polkadot.git" }, "source": { - "shasum": "VLA9zcJiR3GjrTia+9M5lgWjqwSZ8pCrAPrH39BUTrU=", + "shasum": "tYD0ZXtm47IeZjzraQ4UKKO2KI57hpPuISz9atdAKz8=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 7b61c3da..c5ca25d6 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -16,12 +16,14 @@ import { generateTransactionPayload } from './rpc/generateTransactionPayload'; import { send } from './rpc/send'; import { validConfigureSchema, + validExportAccountSchema, validGenerateTransactionPayloadSchema, validGetBlockSchema, validSendSchema, validSignPayloadJSONSchema, validSignPayloadRawSchema } from './util/validation'; +import { exportAccount } from './rpc/exportAccount'; const apiDependentMethods = [ 'getBlock', @@ -65,6 +67,9 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { return await getAddress(); case 'exportSeed': return await exportSeed(); + case 'exportAccount': + assert(request.params, validExportAccountSchema); + return await exportAccount(request.params.jsonPassphrase); case 'getAllTransactions': return await getTransactions(); case 'getBlock': diff --git a/packages/snap/src/rpc/exportAccount.ts b/packages/snap/src/rpc/exportAccount.ts new file mode 100644 index 00000000..848dacc4 --- /dev/null +++ b/packages/snap/src/rpc/exportAccount.ts @@ -0,0 +1,14 @@ +import { getKeyPair } from '../polkadot/account'; +import { showConfirmationDialog } from '../util/confirmation'; + +export async function exportAccount(jsonPassphrase?: string): Promise { + const confirmation = await showConfirmationDialog({ + prompt: 'Do you want to export your account?' + }); + + if (confirmation) { + const keyPair = await getKeyPair(); + return JSON.stringify(keyPair.toJson(jsonPassphrase)); + } + return null; +} diff --git a/packages/snap/src/util/validation.ts b/packages/snap/src/util/validation.ts index 5925b43d..c2b9dae7 100644 --- a/packages/snap/src/util/validation.ts +++ b/packages/snap/src/util/validation.ts @@ -81,3 +81,9 @@ export const validSendSchema: Describe<{ tx: string() }) }); + +export const validExportAccountSchema: Describe<{ + jsonPassphrase: string; +}> = object({ + jsonPassphrase: optional(string()) +}); diff --git a/packages/snap/test/unit/rpc/exportAccount.test.ts b/packages/snap/test/unit/rpc/exportAccount.test.ts new file mode 100644 index 00000000..eb1cd259 --- /dev/null +++ b/packages/snap/test/unit/rpc/exportAccount.test.ts @@ -0,0 +1,41 @@ +import chai, { expect } from 'chai'; +import sinonChai from 'sinon-chai'; +import { getWalletMock } from '../wallet.mock'; +import { exportAccount } from "../../../src/rpc/exportAccount" + +chai.use(sinonChai); + +describe('Test rpc handler function: exportAccount', function () { + const walletStub = getWalletMock(); + + const privateKey = 'aba2dd1a12eeafda3fda62aa6dfa21ca2aa6dfaba13fda6a22ea2dd1eafda1ca' + const nonEncodedResult = '{"encoded":"MFMCAQEwBQYDK2VwBCIEIGFiYTJkZDFhMTJlZWFmZGEzZmRhNjJhYTZkZmEyMWNhzwQ+E9kijYqTHOTMWO+9GtbF4vGTLDF06xUN+vkWW3OhIwMhAM8EPhPZIo2KkxzkzFjvvRrWxeLxkywxdOsVDfr5Fltz","encoding":{"content":["pkcs8","ed25519"],"type":["none"],"version":"3"},"address":"5Gk92fkWPUg6KNHSfP93UcPFhwGurM9RKAKU62Dg6upaCfH7","meta":{}}'; + afterEach(function () { + walletStub.reset(); + }); + + it('should return stringified json account on positive prompt', async function () { + walletStub.request.onFirstCall().returns(true); + walletStub.request + .onThirdCall() + .returns({ privateKey }); + const result = await exportAccount(); + expect(result).to.be.eq(nonEncodedResult); + }); + + it('returned encoded json should be different from non encoded json', async function () { + walletStub.request.onFirstCall().returns(true); + walletStub.request + .onThirdCall() + .returns({ privateKey: 'aba2dd1a12eeafda3fda62aa6dfa21ca2aa6dfaba13fda6a22ea2dd1eafda1ca' }); + const encodedResult = await exportAccount("password"); + + expect(encodedResult).not.to.be.eq(nonEncodedResult) + }); + + it('should return null on negative prompt', async function () { + walletStub.request.onFirstCall().returns(false); + const result = await exportAccount(); + expect(result).to.be.eq(null); + }); +}); diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 0a53afbe..d1dd8fa4 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -12,6 +12,13 @@ export interface ExportSeedRequest { method: 'exportSeed'; } +export interface ExportAccountRequest { + method: 'exportAccount'; + params: { + jsonPassphrase?: string; + } +} + export interface GetTransactionsRequest { method: 'getAllTransactions'; } @@ -76,6 +83,7 @@ export type MetamaskPolkadotRpcRequest = | GetPublicKeyRequest | GetAddressRequest | ExportSeedRequest + | ExportAccountRequest | GetTransactionsRequest | GetBlockRequest | GetBalanceRequest diff --git a/yarn.lock b/yarn.lock index 705e5f9e..3b64f2b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4038,6 +4038,13 @@ __metadata: languageName: node linkType: hard +"@types/file-saver@npm:^2": + version: 2.0.7 + resolution: "@types/file-saver@npm:2.0.7" + checksum: c6b88a1aea8eec58469da2a90828fef6e9d5d590c7094fb959783d7c32878af80d39439734f3d41b78355dadb507f606e3d04a29a160c85411c65251e58df847 + languageName: node + linkType: hard + "@types/filesystem@npm:*": version: 0.0.32 resolution: "@types/filesystem@npm:0.0.32" @@ -8639,6 +8646,7 @@ __metadata: "@testing-library/jest-dom": ^5.16.4 "@testing-library/react": ^13.3.0 "@testing-library/user-event": ^13.5.0 + "@types/file-saver": ^2 "@types/jest": ^27.5.2 "@types/node": ^16.11.39 "@types/react": ^18.0.12 @@ -8648,6 +8656,7 @@ __metadata: eslint: ^8.17.0 eslint-config-prettier: ^9.0.0 eslint-plugin-prettier: ^5.0.0 + file-saver: ^2.0.5 react: ^18.1.0 react-dom: ^18.1.0 react-scripts: 5.0.1 @@ -8901,6 +8910,13 @@ __metadata: languageName: node linkType: hard +"file-saver@npm:^2.0.5": + version: 2.0.5 + resolution: "file-saver@npm:2.0.5" + checksum: 0a361f683786c34b2574aea53744cb70d0a6feb0fa5e3af00f2fcb6c9d40d3049cc1470e38c6c75df24219f247f6fb3076f86943958f580e62ee2ffe897af8b1 + languageName: node + linkType: hard + "filelist@npm:^1.0.4": version: 1.0.4 resolution: "filelist@npm:1.0.4" From 45d6452b65e167bedf94933ed34c78760f9ae6ea Mon Sep 17 00:00:00 2001 From: irubido Date: Tue, 18 Jun 2024 12:55:33 +0200 Subject: [PATCH 3/3] type to number --- packages/snap/src/polkadot/account.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snap/src/polkadot/account.ts b/packages/snap/src/polkadot/account.ts index 3f4c0c56..2966329c 100644 --- a/packages/snap/src/polkadot/account.ts +++ b/packages/snap/src/polkadot/account.ts @@ -26,7 +26,7 @@ export async function getKeyPair(): Promise { return keyring.addFromSeed(stringToU8a(seed)); } -export const getCoinTypeByNetwork = (network: SnapNetworks): 434 | 354 => { +export const getCoinTypeByNetwork = (network: SnapNetworks): number => { switch (network) { case 'kusama': case 'westend':