Add metamask support (#173)

This commit is contained in:
Szymon Szlachtowicz 2022-01-05 03:57:24 +01:00 committed by GitHub
parent f8d24eb264
commit 8a0ac14413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 200 additions and 53 deletions

View File

@ -2,8 +2,10 @@ import React, { useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { ChatState, useChatState } from "../../contexts/chatStateProvider"; import { ChatState, useChatState } from "../../contexts/chatStateProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider"; import { useMessengerContext } from "../../contexts/messengerProvider";
import { CreateIcon } from "../Icons/CreateIcon"; import { CreateIcon } from "../Icons/CreateIcon";
import { UserCreation } from "../UserCreation/UserCreation";
import { Channel } from "./Channel"; import { Channel } from "./Channel";
@ -55,24 +57,34 @@ export function Channels({ onCommunityClick }: ChannelsProps) {
} }
}, [notifications, activeChannel]); }, [notifications, activeChannel]);
const setChatState = useChatState()[1]; const setChatState = useChatState()[1];
const identity = useIdentity();
return ( return (
<ChannelList> <ChannelList>
<GenerateChannels type={"channel"} onCommunityClick={onCommunityClick} /> <GenerateChannels type={"channel"} onCommunityClick={onCommunityClick} />
<Chats> <Chats>
<ChatsBar> {identity ? (
<Heading>Chat</Heading> <>
<EditBtn onClick={() => setChatState(ChatState.ChatCreation)}> <ChatsBar>
<CreateIcon /> <Heading>Chat</Heading>
</EditBtn> <EditBtn onClick={() => setChatState(ChatState.ChatCreation)}>
</ChatsBar> <CreateIcon />
<ChatsList> </EditBtn>
<GenerateChannels </ChatsBar>
type={"group"} <ChatsList>
onCommunityClick={onCommunityClick} <GenerateChannels
/> type={"group"}
<GenerateChannels type={"dm"} onCommunityClick={onCommunityClick} /> onCommunityClick={onCommunityClick}
</ChatsList> />
<GenerateChannels
type={"dm"}
onCommunityClick={onCommunityClick}
/>
</ChatsList>
</>
) : (
<UserCreation permission={true} />
)}
</Chats> </Chats>
</ChannelList> </ChannelList>
); );

View File

@ -2,7 +2,6 @@ import React, { useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { ChatState, useChatState } from "../contexts/chatStateProvider"; import { ChatState, useChatState } from "../contexts/chatStateProvider";
import { useIdentity } from "../contexts/identityProvider";
import { useNarrow } from "../contexts/narrowProvider"; import { useNarrow } from "../contexts/narrowProvider";
import { Channels } from "./Channels/Channels"; import { Channels } from "./Channels/Channels";
@ -20,7 +19,6 @@ import { UserCreationModal } from "./Modals/UserCreationModal";
import { WalletConnectModal } from "./Modals/WalletConnectModal"; import { WalletConnectModal } from "./Modals/WalletConnectModal";
import { WalletModal } from "./Modals/WalletModal"; import { WalletModal } from "./Modals/WalletModal";
import { ToastMessageList } from "./ToastMessages/ToastMessageList"; import { ToastMessageList } from "./ToastMessages/ToastMessageList";
import { UserCreation } from "./UserCreation/UserCreation";
function Modals() { function Modals() {
return ( return (
@ -42,13 +40,12 @@ export function Chat() {
const [state] = useChatState(); const [state] = useChatState();
const [showMembers, setShowMembers] = useState(false); const [showMembers, setShowMembers] = useState(false);
const narrow = useNarrow(); const narrow = useNarrow();
const identity = useIdentity();
return ( return (
<ChatWrapper> <ChatWrapper>
{!narrow && ( {!narrow && (
<ChannelsWrapper> <ChannelsWrapper>
<StyledCommunity /> <StyledCommunity />
{identity ? <Channels /> : <UserCreation permission={true} />} <Channels />
</ChannelsWrapper> </ChannelsWrapper>
)} )}
{state === ChatState.ChatBody && ( {state === ChatState.ChatBody && (

View File

@ -6,6 +6,7 @@ import {
useIdentity, useIdentity,
useNickname, useNickname,
useSetIdentity, useSetIdentity,
useSetNikcname,
} from "../../contexts/identityProvider"; } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider"; import { useModal } from "../../contexts/modalProvider";
import { ButtonNo, ButtonYes } from "../Buttons/buttonStyle"; import { ButtonNo, ButtonYes } from "../Buttons/buttonStyle";
@ -26,6 +27,7 @@ export const LogoutModalName = "LogoutModal";
export const LogoutModal = () => { export const LogoutModal = () => {
const { setModal } = useModal(LogoutModalName); const { setModal } = useModal(LogoutModalName);
const logout = useSetIdentity(); const logout = useSetIdentity();
const setNickname = useSetNikcname();
const identity = useIdentity(); const identity = useIdentity();
const nickname = useNickname(); const nickname = useNickname();
@ -71,6 +73,7 @@ export const LogoutModal = () => {
onClick={() => { onClick={() => {
setModal(false); setModal(false);
logout(undefined); logout(undefined);
setNickname(undefined);
}} }}
> >
Disconnect Disconnect

View File

@ -6,6 +6,7 @@ import {
useIdentity, useIdentity,
useSetIdentity, useSetIdentity,
useSetNikcname, useSetNikcname,
useWalletIdentity,
} from "../../contexts/identityProvider"; } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider"; import { useModal } from "../../contexts/modalProvider";
import { Contact } from "../../models/Contact"; import { Contact } from "../../models/Contact";
@ -36,6 +37,7 @@ import { EmojiKey, UserAddress } from "./ProfileModal";
export const UserCreationModalName = "UserCreationModal"; export const UserCreationModalName = "UserCreationModal";
export function UserCreationModal() { export function UserCreationModal() {
const walletIdentity = useWalletIdentity();
const identity = useIdentity(); const identity = useIdentity();
const setIdentity = useSetIdentity(); const setIdentity = useSetIdentity();
const setNickname = useSetNikcname(); const setNickname = useSetNikcname();
@ -93,7 +95,7 @@ export function UserCreationModal() {
onChange={(e) => setCustomNameInput(e.currentTarget.value)} onChange={(e) => setCustomNameInput(e.currentTarget.value)}
/> />
)} )}
{!nextStep && encryptedIdentity && ( {!nextStep && encryptedIdentity && !walletIdentity && (
<button <button
onClick={async () => { onClick={async () => {
const identity = await decryptIdentity( const identity = await decryptIdentity(
@ -141,10 +143,10 @@ export function UserCreationModal() {
if (nextStep) { if (nextStep) {
setModal(false); setModal(false);
} else { } else {
const identity = Identity.generate(); const identity = walletIdentity || Identity.generate();
setIdentity(identity);
saveIdentity(identity, "noPassword");
setNickname(customNameInput); setNickname(customNameInput);
setIdentity(identity);
!walletIdentity && saveIdentity(identity, "noPassword");
setNextStep(true); setNextStep(true);
} }
}} }}

View File

@ -1,6 +1,13 @@
import React from "react"; import React, { useCallback } from "react";
import { Identity } from "status-communities/dist/cjs";
import { genPrivateKeyWithEntropy } from "status-communities/dist/cjs/utils";
import styled from "styled-components"; import styled from "styled-components";
import {
useSetIdentity,
useSetWalletIdentity,
} from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider"; import { useModal } from "../../contexts/modalProvider";
import { CoinbaseLogo } from "../Icons/CoinbaseLogo"; import { CoinbaseLogo } from "../Icons/CoinbaseLogo";
import { MetamaskLogo } from "../Icons/MetamaskLogo"; import { MetamaskLogo } from "../Icons/MetamaskLogo";
@ -9,14 +16,87 @@ import { WalletConnectLogo } from "../Icons/WalletConnectLogo";
import { CoinbaseModalName } from "./CoinbaseModal"; import { CoinbaseModalName } from "./CoinbaseModal";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
import { Heading, MiddleSection, Section, Text } from "./ModalStyle"; import { Heading, MiddleSection, Section, Text } from "./ModalStyle";
import { UserCreationModalName } from "./UserCreationModal";
import { WalletConnectModalName } from "./WalletConnectModal"; import { WalletConnectModalName } from "./WalletConnectModal";
export const WalletModalName = "WalletModal"; export const WalletModalName = "WalletModal";
export function WalletModal() { export function WalletModal() {
const { setModal } = useModal(WalletModalName); const { setModal } = useModal(WalletModalName);
const setIdentity = useSetIdentity();
const setWalletIdentity = useSetWalletIdentity();
const userCreationModal = useModal(UserCreationModalName);
const { setModal: setWalleConnectModal } = useModal(WalletConnectModalName); const { setModal: setWalleConnectModal } = useModal(WalletConnectModalName);
const { setModal: setCoinbaseModal } = useModal(CoinbaseModalName); const { setModal: setCoinbaseModal } = useModal(CoinbaseModalName);
const { messenger } = useMessengerContext();
const handleMetamaskClick = useCallback(async () => {
const ethereum = (window as any)?.ethereum as any | undefined;
if (ethereum && messenger) {
try {
if (ethereum?.isMetaMask) {
const [account] = await ethereum.request({
method: "eth_requestAccounts",
});
const msgParams = JSON.stringify({
domain: {
chainId: 1,
name: window.location.origin,
version: "1",
},
message: {
action: "Status Chat Key",
onlySignOn: "https://auth.status.im/",
message:
"I'm aware that i am signing message that creates a private chat key for status communicator. And I have double checked everything is fine.",
},
primaryType: "Mail",
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
],
Mail: [
{ name: "action", type: "string" },
{ name: "onlySignOn", type: "string" },
{ name: "message", type: "string" },
],
},
});
const params = [account, msgParams];
const method = "eth_signTypedData_v4";
const signature = await ethereum.request({
method,
params,
from: account,
});
const privateKey = genPrivateKeyWithEntropy(signature);
const loadedIdentity = new Identity(privateKey);
const userInNetwork = await messenger.checkIfUserInWakuNetwork(
loadedIdentity.publicKey
);
if (userInNetwork) {
setIdentity(loadedIdentity);
} else {
setWalletIdentity(loadedIdentity);
userCreationModal.setModal(true);
}
setModal(false);
return;
}
} catch {
alert("Error");
}
}
alert("Metamask not found");
}, [messenger]);
return ( return (
<Modal name={WalletModalName}> <Modal name={WalletModalName}>
@ -34,7 +114,7 @@ export function WalletModal() {
<Heading>Coinbase Wallet</Heading> <Heading>Coinbase Wallet</Heading>
<CoinbaseLogo /> <CoinbaseLogo />
</Wallet> </Wallet>
<Wallet> <Wallet onClick={handleMetamaskClick}>
<Heading>MetaMask</Heading> <Heading>MetaMask</Heading>
<MetamaskLogo /> <MetamaskLogo />
</Wallet> </Wallet>

View File

@ -4,11 +4,15 @@ import { Identity } from "status-communities/dist/cjs";
const IdentityContext = createContext<{ const IdentityContext = createContext<{
identity: Identity | undefined; identity: Identity | undefined;
setIdentity: React.Dispatch<React.SetStateAction<Identity | undefined>>; setIdentity: React.Dispatch<React.SetStateAction<Identity | undefined>>;
walletIdentity: Identity | undefined;
setWalletIdentity: React.Dispatch<React.SetStateAction<Identity | undefined>>;
nickname: string | undefined; nickname: string | undefined;
setNickname: React.Dispatch<React.SetStateAction<string | undefined>>; setNickname: React.Dispatch<React.SetStateAction<string | undefined>>;
}>({ }>({
identity: undefined, identity: undefined,
setIdentity: () => undefined, setIdentity: () => undefined,
walletIdentity: undefined,
setWalletIdentity: () => undefined,
nickname: undefined, nickname: undefined,
setNickname: () => undefined, setNickname: () => undefined,
}); });
@ -21,6 +25,14 @@ export function useSetIdentity() {
return useContext(IdentityContext).setIdentity; return useContext(IdentityContext).setIdentity;
} }
export function useWalletIdentity() {
return useContext(IdentityContext).walletIdentity;
}
export function useSetWalletIdentity() {
return useContext(IdentityContext).setWalletIdentity;
}
export function useNickname() { export function useNickname() {
return useContext(IdentityContext).nickname; return useContext(IdentityContext).nickname;
} }
@ -35,11 +47,21 @@ interface IdentityProviderProps {
export function IdentityProvider({ children }: IdentityProviderProps) { export function IdentityProvider({ children }: IdentityProviderProps) {
const [identity, setIdentity] = useState<Identity | undefined>(undefined); const [identity, setIdentity] = useState<Identity | undefined>(undefined);
const [walletIdentity, setWalletIdentity] = useState<Identity | undefined>(
undefined
);
const [nickname, setNickname] = useState<string | undefined>(undefined); const [nickname, setNickname] = useState<string | undefined>(undefined);
return ( return (
<IdentityContext.Provider <IdentityContext.Provider
value={{ identity, setIdentity, nickname, setNickname }} value={{
identity,
setIdentity,
nickname,
setNickname,
walletIdentity,
setWalletIdentity,
}}
children={children} children={children}
/> />
); );

View File

@ -3,7 +3,7 @@ import { PageDirection, Waku, WakuMessage } from "js-waku";
import { idToContactCodeTopic } from "./contentTopic"; import { idToContactCodeTopic } from "./contentTopic";
import { Identity } from "./identity"; import { Identity } from "./identity";
import { StatusUpdate_StatusType } from "./proto/communities/v1/status_update"; import { StatusUpdate_StatusType } from "./proto/communities/v1/status_update";
import { bufToHex } from "./utils"; import { bufToHex, getLatestUserNickname } from "./utils";
import { ChatIdentity } from "./wire/chat_identity"; import { ChatIdentity } from "./wire/chat_identity";
import { StatusUpdate } from "./wire/status_update"; import { StatusUpdate } from "./wire/status_update";
@ -115,30 +115,11 @@ export class Contacts {
const handleNickname = async (): Promise<void> => { const handleNickname = async (): Promise<void> => {
if (this.identity) { if (this.identity) {
const publicKey = bufToHex(this.identity.publicKey);
const now = new Date().getTime(); const now = new Date().getTime();
let newNickname = ""; const { clock, nickname: newNickname } = await getLatestUserNickname(
let clock = 0; this.identity.publicKey,
await this.waku.store.queryHistory([idToContactCodeTopic(publicKey)], { this.waku
callback: (msgs) => );
msgs.some((e) => {
try {
if (e.payload) {
const chatIdentity = ChatIdentity.decode(e?.payload);
if (chatIdentity) {
if (chatIdentity?.displayName) {
clock = chatIdentity?.clock ?? 0;
newNickname = chatIdentity?.displayName;
}
}
return true;
}
} catch {
return false;
}
}),
pageDirection: PageDirection.BACKWARD,
});
if (this.nickname) { if (this.nickname) {
if (this.nickname !== newNickname) { if (this.nickname !== newNickname) {
@ -150,7 +131,7 @@ export class Contacts {
} }
} else { } else {
this.nickname = newNickname; this.nickname = newNickname;
this.callbackNickname(publicKey, newNickname); this.callbackNickname(bufToHex(this.identity.publicKey), newNickname);
if (clock < now - NICKNAME_BROADCAST_INTERVAL) { if (clock < now - NICKNAME_BROADCAST_INTERVAL) {
await sendNickname(); await sendNickname();
} }

View File

@ -7,7 +7,10 @@ import * as secp256k1 from "secp256k1";
import { hexToBuf } from "./utils"; import { hexToBuf } from "./utils";
export class Identity { export class Identity {
public constructor(public privateKey: Uint8Array) {} private pubKey: Uint8Array;
public constructor(public privateKey: Uint8Array) {
this.pubKey = secp256k1.publicKeyCreate(this.privateKey, true);
}
public static generate(): Identity { public static generate(): Identity {
const privateKey = generatePrivateKey(); const privateKey = generatePrivateKey();
@ -31,6 +34,6 @@ export class Identity {
* Returns the compressed public key. * Returns the compressed public key.
*/ */
public get publicKey(): Uint8Array { public get publicKey(): Uint8Array {
return secp256k1.publicKeyCreate(this.privateKey, true); return this.pubKey;
} }
} }

View File

@ -5,6 +5,7 @@ import { CreateOptions as WakuCreateOptions } from "js-waku/build/main/lib/waku"
import { Chat } from "./chat"; import { Chat } from "./chat";
import { Identity } from "./identity"; import { Identity } from "./identity";
import { ApplicationMetadataMessage_Type } from "./proto/status/v1/application_metadata_message"; import { ApplicationMetadataMessage_Type } from "./proto/status/v1/application_metadata_message";
import { getLatestUserNickname } from "./utils";
import { ApplicationMetadataMessage } from "./wire/application_metadata_message"; import { ApplicationMetadataMessage } from "./wire/application_metadata_message";
import { ChatMessage, Content } from "./wire/chat_message"; import { ChatMessage, Content } from "./wire/chat_message";
@ -249,4 +250,12 @@ export class Messenger {
}); });
} }
} }
async checkIfUserInWakuNetwork(publicKey: Uint8Array): Promise<boolean> {
const { clock, nickname } = await getLatestUserNickname(
publicKey,
this.waku
);
return clock > 0 && nickname !== "";
}
} }

View File

@ -1,5 +1,8 @@
import { ec } from "elliptic"; import { ec } from "elliptic";
import { utils } from "js-waku"; import { PageDirection, utils, Waku } from "js-waku";
import { idToContactCodeTopic } from "./contentTopic";
import { ChatIdentity } from "./proto/communities/v1/chat_identity";
const EC = new ec("secp256k1"); const EC = new ec("secp256k1");
@ -17,3 +20,38 @@ export function compressPublicKey(key: Uint8Array): string {
const PubKey = EC.keyFromPublic(key); const PubKey = EC.keyFromPublic(key);
return "0x" + PubKey.getPublic(true, "hex"); return "0x" + PubKey.getPublic(true, "hex");
} }
export function genPrivateKeyWithEntropy(key: string): Uint8Array {
const pair = EC.genKeyPair({ entropy: key });
return hexToBuf("0x" + pair.getPrivate("hex"));
}
export async function getLatestUserNickname(
key: Uint8Array,
waku: Waku
): Promise<{ clock: number; nickname: string }> {
const publicKey = bufToHex(key);
let nickname = "";
let clock = 0;
await waku.store.queryHistory([idToContactCodeTopic(publicKey)], {
callback: (msgs) =>
msgs.some((e) => {
try {
if (e.payload) {
const chatIdentity = ChatIdentity.decode(e?.payload);
if (chatIdentity) {
if (chatIdentity?.displayName) {
clock = chatIdentity?.clock ?? 0;
nickname = chatIdentity?.displayName;
}
}
return true;
}
} catch {
return false;
}
}),
pageDirection: PageDirection.BACKWARD,
});
return { clock, nickname };
}