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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import {
useIdentity,
useSetIdentity,
useSetNikcname,
useWalletIdentity,
} from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider";
import { Contact } from "../../models/Contact";
@ -36,6 +37,7 @@ import { EmojiKey, UserAddress } from "./ProfileModal";
export const UserCreationModalName = "UserCreationModal";
export function UserCreationModal() {
const walletIdentity = useWalletIdentity();
const identity = useIdentity();
const setIdentity = useSetIdentity();
const setNickname = useSetNikcname();
@ -93,7 +95,7 @@ export function UserCreationModal() {
onChange={(e) => setCustomNameInput(e.currentTarget.value)}
/>
)}
{!nextStep && encryptedIdentity && (
{!nextStep && encryptedIdentity && !walletIdentity && (
<button
onClick={async () => {
const identity = await decryptIdentity(
@ -141,10 +143,10 @@ export function UserCreationModal() {
if (nextStep) {
setModal(false);
} else {
const identity = Identity.generate();
setIdentity(identity);
saveIdentity(identity, "noPassword");
const identity = walletIdentity || Identity.generate();
setNickname(customNameInput);
setIdentity(identity);
!walletIdentity && saveIdentity(identity, "noPassword");
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 {
useSetIdentity,
useSetWalletIdentity,
} from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { CoinbaseLogo } from "../Icons/CoinbaseLogo";
import { MetamaskLogo } from "../Icons/MetamaskLogo";
@ -9,14 +16,87 @@ import { WalletConnectLogo } from "../Icons/WalletConnectLogo";
import { CoinbaseModalName } from "./CoinbaseModal";
import { Modal } from "./Modal";
import { Heading, MiddleSection, Section, Text } from "./ModalStyle";
import { UserCreationModalName } from "./UserCreationModal";
import { WalletConnectModalName } from "./WalletConnectModal";
export const WalletModalName = "WalletModal";
export function WalletModal() {
const { setModal } = useModal(WalletModalName);
const setIdentity = useSetIdentity();
const setWalletIdentity = useSetWalletIdentity();
const userCreationModal = useModal(UserCreationModalName);
const { setModal: setWalleConnectModal } = useModal(WalletConnectModalName);
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 (
<Modal name={WalletModalName}>
@ -34,7 +114,7 @@ export function WalletModal() {
<Heading>Coinbase Wallet</Heading>
<CoinbaseLogo />
</Wallet>
<Wallet>
<Wallet onClick={handleMetamaskClick}>
<Heading>MetaMask</Heading>
<MetamaskLogo />
</Wallet>

View File

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

View File

@ -3,7 +3,7 @@ import { PageDirection, Waku, WakuMessage } from "js-waku";
import { idToContactCodeTopic } from "./contentTopic";
import { Identity } from "./identity";
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 { StatusUpdate } from "./wire/status_update";
@ -115,30 +115,11 @@ export class Contacts {
const handleNickname = async (): Promise<void> => {
if (this.identity) {
const publicKey = bufToHex(this.identity.publicKey);
const now = new Date().getTime();
let newNickname = "";
let clock = 0;
await this.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;
newNickname = chatIdentity?.displayName;
}
}
return true;
}
} catch {
return false;
}
}),
pageDirection: PageDirection.BACKWARD,
});
const { clock, nickname: newNickname } = await getLatestUserNickname(
this.identity.publicKey,
this.waku
);
if (this.nickname) {
if (this.nickname !== newNickname) {
@ -150,7 +131,7 @@ export class Contacts {
}
} else {
this.nickname = newNickname;
this.callbackNickname(publicKey, newNickname);
this.callbackNickname(bufToHex(this.identity.publicKey), newNickname);
if (clock < now - NICKNAME_BROADCAST_INTERVAL) {
await sendNickname();
}

View File

@ -7,7 +7,10 @@ import * as secp256k1 from "secp256k1";
import { hexToBuf } from "./utils";
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 {
const privateKey = generatePrivateKey();
@ -31,6 +34,6 @@ export class Identity {
* Returns the compressed public key.
*/
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 { Identity } from "./identity";
import { ApplicationMetadataMessage_Type } from "./proto/status/v1/application_metadata_message";
import { getLatestUserNickname } from "./utils";
import { ApplicationMetadataMessage } from "./wire/application_metadata_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 { 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");
@ -17,3 +20,38 @@ export function compressPublicKey(key: Uint8Array): string {
const PubKey = EC.keyFromPublic(key);
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 };
}