Skip to content

Commit

Permalink
feat: metamask private key migration (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
ejfitzgerald authored Jul 12, 2021
1 parent 27dbad2 commit cda3ee3
Show file tree
Hide file tree
Showing 14 changed files with 496 additions and 2 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
12
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@@ -1,11 +0,0 @@
**/*.png
**/*.svg
**/*.ttf
Expand All @@ -12,3 +11,4 @@ dist/*
prod/*

yarn.lock
.nvmrc
1 change: 1 addition & 0 deletions packages/eth-migration/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/*
1 change: 1 addition & 0 deletions packages/eth-migration/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/*
33 changes: 33 additions & 0 deletions packages/eth-migration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@fetchai/eth-migration",
"version": "0.8.8",
"main": "build/index.js",
"author": "chainapsis",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"clean": "rm -rf node_modules; rm -rf build",
"build": "tsc",
"dev": "tsc -w",
"test": "mocha --timeout 30000 -r ts-node/register 'src/**/*.spec.ts'",
"lint-test": "eslint \"src/**/*\" && prettier --check \"src/**/*\"",
"lint-fix": "eslint --fix \"src/**/*\" && prettier --write \"src/**/*\""
},
"devDependencies": {
"mocha": "^8.2.1",
"@types/keccak": "^3.0.1"
},
"dependencies": {
"buffer": "^5.4.3",
"crypto-js": "^4.0.0",
"elliptic": "^6.5.3",
"sha.js": "^2.4.11",
"keccak": "^3.0.1",
"web3-utils": "^1.3.6",
"@keplr-wallet/crypto": "^0.8.8",
"@keplr-wallet/cosmos": "^0.8.8"
}
}
40 changes: 40 additions & 0 deletions packages/eth-migration/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import "mocha";
import assert from "assert";
import { Buffer } from "buffer";
import { ethPublicKeyToAddress, parseEthPrivateKey } from "./index";

describe("Test eth private key matches", () => {
it("priv key should be processed", () => {
const privateKey =
"25ee227d82c2222d7e27aa61bbafd644ea949a817532471be2de38bf149071f3";
const expectedAddress = "0x742Fc9aFb9e1D8b474Da7b8a21Fd74f3E1633898";
const expectedRawPubKey =
"ee7bfb4e1a7346f283f568dc248dd7fc1319412cbaae90df66eabab1c41064f95e2d22ac2f41c720f286759df596817a2c7ce7b1e18febaf705fb5ff8db70ee6";
const expectedCompressedPubKey =
"02ee7bfb4e1a7346f283f568dc248dd7fc1319412cbaae90df66eabab1c41064f9";

const parsed = parseEthPrivateKey(Buffer.from(privateKey, "hex"));
assert.strictEqual(parsed?.ethAddress, expectedAddress);

assert.strictEqual(
Buffer.from(parsed?.rawPublicKey).toString("hex"),
expectedRawPubKey
);

assert.strictEqual(
Buffer.from(parsed?.compressedPublicKey).toString("hex"),
expectedCompressedPubKey
);
});

it("can generate a fetch address from a public key", () => {
const publicKey =
"02ee7bfb4e1a7346f283f568dc248dd7fc1319412cbaae90df66eabab1c41064f9";
const expectedFetchAddress = "fetch1s3rfc2n0cjgtqm4e2llvcjtq89yf6q7p2pfxw7";

assert.strictEqual(
ethPublicKeyToAddress(Buffer.from(publicKey, "hex")),
expectedFetchAddress
);
});
});
73 changes: 73 additions & 0 deletions packages/eth-migration/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import EC from "elliptic";
import keccak from "keccak";
import Web3Utils from "web3-utils";
import { PubKeySecp256k1 } from "@keplr-wallet/crypto";
import { Bech32Address } from "@keplr-wallet/cosmos";

const DEFAULT_BECH_PREFIX: string = "fetch";

export interface ParsedEthKey {
ethAddress: string;
rawPublicKey: Uint8Array;
compressedPublicKey: Uint8Array;
}

export function parseEthPrivateKey(
privateKey: Uint8Array
): ParsedEthKey | undefined {
if (privateKey.length !== 32) {
return;
}

// parse the private key
const secp256k1 = new EC.ec("secp256k1");
const key = secp256k1.keyFromPrivate(privateKey);
const rawPublicKey = new Uint8Array(
key.getPublic().encode("array", false).slice(1)
);
const compressedPublicKey = new Uint8Array(
key.getPublic().encodeCompressed("array")
);

// generate the raw address
const rawAddress = `0x${keccak("keccak256")
.update(Buffer.from(rawPublicKey))
.digest("hex")
.slice(24)}`;

// compute the checksum address
const ethAddress = Web3Utils.toChecksumAddress(rawAddress);

// return the parsed output
return {
ethAddress,
rawPublicKey,
compressedPublicKey,
};
}

export function addressMatchesPrivateKey(
address: string,
privateKey: Uint8Array
): boolean {
const parsed = parseEthPrivateKey(privateKey);
if (parsed === undefined) {
return false;
}

return parsed.ethAddress === address;
}

export function ethPublicKeyToAddress(
compressedPubKey: Uint8Array,
prefix?: string
): string | undefined {
if (compressedPubKey.length != 33) {
return;
}

const pubKey = new PubKeySecp256k1(compressedPubKey);
const address = new Bech32Address(pubKey.getAddress());

return address.toBech32(prefix ?? DEFAULT_BECH_PREFIX);
}
12 changes: 12 additions & 0 deletions packages/eth-migration/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"declaration": true,
"rootDir": "src"
},
"include": [
"src/**/*"
]
}
1 change: 1 addition & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@keplr-wallet/stores": "^0.8.9",
"@keplr-wallet/types": "^0.8.8",
"@keplr-wallet/unit": "^0.8.8",
"@fetchai/eth-migration": "^0.8.8",
"argon-dashboard-react": "^1.1.0",
"axios": "^0.21.0",
"bip39": "^3.0.2",
Expand Down
10 changes: 10 additions & 0 deletions packages/extension/src/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@

"register.ledger.title": "Import ledger",

"register.eth-migrate.title": "Migrate from ETH",
"register.eth-migrate.metamask-private-key.title": "Migrate a Metamask Private Key",
"register.eth-migrate.metamask-ledger.title": "Migrate a Metamask Ledger Device",

"register.eth-migrate.eth-address": "Ethereum Address",
"register.eth-migrate.eth-address.error.required": "Your Ethereum Address is required",
"register.eth-migrate.eth-private-key": "Private Key",
"register.eth-migrate.eth-private-key.error.invalid": "Invalid private key",
"register.eth-migrate.eth-private-key.error.required": "Your private key is required",

"register.verify.button.register": "Register",

"register.welcome.title": "You’re all set!",
Expand Down
11 changes: 11 additions & 0 deletions packages/extension/src/pages/register/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import {
} from "./ledger";
import { WelcomePage } from "./welcome";
import { AdditionalSignInPrepend } from "../../config.ui";
import {
MigrateEthereumAddressIntro,
MigrateEthereumAddressPage,
TypeMigrateEth,
} from "./migration";

export enum NunWords {
WORDS12,
Expand Down Expand Up @@ -72,6 +77,12 @@ export const RegisterPage: FunctionComponent = observer(() => {
intro: ImportLedgerIntro,
page: ImportLedgerPage,
},
// TODO: think about moving this into the configuration at some point
{
type: TypeMigrateEth,
intro: MigrateEthereumAddressIntro,
page: MigrateEthereumAddressPage,
},
]);

return (
Expand Down
75 changes: 75 additions & 0 deletions packages/extension/src/pages/register/migration/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { FunctionComponent, useState } from "react";
import { RegisterConfig } from "@keplr-wallet/hooks";
import { observer } from "mobx-react-lite";
import { FormattedMessage } from "react-intl";
import { Button } from "reactstrap";
import { BackButton } from "../index";
import { MigrateMetamaskPrivateKeyPage } from "./metamask-privatekey";

export const TypeMigrateEth = "migrate-from-eth";

enum MigrationMode {
SELECT_MODE,
METAMASK_PRIVATE_KEY,
}

export const MigrateEthereumAddressIntro: FunctionComponent<{
registerConfig: RegisterConfig;
}> = observer(({ registerConfig }) => {
return (
<Button
color="primary"
outline
block
onClick={(e) => {
e.preventDefault();

registerConfig.setType(TypeMigrateEth);
}}
>
<FormattedMessage id="register.eth-migrate.title" />
</Button>
);
});

const MigrationSelectionPage: FunctionComponent<{
setMode: (mode: MigrationMode) => void;
onBack: () => void;
}> = (props) => {
return (
<div>
<Button
color="primary"
outline
block
onClick={() => props.setMode(MigrationMode.METAMASK_PRIVATE_KEY)}
>
<FormattedMessage id="register.eth-migrate.metamask-private-key.title" />
</Button>
<BackButton onClick={props.onBack} />
</div>
);
};

export const MigrateEthereumAddressPage: FunctionComponent<{
registerConfig: RegisterConfig;
}> = observer(({ registerConfig }) => {
const [mode, setMode] = useState<MigrationMode>(MigrationMode.SELECT_MODE);

switch (mode) {
case MigrationMode.SELECT_MODE:
return (
<MigrationSelectionPage
setMode={setMode}
onBack={() => registerConfig.clear()}
/>
);
case MigrationMode.METAMASK_PRIVATE_KEY:
return (
<MigrateMetamaskPrivateKeyPage
registerConfig={registerConfig}
onBack={() => setMode(MigrationMode.SELECT_MODE)}
/>
);
}
});
Loading

0 comments on commit cda3ee3

Please sign in to comment.