Feature/node tester package (#3634)

* create node tester package dir

* start building node tester package

* refactor code + build updates

* fix up types

* add more methods and fix up types

* use node tester sdk inside wallet

* fix frontend state

* Use Node 18 instead of 16

* Fix up dependencies and yarn workspace

* Fix lint error

* Try to fix up linting error

* Remove explorer linting and move it to the existing action

* Add wasm-pack build to linting GH Action

* change lerna to use workspaces and fix linting errors

* Fix up node versions in GitHub Actions and add wasm-pack

* fix build:lint target in sdk

* exclude all worker.js from eslint for sdk

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
This commit is contained in:
Fouad
2023-07-03 16:53:39 +01:00
committed by GitHub
parent 7a1a7c003e
commit fcc5398aab
68 changed files with 1439 additions and 883 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- name: install npm
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
+7 -1
View File
@@ -16,9 +16,15 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build
run: yarn && yarn build && yarn build:ci
- name: Deploy branch to CI www (storybook)
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install Yarn
run: npm install -g yarn
- run: yarn
@@ -1,24 +0,0 @@
name: Linting for Network Explorer (eslint/prettier)
on:
pull_request:
paths:
- 'explorer/**'
defaults:
run:
working-directory: explorer
jobs:
build:
runs-on: custom-runner-linux
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Setup yarn
run: npm install -g yarn
- name: Run ESLint
# GitHub should automatically annotate the PR
run: yarn && yarn lint
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Setup yarn
run: npm install -g yarn
continue-on-error: true
+1 -1
View File
@@ -152,7 +152,7 @@ jobs:
uses: actions/setup-node@v3
if: env.WORKFLOW_CONCLUSION == 'failure'
with:
node-version: 16
node-version: 18
- name: Matrix - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
+1 -1
View File
@@ -167,7 +167,7 @@ jobs:
uses: actions/setup-node@v3
if: env.WORKFLOW_CONCLUSION == 'failure'
with:
node-version: 16
node-version: 18
- name: Matrix - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
+1 -1
View File
@@ -167,7 +167,7 @@ jobs:
uses: actions/setup-node@v3
if: env.WORKFLOW_CONCLUSION == 'failure'
with:
node-version: 16
node-version: 18
- name: Matrix - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
@@ -23,7 +23,7 @@ jobs:
- name: Node v16
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
@@ -29,7 +29,7 @@ jobs:
- name: Node v16
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
@@ -42,7 +42,7 @@ jobs:
- name: Node v16
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
@@ -23,7 +23,7 @@ jobs:
- name: Node v16
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
@@ -28,7 +28,7 @@ jobs:
- name: Node v16
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
@@ -42,7 +42,7 @@ jobs:
- name: Node v16
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
+1 -11
View File
@@ -16,19 +16,9 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
working-directory: clients/webassembly
- name: Build WASM
run: wasm-pack build
working-directory: clients/webassembly
- name: Build dependencies
run: yarn && yarn build
- name: Build storybook
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
- name: Node v16
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install yarn for building application
run: yarn install
+1 -1
View File
@@ -1 +1 @@
16
18
+3 -5
View File
@@ -11,6 +11,7 @@ on:
- 'nym-connect/mobile/package.json'
- 'nym-wallet/src/**'
- 'nym-wallet/package.json'
- 'explorer/**'
pull_request:
paths:
- 'ts-packages/**'
@@ -21,6 +22,7 @@ on:
- 'nym-connect/mobile/package.json'
- 'nym-wallet/src/**'
- 'nym-wallet/package.json'
- 'explorer/**'
jobs:
build:
@@ -33,7 +35,7 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust stable
@@ -42,10 +44,6 @@ jobs:
toolchain: stable
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
working-directory: clients/webassembly
- name: Build WASM
run: wasm-pack build
working-directory: clients/webassembly
- name: Install
run: yarn
- name: Build packages
+1 -1
View File
@@ -1 +1 @@
16
18
+1 -1
View File
@@ -1 +1 @@
16
18
+2 -8
View File
@@ -1,10 +1,4 @@
{
"packages": [
"ts-packages/*",
"nym-wallet",
"nym-connect/**",
"sdk/typescript/examples/docs",
"sdk/typescript/packages/**"
],
"version": "0.0.0"
"version": "0.0.0",
"useWorkspaces": true
}
+1 -1
View File
@@ -1 +1 @@
14
18
+1 -1
View File
@@ -1 +1 @@
14
18
+1 -1
View File
@@ -1 +1 @@
16
18
+1 -1
View File
@@ -31,6 +31,7 @@
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
"@nymproject/sdk": "1",
"@storybook/react": "^6.5.15",
"@tauri-apps/api": "^1.2.0",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
@@ -41,7 +42,6 @@
"lodash": "^4.17.21",
"notistack": "^2.0.3",
"npm-run-all": "^4.1.5",
"nym-client-wasm": "1.1.1",
"qrcode.react": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -1,6 +1,6 @@
import React from 'react';
import { TestStatus } from '@nymproject/sdk';
import { Divider, Typography } from '@mui/material';
import { TestStatus } from 'src/pages/bonding/node-settings/node-test/types';
import { ResultsCard, ResultsCardDetail } from './ResultsCard';
export const Packets = ({
@@ -1,8 +1,8 @@
import React, { useEffect } from 'react';
import { Card, CardContent, CardHeader, Dialog, Divider } from '@mui/material';
import { NymLogo } from '@nymproject/react/logo/NymLogo';
import { ResultsCardDetail } from './ResultsCard';
import { sleep } from 'src/utils/sleep';
import { ResultsCardDetail } from './ResultsCard';
export const PrintResults = ({
packetsSent,
@@ -30,7 +30,7 @@ export const PrintResults = ({
asyncPrint();
window.addEventListener('afterprint', OnPrintRequestComplete);
() => window.removeEventListener('afterprint', OnPrintRequestComplete);
return () => window.removeEventListener('afterprint', OnPrintRequestComplete);
}, []);
return (
@@ -1,7 +1,6 @@
import React from 'react';
import { TestStatus } from '@nymproject/sdk';
import { Grid } from '@mui/material';
import { TestStatus } from 'src/pages/bonding/node-settings/node-test/types';
import { Packets } from './Packets';
import { NodeScore } from './NodeScore';
import { Overview } from './Overview';
@@ -1,55 +0,0 @@
import { WasmGateway, WasmMixNode, WasmNymTopology } from 'nym-client-wasm';
export const createDummyTopology = () => {
const l1Mixnode = new WasmMixNode(
1,
'n1fzv4jc7fanl9s0qj02ge2ezk3kts545kjtek47',
'178.79.143.65',
1789,
'4Yr4qmEHd9sgsuQ83191FR2hD88RfsbMmB4tzhhZWriz',
'8ndjk5oZ6HxUZNScLJJ7hk39XtUqGexdKgW7hSX6kpWG',
1,
'1.10.0',
);
const l2Mixnode = new WasmMixNode(
2,
'n1z93z44vf8ssvdhujjvxcj4rd5e3lz0l60wdk70',
'109.74.197.180',
1789,
'7sVjiMrPYZrDWRujku9QLxgE8noT7NTgBAqizCsu7AoK',
'GepXwRnKZDd8x2nBWAajGGBVvF3mrpVMQBkgfrGuqRCN',
2,
'1.10.0',
);
const l3Mixnode = new WasmMixNode(
3,
'n1ptg680vnmef2cd8l0s9uyc4f0hgf3x8sed6w77',
'176.58.101.80',
1789,
'FoM5Mx9Pxk1g3zEqkS3APgtBeTtTo3M8k7Yu4bV6kK1R',
'DeYjrDC2AcQRVFshiKnbUo6bRvPyZ33QGYR2DLeFJ9qD',
3,
'1.10.0',
);
const gateway = new WasmGateway(
'n16evnn8glr0sham3matj8rg2s24m6x56ayk87ts',
'85.159.212.96',
1789,
9000,
'336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9',
'BtYjoWihiuFihGKQypmpSspbhmWDPxzqeTVSd8ciCpWL',
'1.10.1',
);
const mixnodes = new Map();
mixnodes.set(1, [l1Mixnode]);
mixnodes.set(2, [l2Mixnode]);
mixnodes.set(3, [l3Mixnode]);
const gateways = [gateway];
return new WasmNymTopology(mixnodes, gateways);
};
@@ -1,19 +1,20 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Box, Button } from '@mui/material';
import { Download } from '@mui/icons-material';
import { NodeTestResultResponse, NodeTester, TestStatus, createNodeTesterClient } from '@nymproject/sdk';
import { format } from 'date-fns';
import { AppContext, useBondingContext } from 'src/context';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { Results } from 'src/components/TestNode/Results';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { PrintResults } from 'src/components/TestNode/PrintResults';
import { Download } from '@mui/icons-material';
import { format } from 'date-fns';
import { NodeTestEvent, NodeTestResult, TestStatus } from './types';
import { MAINNET_VALIDATOR_URL, QA_VALIDATOR_URL } from 'src/constants';
export const NodeTestPage = () => {
const [nodeTestWorker, setNodeTestWorker] = useState<Worker>();
const [nodeTestClient, setNodeTestClient] = useState<NodeTester>();
const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const [results, setResults] = useState<NodeTestResult>();
const [results, setResults] = useState<NodeTestResultResponse>();
const [printResults, setPrintResults] = useState(false);
const [testDate, setTestDate] = useState<string>();
@@ -23,15 +24,6 @@ export const NodeTestPage = () => {
const { network } = useContext(AppContext);
const { bondedNode } = useBondingContext();
const loadWorker = () => {
try {
const worker: Worker = new Worker(new URL('./worker.ts', import.meta.url));
setNodeTestWorker(worker);
} catch (e) {
setError('Error loading worker');
}
};
const handleTestTimeout = () => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
@@ -43,56 +35,52 @@ export const NodeTestPage = () => {
}, 15000);
};
const handleWorkerMessages = (ev: MessageEvent<NodeTestEvent>) => {
const eventKind = ev.data.kind;
if (eventKind === 'Error') {
setError(ev.data.args.message);
testStateRef.current = 'Stopped';
}
if (eventKind === 'DisplayTesterResults') {
setResults(ev.data.args.result);
testStateRef.current = 'Complete';
}
setIsLoading(false);
};
const handleTestNode = async () => {
setError(undefined);
setResults(undefined);
setIsLoading(true);
setTestDate(format(new Date(), 'dd/MM/yyyy HH:mm'));
if (nodeTestWorker) {
if (nodeTestClient && bondedNode) {
setResults(undefined);
setTestDate(format(new Date(), 'dd/MM/yyyy HH:mm'));
setIsLoading(true);
setError(undefined);
testStateRef.current = 'Running';
nodeTestWorker.postMessage({
kind: 'TestPacket',
args: {
mixnodeIdentity: bondedNode?.identityKey,
network,
},
} as NodeTestEvent);
handleTestTimeout();
try {
const result = await nodeTestClient.tester.startTest(bondedNode.identityKey);
setResults(result);
testStateRef.current = 'Complete';
} catch (e) {
setError('Node test failed, please try again');
testStateRef.current = 'Stopped';
console.log(e);
} finally {
setIsLoading(false);
}
}
};
useEffect(() => {
loadWorker();
return () => {
if (nodeTestWorker) {
nodeTestWorker.terminate();
}
};
const loadNodeTestClient = useCallback(async () => {
try {
const nodeTesterId = new Date().toISOString(); // make a new tester id for each session
const validator = network === 'MAINNET' ? MAINNET_VALIDATOR_URL : QA_VALIDATOR_URL;
const client = await createNodeTesterClient();
await client.tester.init(validator, nodeTesterId);
setNodeTestClient(client);
} catch (e) {
console.log(e);
setError('Failed to load node tester client, please try again');
}
}, []);
useEffect(() => {
if (nodeTestWorker) {
nodeTestWorker.addEventListener('message', (e) => handleWorkerMessages(e));
}
loadNodeTestClient();
return () => nodeTestWorker?.removeEventListener('message', handleWorkerMessages);
}, [nodeTestWorker]);
return () => {
clearTimeout(timerRef.current);
if (nodeTestClient) {
nodeTestClient.tester.disconnectFromGateway();
nodeTestClient.terminate();
}
};
}, [loadNodeTestClient]);
return (
<Box p={4}>
@@ -1,38 +0,0 @@
import { Network } from 'src/types';
export type NodeTestResult = {
score: number;
sentPackets: number;
receivedPackets: number;
receivedAcks: number;
duplicatePackets: number;
duplicateAcks: number;
};
type Error = {
kind: 'Error';
args: { message: string };
};
type WorkerLoaded = {
kind: 'WorkerLoaded';
};
type DisplayTesterResults = {
kind: 'DisplayTesterResults';
args: {
result: NodeTestResult;
};
};
type TestPacket = {
kind: 'TestPacket';
args: {
mixnodeIdentity: string;
network: Network;
};
};
export type TestStatus = 'Stopped' | 'Running' | 'Complete';
export type NodeTestEvent = Error | DisplayTesterResults | TestPacket | WorkerLoaded;
@@ -1,102 +0,0 @@
// Copyright 2020-2023 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-disable no-restricted-globals */
import { NymNodeTester, set_panic_hook, current_network_topology, NodeTestResult } from 'nym-client-wasm';
import { Network } from 'src/types';
import { NodeTestEvent } from './types';
import { MAINNET_VALIDATOR_URL, QA_VALIDATOR_URL } from 'src/constants';
console.log('Initializing worker');
const postMessage = (data: NodeTestEvent) => self.postMessage(data);
postMessage({
kind: 'WorkerLoaded',
});
const printAndDisplayTestResult = (result: NodeTestResult) => {
result.log_details();
postMessage({
kind: 'DisplayTesterResults',
args: {
result: {
score: result.score(),
sentPackets: result.sent_packets,
receivedPackets: result.received_packets,
receivedAcks: result.received_acks,
duplicatePackets: result.duplicate_packets,
duplicateAcks: result.duplicate_acks,
},
},
});
};
const buildTester = async (network: Network) => {
const validator = network === 'QA' ? QA_VALIDATOR_URL : MAINNET_VALIDATOR_URL;
const topology = await current_network_topology(validator);
const nodeTester = await new NymNodeTester(topology, network);
return nodeTester;
};
async function testNode() {
self.onmessage = async (event: MessageEvent<NodeTestEvent>) => {
const eventKind = event.data.kind;
switch (eventKind) {
case 'TestPacket': {
const { mixnodeIdentity, network } = event.data.args;
const nodeTester = await buildTester(network);
try {
console.log(`Testing mixnode identity: ${mixnodeIdentity}, on network: ${network}.`);
const result = await nodeTester.test_node(mixnodeIdentity);
printAndDisplayTestResult(result);
await nodeTester.disconnect_from_gateway();
console.log('Disconnected from gateway');
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Node test error';
console.log(errorMessage);
nodeTester.disconnect_from_gateway();
postMessage({
kind: 'Error',
args: { message: errorMessage },
});
}
break;
}
default:
return undefined;
}
return undefined;
};
}
async function main() {
// sets up better stack traces in case of in-rust panics
set_panic_hook();
// run test on simplified and dedicated tester:
await testNode();
}
// Let's get started!
main();
+6 -4
View File
@@ -10,14 +10,14 @@
"nym-connect/**",
"explorer",
"types",
"clients/validator",
"clients/webassembly/**"
"clients/validator"
],
"scripts": {
"clean": "lerna run clean",
"build": "run-s build:types build:packages",
"build:types": "lerna run --scope @nymproject/types build --stream",
"build:packages": "run-s build:packages:theme build:packages:react",
"build:packages": "run-s build:packages:theme build:packages:react build:packages:sdk",
"build:packages:sdk": "lerna run --scope @nymproject/sdk build:lint",
"build:packages:theme": "lerna run --scope @nymproject/mui-theme build",
"build:packages:react": "lerna run --scope @nymproject/react build",
"build:react-example": "lerna run --scope @nymproject/react-webpack-with-theme-example build --stream",
@@ -28,7 +28,9 @@
"lint:fix": "lerna run lint:fix --stream",
"tsc": "lerna run tsc --stream",
"types:lint:fix": "lerna run lint:fix --scope @nymproject/types --scope @nymproject/nym-wallet-app",
"audit:fix": "npm_config_yes=true npx yarn-audit-fix -- --dry-run"
"audit:fix": "npm_config_yes=true npx yarn-audit-fix -- --dry-run",
"preinstall": "yarn install:copy-placeholders",
"install:copy-placeholders": "cp scripts/build/yarn/wasm-placeholder/package.json sdk/typescript/packages/nym-client-wasm"
},
"devDependencies": {
"lerna": "^6.6.2",
@@ -0,0 +1,5 @@
{
"name": "@nymproject/nym-client-wasm",
"version": "1.0.0",
"sideEffects": false
}
+1 -1
View File
@@ -27,7 +27,7 @@ module.exports = {
'plugin:jest/recommended',
'plugin:jest/style',
],
ignorePatterns: ['dist/**/*', 'dist', 'node_modules', '**/wasm/worker.js'],
ignorePatterns: ['dist/**/*', 'dist', 'node_modules', '**/worker.js'],
rules: {
'jest/prefer-strict-equal': 'error',
'jest/prefer-to-have-length': 'warn',
@@ -0,0 +1,3 @@
{
"presets": ["@babel/env", "@babel/react"]
}
@@ -0,0 +1,14 @@
# HTML Nyn Node Test
This is an example of using the Nym Mixnet node tester.
You can use this example as a seed for a new project.
## Running the example
Try out the node tester app by running:
```
npm install
npm start
```
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
@@ -0,0 +1,61 @@
{
"name": "@nymproject/sdk-example-node-tester-plain-html",
"description": "An example project that uses WASM node tester and plain HTML",
"version": "1.0.0",
"license": "Apache-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-typescript": "^7.15.0",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.3.0",
"babel-plugin-root-import": "^5.1.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.7.3",
"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",
"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",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"style-loader": "^3.3.1",
"thread-loader": "^3.0.4",
"ts-jest": "^27.0.5",
"ts-loader": "^9.4.2",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.5.0",
"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,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nym WebAssembly Demo</title>
</head>
<body>
<p>
<label>Mixnode Id: </label><input size="85" type="text" id="mixnodeId" value="">
</p>
<p>
<button id="send-button">Test</button>
<button id="disconnect-button">Disconnect from Gateway</button>
<button id="reconnect-button">Reconnect to Gateway</button>
<button id="terminate-button">Terminate worker</button>
</p>
<p>Test a mixnode by entering the mixnode id above and click the Test button</p>
<hr>
<p>
<div id="output"></div>
</p>
</body>
</html>
@@ -0,0 +1,116 @@
import { createNodeTesterClient, NodeTester } from '@nymproject/sdk';
let nodeTester: NodeTester | null = null;
/**
* Display messages that have been sent up the websocket. Colours them blue.
*
* @param {string} message
*/
function displayOutput(message: string, color?: string) {
const timestamp = new Date().toISOString().substr(11, 12);
const sendDiv = document.createElement('div');
const paragraph = document.createElement('p');
paragraph.setAttribute('style', `color: ${color || 'blue'}`);
const paragraphContent = document.createTextNode(`${timestamp} >>> ${message}`);
paragraph.appendChild(paragraphContent);
sendDiv.appendChild(paragraph);
document.getElementById('output')?.appendChild(sendDiv);
}
/**
* The main entry point
*/
async function main() {
nodeTester = await createNodeTesterClient();
// add node tester to the Window globally, so that it can be used from the dev tools console
(window as any).nodeTester = nodeTester;
if (!nodeTester) {
console.error('Oh no! Could not the node test');
return;
}
const nymApiUrl = 'https://validator.nymtech.net/api';
const nodeTesterId = new Date().toISOString(); // make a new tester id for each session
await nodeTester.tester.init(nymApiUrl, nodeTesterId);
const mixnodes = await (await fetch(`${nymApiUrl}/v1/mixnodes/active`)).json();
const exampleMixnodeIdentityKey = mixnodes[0].bond_information.mix_node.identity_key;
const sendButton: HTMLButtonElement = document.querySelector('#send-button') as HTMLButtonElement;
const reconnectButton: HTMLButtonElement = document.querySelector('#reconnect-button') as HTMLButtonElement;
const disconnectButton: HTMLButtonElement = document.querySelector('#disconnect-button') as HTMLButtonElement;
const terminateButton: HTMLButtonElement = document.querySelector('#terminate-button') as HTMLButtonElement;
const mixnodeIdInput = document.getElementById('mixnodeId') as HTMLFormElement;
mixnodeIdInput.value = exampleMixnodeIdentityKey;
reconnectButton.onclick = async function () {
try {
await nodeTester?.tester.reconnectToGateway();
} catch (e: any) {
console.error('Error', e);
displayOutput(`ERROR: ${e.message}`, 'red');
}
};
disconnectButton.onclick = async function () {
try {
await nodeTester?.tester.disconnectFromGateway();
} catch (e: any) {
console.error('Error', e);
displayOutput(`ERROR: ${e.message}`, 'red');
}
};
terminateButton.onclick = async function () {
try {
await nodeTester?.terminate();
} catch (e: any) {
console.error('Error', e);
displayOutput(`ERROR: ${e.message}`, 'red');
}
};
if (sendButton) {
sendButton.onclick = async function () {
const mixnodeId = mixnodeIdInput.value;
if (!nodeTester) {
displayOutput('ERROR: The node tester is not defined');
console.error('The node tester is not defined');
return;
}
if (!mixnodeId) {
displayOutput('ERROR: No mix id specified');
console.error('No mix id specified');
return;
}
if (nodeTester && mixnodeId) {
displayOutput('Starting test...');
try {
const response = await nodeTester.tester.startTest(mixnodeId);
displayOutput('Done!');
if (response) {
displayOutput(JSON.stringify(response, null, 2), 'green');
}
} catch (e: any) {
console.error('Error', e);
displayOutput(`ERROR: ${e.message}`, 'red');
}
}
};
}
}
// wait for the html to load
window.addEventListener('DOMContentLoaded', () => {
// let's do this!
main();
});
@@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "build", "dist"]
}
@@ -0,0 +1,10 @@
{
"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,30 @@
const path = require('path');
const { mergeWithRules } = require('webpack-merge');
const { webpackCommon } = require('../../.webpack/webpack.base');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(
webpackCommon(__dirname, [
{
inject: true,
filename: 'index.html',
template: path.resolve(__dirname, 'src/index.html'),
chunks: ['index'],
},
]),
{
entry: {
index: path.resolve(__dirname, 'src/index.ts'),
},
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',
},
});
@@ -1,20 +1,10 @@
{
"extends": "../tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist",
"declaration": false
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"build",
"dist",
"**/*.stories.*",
"**/*.test.*",
"**/*.spec.*"
]
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "build", "dist", "**/*.stories.*", "**/*.test.*", "**/*.spec.*"]
}
+1
View File
@@ -1 +1,2 @@
src/mixnet/wasm/worker.js
src/mixnet/node-tester/worker.js
+3 -1
View File
@@ -26,10 +26,12 @@
"build:only-this": "scripts/build-prod.sh",
"prebuild:dev": "yarn build:dependencies",
"build:dev": "yarn build:dev:only-this",
"build:dev:only-this": "scripts/build.sh"
"build:dev:only-this": "scripts/build.sh",
"build:lint": "run-s build:dependencies:nym-client-wasm build:dev:only-this"
},
"dependencies": {
"@npmcli/node-gyp": "^3.0.0",
"@nymproject/nym-client-wasm": "1",
"comlink": "^4.3.1",
"lerna": "^6.6.2",
"node-gyp": "^9.3.1"
@@ -3,9 +3,7 @@ import resolve from '@rollup/plugin-node-resolve';
import { wasm } from '@rollup/plugin-wasm';
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
const extensions = [
'.js', '.jsx', '.ts', '.tsx',
];
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
export default {
input: 'src/index.ts',
@@ -17,6 +15,9 @@ export default {
webWorkerLoader({ targetPlatform: 'browser', inline: true }),
resolve({ extensions }),
wasm({ maxFileSize: 10000000, targetEnv: 'browser' }),
typescript({ compilerOptions: { outDir: 'dist/cjs', target: 'es5' }, exclude: 'mixnet/wasm/worker.ts', }),
typescript({
compilerOptions: { outDir: 'dist/cjs', target: 'es5' },
exclude: ['mixnet/wasm/worker.ts', 'mixnet/node-tester/worker.ts'],
}),
],
};
@@ -2,9 +2,7 @@ import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
const extensions = [
'.js', '.jsx', '.ts', '.tsx',
];
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
export default {
input: 'src/index.ts',
@@ -15,6 +13,9 @@ export default {
plugins: [
webWorkerLoader({ targetPlatform: 'browser', inline: true }),
resolve({ extensions }),
typescript({ exclude: 'mixnet/wasm/worker.ts', compilerOptions: { outDir: 'dist/esm' } }),
typescript({
exclude: ['mixnet/wasm/worker.ts', 'mixnet/node-tester/worker.ts'],
compilerOptions: { outDir: 'dist/esm' },
}),
],
};
@@ -0,0 +1,26 @@
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import { wasm } from '@rollup/plugin-wasm';
import replace from '@rollup/plugin-replace';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
export default {
input: 'src/mixnet/node-tester/worker.ts',
output: {
dir: 'dist',
format: 'cjs',
},
plugins: [
resolve({ extensions }),
// this is some nasty monkey patching that removes the WASM URL (because it is handled by the `wasm` plugin)
replace({
values: { "input = new URL('nym_client_wasm_bg.wasm', import.meta.url);": 'input = undefined;' },
delimiters: ['', ''],
preventAssignment: true,
}),
// force the wasm plugin to embed the wasm bundle - this means no downstream bundlers have to worry about handling it
wasm({ maxFileSize: 10000000, targetEnv: 'browser' }),
typescript({ compilerOptions: { declaration: false, target: 'es5' } }),
],
};
+14 -1
View File
@@ -7,7 +7,7 @@ set -o pipefail
rm -rf dist || true
#-------------------------------------------------------
# WEB WORKER
# WEB WORKER (WASM client)
#-------------------------------------------------------
# The web worker needs to be bundled because the WASM bundle needs to be loaded synchronously and all dependencies
# must be included in the worker script (because it is not loaded as an ES Module)
@@ -19,6 +19,19 @@ rollup -c rollup-worker.config.mjs
rm -f src/mixnet/wasm/worker.js
mv dist/worker.js src/mixnet/wasm/worker.js
#-------------------------------------------------------
# WEB WORKER (Node tester)
#-------------------------------------------------------
# The web worker needs to be bundled because the WASM bundle needs to be loaded synchronously and all dependencies
# must be included in the worker script (because it is not loaded as an ES Module)
# build the worker
rollup -c rollup-node-tester-worker.config.mjs
# move it next to the Typescript `mixnet/node-tester/index.ts` so it can be inlined by rollup
rm -f src/mixnet/node-tester/worker.js
mv dist/worker.js src/mixnet/node-tester/worker.js
#-------------------------------------------------------
# ESM
#-------------------------------------------------------
@@ -1,2 +1,4 @@
export * from './wasm';
export * from './wasm/types';
export * from './node-tester';
export * from './node-tester/types';
@@ -0,0 +1,5 @@
const QA_VALIDATOR_URL = 'https://qa-nym-api.qa.nymte.ch/api';
const QWERTY_VALIDATOR_URL = 'https://qwerty-validator-api.qa.nymte.ch/api';
const MAINNET_VALIDATOR_URL = 'https://validator.nymtech.net/api/';
export { QA_VALIDATOR_URL, QWERTY_VALIDATOR_URL, MAINNET_VALIDATOR_URL };
@@ -0,0 +1,50 @@
import InlineWasmWebWorker from 'web-worker:./worker';
import * as Comlink from 'comlink';
import { INodeTesterWorkerAsync, NodeTester, NodeTesterEventKinds } from './types';
/**
* Client for the Nym node tester.
*/
export const createNodeTesterClient = async (): Promise<NodeTester> => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const worker = await createWorker();
// let comlink handle interop with the web worker
const tester = Comlink.wrap<INodeTesterWorkerAsync>(worker);
// expose the method to terminate the worker
const terminate = async () => {
worker.terminate();
};
return { tester, terminate };
};
/**
* Async method to create a web worker that runs the Nym node tester 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) => {
// rollup will inline the built worker script, so that when the SDK is used in
// other projects, they will not need to mess around trying to bundle it
// however, it will make this SDK bundle bigger because of Base64 inline data
const worker = new InlineWasmWebWorker();
worker.addEventListener('error', reject);
worker.addEventListener(
'message',
(msg) => {
worker.removeEventListener('error', reject);
if (msg.data?.kind === NodeTesterEventKinds.Loaded) {
resolve(worker);
} else {
reject(msg);
}
},
{ once: true },
);
});
@@ -0,0 +1,65 @@
export interface INodeTesterWorkerAsync {
init: (validatorUrl: string, nodeTesterId?: string) => Promise<void>;
reconnectToGateway: () => Promise<void>;
disconnectFromGateway: () => Promise<void>;
startTest: (mixnodeIdentityKey: string) => Promise<NodeTestResultResponse | undefined>;
}
export interface INodeTesterWorkerDisposableAsync {
terminate: () => Promise<void>;
}
export interface NodeTester extends INodeTesterWorkerDisposableAsync {
tester: INodeTesterWorkerAsync;
}
export enum NodeTesterEventKinds {
Loaded = 'Loaded',
Connected = 'Connected',
}
export interface NodeTesterLoadedEvent {
kind: NodeTesterEventKinds.Loaded;
args: {
loaded: true;
};
}
export type Network = 'QA' | 'SANDBOX' | 'MAINNET';
export type NodeTestResultResponse = {
score: number;
sentPackets: number;
receivedPackets: number;
receivedAcks: number;
duplicatePackets: number;
duplicateAcks: number;
};
export type Error = {
kind: 'Error';
args: { message: string };
};
export type WorkerLoaded = {
kind: 'WorkerLoaded';
};
export type DisplayTesterResults = {
kind: 'DisplayTesterResults';
args: {
result: NodeTestResultResponse;
};
};
export type TestPacket = {
kind: 'TestPacket';
args: {
mixnodeIdentity: string;
network: Network;
};
};
export type TestStatus = 'Stopped' | 'Running' | 'Complete';
export type NodeTestEvent = Error | DisplayTesterResults | TestPacket | WorkerLoaded;
@@ -0,0 +1,105 @@
// Copyright 2020-2023 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Comlink from 'comlink';
//
// Rollup will replace wasmBytes with a function that loads the WASM bundle from a base64 string embedded in the output.
//
// Doing it this way, saves having to support a large variety of bundlers and their quirks.
//
// @ts-ignore
// eslint-disable-next-line import/no-extraneous-dependencies
import wasmBytes from '@nymproject/nym-client-wasm/nym_client_wasm_bg.wasm';
/* eslint-disable no-restricted-globals */
import init, { NymNodeTester, current_network_topology, NodeTestResult } from '@nymproject/nym-client-wasm';
import type { INodeTesterWorkerAsync, NodeTesterLoadedEvent } from './types';
import { NodeTesterEventKinds } from './types';
/**
* Helper method to send typed messages.
* @param event The strongly typed message to send back to the calling thread.
*/
// eslint-disable-next-line no-restricted-globals
const postMessageWithType = <E>(event: E) => self.postMessage(event);
console.log('[Nym WASM client] Starting Nym WASM web worker...');
const buildTester = async (validatorUrl: string, nodeTesterId?: string): Promise<NymNodeTester> => {
const topology = await current_network_topology(validatorUrl);
return new NymNodeTester(topology, nodeTesterId);
};
async function main() {
const importResult = await init(wasmBytes());
importResult.set_panic_hook();
let nodeTester: NymNodeTester | null = null;
const webWorker: INodeTesterWorkerAsync = {
async init(validatorUrl: string, nodeTesterId?: string) {
nodeTester = await buildTester(validatorUrl, nodeTesterId);
},
async reconnectToGateway() {
if (!nodeTester) {
throw Error('Please run init first');
}
await nodeTester.reconnect_to_gateway();
},
async disconnectFromGateway() {
if (!nodeTester) {
throw Error('Please run init first');
}
await nodeTester.disconnect_from_gateway();
},
async startTest(mixnodeIdentityKey: string) {
if (!nodeTester) {
throw Error('Please run init first');
}
console.log(`Testing mixnode with identity key = ${mixnodeIdentityKey}`);
// TODO: fix typing in Rust code
const result = (await nodeTester.test_node(mixnodeIdentityKey)) as NodeTestResult | undefined;
// return early if there was an error
if (!result) {
return result;
}
// log the result in the worker so that the packet stats are visible somewhere and extract the score
result.log_details();
// eslint-disable-next-line @typescript-eslint/naming-convention
const { duplicate_acks, duplicate_packets, received_acks, received_packets, sent_packets } = result;
// construct the response to avoid any weird proxy effects
return {
score: result.score(),
sentPackets: sent_packets,
receivedPackets: received_packets,
receivedAcks: received_acks,
duplicatePackets: duplicate_packets,
duplicateAcks: duplicate_acks,
};
},
};
// start comlink listening for messages and handle them above
Comlink.expose(webWorker);
// notify any listeners that the web worker has loaded and is ready for testing
postMessageWithType<NodeTesterLoadedEvent>({ kind: NodeTesterEventKinds.Loaded, args: { loaded: true } });
}
main();
@@ -0,0 +1,336 @@
/**
*/
export interface TopologyWasm {
free(): void;
/**
* Specifies whether the client should not refresh the network topology after obtaining
* the first valid instance.
* Supersedes `topology_refresh_rate_ms`.
*/
disable_refreshing: boolean;
/**
* The uniform delay every which clients are querying the directory server
* to try to obtain a compatible network topology to send sphinx packets through.
*/
topology_refresh_rate_ms: bigint;
/**
* During topology refresh, test packets are sent through every single possible network
* path. This timeout determines waiting period until it is decided that the packet
* did not reach its destination.
*/
topology_resolution_timeout_ms: bigint;
}
/**
*/
export interface TrafficWasm {
free(): void;
/**
* The parameter of Poisson distribution determining how long, on average,
* sent packet is going to be delayed at any given mix node.
* So for a packet going through three mix nodes, on average, it will take three times this value
* until the packet reaches its destination.
*/
average_packet_delay_ms: bigint;
/**
* Controls whether the main packet stream constantly produces packets according to the predefined
* poisson distribution.
*/
disable_main_poisson_packet_distribution: boolean;
/**
* The parameter of Poisson distribution determining how long, on average,
* it is going to take another 'real traffic stream' message to be sent.
* If no real packets are available and cover traffic is enabled,
* a loop cover message is sent instead in order to preserve the rate.
*/
message_sending_average_delay_ms: bigint;
/**
* Controls whether the sent sphinx packet use the NON-DEFAULT bigger size.
*/
use_extended_packet_size: boolean;
/**
* Controls whether the sent packets should use outfox as opposed to the default sphinx.
*/
use_outfox: boolean;
}
/**
*/
export interface WasmGateway {
free(): void;
/**
*/
clients_port: number;
/**
*/
host: string;
/**
*/
identity_key: string;
/**
*/
mix_port: number;
/**
*/
owner: string;
/**
*/
sphinx_key: string;
/**
*/
version: string;
}
/**
*/
export interface ReplySurbsWasm {
free(): void;
/**
* Defines the maximum number of reply surbs a remote party is allowed to request from this client at once.
*/
maximum_allowed_reply_surb_request_size: number;
/**
* Defines maximum amount of time given reply key is going to be valid for.
* This is going to be superseded by key rotation once implemented.
*/
maximum_reply_key_age_ms: bigint;
/**
* Defines maximum amount of time given reply surb is going to be valid for.
* This is going to be superseded by key rotation once implemented.
*/
maximum_reply_surb_age_ms: bigint;
/**
* Defines maximum amount of time the client is going to wait for reply surbs before
* deciding it's never going to get them and would drop all pending messages
*/
maximum_reply_surb_drop_waiting_period_ms: bigint;
/**
* Defines the maximum number of reply surbs the client would request.
*/
maximum_reply_surb_request_size: number;
/**
* Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking
* for more even though in theory they wouldn't need to.
*/
maximum_reply_surb_rerequest_waiting_period_ms: bigint;
/**
* Defines the maximum number of reply surbs the client wants to keep in its storage at any times.
*/
maximum_reply_surb_storage_threshold: number;
/**
* Defines the minimum number of reply surbs the client would request.
*/
minimum_reply_surb_request_size: number;
/**
* Defines the minimum number of reply surbs the client wants to keep in its storage at all times.
* It can only allow to go below that value if its to request additional reply surbs.
*/
minimum_reply_surb_storage_threshold: number;
}
/**
*/
export interface GatewayConnectionWasm {
free(): void;
/**
* How long we're willing to wait for a response to a message sent to the gateway,
* before giving up on it.
*/
gateway_response_timeout_ms: bigint;
}
/**
*/
export interface AcknowledgementsWasm {
free(): void;
/**
* Value added to the expected round trip time of an acknowledgement packet before
* it is assumed it was lost and retransmission of the data packet happens.
* In an ideal network with 0 latency, this value would have been 0.
*/
ack_wait_addition_ms: bigint;
/**
* Value multiplied with the expected round trip time of an acknowledgement packet before
* it is assumed it was lost and retransmission of the data packet happens.
* In an ideal network with 0 latency, this value would have been 1.
*/
ack_wait_multiplier: number;
/**
* The parameter of Poisson distribution determining how long, on average,
* sent acknowledgement is going to be delayed at any given mix node.
* So for an ack going through three mix nodes, on average, it will take three times this value
* until the packet reaches its destination.
*/
average_ack_delay_ms: bigint;
}
/**
*/
export interface CoverTrafficWasm {
free(): void;
/**
* Specifies the ratio of `primary_packet_size` to `secondary_packet_size` used in cover traffic.
* Only applicable if `secondary_packet_size` is enabled.
*/
cover_traffic_primary_size_ratio: number;
/**
* Controls whether the dedicated loop cover traffic stream should be enabled.
* (and sending packets, on average, every [Self::loop_cover_traffic_average_delay])
*/
disable_loop_cover_traffic_stream: boolean;
/**
* The parameter of Poisson distribution determining how long, on average,
* it is going to take for another loop cover traffic message to be sent.
*/
loop_cover_traffic_average_delay_ms: bigint;
}
export interface Config {
free(): void;
}
export interface DebugWasm {
free(): void;
/**
* Defines all configuration options related to acknowledgements, such as delays or wait timeouts.
*/
acknowledgements: AcknowledgementsWasm;
/**
* Defines all configuration options related to cover traffic stream(s).
*/
cover_traffic: CoverTrafficWasm;
/**
* Defines all configuration options related to the gateway connection.
*/
gateway_connection: GatewayConnectionWasm;
/**
* Defines all configuration options related to reply SURBs.
*/
reply_surbs: ReplySurbsWasm;
/**
* Defines all configuration options related topology, such as refresh rates or timeouts.
*/
topology: TopologyWasm;
/**
* Defines all configuration options related to traffic streams.
*/
traffic: TrafficWasm;
}
/**
*/
export interface GatewayEndpointConfig {
free(): void;
/**
* gateway_id specifies ID of the gateway to which the client should send messages.
* If initially omitted, a random gateway will be chosen from the available topology.
*/
gateway_id: string;
/**
* Address of the gateway listener to which all client requests should be sent.
*/
gateway_listener: string;
/**
* Address of the gateway owner to which the client should send messages.
*/
gateway_owner: string;
}
/**
*/
export interface NymClient {
free(): void;
/**
* @returns {string}
*/
self_address(): string;
/**
* The simplest message variant where no additional information is attached.
* You're simply sending your `data` to specified `recipient` without any tagging.
*
* Ends up with `NymMessage::Plain` variant
* @param {Uint8Array} message
* @param {string} recipient
* @returns {Promise<any>}
*/
send_regular_message(message: Uint8Array, recipient: string): Promise<any>;
/**
* Creates a message used for a duplex anonymous communication where the recipient
* will never learn of our true identity. This is achieved by carefully sending `reply_surbs`.
*
* Note that if reply_surbs is set to zero then
* this variant requires the client having sent some reply_surbs in the past
* (and thus the recipient also knowing our sender tag).
*
* Ends up with `NymMessage::Repliable` variant
* @param {Uint8Array} message
* @param {string} recipient
* @param {number} reply_surbs
* @returns {Promise<any>}
*/
send_anonymous_message(message: Uint8Array, recipient: string, reply_surbs: number): Promise<any>;
/**
* Attempt to use our internally received and stored `ReplySurb` to send the message back
* to specified recipient whilst not knowing its full identity (or even gateway).
*
* Ends up with `NymMessage::Reply` variant
* @param {Uint8Array} message
* @param {string} recipient_tag
* @returns {Promise<any>}
*/
send_reply(message: Uint8Array, recipient_tag: string): Promise<any>;
}
export interface Topology {
free(): void;
/**
* Specifies whether the client should not refresh the network topology after obtaining
* the first valid instance.
* Supersedes `topology_refresh_rate_ms`.
*/
disable_refreshing: boolean;
/**
* The uniform delay every which clients are querying the directory server
* to try to obtain a compatible network topology to send sphinx packets through.
*/
topology_refresh_rate_ms: bigint;
/**
* During topology refresh, test packets are sent through every single possible network
* path. This timeout determines waiting period until it is decided that the packet
* did not reach its destination.
*/
topology_resolution_timeout_ms: bigint;
}
export interface Traffic {
free(): void;
/**
* The parameter of Poisson distribution determining how long, on average,
* sent packet is going to be delayed at any given mix node.
* So for a packet going through three mix nodes, on average, it will take three times this value
* until the packet reaches its destination.
*/
average_packet_delay_ms: bigint;
/**
* Controls whether the main packet stream constantly produces packets according to the predefined
* poisson distribution.
*/
disable_main_poisson_packet_distribution: boolean;
/**
* The parameter of Poisson distribution determining how long, on average,
* it is going to take another 'real traffic stream' message to be sent.
* If no real packets are available and cover traffic is enabled,
* a loop cover message is sent instead in order to preserve the rate.
*/
message_sending_average_delay_ms: bigint;
/**
* Controls whether the sent sphinx packet use the NON-DEFAULT bigger size.
*/
use_extended_packet_size: boolean;
}
/**
*/
export interface NymClientBuilder {
free(): void;
/**
* @returns {Promise<Promise<any>>}
*/
start_client(): Promise<Promise<any>>;
}
@@ -1,174 +1,6 @@
export interface Acknowledgements {
free(): void;
/**
* Value added to the expected round trip time of an acknowledgement packet before
* it is assumed it was lost and retransmission of the data packet happens.
* In an ideal network with 0 latency, this value would have been 0.
*/
ack_wait_addition_ms: bigint;
/**
* Value multiplied with the expected round trip time of an acknowledgement packet before
* it is assumed it was lost and retransmission of the data packet happens.
* In an ideal network with 0 latency, this value would have been 1.
*/
ack_wait_multiplier: number;
/**
* The parameter of Poisson distribution determining how long, on average,
* sent acknowledgement is going to be delayed at any given mix node.
* So for an ack going through three mix nodes, on average, it will take three times this value
* until the packet reaches its destination.
*/
average_ack_delay_ms: bigint;
}
import type { DebugWasm } from './types-from-wasm-pack';
export interface CoverTraffic {
free(): void;
/**
* Specifies the ratio of `primary_packet_size` to `secondary_packet_size` used in cover traffic.
* Only applicable if `secondary_packet_size` is enabled.
*/
cover_traffic_primary_size_ratio: number;
/**
* Controls whether the dedicated loop cover traffic stream should be enabled.
* (and sending packets, on average, every [Self::loop_cover_traffic_average_delay])
*/
disable_loop_cover_traffic_stream: boolean;
/**
* The parameter of Poisson distribution determining how long, on average,
* it is going to take for another loop cover traffic message to be sent.
*/
loop_cover_traffic_average_delay_ms: bigint;
}
export interface GatewayConnection {
free(): void;
/**
* How long we're willing to wait for a response to a message sent to the gateway,
* before giving up on it.
*/
gateway_response_timeout_ms: bigint;
}
export interface ReplySurbs {
free(): void;
/**
* Defines the maximum number of reply surbs a remote party is allowed to request from this client at once.
*/
maximum_allowed_reply_surb_request_size: number;
/**
* Defines maximum amount of time given reply key is going to be valid for.
* This is going to be superseded by key rotation once implemented.
*/
maximum_reply_key_age_ms: bigint;
/**
* Defines maximum amount of time given reply surb is going to be valid for.
* This is going to be superseded by key rotation once implemented.
*/
maximum_reply_surb_age_ms: bigint;
/**
* Defines maximum amount of time the client is going to wait for reply surbs before
* deciding it's never going to get them and would drop all pending messages
*/
maximum_reply_surb_drop_waiting_period_ms: bigint;
/**
* Defines the maximum number of reply surbs the client would request.
*/
maximum_reply_surb_request_size: number;
/**
* Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking
* for more even though in theory they wouldn't need to.
*/
maximum_reply_surb_rerequest_waiting_period_ms: bigint;
/**
* Defines the maximum number of reply surbs the client wants to keep in its storage at any times.
*/
maximum_reply_surb_storage_threshold: number;
/**
* Defines the minimum number of reply surbs the client would request.
*/
minimum_reply_surb_request_size: number;
/**
* Defines the minimum number of reply surbs the client wants to keep in its storage at all times.
* It can only allow to go below that value if its to request additional reply surbs.
*/
minimum_reply_surb_storage_threshold: number;
}
export interface Debug {
free(): void;
/**
* Defines all configuration options related to acknowledgements, such as delays or wait timeouts.
*/
acknowledgements: Acknowledgements;
/**
* Defines all configuration options related to cover traffic stream(s).
*/
cover_traffic: CoverTraffic;
/**
* Defines all configuration options related to the gateway connection.
*/
gateway_connection: GatewayConnection;
/**
* Defines all configuration options related to reply SURBs.
*/
reply_surbs: ReplySurbs;
/**
* Defines all configuration options related topology, such as refresh rates or timeouts.
*/
topology: Topology;
/**
* Defines all configuration options related to traffic streams.
*/
traffic: Traffic;
}
export interface Topology {
free(): void;
/**
* Specifies whether the client should not refresh the network topology after obtaining
* the first valid instance.
* Supersedes `topology_refresh_rate_ms`.
*/
disable_refreshing: boolean;
/**
* The uniform delay every which clients are querying the directory server
* to try to obtain a compatible network topology to send sphinx packets through.
*/
topology_refresh_rate_ms: bigint;
/**
* During topology refresh, test packets are sent through every single possible network
* path. This timeout determines waiting period until it is decided that the packet
* did not reach its destination.
*/
topology_resolution_timeout_ms: bigint;
}
export interface Traffic {
free(): void;
/**
* The parameter of Poisson distribution determining how long, on average,
* sent packet is going to be delayed at any given mix node.
* So for a packet going through three mix nodes, on average, it will take three times this value
* until the packet reaches its destination.
*/
average_packet_delay_ms: bigint;
/**
* Controls whether the main packet stream constantly produces packets according to the predefined
* poisson distribution.
*/
disable_main_poisson_packet_distribution: boolean;
/**
* The parameter of Poisson distribution determining how long, on average,
* it is going to take another 'real traffic stream' message to be sent.
* If no real packets are available and cover traffic is enabled,
* a loop cover message is sent instead in order to preserve the rate.
*/
message_sending_average_delay_ms: bigint;
/**
* Controls whether the sent sphinx packet use the NON-DEFAULT bigger size.
*/
use_extended_packet_size: boolean;
}
export * from './types-from-wasm-pack';
/**
* Some common mime types, however, you can always just specify the mime-type as a string
@@ -221,7 +53,7 @@ export interface NymClientConfig {
/**
* Optional. Settings for the WASM client.
*/
debug?: Debug;
debug?: DebugWasm;
}
export interface IWebWorker {
@@ -168,43 +168,40 @@ init(wasmBytes())
if (config.gatewayListener) gatewayEndpoint.gateway_listener = config.gatewayListener;
// create the client, passing handlers for events
wrapper.init(
new Config(config.clientId, config.nymApiUrl, gatewayEndpoint, config.debug || default_debug()),
async (message) => {
// fire an event with the raw message
postMessageWithType<RawMessageReceivedEvent>({
kind: EventKinds.RawMessageReceived,
args: { payload: message },
});
wrapper.init(new Config(config.clientId, config.nymApiUrl, config.debug || default_debug()), async (message) => {
// fire an event with the raw message
postMessageWithType<RawMessageReceivedEvent>({
kind: EventKinds.RawMessageReceived,
args: { payload: message },
});
try {
// try to decode the payload to extract the mime-type, headers and payload body
const decodedPayload = decode_payload(message);
const { payload, headers } = decodedPayload;
const mimeType = decodedPayload.mimeType as MimeTypes;
try {
// try to decode the payload to extract the mime-type, headers and payload body
const decodedPayload = decode_payload(message);
const { payload, headers } = decodedPayload;
const mimeType = decodedPayload.mimeType as MimeTypes;
if (wrapper.getTextMimeTypes().includes(mimeType)) {
const stringMessage = parse_utf8_string(payload);
if (wrapper.getTextMimeTypes().includes(mimeType)) {
const stringMessage = parse_utf8_string(payload);
// the payload is a string type (in the options at creation time, string mime-types are set, or fall back
// to defaults, such as `text/plain`, `application/json`, etc)
postMessageWithType<StringMessageReceivedEvent>({
kind: EventKinds.StringMessageReceived,
args: { mimeType, payload: stringMessage, payloadRaw: payload, headers },
});
return;
}
// the payload is a binary type
postMessageWithType<BinaryMessageReceivedEvent>({
kind: EventKinds.BinaryMessageReceived,
args: { mimeType, payload, headers },
// the payload is a string type (in the options at creation time, string mime-types are set, or fall back
// to defaults, such as `text/plain`, `application/json`, etc)
postMessageWithType<StringMessageReceivedEvent>({
kind: EventKinds.StringMessageReceived,
args: { mimeType, payload: stringMessage, payloadRaw: payload, headers },
});
} catch (e) {
console.error('Failed to parse binary message', e);
return;
}
},
);
// the payload is a binary type
postMessageWithType<BinaryMessageReceivedEvent>({
kind: EventKinds.BinaryMessageReceived,
args: { mimeType, payload, headers },
});
} catch (e) {
console.error('Failed to parse binary message', e);
}
});
// start the client sending traffic
await wrapper.start();
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import express from 'express';
import dotenv from 'dotenv';
import { createProxyMiddleware } from 'http-proxy-middleware';
@@ -20,6 +20,7 @@ export const Content: FCWithChildren = () => {
const isMounted = useIsMounted();
if (isMounted()) {
// eslint-disable-next-line no-console
console.log('Content is mounted');
}
+303 -324
View File
File diff suppressed because it is too large Load Diff