on master help sign in
Merge pull request #273 from liam-fitzgerald/feature/metamask-login

Metamask authentication

commit0b933680a9f52a4cfd43098a0dd54ab744167640 authorAnthony Arroyo <anthony@tlon.io> dateMon 16 Sep 2019, 09:08 (7 years ago) parente3544fe0b5533c
6 files changed+187−26
src/lib/metamask.js+27 src/lib/txn.js+17 src/lib/wallet.js+1 src/views/Login.js+7−26 src/views/Login/Metamask.js+68 src/views/Login/Other.js+67
src/lib/metamask.jsnew file+27
@@ -0,0 +1,27 @@
1+export function MetamaskWallet(address) {
2+ this.address = address;
3+}
4+
5+export function FakeMetamaskTransaction(txnData) {
6+ this.txnData = txnData;
7+
8+ this.serialize = function() {
9+ return '0';
10+ };
11+}
12+
13+// Cheat a bit and pretend to sign transaction
14+// Sign it actually when sending
15+export const metamaskSignTransaction = async (txn, from) => {
16+ const metamaskFormattedTxn = {
17+ to: txn.to.toString('hex'),
18+ value: txn.value.toString('hex'),
19+ data: txn.data.toString('hex'),
20+ gasLimit: txn.gasLimit.toString('hex'),
21+ gasPrice: txn.gasPrice.toString('hex'),
22+ nonce: txn.nonce.length === 0 ? '00' : txn.nonce.toString('hex'),
23+ from: from,
24+ };
25+
26+ return new FakeMetamaskTransaction(metamaskFormattedTxn);
27+};
src/lib/txn.js+17
@@ -5,6 +5,7 @@ import retry from 'async-retry';
55import { NETWORK_TYPES } from './network';
66import { ledgerSignTransaction } from './ledger';
77import { trezorSignTransaction } from './trezor';
8+import { metamaskSignTransaction, FakeMetamaskTransaction } from './metamask';
89import { WALLET_TYPES, addHexPrefix } from './wallet';
910import { CHECK_BLOCK_EVERY_MS } from './constants';
1011import patp2dec from './patp2dec';
@@ -89,6 +90,8 @@ const signTransaction = async ({
8990 await ledgerSignTransaction(stx, walletHdPath);
9091 } else if (walletType === WALLET_TYPES.TREZOR) {
9192 await trezorSignTransaction(stx, walletHdPath);
93+ } else if (walletType === WALLET_TYPES.METAMASK) {
94+ return metamaskSignTransaction(stx, wallet.address);
9295 } else {
9396 stx.sign(wallet.privateKey);
9497 }
@@ -97,6 +100,20 @@ const signTransaction = async ({
97100};
98101
99102const sendSignedTransaction = (web3, stx, doubtNonceError) => {
103+ // Handle fake metamask transaction
104+ if (stx instanceof FakeMetamaskTransaction) {
105+ const { ethereum } = window;
106+ const sendTxs = {
107+ method: 'eth_sendTransaction',
108+ params: [stx.txnData],
109+ from: stx.txnData.from,
110+ };
111+ return new Promise((resolve, reject) =>
112+ ethereum.sendAsync(sendTxs, (err, data) =>
113+ err ? reject(err) : resolve(data.result)
114+ )
115+ );
116+ }
100117 const rawTx = hexify(stx.serialize());
101118
102119 return new Promise(async (resolve, reject) => {
src/lib/wallet.js+1
@@ -19,6 +19,7 @@ export const WALLET_TYPES = {
1919 TREZOR: Symbol('TREZOR'),
2020 PRIVATE_KEY: Symbol('PRIVATE_KEY'),
2121 KEYSTORE: Symbol('KEYSTORE'),
22+ METAMASK: Symbol('METAMASK'),
2223};
2324
2425export function EthereumWallet(privateKey) {
src/views/Login.js+7−26
@@ -13,47 +13,28 @@ import Footer from 'components/Footer';
1313import { ForwardButton, OfflineButton } from 'components/Buttons';
1414
1515import Ticket from './Login/Ticket';
16−import Mnemonic from './Login/Mnemonic';
17−import Advanced from './Login/Advanced';
18−import Hardware from './Login/Hardware';
16+import Other from './Login/Other';
1917
2018const NAMES = {
2119 TICKET: 'TICKET',
22− MNEMONIC: 'MNEMONIC',
23− HARDWARE: 'HARDWARE',
24− ADVANCED: 'ADVANCED',
20+ OTHER: 'OTHER',
2521};
2622
2723const VIEWS = {
2824 [NAMES.TICKET]: Ticket,
29− [NAMES.MNEMONIC]: Mnemonic,
30− [NAMES.HARDWARE]: Hardware,
31− [NAMES.ADVANCED]: Advanced,
25+ [NAMES.OTHER]: Other,
3226};
3327
3428const OPTIONS = [
3529 { text: 'Master Ticket', value: NAMES.TICKET },
36− { text: 'Mnemonic', value: NAMES.MNEMONIC },
37− { text: 'Hardware', value: NAMES.HARDWARE },
38− { text: 'Advanced', value: NAMES.ADVANCED },
30+ { text: 'Other', value: NAMES.OTHER },
3931];
4032
4133const walletTypeToViewName = walletType => {
42− switch (walletType) {
43− case WALLET_TYPES.MNEMONIC:
44− return NAMES.MNEMONIC;
45− case WALLET_TYPES.LEDGER:
46− case WALLET_TYPES.TREZOR:
47− return NAMES.HARDWARE;
48− case WALLET_TYPES.KEYSTORE:
49− case WALLET_TYPES.PRIVATE_KEY:
50− return NAMES.ADVANCED;
51− case WALLET_TYPES.TICKET:
52− case WALLET_TYPES.SHARDS:
53− return NAMES.TICKET;
54− default:
55− return NAMES.TICKET;
34+ if (walletType === WALLET_TYPES.TICKET || !walletType) {
35+ return NAMES.TICKET;
5636 }
37+ return NAMES.OTHER;
5738};
5839
5940export default function Login() {
src/views/Login/Metamask.jsnew file+68
@@ -0,0 +1,68 @@
1+import React, { useCallback } from 'react';
2+import { Grid, Text, LinkButton, H5 } from 'indigo-react';
3+import { Just } from 'folktale/maybe';
4+
5+import { useWallet } from 'store/wallet';
6+
7+import { WALLET_TYPES } from 'lib/wallet';
8+import { MetamaskWallet } from 'lib/metamask';
9+import useLoginView from 'lib/useLoginView';
10+
11+import SubmitButton from 'form/SubmitButton';
12+import BridgeForm from 'form/BridgeForm';
13+
14+export default function Metamask({ className, goHome }) {
15+ useLoginView(WALLET_TYPES.METAMASK);
16+ const { setWallet, setWalletType } = useWallet();
17+
18+ const onSubmit = useCallback(async () => {
19+ const accounts = await window.ethereum.enable();
20+ const wallet = new MetamaskWallet(accounts[0]);
21+ setWallet(Just(wallet));
22+ setWalletType(WALLET_TYPES.METAMASK);
23+ }, [setWallet, setWalletType]);
24+
25+ return (
26+ <Grid className={className}>
27+ {(window.ethereum && Login({ onSubmit, goHome })) || Unsupported()}
28+ </Grid>
29+ );
30+}
31+
32+function Unsupported() {
33+ return (
34+ <>
35+ <Grid.Item full as={H5}>
36+ Unsupported
37+ </Grid.Item>
38+ <Grid.Item full as={Text} className="f6 mb3">
39+ Metamask is not installed on this browser. <br />
40+ <LinkButton href="https://metamask.io" as={LinkButton}>
41+ Get Metamask
42+ </LinkButton>
43+ </Grid.Item>
44+ </>
45+ );
46+}
47+function Login({ onSubmit, goHome }) {
48+ return (
49+ <>
50+ <Grid.Item full as={H5}>
51+ Authenticate with Metamask
52+ </Grid.Item>
53+ <Grid.Item full as={Text} className="f6 mb3">
54+ If you wish to login with a different wallet, please change it in the
55+ Metamask extension
56+ </Grid.Item>
57+ <BridgeForm onSubmit={onSubmit} afterSubmit={goHome}>
58+ {({ handleSubmit }) => (
59+ <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
60+ Login
61+ {window.ethereum.selectedAddress &&
62+ ' as ' + window.ethereum.selectedAddress}
63+ </Grid.Item>
64+ )}
65+ </BridgeForm>
66+ </>
67+ );
68+}
src/views/Login/Other.jsnew file+67
@@ -0,0 +1,67 @@
1+import React, { useState, useMemo } from 'react';
2+
3+import useBreakpoints from 'lib/useBreakpoints';
4+
5+import Accordion from 'components/Accordion';
6+
7+import PrivateKey from './PrivateKey';
8+import Keystore from './Keystore';
9+import Ledger from './Ledger';
10+import Metamask from './Metamask';
11+import Trezor from './Trezor';
12+import Mnemonic from './Mnemonic';
13+
14+const NAMES = {
15+ PRIVATE_KEY: 'PRIVATE_KEY',
16+ KEYSTORE: 'KEYSTORE',
17+ LEDGER: 'LEDGER',
18+ TREZOR: 'TREZOR',
19+ METAMASK: 'METAMASK',
20+ MNEMONIC: 'MNEMONIC',
21+};
22+
23+const VIEWS = {
24+ [NAMES.PRIVATE_KEY]: PrivateKey,
25+ [NAMES.KEYSTORE]: Keystore,
26+ [NAMES.LEDGER]: Ledger,
27+ [NAMES.TREZOR]: Trezor,
28+ [NAMES.METAMASK]: Metamask,
29+ [NAMES.MNEMONIC]: Mnemonic,
30+};
31+
32+const OPTIONS = [
33+ { text: 'Mnemonic (BIP 39)', value: NAMES.MNEMONIC },
34+ { text: 'Ledger', value: NAMES.LEDGER },
35+ { text: 'Trezor', value: NAMES.TREZOR },
36+ { text: 'Metamask', value: NAMES.METAMASK },
37+ { text: 'Ethereum Private Key', value: NAMES.PRIVATE_KEY },
38+ { text: 'Ethereum Keystore', value: NAMES.KEYSTORE },
39+];
40+
41+const isHardware = name => !![NAMES.TREZOR, NAMES.LEDGER].find(n => n === name);
42+
43+export default function Other({ className, ...rest }) {
44+ const [currentTab, setCurrentTab] = useState(undefined);
45+
46+ const hardwareAllowed = useBreakpoints([false, true, true]);
47+ const options = useMemo(
48+ () =>
49+ OPTIONS.map(o =>
50+ !isHardware(o.value)
51+ ? o
52+ : { ...o, disabled: !hardwareAllowed && ' not available on mobile' }
53+ ),
54+ [hardwareAllowed]
55+ );
56+ return (
57+ <Accordion
58+ className={className}
59+ views={VIEWS}
60+ options={options}
61+ currentTab={currentTab}
62+ onTabChange={setCurrentTab}
63+ //
64+ {...rest}
65+ />
66+ );
67+}