Skip to content
This repository has been archived by the owner on Dec 3, 2024. It is now read-only.

Commit

Permalink
feat: export account json rpc (#234)
Browse files Browse the repository at this point in the history
contains changes from #233 

new rpc returns stringified json of keyring pair
example implements file saver and json passphrase encoding field
  • Loading branch information
irubido authored Jun 18, 2024
1 parent 7bef7c2 commit 0212428
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 7 deletions.
10 changes: 10 additions & 0 deletions packages/adapter/src/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ export async function exportSeed(this: MetamaskPolkadotSnap): Promise<string> {
return (await sendSnapMethod({ method: 'exportSeed' }, this.snapId)) as string;
}

export async function exportAccount(
this: MetamaskPolkadotSnap,
jsonPassphrase?: string
): Promise<string> {
return (await sendSnapMethod(
{ method: 'exportAccount', params: { jsonPassphrase } },
this.snapId
)) as string;
}

export async function setConfiguration(
this: MetamaskPolkadotSnap,
config: SnapConfig
Expand Down
2 changes: 2 additions & 0 deletions packages/adapter/src/snap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SnapConfig } from '@chainsafe/metamask-polkadot-types';
import {
exportAccount,
exportSeed,
generateTransactionPayload,
getAddress,
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions packages/adapter/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface MetamaskSnapApi {

exportSeed(): Promise<string>;

exportAccount(jsonPassphrase?: string): Promise<string>;

getLatestBlock(): Promise<BlockInfo>;

setConfiguration(configuration: SnapConfig): Promise<void>;
Expand Down
2 changes: 2 additions & 0 deletions packages/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
42 changes: 36 additions & 6 deletions packages/example/src/components/Account/Account.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import {
Box,
Button,
Expand All @@ -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';

Expand All @@ -22,14 +24,27 @@ export interface AccountProps {

export const Account = (props: AccountProps): React.JSX.Element => {
const [state] = useContext(MetaMaskContext);
const [exportJsonPassphrase, setExportJsonPassphrase] = useState<string>('');

const handleExport = async (): Promise<void> => {
const handleExportSeed = async (): Promise<void> => {
if (!state.polkadotSnap.snap) return;
const api = state.polkadotSnap.snap.getMetamaskSnapApi();
const privateKey = await api.exportSeed();
alert(privateKey);
};

const handleExportAccount = async (): Promise<void> => {
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<HTMLInputElement>): void => {
setExportJsonPassphrase(event.target.value);
};

return (
<Card style={{ margin: '1rem 0' }}>
<CardHeader title="Account details" />
Expand All @@ -54,10 +69,25 @@ export const Account = (props: AccountProps): React.JSX.Element => {
</Typography>
</Grid>
</Grid>
<Grid container item xs={12} justifyContent="flex-end">
<Button color="secondary" variant={'contained'} onClick={handleExport}>
Export private key
</Button>
<Grid container item xs={12} justifyContent="center">
<Grid item xs={4}>
<Button color="secondary" variant={'contained'} onClick={handleExportSeed}>
Export private key
</Button>
</Grid>
<Grid item xs={4}>
<Button color="secondary" variant={'contained'} onClick={handleExportAccount}>
Export account as json
</Button>
<TextField
onChange={handleChange}
value={exportJsonPassphrase}
size="small"
id="recipient"
label="optional json passphrase"
variant="outlined"
/>
</Grid>
</Grid>
</CardContent>
</Card>
Expand Down
2 changes: 1 addition & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions packages/snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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':
Expand Down
14 changes: 14 additions & 0 deletions packages/snap/src/rpc/exportAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getKeyPair } from '../polkadot/account';
import { showConfirmationDialog } from '../util/confirmation';

export async function exportAccount(jsonPassphrase?: string): Promise<string> {
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;
}
6 changes: 6 additions & 0 deletions packages/snap/src/util/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,9 @@ export const validSendSchema: Describe<{
tx: string()
})
});

export const validExportAccountSchema: Describe<{
jsonPassphrase: string;
}> = object({
jsonPassphrase: optional(string())
});
41 changes: 41 additions & 0 deletions packages/snap/test/unit/rpc/exportAccount.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
8 changes: 8 additions & 0 deletions packages/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export interface ExportSeedRequest {
method: 'exportSeed';
}

export interface ExportAccountRequest {
method: 'exportAccount';
params: {
jsonPassphrase?: string;
}
}

export interface GetTransactionsRequest {
method: 'getAllTransactions';
}
Expand Down Expand Up @@ -76,6 +83,7 @@ export type MetamaskPolkadotRpcRequest =
| GetPublicKeyRequest
| GetAddressRequest
| ExportSeedRequest
| ExportAccountRequest
| GetTransactionsRequest
| GetBlockRequest
| GetBalanceRequest
Expand Down
16 changes: 16 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 0212428

Please sign in to comment.