mirror of https://github.com/status-im/js-waku.git
Merge pull request #216 from status-im/213-derivation
This commit is contained in:
commit
90e39d3e0a
|
@ -72,7 +72,8 @@
|
||||||
"wakuv",
|
"wakuv",
|
||||||
"wakunode",
|
"wakunode",
|
||||||
"webfonts",
|
"webfonts",
|
||||||
"websockets"
|
"websockets",
|
||||||
|
"wifi"
|
||||||
],
|
],
|
||||||
"flagWords": [],
|
"flagWords": [],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.4",
|
"@material-ui/core": "^4.11.4",
|
||||||
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
@ -3451,6 +3452,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@material-ui/icons": {
|
||||||
|
"version": "4.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz",
|
||||||
|
"integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.4.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@material-ui/core": "^4.0.0",
|
||||||
|
"@types/react": "^16.8.6 || ^17.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@material-ui/styles": {
|
"node_modules/@material-ui/styles": {
|
||||||
"version": "4.11.4",
|
"version": "4.11.4",
|
||||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
|
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
|
||||||
|
@ -26510,6 +26533,14 @@
|
||||||
"react-transition-group": "^4.4.0"
|
"react-transition-group": "^4.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@material-ui/icons": {
|
||||||
|
"version": "4.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz",
|
||||||
|
"integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.4.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@material-ui/styles": {
|
"@material-ui/styles": {
|
||||||
"version": "4.11.4",
|
"version": "4.11.4",
|
||||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
|
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"homepage": "/js-waku/eth-dm",
|
"homepage": "/js-waku/eth-dm",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.4",
|
"@material-ui/core": "^4.11.4",
|
||||||
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
|
|
@ -1,40 +1,74 @@
|
||||||
import '@ethersproject/shims';
|
import '@ethersproject/shims';
|
||||||
|
|
||||||
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { Environment, getStatusFleetNodes, Waku, WakuMessage } from 'js-waku';
|
import { Waku } from 'js-waku';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { Web3Provider } from '@ethersproject/providers';
|
import { Web3Provider } from '@ethersproject/providers';
|
||||||
import {
|
import { KeyPair } from './crypto';
|
||||||
createPublicKeyMessage,
|
import { Message } from './messaging/Messages';
|
||||||
generateEthDmKeyPair,
|
|
||||||
KeyPair,
|
|
||||||
validatePublicKeyMessage,
|
|
||||||
} from './crypto';
|
|
||||||
import * as EthCrypto from 'eth-crypto';
|
|
||||||
import { decode, DirectMessage, encode, PublicKeyMessage } from './messages';
|
|
||||||
import { Message, Messages } from './Messages';
|
|
||||||
import 'fontsource-roboto';
|
import 'fontsource-roboto';
|
||||||
import { Button } from '@material-ui/core';
|
import { AppBar, IconButton, Toolbar, Typography } from '@material-ui/core';
|
||||||
import { SendMessage } from './SendMessage';
|
import KeyPairHandling from './key_pair_handling/KeyPairHandling';
|
||||||
|
import InitWaku from './InitWaku';
|
||||||
export const PublicKeyContentTopic = '/eth-dm/1/public-key/json';
|
import {
|
||||||
export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json';
|
createMuiTheme,
|
||||||
|
ThemeProvider,
|
||||||
|
makeStyles,
|
||||||
|
} from '@material-ui/core/styles';
|
||||||
|
import { teal, purple, green } from '@material-ui/core/colors';
|
||||||
|
import WifiIcon from '@material-ui/icons/Wifi';
|
||||||
|
import BroadcastPublicKey from './BroadcastPublicKey';
|
||||||
|
import Messaging from './messaging/Messaging';
|
||||||
|
|
||||||
declare let window: any;
|
declare let window: any;
|
||||||
|
|
||||||
|
const theme = createMuiTheme({
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: purple[500],
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: teal[600],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
textAlign: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: '100vh',
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
// height: '200p',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
flex: 1,
|
||||||
|
margin: '10px',
|
||||||
|
},
|
||||||
|
wakuStatus: {},
|
||||||
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [waku, setWaku] = useState<Waku>();
|
const [waku, setWaku] = useState<Waku>();
|
||||||
const [provider, setProvider] = useState<Web3Provider>();
|
const [provider, setProvider] = useState<Web3Provider>();
|
||||||
const [ethDmKeyPair, setEthDmKeyPair] = useState<KeyPair>();
|
const [ethDmKeyPair, setEthDmKeyPair] = useState<KeyPair | undefined>();
|
||||||
const [publicKeyMsg, setPublicKeyMsg] = useState<PublicKeyMessage>();
|
|
||||||
const [publicKeys, setPublicKeys] = useState<Map<string, string>>(new Map());
|
const [publicKeys, setPublicKeys] = useState<Map<string, string>>(new Map());
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [address, setAddress] = useState<string>();
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (provider) return;
|
if (provider) return;
|
||||||
try {
|
try {
|
||||||
window.ethereum.enable();
|
window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
const _provider = new ethers.providers.Web3Provider(window.ethereum);
|
const _provider = new ethers.providers.Web3Provider(window.ethereum);
|
||||||
setProvider(_provider);
|
setProvider(_provider);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -43,193 +77,66 @@ function App() {
|
||||||
}, [provider]);
|
}, [provider]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (waku) return;
|
provider
|
||||||
initWaku()
|
?.getSigner()
|
||||||
.then((wakuNode) => {
|
.getAddress()
|
||||||
console.log('waku: ready');
|
.then((address) => setAddress(address));
|
||||||
setWaku(wakuNode);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error('Failed to initiate Waku', e);
|
|
||||||
});
|
});
|
||||||
}, [waku]);
|
|
||||||
|
|
||||||
const generateKeyPair = () => {
|
|
||||||
if (ethDmKeyPair) return;
|
|
||||||
if (!provider) return;
|
|
||||||
|
|
||||||
generateEthDmKeyPair(provider.getSigner())
|
|
||||||
.then((keyPair) => {
|
|
||||||
setEthDmKeyPair(keyPair);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error('Failed to generate Key Pair', e);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const observerPublicKeyMessage = handlePublicKeyMessage.bind(
|
|
||||||
{},
|
|
||||||
setPublicKeys
|
|
||||||
);
|
|
||||||
|
|
||||||
const observerDirectMessage = ethDmKeyPair
|
|
||||||
? handleDirectMessage.bind({}, setMessages, ethDmKeyPair.privateKey)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!waku) return;
|
|
||||||
waku.relay.addObserver(observerPublicKeyMessage, [PublicKeyContentTopic]);
|
|
||||||
|
|
||||||
return function cleanUp() {
|
|
||||||
if (!waku) return;
|
|
||||||
waku.relay.deleteObserver(observerPublicKeyMessage, [
|
|
||||||
PublicKeyContentTopic,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!waku) return;
|
|
||||||
if (!observerDirectMessage) return;
|
|
||||||
waku.relay.addObserver(observerDirectMessage, [DirectMessageContentTopic]);
|
|
||||||
|
|
||||||
return function cleanUp() {
|
|
||||||
if (!waku) return;
|
|
||||||
if (!observerDirectMessage) return;
|
|
||||||
waku.relay.deleteObserver(observerDirectMessage, [
|
|
||||||
DirectMessageContentTopic,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const broadcastPublicKey = () => {
|
|
||||||
if (!ethDmKeyPair) return;
|
|
||||||
if (!provider) return;
|
|
||||||
if (!waku) return;
|
|
||||||
|
|
||||||
if (publicKeyMsg) {
|
|
||||||
const wakuMsg = encodePublicKeyWakuMessage(publicKeyMsg);
|
|
||||||
waku.lightPush.push(wakuMsg).catch((e) => {
|
|
||||||
console.error('Failed to send Public Key Message');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createPublicKeyMessage(provider.getSigner(), ethDmKeyPair.publicKey)
|
|
||||||
.then((msg) => {
|
|
||||||
setPublicKeyMsg(msg);
|
|
||||||
const wakuMsg = encodePublicKeyWakuMessage(msg);
|
|
||||||
waku.lightPush.push(wakuMsg).catch((e) => {
|
|
||||||
console.error('Failed to send Public Key Message');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error('Failed to creat Eth-Dm Publication message', e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const wakuReady = !!waku ? 'Waku is ready' : 'Waku is loading';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<ThemeProvider theme={theme}>
|
||||||
<header className="App-header">
|
<div className={classes.root}>
|
||||||
{wakuReady}
|
<AppBar className={classes.appBar} position="static">
|
||||||
<div
|
<Toolbar>
|
||||||
style={{
|
<Typography>Ethereum Direct Message</Typography>
|
||||||
display: 'flex',
|
<IconButton
|
||||||
alignItems: 'center',
|
edge="end"
|
||||||
flexWrap: 'wrap',
|
className={classes.wakuStatus}
|
||||||
}}
|
aria-label="waku-status"
|
||||||
>
|
>
|
||||||
<Button
|
<WifiIcon
|
||||||
variant="contained"
|
color={waku ? undefined : 'disabled'}
|
||||||
color="primary"
|
style={waku ? { color: green[500] } : {}}
|
||||||
onClick={generateKeyPair}
|
/>
|
||||||
disabled={!provider}
|
</IconButton>
|
||||||
>
|
</Toolbar>
|
||||||
Generate Eth-DM Key Pair
|
</AppBar>
|
||||||
</Button>
|
|
||||||
<Button
|
<div className={classes.container}>
|
||||||
variant="contained"
|
<main className={classes.main}>
|
||||||
color="primary"
|
<InitWaku
|
||||||
onClick={broadcastPublicKey}
|
ethDmKeyPair={ethDmKeyPair}
|
||||||
disabled={!ethDmKeyPair || !waku}
|
setMessages={setMessages}
|
||||||
>
|
setPublicKeys={setPublicKeys}
|
||||||
Broadcast Eth-DM Public Key
|
setWaku={setWaku}
|
||||||
</Button>
|
waku={waku}
|
||||||
|
address={address}
|
||||||
|
/>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Eth-DM Key Pair</legend>
|
||||||
|
<KeyPairHandling
|
||||||
|
ethDmKeyPair={ethDmKeyPair}
|
||||||
|
setEthDmKeyPair={(keyPair) => setEthDmKeyPair(keyPair)}
|
||||||
|
/>
|
||||||
|
<BroadcastPublicKey
|
||||||
|
signer={provider?.getSigner()}
|
||||||
|
ethDmKeyPair={ethDmKeyPair}
|
||||||
|
waku={waku}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Messaging</legend>
|
||||||
|
<Messaging
|
||||||
|
recipients={publicKeys}
|
||||||
|
waku={waku}
|
||||||
|
messages={messages}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<SendMessage recipients={publicKeys} waku={waku} />
|
|
||||||
<Messages messages={messages} />
|
|
||||||
</header>
|
|
||||||
</div>
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
async function initWaku(): Promise<Waku> {
|
|
||||||
const waku = await Waku.create({});
|
|
||||||
|
|
||||||
const nodes = await getNodes();
|
|
||||||
await Promise.all(
|
|
||||||
nodes.map((addr) => {
|
|
||||||
return waku.dial(addr);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return waku;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNodes() {
|
|
||||||
// Works with react-scripts
|
|
||||||
if (process?.env?.NODE_ENV === 'development') {
|
|
||||||
return getStatusFleetNodes(Environment.Test);
|
|
||||||
} else {
|
|
||||||
return getStatusFleetNodes(Environment.Prod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodePublicKeyWakuMessage(ethDmMsg: PublicKeyMessage): WakuMessage {
|
|
||||||
const payload = encode(ethDmMsg);
|
|
||||||
return WakuMessage.fromBytes(payload, PublicKeyContentTopic);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePublicKeyMessage(
|
|
||||||
setter: Dispatch<SetStateAction<Map<string, string>>>,
|
|
||||||
msg: WakuMessage
|
|
||||||
) {
|
|
||||||
if (!msg.payload) return;
|
|
||||||
const publicKeyMsg: PublicKeyMessage = decode(msg.payload);
|
|
||||||
const res = validatePublicKeyMessage(publicKeyMsg);
|
|
||||||
console.log(`Public Key Message Received, valid: ${res}`, publicKeyMsg);
|
|
||||||
|
|
||||||
setter((prevPks: Map<string, string>) => {
|
|
||||||
prevPks.set(publicKeyMsg.ethAddress, publicKeyMsg.ethDmPublicKey);
|
|
||||||
return new Map(prevPks);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDirectMessage(
|
|
||||||
setter: Dispatch<SetStateAction<Message[]>>,
|
|
||||||
privateKey: string,
|
|
||||||
wakuMsg: WakuMessage
|
|
||||||
) {
|
|
||||||
console.log('Waku Message received:', wakuMsg);
|
|
||||||
if (!wakuMsg.payload) return;
|
|
||||||
const directMessage: DirectMessage = decode(wakuMsg.payload);
|
|
||||||
const text = await EthCrypto.decryptWithPrivateKey(
|
|
||||||
privateKey,
|
|
||||||
directMessage.encMessage
|
|
||||||
);
|
|
||||||
|
|
||||||
const timestamp = wakuMsg.timestamp ? wakuMsg.timestamp : new Date();
|
|
||||||
|
|
||||||
console.log('Message decrypted:', text);
|
|
||||||
setter((prevMsgs: Message[]) => {
|
|
||||||
const copy = prevMsgs.slice();
|
|
||||||
copy.push({
|
|
||||||
text: text,
|
|
||||||
timestamp: timestamp,
|
|
||||||
});
|
|
||||||
return copy;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Button } from '@material-ui/core';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { createPublicKeyMessage, KeyPair } from './crypto';
|
||||||
|
import { encode, PublicKeyMessage } from './messaging/wire';
|
||||||
|
import { WakuMessage, Waku } from 'js-waku';
|
||||||
|
import { Signer } from '@ethersproject/abstract-signer';
|
||||||
|
import { PublicKeyContentTopic } from './InitWaku';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
ethDmKeyPair: KeyPair | undefined;
|
||||||
|
waku: Waku | undefined;
|
||||||
|
signer: Signer | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BroadcastPublicKey({
|
||||||
|
signer,
|
||||||
|
ethDmKeyPair,
|
||||||
|
waku,
|
||||||
|
}: Props) {
|
||||||
|
const [publicKeyMsg, setPublicKeyMsg] = useState<PublicKeyMessage>();
|
||||||
|
|
||||||
|
const broadcastPublicKey = () => {
|
||||||
|
if (!ethDmKeyPair) return;
|
||||||
|
if (!signer) return;
|
||||||
|
if (!waku) return;
|
||||||
|
|
||||||
|
if (publicKeyMsg) {
|
||||||
|
const wakuMsg = encodePublicKeyWakuMessage(publicKeyMsg);
|
||||||
|
waku.lightPush.push(wakuMsg).catch((e) => {
|
||||||
|
console.error('Failed to send Public Key Message', e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createPublicKeyMessage(signer, ethDmKeyPair.publicKey)
|
||||||
|
.then((msg) => {
|
||||||
|
setPublicKeyMsg(msg);
|
||||||
|
const wakuMsg = encodePublicKeyWakuMessage(msg);
|
||||||
|
waku.lightPush.push(wakuMsg).catch((e) => {
|
||||||
|
console.error('Failed to send Public Key Message', e);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Failed to create public key message', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={broadcastPublicKey}
|
||||||
|
disabled={!ethDmKeyPair || !waku}
|
||||||
|
>
|
||||||
|
Broadcast Eth-DM Public Key
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodePublicKeyWakuMessage(ethDmMsg: PublicKeyMessage): WakuMessage {
|
||||||
|
const payload = encode(ethDmMsg);
|
||||||
|
return WakuMessage.fromBytes(payload, PublicKeyContentTopic);
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { Dispatch, SetStateAction, useEffect } from 'react';
|
||||||
|
import { Environment, getStatusFleetNodes, Waku, WakuMessage } from 'js-waku';
|
||||||
|
import { decode, DirectMessage, PublicKeyMessage } from './messaging/wire';
|
||||||
|
import { decryptMessage, KeyPair, validatePublicKeyMessage } from './crypto';
|
||||||
|
import { Message } from './messaging/Messages';
|
||||||
|
|
||||||
|
export const PublicKeyContentTopic = '/eth-dm/1/public-key/json';
|
||||||
|
export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
waku: Waku | undefined;
|
||||||
|
setWaku: (waku: Waku) => void;
|
||||||
|
ethDmKeyPair: KeyPair | undefined;
|
||||||
|
setPublicKeys: Dispatch<SetStateAction<Map<string, string>>>;
|
||||||
|
setMessages: Dispatch<SetStateAction<Message[]>>;
|
||||||
|
address: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does all the waku initialization
|
||||||
|
*/
|
||||||
|
export default function InitWaku({
|
||||||
|
waku,
|
||||||
|
setWaku,
|
||||||
|
ethDmKeyPair,
|
||||||
|
setPublicKeys,
|
||||||
|
setMessages,
|
||||||
|
address,
|
||||||
|
}: Props) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (waku) return;
|
||||||
|
initWaku()
|
||||||
|
.then((wakuNode) => {
|
||||||
|
console.log('waku: ready');
|
||||||
|
setWaku(wakuNode);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Failed to initiate Waku', e);
|
||||||
|
});
|
||||||
|
}, [waku, setWaku]);
|
||||||
|
|
||||||
|
const observerPublicKeyMessage = handlePublicKeyMessage.bind(
|
||||||
|
{},
|
||||||
|
ethDmKeyPair?.publicKey,
|
||||||
|
setPublicKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
const observerDirectMessage =
|
||||||
|
ethDmKeyPair && address
|
||||||
|
? handleDirectMessage.bind(
|
||||||
|
{},
|
||||||
|
setMessages,
|
||||||
|
ethDmKeyPair.privateKey,
|
||||||
|
address
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!waku) return;
|
||||||
|
waku.relay.addObserver(observerPublicKeyMessage, [PublicKeyContentTopic]);
|
||||||
|
|
||||||
|
return function cleanUp() {
|
||||||
|
if (!waku) return;
|
||||||
|
waku.relay.deleteObserver(observerPublicKeyMessage, [
|
||||||
|
PublicKeyContentTopic,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!waku) return;
|
||||||
|
if (!observerDirectMessage) return;
|
||||||
|
waku.relay.addObserver(observerDirectMessage, [DirectMessageContentTopic]);
|
||||||
|
|
||||||
|
return function cleanUp() {
|
||||||
|
if (!waku) return;
|
||||||
|
if (!observerDirectMessage) return;
|
||||||
|
waku.relay.deleteObserver(observerDirectMessage, [
|
||||||
|
DirectMessageContentTopic,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Returns an empty fragment.
|
||||||
|
// Taking advantages of React's state management and useEffect()
|
||||||
|
// Not sure it is best practice but it works.
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initWaku(): Promise<Waku> {
|
||||||
|
const waku = await Waku.create({});
|
||||||
|
|
||||||
|
const nodes = await getNodes();
|
||||||
|
await Promise.all(
|
||||||
|
nodes.map((addr) => {
|
||||||
|
return waku.dial(addr);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return waku;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodes() {
|
||||||
|
// Works with react-scripts
|
||||||
|
if (process?.env?.NODE_ENV === 'development') {
|
||||||
|
return getStatusFleetNodes(Environment.Test);
|
||||||
|
} else {
|
||||||
|
return getStatusFleetNodes(Environment.Prod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePublicKeyMessage(
|
||||||
|
myPublicKey: string | undefined,
|
||||||
|
setter: Dispatch<SetStateAction<Map<string, string>>>,
|
||||||
|
msg: WakuMessage
|
||||||
|
) {
|
||||||
|
if (!msg.payload) return;
|
||||||
|
const publicKeyMsg: PublicKeyMessage = decode(msg.payload);
|
||||||
|
if (publicKeyMsg.ethDmPublicKey === myPublicKey) return;
|
||||||
|
const res = validatePublicKeyMessage(publicKeyMsg);
|
||||||
|
console.log(`Public Key Message Received, valid: ${res}`, publicKeyMsg);
|
||||||
|
|
||||||
|
setter((prevPks: Map<string, string>) => {
|
||||||
|
prevPks.set(publicKeyMsg.ethAddress, publicKeyMsg.ethDmPublicKey);
|
||||||
|
return new Map(prevPks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDirectMessage(
|
||||||
|
setter: Dispatch<SetStateAction<Message[]>>,
|
||||||
|
privateKey: string,
|
||||||
|
address: string,
|
||||||
|
wakuMsg: WakuMessage
|
||||||
|
) {
|
||||||
|
console.log('Waku Message received:', wakuMsg);
|
||||||
|
if (!wakuMsg.payload) return;
|
||||||
|
const directMessage: DirectMessage = decode(wakuMsg.payload);
|
||||||
|
if (directMessage.toAddress !== address) return;
|
||||||
|
|
||||||
|
const text = await decryptMessage(privateKey, directMessage);
|
||||||
|
|
||||||
|
const timestamp = wakuMsg.timestamp ? wakuMsg.timestamp : new Date();
|
||||||
|
|
||||||
|
console.log('Message decrypted:', text);
|
||||||
|
setter((prevMsgs: Message[]) => {
|
||||||
|
const copy = prevMsgs.slice();
|
||||||
|
copy.push({
|
||||||
|
text: text,
|
||||||
|
timestamp: timestamp,
|
||||||
|
});
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
export interface Message {
|
|
||||||
text: string;
|
|
||||||
timestamp: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
messages: Message[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Messages(props: Props) {
|
|
||||||
const messages = props.messages.map((msg) => {
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
{formatDisplayDate(msg.timestamp)} {msg.text}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return <ul>{messages}</ul>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDisplayDate(timestamp: Date): string {
|
|
||||||
return timestamp.toLocaleString([], {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -3,15 +3,11 @@ import '@ethersproject/shims';
|
||||||
import * as EthCrypto from 'eth-crypto';
|
import * as EthCrypto from 'eth-crypto';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { Signer } from '@ethersproject/abstract-signer';
|
import { Signer } from '@ethersproject/abstract-signer';
|
||||||
import { PublicKeyMessage } from './messages';
|
import { DirectMessage, PublicKeyMessage } from './messaging/wire';
|
||||||
|
|
||||||
const Salt =
|
|
||||||
'Salt for Eth-Dm, do not share a signature of this message or others could decrypt your messages';
|
|
||||||
|
|
||||||
export interface KeyPair {
|
export interface KeyPair {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
address: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,15 +15,8 @@ export interface KeyPair {
|
||||||
* the entropy for the EthCrypto keypair. Note that the entropy is hashed with keccak256
|
* the entropy for the EthCrypto keypair. Note that the entropy is hashed with keccak256
|
||||||
* to make the private key.
|
* to make the private key.
|
||||||
*/
|
*/
|
||||||
export async function generateEthDmKeyPair(
|
export async function generateEthDmKeyPair(): Promise<KeyPair> {
|
||||||
web3Signer: Signer
|
return EthCrypto.createIdentity();
|
||||||
): Promise<KeyPair> {
|
|
||||||
const signature = await web3Signer.signMessage(Salt);
|
|
||||||
// Need to remove '0x' prefix to allow buffer to decode the hex string.
|
|
||||||
const sigBuf = Buffer.from(signature.slice(2), 'hex');
|
|
||||||
const entropy = Buffer.concat([sigBuf, sigBuf]);
|
|
||||||
const keys = EthCrypto.createIdentity(entropy);
|
|
||||||
return keys;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,8 +58,24 @@ export function validatePublicKeyMessage(msg: PublicKeyMessage): boolean {
|
||||||
* context.
|
* context.
|
||||||
*/
|
*/
|
||||||
function formatPublicKeyForSignature(ethDmPublicKey: string): string {
|
function formatPublicKeyForSignature(ethDmPublicKey: string): string {
|
||||||
const txt = JSON.stringify({
|
return JSON.stringify({
|
||||||
ethDmPublicKey,
|
ethDmPublicKey,
|
||||||
});
|
});
|
||||||
return txt;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a Direct Message using the private key.
|
||||||
|
*/
|
||||||
|
export function decryptMessage(
|
||||||
|
privateKey: string,
|
||||||
|
directMessage: DirectMessage
|
||||||
|
) {
|
||||||
|
return EthCrypto.decryptWithPrivateKey(privateKey, directMessage.encMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt message with given Public Key
|
||||||
|
*/
|
||||||
|
export async function encryptMessage(publicKey: string, message: string) {
|
||||||
|
return await EthCrypto.encryptWithPublicKey(publicKey, message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { Button } from '@material-ui/core';
|
||||||
|
import { LoadKeyPair } from './LoadKeyPair';
|
||||||
|
import { SaveKeyPair } from './SaveKeyPair';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { generateEthDmKeyPair, KeyPair } from '../crypto';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import PasswordInput from './PasswordInput';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
textAlign: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
margin: '5px',
|
||||||
|
},
|
||||||
|
generate: { margin: '5px' },
|
||||||
|
storage: {
|
||||||
|
margin: '5px',
|
||||||
|
},
|
||||||
|
loadSave: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
margin: '5px',
|
||||||
|
},
|
||||||
|
loadSaveButton: {
|
||||||
|
margin: '5px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
ethDmKeyPair: KeyPair | undefined;
|
||||||
|
setEthDmKeyPair: (keyPair: KeyPair) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function KeyPairHandling({
|
||||||
|
ethDmKeyPair,
|
||||||
|
setEthDmKeyPair,
|
||||||
|
}: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [password, setPassword] = useState<string>();
|
||||||
|
|
||||||
|
const generateKeyPair = () => {
|
||||||
|
if (ethDmKeyPair) return;
|
||||||
|
|
||||||
|
generateEthDmKeyPair()
|
||||||
|
.then((keyPair) => {
|
||||||
|
setEthDmKeyPair(keyPair);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Failed to generate Key Pair', e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Button
|
||||||
|
className={classes.generate}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={generateKeyPair}
|
||||||
|
disabled={!!ethDmKeyPair}
|
||||||
|
>
|
||||||
|
Generate Eth-DM Key Pair
|
||||||
|
</Button>
|
||||||
|
<div className={classes.storage}>
|
||||||
|
<PasswordInput
|
||||||
|
password={password}
|
||||||
|
setPassword={(p) => setPassword(p)}
|
||||||
|
/>
|
||||||
|
<div className={classes.loadSave}>
|
||||||
|
<div className={classes.loadSaveButton}>
|
||||||
|
<LoadKeyPair
|
||||||
|
setEthDmKeyPair={(keyPair) => setEthDmKeyPair(keyPair)}
|
||||||
|
disabled={!!ethDmKeyPair}
|
||||||
|
password={password}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.loadSaveButton}>
|
||||||
|
<SaveKeyPair ethDmKeyPair={ethDmKeyPair} password={password} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Button } from '@material-ui/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { loadKeyPairFromStorage } from './key_pair_storage';
|
||||||
|
import { KeyPair } from '../crypto';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
setEthDmKeyPair: (keyPair: KeyPair) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
password: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadKeyPair({ password, disabled, setEthDmKeyPair }: Props) {
|
||||||
|
const loadKeyPair = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
if (!password) return;
|
||||||
|
loadKeyPairFromStorage(password).then((keyPair: KeyPair | undefined) => {
|
||||||
|
if (!keyPair) return;
|
||||||
|
console.log('EthDm KeyPair loaded from storage');
|
||||||
|
setEthDmKeyPair(keyPair);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={loadKeyPair}
|
||||||
|
disabled={!password || disabled}
|
||||||
|
>
|
||||||
|
Load Eth-DM Key Pair from storage
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { TextField } from '@material-ui/core';
|
||||||
|
import React, { ChangeEvent } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
password: string | undefined;
|
||||||
|
setPassword: (password: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PasswordInput({ password, setPassword }: Props) {
|
||||||
|
const handlePasswordChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPassword(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
id="password-input"
|
||||||
|
label="Password"
|
||||||
|
variant="filled"
|
||||||
|
type="password"
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
value={password}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Button } from '@material-ui/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { KeyPair } from '../crypto';
|
||||||
|
import { saveKeyPairToStorage } from './key_pair_storage';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
ethDmKeyPair: KeyPair | undefined;
|
||||||
|
password: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveKeyPair({ password, ethDmKeyPair }: Props) {
|
||||||
|
const saveKeyPair = () => {
|
||||||
|
if (!ethDmKeyPair) return;
|
||||||
|
if (!password) return;
|
||||||
|
saveKeyPairToStorage(ethDmKeyPair, password).then(() => {
|
||||||
|
console.log('EthDm KeyPair saved to storage');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={saveKeyPair}
|
||||||
|
disabled={!password || !ethDmKeyPair}
|
||||||
|
>
|
||||||
|
Save Eth-DM Key Pair to storage
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { KeyPair } from '../crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save keypair to storage, encrypted with password
|
||||||
|
*/
|
||||||
|
export async function saveKeyPairToStorage(
|
||||||
|
ethDmKeyPair: KeyPair,
|
||||||
|
password: string
|
||||||
|
) {
|
||||||
|
const { salt, iv, cipher } = await encryptKey(ethDmKeyPair, password);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
salt: new Buffer(salt).toString('hex'),
|
||||||
|
iv: new Buffer(iv).toString('hex'),
|
||||||
|
cipher: new Buffer(cipher).toString('hex'),
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem('cipherEthDmKeyPair', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load keypair from storage, decrypted using password
|
||||||
|
*/
|
||||||
|
export async function loadKeyPairFromStorage(
|
||||||
|
password: string
|
||||||
|
): Promise<KeyPair | undefined> {
|
||||||
|
const str = localStorage.getItem('cipherEthDmKeyPair');
|
||||||
|
if (!str) return;
|
||||||
|
const data = JSON.parse(str);
|
||||||
|
|
||||||
|
const salt = new Buffer(data.salt, 'hex');
|
||||||
|
const iv = new Buffer(data.iv, 'hex');
|
||||||
|
const cipher = new Buffer(data.cipher, 'hex');
|
||||||
|
|
||||||
|
return await decryptKey(salt, iv, cipher, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use password user as key material for wrap key.
|
||||||
|
*/
|
||||||
|
function getKeyMaterial(password: string): Promise<CryptoKey> {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
return window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
enc.encode(password),
|
||||||
|
{ name: 'PBKDF2' },
|
||||||
|
false,
|
||||||
|
['deriveBits', 'deriveKey']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get key to store password
|
||||||
|
*/
|
||||||
|
function getWrapKey(keyMaterial: CryptoKey, salt: Uint8Array) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: salt,
|
||||||
|
iterations: 100000,
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt Eth-DM KeyPair using provided password
|
||||||
|
*/
|
||||||
|
async function encryptKey(ethDmKeyPair: KeyPair, password: string) {
|
||||||
|
const keyMaterial = await getKeyMaterial(password);
|
||||||
|
const salt = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const wrappingKey = await getWrapKey(keyMaterial, salt);
|
||||||
|
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const encodedKeyPair = enc.encode(JSON.stringify(ethDmKeyPair));
|
||||||
|
|
||||||
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const cipher = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
wrappingKey,
|
||||||
|
encodedKeyPair
|
||||||
|
);
|
||||||
|
|
||||||
|
return { salt, iv, cipher };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a key from a password, and use the key to decrypt the cipher key pair.
|
||||||
|
*/
|
||||||
|
async function decryptKey(
|
||||||
|
salt: Buffer,
|
||||||
|
iv: Buffer,
|
||||||
|
cipherKeyPair: Buffer,
|
||||||
|
password: string
|
||||||
|
): Promise<KeyPair | undefined> {
|
||||||
|
const keyMaterial = await getKeyMaterial(password);
|
||||||
|
const key = await getWrapKey(keyMaterial, salt);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let decrypted = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
cipherKeyPair
|
||||||
|
);
|
||||||
|
|
||||||
|
let dec = new TextDecoder();
|
||||||
|
return JSON.parse(dec.decode(decrypted));
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { List, ListItem, ListItemText } from '@material-ui/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear text message
|
||||||
|
*/
|
||||||
|
export interface Message {
|
||||||
|
text: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
messages: Message[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Messages({ messages }: Props) {
|
||||||
|
return <List dense={true}>{generate(messages)}</List>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate(messages: Message[]) {
|
||||||
|
return messages.map((msg) => {
|
||||||
|
const text = `<${formatDisplayDate(msg.timestamp)}> ${msg.text}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText key={formatDisplayDate(msg.timestamp)} primary={text} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDisplayDate(timestamp: Date): string {
|
||||||
|
return timestamp.toLocaleString([], {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import Messages, { Message } from './Messages';
|
||||||
|
import { Waku } from 'js-waku';
|
||||||
|
import SendMessage from './SendMessage';
|
||||||
|
import { makeStyles } from '@material-ui/core';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'left',
|
||||||
|
flexDirection: 'column',
|
||||||
|
margin: '5px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
waku: Waku | undefined;
|
||||||
|
recipients: Map<string, string>;
|
||||||
|
messages: Message[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Messaging({ waku, recipients, messages }: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<SendMessage recipients={recipients} waku={waku} />
|
||||||
|
<Messages messages={messages} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,9 +8,9 @@ import {
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import React, { ChangeEvent, useState, KeyboardEvent } from 'react';
|
import React, { ChangeEvent, useState, KeyboardEvent } from 'react';
|
||||||
import { Waku, WakuMessage } from 'js-waku';
|
import { Waku, WakuMessage } from 'js-waku';
|
||||||
import * as EthCrypto from 'eth-crypto';
|
import { DirectMessage, encode } from './wire';
|
||||||
import { DirectMessage, encode } from './messages';
|
import { encryptMessage } from '../crypto';
|
||||||
import { DirectMessageContentTopic } from './App';
|
import { DirectMessageContentTopic } from '../InitWaku';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
formControl: {
|
formControl: {
|
||||||
|
@ -28,13 +28,11 @@ export interface Props {
|
||||||
recipients: Map<string, string>;
|
recipients: Map<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SendMessage(props: Props) {
|
export default function SendMessage({ waku, recipients }: Props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [recipient, setRecipient] = useState<string>('');
|
const [recipient, setRecipient] = useState<string>('');
|
||||||
const [message, setMessage] = useState<string>();
|
const [message, setMessage] = useState<string>();
|
||||||
|
|
||||||
const waku = props.waku;
|
|
||||||
|
|
||||||
const handleRecipientChange = (
|
const handleRecipientChange = (
|
||||||
event: ChangeEvent<{ name?: string; value: unknown }>
|
event: ChangeEvent<{ name?: string; value: unknown }>
|
||||||
) => {
|
) => {
|
||||||
|
@ -45,7 +43,7 @@ export function SendMessage(props: Props) {
|
||||||
setMessage(event.target.value);
|
setMessage(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = Array.from(props.recipients.keys()).map((recipient) => {
|
const items = Array.from(recipients.keys()).map((recipient) => {
|
||||||
return (
|
return (
|
||||||
<MenuItem key={recipient} value={recipient}>
|
<MenuItem key={recipient} value={recipient}>
|
||||||
{recipient}
|
{recipient}
|
||||||
|
@ -63,7 +61,7 @@ export function SendMessage(props: Props) {
|
||||||
if (!waku) return;
|
if (!waku) return;
|
||||||
if (!recipient) return;
|
if (!recipient) return;
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
const publicKey = props.recipients.get(recipient);
|
const publicKey = recipients.get(recipient);
|
||||||
if (!publicKey) return;
|
if (!publicKey) return;
|
||||||
|
|
||||||
sendMessage(waku, recipient, publicKey, message, (res) => {
|
sendMessage(waku, recipient, publicKey, message, (res) => {
|
||||||
|
@ -111,7 +109,7 @@ async function encodeEncryptedWakuMessage(
|
||||||
publicKey: string,
|
publicKey: string,
|
||||||
address: string
|
address: string
|
||||||
): Promise<WakuMessage> {
|
): Promise<WakuMessage> {
|
||||||
const encryptedMsg = await EthCrypto.encryptWithPublicKey(publicKey, message);
|
const encryptedMsg = await encryptMessage(publicKey, message);
|
||||||
|
|
||||||
const directMsg: DirectMessage = {
|
const directMsg: DirectMessage = {
|
||||||
toAddress: address,
|
toAddress: address,
|
Loading…
Reference in New Issue