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

feat: export account json rpc #234

Merged
merged 5 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove jsonPassphrase there is no usage of it in snap

Copy link
Contributor Author

@irubido irubido Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left it optional because polkadot js extension must have a passphrase to successfully import account.
Leaving it as empty field does not allow restoring.
Screenshot from 2024-06-18 13-20-33

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but like reviewing exportAccount.test.ts seems like there is no point of using it

If the extension uses a password field that is not used anywhere (bad U, itX) should not affect our API

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we wouldn't allow this option, we lose nothing extending the already existing
KeyringPair.toJson(passphrase?: string) method.

): 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": "tetP/U2cSxYl1CqUrLT+d1HD74GQyUy/MDUQ7h7qlEQ=",
"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
2 changes: 1 addition & 1 deletion packages/snap/src/polkadot/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function getKeyPair(): Promise<KeyringPair> {
return keyring.addFromSeed(stringToU8a(seed));
}

export const getCoinTypeByNetwork = (network: SnapNetworks): number => {
export const getCoinTypeByNetwork = (network: SnapNetworks): 434 | 354 => {
switch (network) {
case 'kusama':
case 'westend':
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;
}
9 changes: 6 additions & 3 deletions packages/snap/src/rpc/exportSeed.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> {
const configuration = await getConfiguration();
const coinType = getCoinTypeByNetwork(configuration.networkName);

// ask for confirmation
const confirmation = await showConfirmationDialog({
prompt: 'Do you want to export your seed?'
Expand All @@ -12,7 +15,7 @@ export async function exportSeed(): Promise<string | null> {
if (confirmation) {
const bip44Node = (await snap.request({
method: 'snap_getBip44Entropy',
params: { coinType: kusamaCoinType }
params: { coinType: coinType }
})) as JsonBIP44CoinTypeNode;
return bip44Node.privateKey.slice(0, 32);
}
Expand Down
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);
});
});
6 changes: 3 additions & 3 deletions packages/snap/test/unit/rpc/exportSeed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
});
});
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
Loading