Compare commits

...

4 Commits

Author SHA1 Message Date
Mark Sinclair a8ebe720b1 wasm-client: add method to validate a recipient's address 2022-11-29 14:16:36 +00:00
Mark Sinclair c720b430b7 sdk: examples: UI to send and show sender client address 2022-11-29 14:15:47 +00:00
Mark Sinclair 934a8c3735 sdk: add headers to wasm client 2022-11-29 14:15:47 +00:00
Mark Sinclair ab1479f363 wasm-client: make headers Option<String> and add to all methods 2022-11-29 14:15:47 +00:00
7 changed files with 179 additions and 60 deletions
@@ -42,19 +42,28 @@ pub struct StringMessage {
/// Create a new binary message with a user-specified `kind`. /// Create a new binary message with a user-specified `kind`.
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_binary_message(kind: u8, payload: Vec<u8>) -> Vec<u8> { pub fn create_binary_message(kind: u8, payload: Vec<u8>, headers: Option<String>) -> Vec<u8> {
create_binary_message_with_headers(kind, payload, "".to_string()) create_binary_message_with_headers(kind, payload, headers)
} }
/// Create a new message with a UTF-8 encoded string `payload` and a user-specified `kind`. /// Create a new message with a UTF-8 encoded string `payload` and a user-specified `kind`.
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_binary_message_from_string(kind: u8, payload: String) -> Vec<u8> { pub fn create_binary_message_from_string(
create_binary_message_with_headers(kind, payload.as_bytes().to_vec(), "".to_string()) kind: u8,
payload: String,
headers: Option<String>,
) -> Vec<u8> {
create_binary_message_with_headers(kind, payload.as_bytes().to_vec(), headers)
} }
/// Create a new binary message with a user-specified `kind`, and `headers` as a string. /// Create a new binary message with a user-specified `kind`, and `headers` as a string.
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_binary_message_with_headers(kind: u8, payload: Vec<u8>, headers: String) -> Vec<u8> { pub fn create_binary_message_with_headers(
kind: u8,
payload: Vec<u8>,
headers: Option<String>,
) -> Vec<u8> {
let headers = headers.unwrap_or_else(|| "".to_string());
let headers = headers.as_bytes().to_vec(); let headers = headers.as_bytes().to_vec();
let size = (headers.len() as u64).to_be_bytes().to_vec(); let size = (headers.len() as u64).to_be_bytes().to_vec();
vec![vec![kind], size, headers, payload].concat() vec![vec![kind], size, headers, payload].concat()
@@ -169,7 +178,7 @@ mod tests {
let message_as_bytes = create_binary_message_with_headers( let message_as_bytes = create_binary_message_with_headers(
42u8, 42u8,
vec![0u8, 1u8, 2u8], vec![0u8, 1u8, 2u8],
"test headers".to_string(), Some("test headers".to_string()),
); );
// calculate header size // calculate header size
@@ -197,7 +206,7 @@ mod tests {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn test_binary_with_empty_headers() { fn test_binary_with_empty_headers() {
let message_as_bytes = let message_as_bytes =
create_binary_message_with_headers(42u8, vec![0u8, 1u8, 2u8], "".to_string()); create_binary_message_with_headers(42u8, vec![0u8, 1u8, 2u8], Some("".to_string()));
let expected_size = 0; let expected_size = 0;
+2
View File
@@ -9,6 +9,8 @@ pub mod binary_message_helper;
mod client; mod client;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub mod gateway_selector; pub mod gateway_selector;
#[cfg(target_arch = "wasm32")]
pub mod validation;
#[wasm_bindgen] #[wasm_bindgen]
pub fn set_panic_hook() { pub fn set_panic_hook() {
+35
View File
@@ -0,0 +1,35 @@
use nymsphinx::addressing::clients::Recipient;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn validate_recipient(recipient: String) -> Result<(), JsError> {
match Recipient::try_from_base58_string(recipient) {
Ok(_) => Ok(()),
Err(e) => Err(JsError::new(format!("{}", e).as_str())),
}
}
#[cfg(test)]
mod tests {
use super::validate_recipient;
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn test_recipient_validation_ok() {
let res = validate_recipient("DyQmXnst5NGGjzUZxRC5Bjs5bd7CBF3xMpsSmbRiizr2.GH6YTBP2NXU3AVqd8WjiTMVyeMjunXMEsp7gVCMEJqpD@336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9".to_string());
assert!(res.is_ok())
}
#[wasm_bindgen_test]
fn test_recipient_validation_fails() {
assert!(validate_recipient(" DyQmXnst5NGGjzUZxRC5Bjs5bd7CBF3xMpsSmbRiizr2.GH6YTBP2NXU3AVqd8WjiTMVyeMjunXMEsp7gVCMEJqpD@336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9".to_string()).is_err());
assert!(validate_recipient(
"DyQmXnst5NGGjzUZxRC5BjbRiizr2.GH6YTBP2NXU3AVqd8WD@336yuXAeGEgedRfqTJZQH1bHv1SjCZYarc9"
.to_string()
)
.is_err());
assert!(validate_recipient("🙀🙀🙀🙀".to_string()).is_err());
assert!(validate_recipient("".to_string()).is_err());
assert!(validate_recipient(" ".to_string()).is_err());
}
}
@@ -5,9 +5,12 @@ import {
Chip, Chip,
CircularProgress, CircularProgress,
Container, Container,
FormControlLabel,
FormGroup,
InputAdornment, InputAdornment,
Link, Link,
Stack, Stack,
Switch,
TextField, TextField,
Tooltip, Tooltip,
Typography, Typography,
@@ -20,6 +23,8 @@ import CallReceivedIcon from '@mui/icons-material/CallReceived';
import PersonIcon from '@mui/icons-material/Person'; import PersonIcon from '@mui/icons-material/Person';
import PersonOffIcon from '@mui/icons-material/PersonOff'; import PersonOffIcon from '@mui/icons-material/PersonOff';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import { NymLogo } from '@nymproject/react/logo/NymLogo'; import { NymLogo } from '@nymproject/react/logo/NymLogo';
import { NymThemeProvider } from '@nymproject/mui-theme'; import { NymThemeProvider } from '@nymproject/mui-theme';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
@@ -28,9 +33,10 @@ import { DropzoneDialog } from 'react-mui-dropzone';
import UploadFileIcon from '@mui/icons-material/UploadFile'; import UploadFileIcon from '@mui/icons-material/UploadFile';
import ArticleIcon from '@mui/icons-material/Article'; import ArticleIcon from '@mui/icons-material/Article';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import { Headers } from '@nymproject/sdk';
import { ThemeToggle } from './ThemeToggle'; import { ThemeToggle } from './ThemeToggle';
import { AppContextProvider, useAppContext } from './context'; import { AppContextProvider, useAppContext } from './context';
import { MixnetContextProvider, parseBinaryMessageHeaders, useMixnetContext } from './context/mixnet'; import { BinaryMessageHeaders, MixnetContextProvider, useMixnetContext } from './context/mixnet';
export const AppTheme: React.FC = ({ children }) => { export const AppTheme: React.FC = ({ children }) => {
const { mode } = useAppContext(); const { mode } = useAppContext();
@@ -45,6 +51,7 @@ interface Log {
fileDownloadUrl?: string; fileDownloadUrl?: string;
filesize?: number; filesize?: number;
timestamp: Date; timestamp: Date;
headers?: Headers;
} }
interface UploadState { interface UploadState {
@@ -52,11 +59,48 @@ interface UploadState {
files: File[]; files: File[];
} }
const ClientAddress: React.FC<{ label?: string; tooltip?: string; address?: string }> = ({
label,
address,
tooltip,
}) => {
const copy = useClipboard();
if (!address) {
return <Chip label="Anonymous" icon={<VisibilityOffIcon />} />;
}
const addressShort = `${address.slice(0, 24)}...`;
return (
<Tooltip arrow title={tooltip || ''}>
<Chip
clickable
label={
label ? (
<>
<strong>{label}</strong> {addressShort}
</>
) : (
<>{addressShort}</>
)
}
onClick={() => {
if (address) {
copy.copy(address);
}
}}
icon={<ContentCopyIcon />}
/>
</Tooltip>
);
};
export const Content: React.FC = () => { export const Content: React.FC = () => {
const theme = useTheme(); const theme = useTheme();
const { isReady, address, connect, events, sendTextMessage, sendBinaryMessage } = useMixnetContext(); const { isReady, address, connect, events, sendTextMessage, sendBinaryMessage } = useMixnetContext();
const copy = useClipboard();
const [revealSenderAddress, setRevealSenderAddress] = React.useState(false);
const [sendToSelf, setSendToSelf] = React.useState(false); const [sendToSelf, setSendToSelf] = React.useState(false);
const [recipient, setRecipient] = React.useState<string>(); const [recipient, setRecipient] = React.useState<string>();
const handleRecipientChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleRecipientChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -94,13 +138,8 @@ export const Content: React.FC = () => {
React.useEffect(() => { React.useEffect(() => {
if (isReady) { if (isReady) {
// // mixnet v1 const validatorApiUrl = 'https://validator.nymtech.net/api';
// const validatorApiUrl = 'https://validator.nymtech.net/api'; const preferredGatewayIdentityKey = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM';
// const preferredGatewayIdentityKey = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM';
// mixnet v2
const validatorApiUrl = 'https://qwerty-validator-api.qa.nymte.ch/api'; // "http://localhost:8081";
const preferredGatewayIdentityKey = undefined; // '36vfvEyBzo5cWEFbnP7fqgY39kFw9PQhvwzbispeNaxL';
connect({ connect({
clientId: 'Example Client', clientId: 'Example Client',
@@ -117,6 +156,7 @@ export const Content: React.FC = () => {
kind: 'rx', kind: 'rx',
timestamp: new Date(), timestamp: new Date(),
message: e.args.payload, message: e.args.payload,
headers: e.args.headers,
}); });
setLogTrigger(Date.now()); setLogTrigger(Date.now());
}); });
@@ -142,8 +182,12 @@ export const Content: React.FC = () => {
React.useEffect(() => { React.useEffect(() => {
if (events) { if (events) {
const unsubcribe = events.subscribeToBinaryMessageReceivedEvent((e) => { const unsubcribe = events.subscribeToBinaryMessageReceivedEvent((e) => {
// the headers will be JSON (see the mixnet context for how they are created), so parse them if (!e.args.headers) {
const headers = parseBinaryMessageHeaders(e.args.headers); console.error('Expected headers, got undefined 😢', e.args);
return;
}
const headers = e.args.headers as BinaryMessageHeaders;
const blob = new Blob([new Uint8Array(e.args.payload)], { type: headers.mimeType }); const blob = new Blob([new Uint8Array(e.args.payload)], { type: headers.mimeType });
log.current.push({ log.current.push({
@@ -152,6 +196,7 @@ export const Content: React.FC = () => {
filename: headers.filename, filename: headers.filename,
fileDownloadUrl: URL.createObjectURL(blob), fileDownloadUrl: URL.createObjectURL(blob),
filesize: e.args.payload.length, filesize: e.args.payload.length,
headers: e.args.headers,
}); });
setLogTrigger(Date.now()); setLogTrigger(Date.now());
}); });
@@ -184,7 +229,8 @@ export const Content: React.FC = () => {
message, message,
}); });
setLogTrigger(Date.now()); setLogTrigger(Date.now());
await sendTextMessage({ payload: message, recipient }); const senderAddress = revealSenderAddress ? address : undefined;
await sendTextMessage({ payload: message, recipient, headers: { senderAddress } });
await Promise.all( await Promise.all(
files.map(async (f) => { files.map(async (f) => {
@@ -200,7 +246,7 @@ export const Content: React.FC = () => {
return await sendBinaryMessage({ return await sendBinaryMessage({
payload: new Uint8Array(buffer), payload: new Uint8Array(buffer),
recipient, recipient,
headers: { filename: f.name, mimeType: f.type }, headers: { filename: f.name, mimeType: f.type, senderAddress },
}); });
} catch (e) { } catch (e) {
addErrorLog('Failed to send file', f.name); addErrorLog('Failed to send file', f.name);
@@ -249,20 +295,7 @@ export const Content: React.FC = () => {
) : ( ) : (
<> <>
<Chip color="success" icon={<CheckCircleIcon />} label="Connected" variant="outlined" /> <Chip color="success" icon={<CheckCircleIcon />} label="Connected" variant="outlined" />
{address && ( <ClientAddress address={address} tooltip="Copy your client address to the clipboard" />
<Tooltip arrow title="Copy your client address to the clipboard">
<Chip
clickable
label={`${address.slice(0, 24)}...`}
onClick={() => {
if (address) {
copy.copy(address);
}
}}
icon={<ContentCopyIcon />}
/>
</Tooltip>
)}
</> </>
)} )}
</Stack> </Stack>
@@ -347,9 +380,34 @@ export const Content: React.FC = () => {
/> />
</Box> </Box>
<Button variant="contained" sx={{ width: 100 }} onClick={handleSend}> <Stack direction="row" spacing={2}>
Send <Button variant="contained" sx={{ width: 100 }} onClick={handleSend}>
</Button> Send
</Button>
<FormGroup>
<FormControlLabel
control={
<Switch
color={revealSenderAddress ? 'warning' : 'default'}
onClick={() => setRevealSenderAddress((prevState) => !prevState)}
/>
}
label={
revealSenderAddress ? (
<Stack direction="row" spacing={1}>
<VisibilityIcon color="warning" />
<Typography color={theme.palette.warning.main}>Reveal your address to the recipient</Typography>
</Stack>
) : (
<Stack direction="row" spacing={1}>
<VisibilityOffIcon />
<Typography>Hide your address from the recipient</Typography>
</Stack>
)
}
/>
</FormGroup>
</Stack>
</Stack> </Stack>
)} )}
</Box> </Box>
@@ -398,6 +456,16 @@ export const Content: React.FC = () => {
)} )}
</> </>
)} )}
{item.kind === 'rx' &&
(item.headers?.senderAddress ? (
<ClientAddress
label="Sender"
tooltip="Click to copy the message sender's address"
address={item.headers?.senderAddress}
/>
) : (
<ClientAddress label="Sender" />
))}
</Stack> </Stack>
</Box> </Box>
))} ))}
@@ -1,14 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { createNymMixnetClient, IWebWorkerEvents, NymClientConfig, NymMixnetClient } from '@nymproject/sdk'; import { createNymMixnetClient, IWebWorkerEvents, NymClientConfig, NymMixnetClient, Headers } from '@nymproject/sdk';
export interface BinaryMessageHeaders { export interface BinaryMessageHeaders extends Headers {
filename: string; filename: string;
mimeType: string; mimeType: string;
} }
export const parseBinaryMessageHeaders = (headers: string): BinaryMessageHeaders =>
JSON.parse(headers) as BinaryMessageHeaders;
interface State { interface State {
// data // data
isReady: boolean; isReady: boolean;
@@ -17,7 +14,7 @@ interface State {
// methods // methods
connect: (config: NymClientConfig) => Promise<void>; connect: (config: NymClientConfig) => Promise<void>;
sendTextMessage: (args: { payload: string; recipient: string }) => Promise<void>; sendTextMessage: (args: { payload: string; recipient: string; headers?: Headers }) => Promise<void>;
sendBinaryMessage: (args: { payload: Uint8Array; recipient: string; headers: BinaryMessageHeaders }) => Promise<void>; sendBinaryMessage: (args: { payload: Uint8Array; recipient: string; headers: BinaryMessageHeaders }) => Promise<void>;
} }
@@ -62,7 +59,7 @@ export const MixnetContextProvider: React.FC = ({ children }) => {
await nym.current.client.start(config); await nym.current.client.start(config);
}; };
const sendTextMessage = async (args: { payload: string; recipient: string }) => { const sendTextMessage = async (args: { payload: string; recipient: string; headers?: Headers }) => {
if (!nym.current?.client) { if (!nym.current?.client) {
console.error('Nym client has not initialised. Please wrap in useEffect on `isReady` prop of this context.'); console.error('Nym client has not initialised. Please wrap in useEffect on `isReady` prop of this context.');
return; return;
@@ -76,8 +73,7 @@ export const MixnetContextProvider: React.FC = ({ children }) => {
return; return;
} }
// convert headers to JSON // convert headers to JSON
const sendArgs = { ...args, headers: JSON.stringify(args.headers) }; await nym.current.client.sendBinaryMessage(args);
await nym.current.client.sendBinaryMessage(sendArgs);
}; };
const value = React.useMemo<State>( const value = React.useMemo<State>(
@@ -27,7 +27,7 @@ export interface NymClientConfig {
* Optional. The identity key of the preferred gateway to connect to. * Optional. The identity key of the preferred gateway to connect to.
*/ */
preferredGatewayIdentityKey?: string; preferredGatewayIdentityKey?: string;
/** /**
* Optional. The listener websocket of the preferred gateway to connect to. * Optional. The listener websocket of the preferred gateway to connect to.
*/ */
@@ -39,11 +39,16 @@ export interface NymClientConfig {
debug?: wasm_bindgen.Debug; debug?: wasm_bindgen.Debug;
} }
export interface Headers {
senderAddress?: string;
[key: string]: unknown;
}
export interface IWebWorker { export interface IWebWorker {
start: (config: NymClientConfig) => void; start: (config: NymClientConfig) => void;
selfAddress: () => string | undefined; selfAddress: () => string | undefined;
sendMessage: (args: { payload: string; recipient: string }) => void; sendMessage: (args: { payload: string; recipient: string; headers?: Headers }) => void;
sendBinaryMessage: (args: { payload: Uint8Array; recipient: string; headers?: string }) => void; sendBinaryMessage: (args: { payload: Uint8Array; recipient: string; headers?: Headers }) => void;
} }
export enum EventKinds { export enum EventKinds {
@@ -72,6 +77,7 @@ export interface StringMessageReceivedEvent {
args: { args: {
kind: number; kind: number;
payload: string; payload: string;
headers?: Headers;
}; };
} }
@@ -80,7 +86,7 @@ export interface BinaryMessageReceivedEvent {
args: { args: {
kind: number; kind: number;
payload: Uint8Array; payload: Uint8Array;
headers: string; headers?: Headers;
}; };
} }
@@ -16,6 +16,7 @@ import type {
StringMessageReceivedEvent, StringMessageReceivedEvent,
BinaryMessageReceivedEvent, BinaryMessageReceivedEvent,
NymClientConfig, NymClientConfig,
Headers,
} from './types'; } from './types';
import { EventKinds } from './types'; import { EventKinds } from './types';
@@ -91,12 +92,13 @@ class ClientWrapper {
this.client = await this.client.start(); this.client = await this.client.start();
}; };
sendMessage = async ({ payload, recipient }: { recipient: string; payload: string }) => { sendMessage = async ({ payload, recipient, headers }: { recipient: string; payload: string; headers?: Headers }) => {
if (!this.client) { if (!this.client) {
console.error('Client has not been initialised. Please call `init` first.'); console.error('Client has not been initialised. Please call `init` first.');
return; return;
} }
const message = wasm_bindgen.create_binary_message_from_string(PAYLOAD_KIND_TEXT, payload); const headersAsJsonString = headers ? JSON.stringify(headers) : '';
const message = wasm_bindgen.create_binary_message_from_string(PAYLOAD_KIND_TEXT, payload, headersAsJsonString);
this.client = await this.client.send_binary_message(message, recipient); this.client = await this.client.send_binary_message(message, recipient);
}; };
@@ -107,13 +109,14 @@ class ClientWrapper {
}: { }: {
recipient: string; recipient: string;
payload: Uint8Array; payload: Uint8Array;
headers?: string; headers?: Headers;
}) => { }) => {
if (!this.client) { if (!this.client) {
console.error('Client has not been initialised. Please call `init` first.'); console.error('Client has not been initialised. Please call `init` first.');
return; return;
} }
const message = wasm_bindgen.create_binary_message_with_headers(PAYLOAD_KIND_BINARY, payload, headers || ''); const headersAsJsonString = headers ? JSON.stringify(headers) : '';
const message = wasm_bindgen.create_binary_message_with_headers(PAYLOAD_KIND_BINARY, payload, headersAsJsonString);
this.client = await this.client.send_binary_message(message, recipient); this.client = await this.client.send_binary_message(message, recipient);
}; };
} }
@@ -133,11 +136,10 @@ wasm_bindgen(wasmUrl)
config.validatorApiUrl, config.validatorApiUrl,
config.preferredGatewayIdentityKey, config.preferredGatewayIdentityKey,
); );
// set a different gatewayListener in order to avoid workaround ws over https error // set a different gatewayListener in order to avoid workaround ws over https error
if (config.gatewayListener) if (config.gatewayListener) gatewayEndpoint.gateway_listener = config.gatewayListener;
gatewayEndpoint.gateway_listener = config.gatewayListener;
// create the client, passing handlers for events // create the client, passing handlers for events
wrapper.init( wrapper.init(
new wasm_bindgen.Config( new wasm_bindgen.Config(
@@ -153,19 +155,20 @@ wasm_bindgen(wasmUrl)
async (message) => { async (message) => {
try { try {
const { kind, payload, headers } = await wasm_bindgen.parse_binary_message_with_headers(message); const { kind, payload, headers } = await wasm_bindgen.parse_binary_message_with_headers(message);
const parsedHeaders = headers?.length > 0 ? JSON.parse(headers) : undefined;
switch (kind) { switch (kind) {
case PAYLOAD_KIND_TEXT: { case PAYLOAD_KIND_TEXT: {
const stringMessage = await wasm_bindgen.parse_string_message_with_headers(message); const stringMessage = await wasm_bindgen.parse_string_message_with_headers(message);
postMessageWithType<StringMessageReceivedEvent>({ postMessageWithType<StringMessageReceivedEvent>({
kind: EventKinds.StringMessageReceived, kind: EventKinds.StringMessageReceived,
args: { kind, payload: stringMessage.payload }, args: { kind, payload: stringMessage.payload, headers: parsedHeaders },
}); });
break; break;
} }
case PAYLOAD_KIND_BINARY: case PAYLOAD_KIND_BINARY:
postMessageWithType<BinaryMessageReceivedEvent>({ postMessageWithType<BinaryMessageReceivedEvent>({
kind: EventKinds.BinaryMessageReceived, kind: EventKinds.BinaryMessageReceived,
args: { kind, payload, headers: headers || '' }, args: { kind, payload, headers: parsedHeaders },
}); });
break; break;
default: default: