Add README and example structure
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"license": "Apache 2.0",
|
||||
"workspaces": [
|
||||
"sdk/typescript/**",
|
||||
"ts-packages/*",
|
||||
"nym-wallet",
|
||||
"nym-connect",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Nym SDK
|
||||
|
||||
Welcome to the Nym SDK. This is a good starting point for finding resources to build software using Nym components.
|
||||
|
||||
The SDK is split into technologies and platforms:
|
||||
|
||||
- [Typescript](typescript) - packages for the Javascript ecosystem leveraging Nym's Typescript and WASM clients. Use these to build browser apps, web apps and mobile apps that can make use of the Nym mixnet and Coconut credentials
|
||||
|
||||
Coming soon:
|
||||
|
||||
- [iOS](ios) - native libraries for iOS, that can be used directly or from React Native using wrappers
|
||||
- [Android](android) - native libraries for iOS, that can be used via JNI or from React Native using wrappers
|
||||
@@ -0,0 +1,18 @@
|
||||
# See http://editorconfig.org/
|
||||
# EditorConfig helps developers define and maintain consistent coding styles
|
||||
# between different editors and IDEs. The EditorConfig project consists of a
|
||||
# file format for defining coding styles and a collection of text editor plugins
|
||||
# that enable editors to read the file format and adhere to defined styles.
|
||||
# EditorConfig files are easily readable and they work nicely with version
|
||||
# control systems.
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@nymproject/eslint-config-react-typescript"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
#### Mixnet
|
||||
|
||||
- threaded mixnet client that uses the Nym WASM client
|
||||
@@ -0,0 +1,38 @@
|
||||
# Nym SDK (Typescript)
|
||||
|
||||
The Nym SDK for Typescript will get you creating apps that can use the Nym Mixnet and Coconut credentials quickly.
|
||||
|
||||
## TL;DR
|
||||
|
||||
Include the SDK in your project:
|
||||
|
||||
```
|
||||
npm install @nymproject/sdk
|
||||
```
|
||||
|
||||
Open a connection to a Gateway on the Nym Mixnet:
|
||||
|
||||
```ts
|
||||
import { client } from '@nymproject/sdk';
|
||||
|
||||
const session = await client.connect('<<GATEWAY>>');
|
||||
```
|
||||
|
||||
This will start the WASM client on a worker thread, so that your code can stay nice and snappy.
|
||||
|
||||
Send a message to another user (you will need to know their address at a Gateway):
|
||||
|
||||
```ts
|
||||
const result = await client.send('<<USER ADDRESS>>', 'Hello Timmy!');
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- [Plain HTML + Javascript](examples/plain-html) - very simple chat app written in plain Javascript
|
||||
- [Chat App](examples/chat) - simple chat app written in React with Webpack
|
||||
|
||||
Coming soon:
|
||||
|
||||
- [Node tester](examples/node-tester) - a React app that sends test packets to a mixnode and measure the network speed
|
||||
- [Mixnet topology viewer](examples/topology) - a Svelte app that shows the mixnodes current in the active set
|
||||
- [Get a bandwidth voucher](examples/coconut-bandwidth-voucher) - get a bandwidth voucher to use the mixnet
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/env", "@babel/react"]
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# Example with React + Typescript + Webpack 5 + MUI
|
||||
|
||||
An example of using default Webpack and Typescript settings with React and MUI, including theming.
|
||||
|
||||
You can use this example as a seed for a new project.
|
||||
|
||||
Remember to build the dependency packages from the root of this repo by running:
|
||||
|
||||
```
|
||||
yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
If you need to make changes to the dependency packages, you can run `yarn watch` in that package to watch for chagnes and build them. This project will pick up the changes in the built package and hot-reload / recompile.
|
||||
|
||||
## Features
|
||||
|
||||
### Yarn workspaces
|
||||
|
||||
Packages from `ts-packages` are shared using Yarn workspaces. Make sure you add you new project to [package.json](../../package.json) to use the shared packages.
|
||||
|
||||
> ⚠️ **Warning**: Yarn workspaces will share all dependencies between projects and works by falling back to parent directories until a `node_modules` directory is found. So be careful when messing around with `node_modules` and resolution, because unexpected things could happen - for example, if you do not run `yarn` from the root and you have a `node_modules` in a directory that is a parent of the directory where you checkout out this repository, that `node_modules` will be used for resolving packages 🙀.
|
||||
|
||||
### Typescript
|
||||
|
||||
Shared Typescript config is in [tsconfig.json](./tsconfig.json), with specific production settings in [tsconfig.prod.json](./tsconfig.prod.json) that:
|
||||
|
||||
- exclude Storybook stories and Jest tests
|
||||
- do not output typing `*.d.ts` files
|
||||
|
||||
### Webpack
|
||||
|
||||
Inherit config for Webpack 5 with additional tweaks including:
|
||||
|
||||
- favicon generation from [favicon asset files](../../../../assets/favicon/favicon.png)
|
||||
- asset handling (svg, png, fonts, css, etc)
|
||||
- minification
|
||||
|
||||
The development settings include:
|
||||
|
||||
- `ts-loader` for quick transpilation
|
||||
- threaded type checking using `tsc`
|
||||
- hot reloading using `react-refresh`
|
||||
|
||||
### Storybook
|
||||
|
||||
Storybook is available in [@nymproject/react](../../../../ts-packages/react-components/src/stories/Introduction.stories.mdx) and can be run using `yarn storybook`.
|
||||
|
||||
### MUI and theming
|
||||
|
||||
The [Nym theme](../mui-theme/src/theme/theme.ts) provides a theme provider that you can add as follows:
|
||||
|
||||
```typescript jsx
|
||||
export const App: React.FC = () => (
|
||||
<AppContextProvider>
|
||||
<AppTheme>
|
||||
<Content />
|
||||
</AppTheme>
|
||||
</AppContextProvider>
|
||||
);
|
||||
|
||||
export const AppTheme: React.FC = ({ children }) => {
|
||||
const { mode } = useAppContext();
|
||||
|
||||
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
|
||||
};
|
||||
|
||||
export const Content: React.FC = () => {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
And augment typings for the Theme by adding [mui-theme.d.ts](./src/theme/mui-theme.d.ts):
|
||||
|
||||
```typescript
|
||||
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
|
||||
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface Theme extends NymTheme {}
|
||||
interface ThemeOptions extends Partial<NymTheme> {}
|
||||
interface Palette extends NymPaletteWithExtensions {}
|
||||
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
|
||||
}
|
||||
```
|
||||
|
||||
Adding the above, means that any component now has the correct typings, for example, below the Nym palette interface is available for all MUI `Theme` instances with code completion for VSCode and IntelliJ:
|
||||
|
||||
```typescript jsx
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
...
|
||||
|
||||
<Typography sx={{ color: (theme) => theme.palette.nym.networkExplorer.mixnodes.status.active }}>
|
||||
The quick brown fox jumps over the white fence
|
||||
</Typography>
|
||||
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "@nymproject/sdk-example-react-webpack-wasm",
|
||||
"description": "An example project that uses WASM, React, Webpack, Typescript and the Nym theme + components library",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"@mui/material": "^5.0.1",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@mui/icons-material": "^5.5.0",
|
||||
"@mui/lab": "^5.0.0-alpha.72",
|
||||
"@nymproject/mui-theme" : "^1.0.0",
|
||||
"@nymproject/react" : "^1.0.0",
|
||||
"@nymproject/sdk": "1",
|
||||
"use-clipboard-copy": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.14.5",
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@nymproject/webpack": "^1.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^17.0.34",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-root-import": "^5.1.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^6.2.0",
|
||||
"css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-root-import": "^1.0.4",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"favicons": "^6.2.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^7.2.1",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "^27.1.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"prettier": "^2.5.1",
|
||||
"react-refresh-typescript": "^2.0.3",
|
||||
"style-loader": "^3.2.1",
|
||||
"thread-loader": "^3.0.4",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.64.3",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.5.0",
|
||||
"webpack-favicons": "^1.3.8",
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --progress --port 3000",
|
||||
"build": "webpack build --progress --config webpack.prod.js",
|
||||
"build:dev": "webpack build --progress",
|
||||
"build:serve": "npx serve dist",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"tsc": "tsc",
|
||||
"tsc:watch": "tsc --watch",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import { NymLogo } from '@nymproject/react/logo/NymLogo';
|
||||
import { NymThemeProvider } from '@nymproject/mui-theme';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
import { AppContextProvider, useAppContext } from './context';
|
||||
import { MixnetContextProvider, useMixnetContext } from './context/mixnet';
|
||||
|
||||
export const AppTheme: React.FC = ({ children }) => {
|
||||
const { mode } = useAppContext();
|
||||
|
||||
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
|
||||
};
|
||||
|
||||
interface Log {
|
||||
kind: 'tx' | 'rx';
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export const Content: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const { isReady, address, connect, events, sendTextMessage } = useMixnetContext();
|
||||
const copy = useClipboard();
|
||||
|
||||
const [sendToSelf, setSendToSelf] = React.useState(false);
|
||||
const [recipient, setRecipient] = React.useState<string>();
|
||||
const handleRecipientChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRecipient(event.target.value);
|
||||
};
|
||||
|
||||
const [message, setMessage] = React.useState<string>('This is a test message');
|
||||
const handleMessageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setMessage(event.target.value);
|
||||
};
|
||||
|
||||
const log = React.useRef<Log[]>([]);
|
||||
const [_logTrigger, setLogTrigger] = React.useState(Date.now());
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isReady) {
|
||||
// // mixnet v1
|
||||
// const validatorApiUrl = 'https://validator.nymtech.net/api';
|
||||
// const preferredGatewayIdentityKey = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM';
|
||||
|
||||
// mixnet v2
|
||||
const validatorApiUrl = 'https://qwerty-validator-api.qa.nymte.ch/api'; // "http://localhost:8081";
|
||||
const preferredGatewayIdentityKey = undefined; // '36vfvEyBzo5cWEFbnP7fqgY39kFw9PQhvwzbispeNaxL';
|
||||
|
||||
connect({
|
||||
clientId: 'Example Client',
|
||||
validatorApiUrl,
|
||||
preferredGatewayIdentityKey,
|
||||
});
|
||||
}
|
||||
}, [isReady]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (events) {
|
||||
const unsubcribe = events.subscribeToTextMessageReceivedEvent((e) => {
|
||||
log.current.push({
|
||||
kind: 'rx',
|
||||
timestamp: new Date(),
|
||||
message: e.args.message,
|
||||
});
|
||||
setLogTrigger(Date.now());
|
||||
});
|
||||
|
||||
// cleanup on unmount
|
||||
return unsubcribe;
|
||||
}
|
||||
|
||||
// no cleanup
|
||||
return undefined;
|
||||
}, [events]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!message) {
|
||||
console.error('No message set');
|
||||
return;
|
||||
}
|
||||
if (!recipient) {
|
||||
console.error('No recipient set');
|
||||
return;
|
||||
}
|
||||
|
||||
log.current.push({
|
||||
kind: 'tx',
|
||||
timestamp: new Date(),
|
||||
message,
|
||||
});
|
||||
setLogTrigger(Date.now());
|
||||
await sendTextMessage({ message, recipient });
|
||||
};
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 4 }}>
|
||||
<Box display="flex" flexDirection="row-reverse" pb={2}>
|
||||
<ThemeToggle />
|
||||
</Box>
|
||||
<NymLogo height={50} />
|
||||
<h1>Nym Mixnet Chat App</h1>
|
||||
<Box mb={5}>
|
||||
<Typography>
|
||||
This is an example app that uses React, Typescript, Webpack and the Nym theme + components with the WASM
|
||||
Mixnet Client.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mb={4}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
{!isReady ? (
|
||||
<>
|
||||
<CircularProgress size={theme.typography.fontSize * 1.5} />
|
||||
<Typography>Connecting...</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Chip color="success" icon={<CheckCircleIcon />} label="Connected" variant="outlined" />
|
||||
{address && (
|
||||
<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>
|
||||
{isReady && address && (
|
||||
<Stack direction="column" mt={6} spacing={4}>
|
||||
{!sendToSelf ? (
|
||||
<TextField
|
||||
id="recipient"
|
||||
label="Recipient address"
|
||||
required
|
||||
value={recipient}
|
||||
onChange={handleRecipientChange}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title="Use your own address to send messages to yourself" arrow>
|
||||
<PersonIcon
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
if (address) {
|
||||
setSendToSelf(true);
|
||||
setRecipient(address);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
id="recipientSendToSelf"
|
||||
label="Recipient address"
|
||||
value={address}
|
||||
onChange={() => undefined}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title="Use your own address to send messages to yourself" arrow>
|
||||
<PersonIcon sx={{ cursor: 'pointer' }} onClick={() => setSendToSelf(false)} />
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
id="message"
|
||||
required
|
||||
label="Enter some text to send"
|
||||
multiline
|
||||
rows={4}
|
||||
value={message}
|
||||
onChange={handleMessageChange}
|
||||
/>
|
||||
<Button variant="contained" sx={{ width: 100 }} onClick={handleSend}>
|
||||
Send
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
{log.current.map((item) => (
|
||||
<Box key={item.kind + item.timestamp.toISOString()}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="start"
|
||||
sx={{ color: item.kind === 'tx' ? theme.palette.success.main : theme.palette.primary.main }}
|
||||
>
|
||||
{item.kind === 'tx' ? <ArrowForwardIcon /> : <ArrowBackIcon />}
|
||||
<Chip variant="outlined" label={item.timestamp.toLocaleTimeString()} />
|
||||
<Typography>{item.message}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const App: React.FC = () => (
|
||||
<AppContextProvider>
|
||||
<MixnetContextProvider>
|
||||
<AppTheme>
|
||||
<Content />
|
||||
</AppTheme>
|
||||
</MixnetContextProvider>
|
||||
</AppContextProvider>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import { useAppContext } from './context';
|
||||
|
||||
export const ThemeToggle: React.FC = () => {
|
||||
const { mode, toggleMode } = useAppContext();
|
||||
return (
|
||||
<Button variant="outlined" color="secondary" onClick={toggleMode} sx={{ display: 'flex', alignItems: 'centre' }}>
|
||||
{mode === 'dark' ? (
|
||||
<DarkModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
|
||||
) : (
|
||||
<LightModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
|
||||
)}
|
||||
<Typography ml={1} color={(theme) => theme.palette.primary.light}>
|
||||
Switch to {mode === 'dark' ? 'light mode' : 'dark mode'}
|
||||
</Typography>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
import('./index')
|
||||
.catch(error => console.error('Unable to load app :-(', error));
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PaletteMode } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
|
||||
interface State {
|
||||
mode: PaletteMode;
|
||||
toggleMode: () => void;
|
||||
}
|
||||
|
||||
const AppContext = React.createContext<State | undefined>(undefined);
|
||||
|
||||
export const useAppContext = (): State => {
|
||||
const context = React.useContext<State | undefined>(AppContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Please include a `import { AppContextProvider } from "./context"` before using this hook');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AppContextProvider: React.FC = ({ children }) => {
|
||||
// light/dark mode
|
||||
const [mode, setMode] = React.useState<PaletteMode>('dark');
|
||||
|
||||
const value = React.useMemo<State>(
|
||||
() => ({
|
||||
mode,
|
||||
toggleMode: () => setMode((prevMode) => (prevMode !== 'light' ? 'light' : 'dark')),
|
||||
}),
|
||||
[mode],
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from 'react';
|
||||
import { createNymMixnetClient, IWebWorkerEvents, NymClientConfig, NymMixnetClient } from '@nymproject/sdk';
|
||||
|
||||
interface State {
|
||||
// data
|
||||
isReady: boolean;
|
||||
address?: string;
|
||||
events?: IWebWorkerEvents;
|
||||
|
||||
// methods
|
||||
connect: (config: NymClientConfig) => Promise<void>;
|
||||
sendTextMessage: (args: { message: string; recipient: string }) => Promise<void>;
|
||||
}
|
||||
|
||||
const MixnetContext = React.createContext<State | undefined>(undefined);
|
||||
|
||||
export const useMixnetContext = (): State => {
|
||||
const context = React.useContext<State | undefined>(MixnetContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Please include a `import { MixnetContextProvider } from "./context"` before using this hook');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const MixnetContextProvider: React.FC = ({ children }) => {
|
||||
const [isReady, setReady] = React.useState<boolean>(false);
|
||||
const [address, setAddress] = React.useState<string>();
|
||||
|
||||
const nym = React.useRef<NymMixnetClient | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// on mount of the provider, create the client
|
||||
(async () => {
|
||||
nym.current = await createNymMixnetClient();
|
||||
if (nym.current?.events) {
|
||||
nym.current.events.subscribeToConnected((e) => {
|
||||
setAddress(e.args.address);
|
||||
});
|
||||
}
|
||||
setReady(true);
|
||||
})();
|
||||
|
||||
//
|
||||
}, []);
|
||||
|
||||
const connect = async (config: NymClientConfig) => {
|
||||
if (!nym.current?.client) {
|
||||
console.error('Nym client has not initialised. Please wrap in useEffect on `isReady` prop of this context.');
|
||||
return;
|
||||
}
|
||||
await nym.current.client.start(config);
|
||||
};
|
||||
|
||||
const sendTextMessage = async (args: { message: string; recipient: string }) => {
|
||||
if (!nym.current?.client) {
|
||||
console.error('Nym client has not initialised. Please wrap in useEffect on `isReady` prop of this context.');
|
||||
return;
|
||||
}
|
||||
await nym.current.client.sendMessage(args);
|
||||
};
|
||||
|
||||
const value = React.useMemo<State>(
|
||||
() => ({
|
||||
isReady,
|
||||
events: nym.current?.events,
|
||||
address,
|
||||
connect,
|
||||
sendTextMessage,
|
||||
}),
|
||||
[isReady, nym.current, address],
|
||||
);
|
||||
|
||||
return <MixnetContext.Provider value={value}>{children}</MixnetContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nym Example with React, Typescript, Webpack</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './App';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
@@ -0,0 +1,13 @@
|
||||
import React, { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { App } from '../App';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(() => {
|
||||
render(<App />);
|
||||
});
|
||||
it('should render without exploding', () => {
|
||||
const { container } = render(<App />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */
|
||||
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
|
||||
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
|
||||
|
||||
/**
|
||||
* If you are unfamiliar with Material UI theming, please read the following first:
|
||||
* - https://mui.com/customization/theming/
|
||||
* - https://mui.com/customization/palette/
|
||||
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
|
||||
*
|
||||
* This file adds typings to the theme using Typescript's module augmentation.
|
||||
*
|
||||
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
|
||||
* look at the recommendations from Material UI docs for implementation:
|
||||
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
|
||||
* - https://mui.com/customization/palette/#adding-new-colors
|
||||
*
|
||||
*
|
||||
* IMPORTANT:
|
||||
*
|
||||
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
|
||||
* `type Foo = { ... }` - this is necessary to merge the definitions.
|
||||
*/
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
/**
|
||||
* This augments the definitions of the MUI Theme with the Nym theme, as well as
|
||||
* a partial `ThemeOptions` type used by `createTheme`
|
||||
*
|
||||
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
|
||||
*/
|
||||
interface Theme extends NymTheme {}
|
||||
interface ThemeOptions extends Partial<NymTheme> {}
|
||||
interface Palette extends NymPaletteWithExtensions {}
|
||||
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist",
|
||||
"declaration": false
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
"dist",
|
||||
"**/*.stories.*",
|
||||
"**/*.test.*",
|
||||
"**/*.spec.*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
const path = require('path');
|
||||
const { mergeWithRules } = require('webpack-merge');
|
||||
const { webpackCommon } = require('@nymproject/webpack');
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
test: 'match',
|
||||
use: 'replace',
|
||||
},
|
||||
},
|
||||
})(
|
||||
webpackCommon(__dirname, [
|
||||
{
|
||||
inject: true,
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, 'src/index.html'),
|
||||
chunks: ['bootstrap'],
|
||||
},
|
||||
]),
|
||||
{
|
||||
entry: {
|
||||
bootstrap: path.resolve(__dirname, 'src/bootstrap.ts'),
|
||||
worker: require.resolve('@nymproject/sdk/mixnet/wasm/worker.js'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,67 @@
|
||||
const { mergeWithRules } = require('webpack-merge');
|
||||
const webpack = require('webpack');
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
const ReactRefreshTypeScript = require('react-refresh-typescript');
|
||||
const commonConfig = require('./webpack.common');
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
test: 'match',
|
||||
use: 'replace',
|
||||
},
|
||||
},
|
||||
})(commonConfig, {
|
||||
mode: 'development',
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
getCustomTransformers: () => ({
|
||||
before: [ReactRefreshTypeScript()],
|
||||
}),
|
||||
// `ts-loader` does not work with HMR unless `transpileOnly` is used.
|
||||
// If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative.
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ReactRefreshWebpackPlugin(),
|
||||
|
||||
// this can be included automatically by the dev server, however build mode fails if missing
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
|
||||
target: 'web',
|
||||
|
||||
devServer: {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
historyApiFallback: true,
|
||||
},
|
||||
|
||||
// recommended for faster rebuild
|
||||
optimization: {
|
||||
runtimeChunk: true,
|
||||
removeAvailableModules: false,
|
||||
removeEmptyChunks: false,
|
||||
splitChunks: false,
|
||||
},
|
||||
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
// restart on config change
|
||||
config: ['./webpack.config.js'],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
const { mergeWithRules } = require('webpack-merge');
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const commonConfig = require('./webpack.common');
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
test: 'match',
|
||||
use: 'replace',
|
||||
},
|
||||
},
|
||||
})(commonConfig, {
|
||||
mode: 'production',
|
||||
|
||||
// TODO: no source maps, add back
|
||||
devtool: false,
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer: ['...', new CssMinimizerPlugin()],
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[contenthash].css',
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
pathinfo: false,
|
||||
filename: '[name].[contenthash].js',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
# `@nym-project/nym-client-wasm`
|
||||
|
||||
This package contains a WASM client for using the Nym mixnet.
|
||||
|
||||
## Usage
|
||||
|
||||
You should use `@nym-project/sdk` instead of using this package directory.
|
||||
|
||||
If you want to use it directly, you'll need to configure bundling and interop as suits your project.
|
||||
|
||||
## Build
|
||||
|
||||
The build process is a bit nasty. This is necessary to make the package easier to consume.
|
||||
|
||||
Build the package by running the following from this directory:
|
||||
|
||||
```
|
||||
scripts/build.sh
|
||||
```
|
||||
|
||||
The following files will be copied into this directory, so that it becomes a package that can be used by the `@nym-project/sdk` package:
|
||||
|
||||
```
|
||||
nym_client_wasm.d.ts
|
||||
nym_client_wasm.js
|
||||
nym_client_wasm_bg.wasm
|
||||
nym_client_wasm_bg.wasm.d.ts
|
||||
package.json
|
||||
```
|
||||
|
||||
## Publish to `npm`
|
||||
|
||||
First build the package with the instructions above.
|
||||
|
||||
Publish to `npm` with:
|
||||
|
||||
```
|
||||
npm publish
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
# change to the same directory as the script, then go up one
|
||||
cd "$(dirname "$0")"
|
||||
cd ..
|
||||
|
||||
# clear out any files and suppress missing file errors
|
||||
rm nym_client_wasm* package.json || true
|
||||
|
||||
# let wasm-pack build the files and put them in the output location rather than `./pkg`
|
||||
cd ../../../../clients/webassembly
|
||||
wasm-pack build --scope nymproject --target no-modules --out-dir ../../sdk/typescript/packages/nym-client-wasm
|
||||
|
||||
# clean up some files that come with the build
|
||||
cd ../../sdk/typescript/packages/nym-client-wasm
|
||||
rm README.md LICENSE_APACHE
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@nymproject/sdk",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Nym Technologies SA",
|
||||
"main": "dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/worker.js",
|
||||
"dist/nym_client_wasm.d.ts",
|
||||
"dist/nym_client_wasm.js",
|
||||
"dist/nym_client_wasm_bg.wasm",
|
||||
"dist/nym_client_wasm_bg.wasm.d.ts"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./mixnet/wasm/*": "./dist/mixnet/wasm/*"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "tsc -w",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit true",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"build": "tsc",
|
||||
"postbuild": "cp ../nym-client-wasm/nym_client_wasm* dist/mixnet/wasm"
|
||||
},
|
||||
"dependencies": {
|
||||
"comlink": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.8.4",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-root-import": "^1.0.4",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react-hooks": "^4.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-console
|
||||
export const notImplementedYet = () => console.log('Not implement, coming soon...');
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './coconut';
|
||||
export * from './mixnet';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './wasm';
|
||||
export * from './wasm/types';
|
||||
@@ -0,0 +1,16 @@
|
||||
# Mixnet WASM web worker
|
||||
|
||||
This directory contains code that must be bundled as a web worker, so there are some restrictions:
|
||||
- limited options for importing scripts
|
||||
- `wasm-pack` needs synchronous loading for WASM blobs
|
||||
|
||||
# Features
|
||||
|
||||
- `comlink` provides messaging wrapper between calling thread and web worker thread
|
||||
- [worker.ts](./worker.ts) must be bundled as an entry point for the worker
|
||||
- `URL(..)` types used so the bundler can identify dependent code and bundle correctly
|
||||
|
||||
# TODO
|
||||
|
||||
- add support for using `Transfer` to move binary objects from the main thread to the web worker
|
||||
- wire up handler for receiving binary messages
|
||||
@@ -0,0 +1,123 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import {
|
||||
EventHandlerFn,
|
||||
EventKinds,
|
||||
IWebWorker,
|
||||
IWebWorkerEvents,
|
||||
ConnectedEvent,
|
||||
LoadedEvent,
|
||||
TextMessageReceivedEvent,
|
||||
BinaryMessageReceivedEvent,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Client for the Nym mixnet.
|
||||
*/
|
||||
export interface NymMixnetClient {
|
||||
client: Comlink.Remote<IWebWorker>;
|
||||
events: IWebWorkerEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client to send and receive traffic from the Nym mixnet.
|
||||
*
|
||||
*/
|
||||
export const createNymMixnetClient = async (): Promise<NymMixnetClient> => {
|
||||
// create a web worker that runs the WASM client on another thread and wait until it signals that it is ready
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
const worker = await createWorker();
|
||||
|
||||
// stores the subscriptions for events
|
||||
const subscriptions: {
|
||||
[key: string]: Array<EventHandlerFn<unknown>>;
|
||||
} = {};
|
||||
|
||||
/**
|
||||
* Helper method to get typed subscriptions
|
||||
*/
|
||||
const getSubscriptions = <E>(key: EventKinds): Array<EventHandlerFn<E>> => {
|
||||
if (!subscriptions[key]) {
|
||||
subscriptions[key] = [];
|
||||
}
|
||||
return subscriptions[key] as Array<EventHandlerFn<E>>;
|
||||
};
|
||||
|
||||
// listen to messages from the worker, parse them and let the subscribers handle them, catching any unhandled exceptions
|
||||
worker.addEventListener('message', (msg) => {
|
||||
if (msg.data && msg.data.kind) {
|
||||
const subscribers = subscriptions[msg.data.kind];
|
||||
(subscribers || []).forEach((s) => {
|
||||
try {
|
||||
// let the subscriber handle the message
|
||||
s(msg.data);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Unhandled error in event handler', msg.data, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// manage the subscribers, returning self-unsubscribe methods
|
||||
const events: IWebWorkerEvents = {
|
||||
subscribeToConnected: (handler) => {
|
||||
getSubscriptions<ConnectedEvent>(EventKinds.Connected).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<ConnectedEvent>(EventKinds.Connected).unshift(handler);
|
||||
};
|
||||
},
|
||||
subscribeToLoaded: (handler) => {
|
||||
getSubscriptions<LoadedEvent>(EventKinds.Loaded).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<LoadedEvent>(EventKinds.Loaded).unshift(handler);
|
||||
};
|
||||
},
|
||||
subscribeToTextMessageReceivedEvent: (handler) => {
|
||||
getSubscriptions<TextMessageReceivedEvent>(EventKinds.TextMessageReceived).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<TextMessageReceivedEvent>(EventKinds.TextMessageReceived).unshift(handler);
|
||||
};
|
||||
},
|
||||
subscribeToBinaryMessageReceivedEvent: (handler) => {
|
||||
getSubscriptions<BinaryMessageReceivedEvent>(EventKinds.BinaryMessageReceived).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<BinaryMessageReceivedEvent>(EventKinds.BinaryMessageReceived).unshift(handler);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// let comlink handle interop with the web worker
|
||||
const client = Comlink.wrap<IWebWorker>(worker);
|
||||
|
||||
// pass the client interop and subscription manage back to the caller
|
||||
return {
|
||||
client,
|
||||
events,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Async method to create a web worker that runs the Nym client on another thread. It will only return once the worker
|
||||
* has passed back a `Loaded` event to the calling thread.
|
||||
*
|
||||
* @return The instance of the web worker.
|
||||
*/
|
||||
const createWorker = async () =>
|
||||
new Promise<Worker>((resolve, reject) => {
|
||||
const worker = new Worker(
|
||||
new URL('./worker.js', import.meta.url), // NB: this path is relative to the `dist` directory of this bundle
|
||||
);
|
||||
worker.addEventListener('error', reject);
|
||||
worker.addEventListener(
|
||||
'message',
|
||||
(msg) => {
|
||||
worker.removeEventListener('error', reject);
|
||||
if (msg.data?.kind === EventKinds.Loaded) {
|
||||
resolve(worker);
|
||||
} else {
|
||||
reject(msg);
|
||||
}
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/// <reference path="../../../../nym-client-wasm/nym_client_wasm.d.ts" />
|
||||
|
||||
export type OnMessageFn = (message: string) => void;
|
||||
|
||||
export type OnConnectFn = (address?: string) => void;
|
||||
|
||||
export type EventHandlerFn<E> = (e: E) => void | Promise<void>;
|
||||
|
||||
export type EventHandlerSubscribeFn<E> = (fn: EventHandlerFn<E>) => EventHandlerUnsubscribeFn;
|
||||
|
||||
export type EventHandlerUnsubscribeFn = () => void;
|
||||
|
||||
export interface NymClientConfig {
|
||||
/**
|
||||
* A human-readable id for the client.
|
||||
*/
|
||||
clientId: string;
|
||||
|
||||
/**
|
||||
* The URL of a validator API to query for the network topology.
|
||||
*/
|
||||
validatorApiUrl: string;
|
||||
|
||||
/**
|
||||
* Optional. The identity key of the preferred gateway to connect to.
|
||||
*/
|
||||
preferredGatewayIdentityKey?: string;
|
||||
|
||||
/**
|
||||
* Optional. Settings for the WASM client.
|
||||
*/
|
||||
debug?: wasm_bindgen.Debug;
|
||||
}
|
||||
|
||||
export interface IWebWorker {
|
||||
start: (config: NymClientConfig) => void;
|
||||
selfAddress: () => string | undefined;
|
||||
sendMessage: (args: { message: string; recipient: string }) => void;
|
||||
sendBinaryMessage: (args: { message: Uint8Array; recipient: string }) => void;
|
||||
}
|
||||
|
||||
export enum EventKinds {
|
||||
Loaded = 'Loaded',
|
||||
Connected = 'Connected',
|
||||
TextMessageReceived = 'TextMessageReceived',
|
||||
BinaryMessageReceived = 'BinaryMessageReceived',
|
||||
}
|
||||
|
||||
export interface LoadedEvent {
|
||||
kind: EventKinds.Loaded;
|
||||
args: {
|
||||
loaded: true;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConnectedEvent {
|
||||
kind: EventKinds.Connected;
|
||||
args: {
|
||||
address?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TextMessageReceivedEvent {
|
||||
kind: EventKinds.TextMessageReceived;
|
||||
args: {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BinaryMessageReceivedEvent {
|
||||
kind: EventKinds.TextMessageReceived;
|
||||
args: {
|
||||
message: Uint8Array;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWebWorkerEvents {
|
||||
subscribeToLoaded: EventHandlerSubscribeFn<LoadedEvent>;
|
||||
subscribeToConnected: EventHandlerSubscribeFn<ConnectedEvent>;
|
||||
subscribeToTextMessageReceivedEvent: EventHandlerSubscribeFn<TextMessageReceivedEvent>;
|
||||
subscribeToBinaryMessageReceivedEvent: EventHandlerSubscribeFn<BinaryMessageReceivedEvent>;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/* eslint-disable no-console,no-restricted-globals */
|
||||
/// <reference path="../../../../nym-client-wasm/nym_client_wasm.d.ts" />
|
||||
/**
|
||||
* NB: URL syntax is used so that bundlers like webpack can load this package's code when inside the final bundle
|
||||
* the files from ../../../../nym-client-wasm will be copied into the dist directory of this package, so all import
|
||||
* paths are _relative to the output directory_ of this package (`dist`) - don't get confused!
|
||||
*/
|
||||
import * as Comlink from 'comlink';
|
||||
import type {
|
||||
ConnectedEvent,
|
||||
IWebWorker,
|
||||
LoadedEvent,
|
||||
OnMessageFn,
|
||||
OnConnectFn,
|
||||
TextMessageReceivedEvent,
|
||||
NymClientConfig,
|
||||
} from './types';
|
||||
import { EventKinds } from './types';
|
||||
|
||||
// web workers are only allowed to load external scripts as the load
|
||||
importScripts(new URL('./nym_client_wasm.js', import.meta.url));
|
||||
|
||||
console.log('[Nym WASM client] Starting Nym WASM web worker...');
|
||||
|
||||
// again, construct a URL that can be used by a bundler to repackage the WASM binary
|
||||
const wasmUrl = new URL('./nym_client_wasm_bg.wasm', import.meta.url);
|
||||
|
||||
/**
|
||||
* Helper method to send typed messages.
|
||||
* @param event The strongly typed message to send back to the calling thread.
|
||||
*/
|
||||
const postMessageWithType = <E>(event: E) => self.postMessage(event);
|
||||
|
||||
/**
|
||||
* This class holds the state of the Nym WASM client and provides any interop needed.
|
||||
*/
|
||||
class ClientWrapper {
|
||||
client: wasm_bindgen.NymClient | null = null;
|
||||
|
||||
/**
|
||||
* Creates the WASM client and initialises it.
|
||||
*/
|
||||
init = (config: wasm_bindgen.Config, onConnectHandler: OnConnectFn, onMessageHandler: OnMessageFn) => {
|
||||
this.client = new wasm_bindgen.NymClient(config);
|
||||
this.client.set_on_message(onMessageHandler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the address of this client.
|
||||
*/
|
||||
selfAddress = () => {
|
||||
if (!this.client) {
|
||||
console.error('Client has not been initialised. Please call `init` first.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.client.self_address();
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects to the gateway and starts the client sending traffic.
|
||||
*/
|
||||
start = async () => {
|
||||
if (!this.client) {
|
||||
console.error('Client has not been initialised. Please call `init` first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// this is current limitation of wasm in rust - for async methods you can't take self by reference...
|
||||
// I'm trying to figure out if I can somehow hack my way around it, but for time being you have to re-assign
|
||||
// the object (it's the same one)
|
||||
this.client = await this.client.start();
|
||||
};
|
||||
|
||||
sendMessage = async ({ message, recipient }: { recipient: string; message: string }) => {
|
||||
if (!this.client) {
|
||||
console.error('Client has not been initialised. Please call `init` first.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.client = await this.client.send_message(message, recipient);
|
||||
};
|
||||
|
||||
sendBinaryMessage = async ({ message, recipient }: { recipient: string; message: Uint8Array }) => {
|
||||
if (!this.client) {
|
||||
console.error('Client has not been initialised. Please call `init` first.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.client = await this.client.send_binary_message(message, recipient);
|
||||
};
|
||||
}
|
||||
|
||||
// load WASM binary
|
||||
wasm_bindgen(wasmUrl)
|
||||
.then((importResult) => {
|
||||
// sets up better stack traces in case of in-rust panics
|
||||
importResult.set_panic_hook();
|
||||
|
||||
// this wrapper handles any state that the wasm-pack interop needs, e.g. holding an instance of the instantiated WASM code
|
||||
const wrapper = new ClientWrapper();
|
||||
|
||||
const startHandler = async (config: NymClientConfig) => {
|
||||
// fetch the gateway details (randomly chosen if no preferred gateway is set)
|
||||
const gatewayEndpoint = await wasm_bindgen.get_gateway(
|
||||
config.validatorApiUrl,
|
||||
config.preferredGatewayIdentityKey,
|
||||
);
|
||||
|
||||
// create the client, passing handlers for events
|
||||
wrapper.init(
|
||||
new wasm_bindgen.Config(
|
||||
config.clientId,
|
||||
config.validatorApiUrl,
|
||||
gatewayEndpoint,
|
||||
config.debug || wasm_bindgen.default_debug(),
|
||||
),
|
||||
() => {
|
||||
console.log();
|
||||
},
|
||||
(message) => {
|
||||
postMessageWithType<TextMessageReceivedEvent>({ kind: EventKinds.TextMessageReceived, args: { message } });
|
||||
},
|
||||
);
|
||||
|
||||
// start the client sending traffic
|
||||
await wrapper.start();
|
||||
|
||||
// get the address
|
||||
const address = wrapper.selfAddress();
|
||||
postMessageWithType<ConnectedEvent>({ kind: EventKinds.Connected, args: { address } });
|
||||
};
|
||||
|
||||
// implement the public logic of this web worker (message exchange between the worker and caller is done by https://www.npmjs.com/package/comlink)
|
||||
const webWorker: IWebWorker = {
|
||||
start(config) {
|
||||
console.log('[Nym WASM client] Starting...', { config });
|
||||
startHandler(config).catch((e) => console.error('[Nym WASM client] Failed to start', e));
|
||||
},
|
||||
selfAddress() {
|
||||
return wrapper.selfAddress();
|
||||
},
|
||||
sendMessage(args) {
|
||||
wrapper.sendMessage(args).catch((e) => console.error('[Nym WASM client] Failed to send message', e));
|
||||
},
|
||||
sendBinaryMessage(args) {
|
||||
wrapper.sendBinaryMessage(args).catch((e) => console.error('[Nym WASM client] Failed to send message', e));
|
||||
},
|
||||
};
|
||||
|
||||
// start comlink listening for messages and handle them above
|
||||
Comlink.expose(webWorker);
|
||||
|
||||
// notify any listeners that the web worker has loaded - HOWEVER, the client has not been created and connected,
|
||||
// listen for EventKinds.Connected before sending messages
|
||||
postMessageWithType<LoadedEvent>({ kind: EventKinds.Loaded, args: { loaded: true } });
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('[Worker thread] failed to start', e);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2021",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext",
|
||||
"webworker"
|
||||
],
|
||||
"outDir": "./dist/",
|
||||
"module": "ES2020",
|
||||
"target": "es2021",
|
||||
"allowJs": false,
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"declaration": true,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"jest.config.js",
|
||||
"webpack.config.js",
|
||||
"webpack.prod.js",
|
||||
"webpack.common.js",
|
||||
"node_modules",
|
||||
"**/node_modules",
|
||||
"dist",
|
||||
"**/dist",
|
||||
"scripts",
|
||||
"jest",
|
||||
"__tests__",
|
||||
"**/__tests__",
|
||||
"__jest__",
|
||||
"**/__jest__",
|
||||
"config/*",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../ts-packages/tsconfig.json"
|
||||
}
|
||||
@@ -7924,6 +7924,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
comlink@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.3.1.tgz#0c6b9d69bcd293715c907c33fe8fc45aecad13c5"
|
||||
integrity sha512-+YbhUdNrpBZggBAHWcgQMLPLH1KDF3wJpeqrCKieWQ8RL7atmgsgTQko1XEBK6PsecfopWNntopJ+ByYG1lRaA==
|
||||
|
||||
comma-separated-tokens@^1.0.0:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
|
||||
@@ -18669,6 +18674,11 @@ typescript@^4.6.2:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4"
|
||||
integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==
|
||||
|
||||
typescript@^4.8.4:
|
||||
version "4.8.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
|
||||
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.15.2"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.2.tgz#1ed2c976f448063b1f87adb68c741be79959f951"
|
||||
|
||||
Reference in New Issue
Block a user