Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5c3f39a57 | |||
| 76f999fc88 | |||
| 2fce8c7ca3 | |||
| 468bd8b5d1 | |||
| 45022b1671 | |||
| 3b3c5beae4 | |||
| 650917e216 | |||
| c02adaa019 | |||
| d01c34263a | |||
| f247e028f2 | |||
| 20fe8dd028 | |||
| 89edabf796 | |||
| bf5352906f | |||
| 8eb9999876 | |||
| c0f582b336 | |||
| 133a855e01 | |||
| 98149dde87 | |||
| 5e733a5ebf | |||
| 5647ae6a41 | |||
| 4ed9d8fb7a | |||
| a2081af603 | |||
| 5b62fd76ba | |||
| 77a34fe3bf | |||
| 630c4922ac | |||
| 6edbece3ad | |||
| 8529a3c351 | |||
| 453e1cbe70 | |||
| 94a3599b4d | |||
| a6bc54461a | |||
| 4f0c40dab7 | |||
| 3eff6e5e3b | |||
| a519f4ccb8 | |||
| a3ba3bfc5a | |||
| 988df7cff7 | |||
| 260f8e9714 | |||
| d28d0ac39e | |||
| dce4d6b34b | |||
| bc47e9a1b2 | |||
| 3b693741b2 | |||
| cb277fe487 | |||
| 8bb29f4d07 | |||
| e753f24ed1 | |||
| c7cd962627 | |||
| 00467e4440 | |||
| f3d1000472 | |||
| 597aae1a20 | |||
| 40a3cd28b7 | |||
| a4950485d1 |
@@ -6,6 +6,8 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-ubuntu-22.04
|
||||
env:
|
||||
NEXT_PUBLIC_SITE_URL: https://nymtech.net/docs
|
||||
defaults:
|
||||
run:
|
||||
working-directory: documentation/docs
|
||||
@@ -41,6 +43,8 @@ jobs:
|
||||
run: pnpm i
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
- name: Generate sitemap
|
||||
run: npx next-sitemap
|
||||
- name: Move files to /dist/
|
||||
run: ../scripts/move-to-dist.sh
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -51,25 +51,3 @@ jobs:
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nym-wallet
|
||||
NYM_PROJECT_NAME: "nym-wallet"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -10,8 +10,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
rust: [stable, beta]
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest]
|
||||
rust: [ stable, beta ]
|
||||
os: [ ubuntu-22.04, windows-latest, macos-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -93,38 +93,3 @@ jobs:
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets -- -D warnings
|
||||
|
||||
notification:
|
||||
needs: build
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- name: Collect jobs status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
- name: install npm
|
||||
uses: actions/setup-node@v4
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Matrix - Node Install
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nightly
|
||||
NYM_PROJECT_NAME: "Nym nightly build"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_NIGHTLY }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, macos-latest, windows-latest]
|
||||
os: [ ubuntu-22.04, macos-latest, windows-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -55,38 +55,3 @@ jobs:
|
||||
with:
|
||||
command: clippy
|
||||
args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings
|
||||
|
||||
notification:
|
||||
needs: build
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- name: Collect jobs status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
- name: install npm
|
||||
uses: actions/setup-node@v4
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Matrix - Node Install
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nightly
|
||||
NYM_PROJECT_NAME: "nym-wallet-nightly-build"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_NIGHTLY }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -24,34 +24,3 @@ jobs:
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications/deny.message
|
||||
notification:
|
||||
needs: cargo-deny
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
- name: Download report from previous job
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications
|
||||
- name: install npm
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: security
|
||||
NYM_PROJECT_NAME: "Daily security report"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_AUDIT }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -4,51 +4,23 @@ This is a collection of scripts and files to support GitHub Actions.
|
||||
|
||||
## Sending Notifications
|
||||
|
||||
These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub Actions.
|
||||
|
||||
### Adding notifications to a GitHub Action
|
||||
|
||||
```
|
||||
jobs:
|
||||
build:
|
||||
...
|
||||
- name: Notifications - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files/notifications
|
||||
- name: Notifications - Send
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: "my-component"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
```
|
||||
|
||||
Notifications are run by adding the snippet above to a GitHub Action, and:
|
||||
|
||||
1. Installing node packages needed at run time
|
||||
2. Set the env vars as required:
|
||||
- `NYM_NOTIFICATION_KIND` matches the directory in `.github/workflows/support-files/${NYM_NOTIFICATION_KIND}` to provide the templates and extra scripting in `index.js`
|
||||
- Matrix credentials, room and other env vars for the status of the build and repo
|
||||
3. Replacing the default entry point shell script on the `keybaseio/client:stable-node` docker image to run `.github/workflows/support-files/notifications/entry_point.sh`
|
||||
These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub
|
||||
Actions.
|
||||
|
||||
### Running locally
|
||||
|
||||
You will need:
|
||||
|
||||
- Node 16 LTS
|
||||
- npm
|
||||
|
||||
Copy `.github/workflows/support-files/.env.example` to `.github/workflows/support-files/.env` and valid Matrix credentials.
|
||||
Copy `.github/workflows/support-files/.env.example` to `.github/workflows/support-files/.env` and valid Matrix
|
||||
credentials.
|
||||
|
||||
Then run `npm install` to get dependencies.
|
||||
|
||||
Start development mode for the notification type you want either by passing the value as an env var called `NYM_NOTIFICATION_KIND` or set the `.env` file values correctly.
|
||||
Start development mode for the notification type you want either by passing the value as an env var called
|
||||
`NYM_NOTIFICATION_KIND` or set the `.env` file values correctly.
|
||||
|
||||
```bash
|
||||
cd .github/workflows/support-files
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# pass exit codes out to GitHub Actions
|
||||
set -euxo pipefail
|
||||
|
||||
# change to the directory that contains this script
|
||||
cd "${0%/*}"
|
||||
|
||||
# run the node script
|
||||
node send_message.js
|
||||
@@ -1,126 +0,0 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const { sendMatrixMessage } = require('./send_message_to_matrix');
|
||||
|
||||
let context = {
|
||||
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect','security','ci-docs','cd-docs','ci-dev','cd-dev'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that all required env and context vars are available
|
||||
*/
|
||||
function validateContext() {
|
||||
if (!context.env.NYM_NOTIFICATION_KIND) {
|
||||
throw new Error(
|
||||
'Please set env var NYM_NOTIFICATION_KIND with the project kind that matches a directory in ".github/workflows/support-files"',
|
||||
);
|
||||
}
|
||||
if (!context.kinds.includes(context.env.NYM_NOTIFICATION_KIND)) {
|
||||
throw new Error(`Env var NYM_NOTIFICATION_KIND is not in ${context.kinds}`);
|
||||
}
|
||||
if (!context.env.NYM_PROJECT_NAME) {
|
||||
throw new Error(
|
||||
'Please set env var NYM_PROJECT_NAME with the project name for displaying in notification messages',
|
||||
);
|
||||
}
|
||||
if (context.env.MATRIX_ROOM) {
|
||||
if (!context.env.MATRIX_SERVER) {
|
||||
throw new Error(
|
||||
'Matrix server is not defined. Please set env var MATRIX_SERVER',
|
||||
);
|
||||
}
|
||||
if (!context.env.MATRIX_USER_ID) {
|
||||
throw new Error(
|
||||
'Matrix user id is not defined. Please set env var MATRIX_USER_ID',
|
||||
);
|
||||
}
|
||||
if (!context.env.MATRIX_TOKEN) {
|
||||
throw new Error(
|
||||
'Matrix token is not defined. Please set env var MATRIX_TOKEN',
|
||||
);
|
||||
}
|
||||
if (!context.env.MATRIX_DEVICE_ID) {
|
||||
throw new Error(
|
||||
'Matrix device id is not defined. Please set env var MATRIX_DEVICE_ID',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context that will be available in the templates for rendering notifications
|
||||
*/
|
||||
function createTemplateContext() {
|
||||
const options = { dateStyle: 'full', timeStyle: 'long' };
|
||||
context.timestamp = new Date().toLocaleString(undefined, options);
|
||||
|
||||
// add environment to template context and validate
|
||||
context.env = process.env;
|
||||
try {
|
||||
validateContext();
|
||||
} catch (e) {
|
||||
if(process.env.SHOW_DEBUG) {
|
||||
// recursively print the context for easy debugging and rethrow the error
|
||||
console.dir({ context }, { depth: null });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
context.kind = context.env.NYM_NOTIFICATION_KIND;
|
||||
|
||||
if (!context.env.GIT_BRANCH_NAME) {
|
||||
context.env.GIT_BRANCH_NAME = context.env.GITHUB_REF.split('/')
|
||||
.slice(2)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
context.status = process.env.IS_SUCCESS === 'true' ? 'success' : 'failure';
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the `kind` set in the context to process the context and generate a notification message
|
||||
* @returns {Promise<string>} A string notification message body
|
||||
*/
|
||||
async function processKindScript() {
|
||||
const script = require(`../${context.kind}`);
|
||||
if (!script.addToContextAndValidate) {
|
||||
throw new Error(
|
||||
`"./${context.kind}/index.js" does not export a method called "async addToContextAndValidate(context)"`,
|
||||
);
|
||||
}
|
||||
if (!script.getMessageBody) {
|
||||
throw new Error(
|
||||
`"./${context.kind}/index.js" does not export a method called "async getMessageBody(context)"`,
|
||||
);
|
||||
}
|
||||
|
||||
// call the script to modify and validate the context
|
||||
await script.addToContextAndValidate(context);
|
||||
|
||||
// let the script create a message body and return the result as a string for sending
|
||||
return await script.getMessageBody(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function, as async so that await syntax is available
|
||||
*/
|
||||
async function main() {
|
||||
createTemplateContext();
|
||||
console.log(`Sending notification for kind "${context.kind}"...`);
|
||||
const messageBody = await processKindScript();
|
||||
if(process.env.SHOW_DEBUG) {
|
||||
console.log('-----------------------------------------');
|
||||
console.log(messageBody);
|
||||
console.log('-----------------------------------------');
|
||||
}
|
||||
if(context.env.MATRIX_ROOM) {
|
||||
await sendMatrixMessage(context, messageBody, context.env.MATRIX_ROOM)
|
||||
}
|
||||
if(context.env.MATRIX_ROOM_OF_SHAME && context.env.IS_SUCCESS !== 'true') {
|
||||
// when a job fails
|
||||
await sendMatrixMessage(context, messageBody, context.env.MATRIX_ROOM_OF_SHAME)
|
||||
}
|
||||
}
|
||||
|
||||
// call main function and let NodeJS handle the promise
|
||||
main();
|
||||
@@ -1,67 +0,0 @@
|
||||
const sdk = require('matrix-js-sdk');
|
||||
global.Olm = require('olm');
|
||||
const { LocalStorage } = require('node-localstorage');
|
||||
const localStorage = new LocalStorage('./scratch');
|
||||
const {
|
||||
LocalStorageCryptoStore,
|
||||
} = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store');
|
||||
var showdown = require('showdown');
|
||||
|
||||
// hide all matrix client output
|
||||
console.error = (error) => console.log('❌ error: ', error);
|
||||
process.stderr.write = () => {};
|
||||
process.stdout.write = () => {};
|
||||
|
||||
|
||||
function createClient(context, room, message) {
|
||||
const server = context.env.MATRIX_SERVER;
|
||||
const token = context.env.MATRIX_TOKEN;
|
||||
const deviceId = context.env.MATRIX_DEVICE_ID;
|
||||
const userId = context.env.MATRIX_USER_ID;
|
||||
|
||||
const client = sdk.createClient({
|
||||
baseUrl: server,
|
||||
accessToken: token,
|
||||
userId,
|
||||
deviceId,
|
||||
sessionStore: new sdk.WebStorageSessionStore(localStorage),
|
||||
cryptoStore: new LocalStorageCryptoStore(localStorage),
|
||||
});
|
||||
|
||||
client.on('sync', async function(state, prevState, res) {
|
||||
if (state !== 'PREPARED') return;
|
||||
client.setGlobalErrorOnUnknownDevices(false);
|
||||
try {
|
||||
await client.joinRoom(room);
|
||||
await client.sendEvent(
|
||||
room,
|
||||
'm.room.message',
|
||||
{
|
||||
msgtype: 'm.text',
|
||||
format: 'org.matrix.custom.html',
|
||||
body: message,
|
||||
formatted_body: message,
|
||||
},
|
||||
'',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Job failed: ' + error.message);
|
||||
}
|
||||
client.stopClient();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
async function sendMatrixMessage(contextArg, messageAsMarkdown, roomId) {
|
||||
const converter = new showdown.Converter();
|
||||
const messageAsHtml = converter.makeHtml(messageAsMarkdown);
|
||||
const client = createClient(contextArg, roomId, messageAsHtml);
|
||||
await client.initCrypto();
|
||||
await client.startClient({ initialSyncLimit: 1 });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendMatrixMessage,
|
||||
};
|
||||
@@ -76,3 +76,4 @@ CLAUDE.md
|
||||
.claude/settings.json
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
|
||||
@@ -4,6 +4,82 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.4-quark] (2026-02-24)
|
||||
|
||||
- Enhance CI workflow with feature inputs ([#6462])
|
||||
- Chore/revert 6433 ([#6445])
|
||||
- Lp/stateless handshake ([#6437])
|
||||
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/client/internal-dev ([#6435])
|
||||
- build(deps-dev): bump webpack from 5.102.1 to 5.104.1 ([#6432])
|
||||
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/mix-fetch/internal-dev ([#6431])
|
||||
- build(deps-dev): bump webpack from 5.94.0 to 5.104.1 in /nym-credential-proxy/vpn-api-lib-wasm/internal-dev ([#6430])
|
||||
- build(deps-dev): bump webpack from 5.77.0 to 5.104.1 in /wasm/zknym-lib/internal-dev ([#6429])
|
||||
- build(deps-dev): bump webpack from 5.76.0 to 5.105.0 in /clients/native/examples/js-examples/websocket ([#6428])
|
||||
- HTTP & DNS Improvements ([#6423])
|
||||
- Endpoint for exit GW IPs ([#6418])
|
||||
- build(deps): bump bytes from 1.6.0 to 1.11.1 in /contracts ([#6416])
|
||||
- build(deps): bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 ([#6415])
|
||||
- build(deps): bump bytes from 1.11.0 to 1.11.1 ([#6414])
|
||||
- build(deps): bump mikefarah/yq from 4.50.1 to 4.52.2 ([#6407])
|
||||
- build(deps-dev): bump eslint from 8.57.1 to 9.26.0 ([#6405])
|
||||
- Update reqwest to v0.13.1 ([#6401])
|
||||
- build(deps): bump next from 15.5.9 to 16.1.5 in /documentation/docs ([#6387])
|
||||
- build(deps): bump next from 15.4.10 to 16.1.5 in /nym-node-status-api/nym-node-status-ui ([#6385])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 ([#6369])
|
||||
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 ([#6360])
|
||||
- build(deps-dev): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/codegen/contract-clients ([#6359])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/packages/nodejs-client ([#6354])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /documentation/docs ([#6353])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /clients/native/examples/js-examples/websocket ([#6351])
|
||||
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 in /documentation/docs ([#6350])
|
||||
- build(deps): bump diff from 5.2.0 to 5.2.2 in /documentation/docs ([#6345])
|
||||
- Max/crates publishing tweaks ([#6343])
|
||||
- build(deps): bump h3 from 1.15.4 to 1.15.5 ([#6339])
|
||||
- build(deps): bump h3 from 1.15.4 to 1.15.5 in /documentation/docs ([#6332])
|
||||
- build(deps): bump undici from 6.21.3 to 6.23.0 in /documentation/docs ([#6325])
|
||||
- build(deps): bump rsa from 0.9.8 to 0.9.10 ([#6311])
|
||||
- build(deps): bump qs and express in /wasm/mix-fetch/internal-dev ([#6308])
|
||||
- build(deps): bump qs and express in /clients/native/examples/js-examples/websocket ([#6307])
|
||||
- feat: introduce on-disk cache persistance for major nym-api caches ([#6302])
|
||||
- Fix migrations in the Data Observatory ([#6271])
|
||||
|
||||
[#6462]: https://github.com/nymtech/nym/pull/6462
|
||||
[#6445]: https://github.com/nymtech/nym/pull/6445
|
||||
[#6437]: https://github.com/nymtech/nym/pull/6437
|
||||
[#6435]: https://github.com/nymtech/nym/pull/6435
|
||||
[#6432]: https://github.com/nymtech/nym/pull/6432
|
||||
[#6431]: https://github.com/nymtech/nym/pull/6431
|
||||
[#6430]: https://github.com/nymtech/nym/pull/6430
|
||||
[#6429]: https://github.com/nymtech/nym/pull/6429
|
||||
[#6428]: https://github.com/nymtech/nym/pull/6428
|
||||
[#6423]: https://github.com/nymtech/nym/pull/6423
|
||||
[#6418]: https://github.com/nymtech/nym/pull/6418
|
||||
[#6416]: https://github.com/nymtech/nym/pull/6416
|
||||
[#6415]: https://github.com/nymtech/nym/pull/6415
|
||||
[#6414]: https://github.com/nymtech/nym/pull/6414
|
||||
[#6407]: https://github.com/nymtech/nym/pull/6407
|
||||
[#6405]: https://github.com/nymtech/nym/pull/6405
|
||||
[#6401]: https://github.com/nymtech/nym/pull/6401
|
||||
[#6387]: https://github.com/nymtech/nym/pull/6387
|
||||
[#6385]: https://github.com/nymtech/nym/pull/6385
|
||||
[#6369]: https://github.com/nymtech/nym/pull/6369
|
||||
[#6360]: https://github.com/nymtech/nym/pull/6360
|
||||
[#6359]: https://github.com/nymtech/nym/pull/6359
|
||||
[#6354]: https://github.com/nymtech/nym/pull/6354
|
||||
[#6353]: https://github.com/nymtech/nym/pull/6353
|
||||
[#6351]: https://github.com/nymtech/nym/pull/6351
|
||||
[#6350]: https://github.com/nymtech/nym/pull/6350
|
||||
[#6345]: https://github.com/nymtech/nym/pull/6345
|
||||
[#6343]: https://github.com/nymtech/nym/pull/6343
|
||||
[#6339]: https://github.com/nymtech/nym/pull/6339
|
||||
[#6332]: https://github.com/nymtech/nym/pull/6332
|
||||
[#6325]: https://github.com/nymtech/nym/pull/6325
|
||||
[#6311]: https://github.com/nymtech/nym/pull/6311
|
||||
[#6308]: https://github.com/nymtech/nym/pull/6308
|
||||
[#6307]: https://github.com/nymtech/nym/pull/6307
|
||||
[#6302]: https://github.com/nymtech/nym/pull/6302
|
||||
[#6271]: https://github.com/nymtech/nym/pull/6271
|
||||
|
||||
## [2026.3-parmigiano] (2026-02-10)
|
||||
|
||||
- chore: disable LP on parmigiano branch ([#6422])
|
||||
|
||||
Generated
+1636
-1635
File diff suppressed because it is too large
Load Diff
+5
-4
@@ -309,8 +309,10 @@ nix = "0.30.1"
|
||||
notify = "5.1.0"
|
||||
num_enum = "0.7.5"
|
||||
once_cell = "1.21.3"
|
||||
opentelemetry = "0.19.0"
|
||||
opentelemetry-jaeger = "0.18.0"
|
||||
opentelemetry = "0.31.0"
|
||||
opentelemetry_sdk = "0.31.0"
|
||||
opentelemetry-otlp = "0.31.0"
|
||||
tonic = "0.14.4"
|
||||
parking_lot = "0.12.3"
|
||||
pem = "0.8"
|
||||
petgraph = "0.6.5"
|
||||
@@ -368,9 +370,8 @@ tower = "0.5.2"
|
||||
tower-http = "0.6.6"
|
||||
tracing = "0.1.41"
|
||||
tracing-log = "0.2"
|
||||
tracing-opentelemetry = "0.19.0"
|
||||
tracing-opentelemetry = "0.32.1"
|
||||
tracing-subscriber = "0.3.20"
|
||||
tracing-tree = "0.2.2"
|
||||
tracing-indicatif = "0.3.9"
|
||||
tracing-test = "0.2.5"
|
||||
ts-rs = "10.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.70"
|
||||
version = "1.1.71"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
+12
-12
@@ -513,9 +513,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -3067,9 +3067,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
@@ -4989,9 +4989,9 @@
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -6870,9 +6870,9 @@
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"side-channel": "^1.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.70"
|
||||
version = "1.1.71"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -19,12 +19,15 @@ serde_json = { workspace = true, optional = true }
|
||||
|
||||
## tracing
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
|
||||
tracing-tree = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
opentelemetry-jaeger = { workspace = true, features = ["rt-tokio", "collector_client", "isahc_collector_client"], optional = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
opentelemetry = { workspace = true, features = ["rt-tokio"], optional = true }
|
||||
opentelemetry = { workspace = true, features = ["trace"], optional = true }
|
||||
|
||||
## otel-otlp (modern OTLP export to SigNoz/any OTLP collector)
|
||||
opentelemetry_sdk = { workspace = true, features = ["trace"], optional = true }
|
||||
opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "trace", "tls-roots"], optional = true }
|
||||
tonic = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
@@ -35,13 +38,14 @@ default = []
|
||||
openapi = ["utoipa"]
|
||||
output_format = ["serde_json", "dep:clap"]
|
||||
bin_info_schema = ["schemars"]
|
||||
basic_tracing = ["dep:tracing", "tracing-subscriber"]
|
||||
tracing = [
|
||||
basic_tracing = ["dep:tracing", "dep:tracing-subscriber"]
|
||||
otel-otlp = [
|
||||
"basic_tracing",
|
||||
"tracing-tree",
|
||||
"opentelemetry-jaeger",
|
||||
"tracing-opentelemetry",
|
||||
"opentelemetry",
|
||||
"dep:opentelemetry",
|
||||
"dep:opentelemetry_sdk",
|
||||
"dep:opentelemetry-otlp",
|
||||
"dep:tracing-opentelemetry",
|
||||
"dep:tonic",
|
||||
]
|
||||
clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"]
|
||||
models = []
|
||||
|
||||
@@ -4,16 +4,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::IsTerminal;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use opentelemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use opentelemetry_jaeger;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_opentelemetry;
|
||||
// Re-export tracing_subscriber for consumers that need to compose layers
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub use tracing_subscriber;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_tree;
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -69,40 +62,106 @@ pub fn setup_tracing_logger() {
|
||||
build_tracing_logger().init()
|
||||
}
|
||||
|
||||
// TODO: This has to be a macro, running it as a function does not work for the file_appender for some reason
|
||||
#[cfg(feature = "tracing")]
|
||||
#[macro_export]
|
||||
macro_rules! setup_tracing {
|
||||
($service_name: expr) => {
|
||||
use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt;
|
||||
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt;
|
||||
/// Initialize an OpenTelemetry tracing layer that exports spans via OTLP/gRPC.
|
||||
///
|
||||
/// This produces a layer compatible with `tracing_subscriber::registry()` that
|
||||
/// sends traces to any OTLP-compatible collector (SigNoz, Grafana Tempo, etc).
|
||||
///
|
||||
/// Returns both the tracing layer and the [`SdkTracerProvider`] so the caller
|
||||
/// can invoke [`SdkTracerProvider::shutdown`] for graceful flush on exit.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `service_name` - The service name reported to the collector (e.g. "nym-node")
|
||||
/// * `endpoint` - The OTLP/gRPC collector endpoint (e.g. "http://localhost:4317"
|
||||
/// or "https://ingest.eu.signoz.cloud:443" for SigNoz Cloud)
|
||||
/// * `ingestion_key` - Optional SigNoz Cloud ingestion key. When provided, it is
|
||||
/// sent as the `signoz-ingestion-key` gRPC metadata header on every export.
|
||||
/// * `environment` - Deployment environment label (e.g. "sandbox", "mainnet", "canary").
|
||||
/// Attached as the `deployment.environment` OTel resource attribute.
|
||||
/// * `sample_ratio` - Trace sampling ratio in 0.0..=1.0 (e.g. 0.1 = 10% of traces).
|
||||
/// Used to limit cost when exporting from many nodes; clamped to [0.0, 1.0].
|
||||
/// * `export_timeout_secs` - Timeout in seconds for each OTLP export batch. Prevents
|
||||
/// unbounded blocking if the collector is slow or unreachable.
|
||||
#[cfg(feature = "otel-otlp")]
|
||||
pub fn init_otel_layer<S>(
|
||||
service_name: &str,
|
||||
endpoint: &str,
|
||||
ingestion_key: Option<&str>,
|
||||
environment: &str,
|
||||
sample_ratio: f64,
|
||||
export_timeout_secs: u64,
|
||||
) -> Result<
|
||||
(
|
||||
tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>,
|
||||
opentelemetry_sdk::trace::SdkTracerProvider,
|
||||
),
|
||||
Box<dyn std::error::Error + Send + Sync>,
|
||||
>
|
||||
where
|
||||
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
|
||||
{
|
||||
use opentelemetry::trace::TracerProvider as _;
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use opentelemetry_otlp::WithTonicConfig;
|
||||
use opentelemetry_sdk::trace::Sampler;
|
||||
use std::time::Duration;
|
||||
|
||||
let registry = nym_bin_common::logging::tracing_subscriber::Registry::default()
|
||||
.with(nym_bin_common::logging::tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(
|
||||
nym_bin_common::logging::tracing_tree::HierarchicalLayer::new(4)
|
||||
.with_targets(true)
|
||||
.with_bracketed_fields(true),
|
||||
);
|
||||
// Validate endpoint URI early to fail with a clear message
|
||||
if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
|
||||
return Err(format!(
|
||||
"invalid OTLP endpoint URI: {endpoint} (must start with http:// or https://)"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let tracer = nym_bin_common::logging::opentelemetry_jaeger::new_collector_pipeline()
|
||||
.with_endpoint("http://44.199.230.10:14268/api/traces")
|
||||
.with_service_name($service_name)
|
||||
.with_isahc()
|
||||
.with_trace_config(
|
||||
nym_bin_common::logging::opentelemetry::sdk::trace::config().with_sampler(
|
||||
nym_bin_common::logging::opentelemetry::sdk::trace::Sampler::TraceIdRatioBased(
|
||||
0.1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.install_batch(nym_bin_common::logging::opentelemetry::runtime::Tokio)
|
||||
.expect("Could not init tracer");
|
||||
let sample_ratio_clamped = sample_ratio.clamp(0.0, 1.0);
|
||||
|
||||
let telemetry = nym_bin_common::logging::tracing_opentelemetry::layer().with_tracer(tracer);
|
||||
let mut builder = opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(Duration::from_secs(export_timeout_secs));
|
||||
|
||||
registry.with(telemetry).init();
|
||||
};
|
||||
// Explicitly configure TLS when the endpoint uses HTTPS
|
||||
if endpoint.starts_with("https://") {
|
||||
builder =
|
||||
builder.with_tls_config(tonic::transport::ClientTlsConfig::new().with_native_roots());
|
||||
}
|
||||
|
||||
if let Some(key) = ingestion_key {
|
||||
let mut metadata = tonic::metadata::MetadataMap::new();
|
||||
metadata.insert(
|
||||
"signoz-ingestion-key",
|
||||
key.parse()
|
||||
.map_err(|_| "invalid ingestion key format (value redacted)")?,
|
||||
);
|
||||
builder = builder.with_metadata(metadata);
|
||||
}
|
||||
|
||||
let exporter = builder
|
||||
.build()
|
||||
.map_err(|e| format!("failed to build OTLP exporter for endpoint {endpoint}: {e}"))?;
|
||||
|
||||
let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
|
||||
.with_sampler(Sampler::TraceIdRatioBased(sample_ratio_clamped))
|
||||
.with_batch_exporter(exporter)
|
||||
.with_resource(
|
||||
opentelemetry_sdk::Resource::builder()
|
||||
.with_service_name(service_name.to_owned())
|
||||
.with_attribute(opentelemetry::KeyValue::new(
|
||||
"deployment.environment",
|
||||
environment.to_owned(),
|
||||
))
|
||||
.build(),
|
||||
)
|
||||
.build();
|
||||
|
||||
opentelemetry::global::set_tracer_provider(tracer_provider.clone());
|
||||
let tracer = tracer_provider.tracer(service_name.to_owned());
|
||||
|
||||
Ok((
|
||||
tracing_opentelemetry::layer().with_tracer(tracer),
|
||||
tracer_provider,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn banner(crate_name: &str, crate_version: &str) -> String {
|
||||
|
||||
@@ -128,54 +128,95 @@ impl ManagedConnection {
|
||||
|
||||
async fn run(self) {
|
||||
let address = self.address;
|
||||
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
|
||||
let connect_start = tokio::time::Instant::now();
|
||||
let connection_fut = TcpStream::connect(address);
|
||||
|
||||
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
debug!("Managed to establish connection to {}", self.address);
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
error!("Failed to perform Noise handshake with {address} - {err}");
|
||||
// we failed to finish the noise handshake - increase reconnection attempt
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!("Noise initiator handshake completed for {:?}", address);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("failed to establish connection to {address} (err: {err})",);
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
debug!(
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.timeout",
|
||||
peer = %address,
|
||||
timeout_ms = self.connection_timeout.as_millis() as u64,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "timeout",
|
||||
"failed to connect to {address} within {:?}",
|
||||
self.connection_timeout
|
||||
);
|
||||
|
||||
// we failed to connect - increase reconnection attempt
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Take whatever the receiver channel produces and put it on the connection.
|
||||
// We could have as well used conn.send_all(receiver.map(Ok)), but considering we don't care
|
||||
// about neither receiver nor the connection, it doesn't matter which one gets consumed
|
||||
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
|
||||
warn!("Failed to forward packets to {address}: {err}");
|
||||
warn!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packets to {address}: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"connection manager to {address} is finished. Either the connection failed or mixnet client got dropped",
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -272,16 +313,18 @@ impl SendWithoutResponse for Client {
|
||||
trace!("Sending packet to {address}");
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
// once we're addressing by node_id (and thus have full node info here),
|
||||
// we could simply infer supported encoding based on their version
|
||||
// use the mix packet type / flags to pick encoding per packet
|
||||
let framed_packet =
|
||||
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
|
||||
|
||||
let Some(sender) = self.active_connections.get_mut(&address) else {
|
||||
// there was never a connection to begin with
|
||||
debug!("establishing initial connection to {address}");
|
||||
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet
|
||||
// for sending for as soon as the connection is created
|
||||
debug!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "not_connected",
|
||||
"establishing initial connection to {address}"
|
||||
);
|
||||
self.make_connection(address, framed_packet);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
@@ -289,15 +332,24 @@ impl SendWithoutResponse for Client {
|
||||
));
|
||||
};
|
||||
|
||||
let channel_capacity = sender.channel.max_capacity();
|
||||
let channel_available = sender.channel.capacity();
|
||||
let channel_used = channel_capacity - channel_available;
|
||||
|
||||
let sending_res = sender.channel.try_send(framed_packet);
|
||||
drop(sender);
|
||||
|
||||
sending_res.map_err(|err| {
|
||||
match err {
|
||||
TrySendError::Full(_) => {
|
||||
debug!("Connection to {address} seems to not be able to handle all the traffic - dropping the current packet");
|
||||
// it's not a 'big' error, but we did not manage to send the packet
|
||||
// if the queue is full, we can't really do anything but to drop the packet
|
||||
warn!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "full_dropped",
|
||||
channel_capacity,
|
||||
channel_used,
|
||||
"dropping packet: connection buffer to {address} is full ({channel_used}/{channel_capacity})"
|
||||
);
|
||||
io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"connection queue is full",
|
||||
@@ -305,11 +357,13 @@ impl SendWithoutResponse for Client {
|
||||
}
|
||||
TrySendError::Closed(dropped) => {
|
||||
debug!(
|
||||
"Connection to {address} seems to be dead. attempting to re-establish it...",
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "closed_reconnecting",
|
||||
channel_capacity,
|
||||
channel_used,
|
||||
"connection to {address} dead, attempting re-establishment"
|
||||
);
|
||||
|
||||
// it's not a 'big' error, but we did not manage to send the packet, but queue
|
||||
// it up to send it as soon as the connection is re-established
|
||||
self.make_connection(address, dropped);
|
||||
io::Error::new(
|
||||
io::ErrorKind::ConnectionAborted,
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
# Multi-stage Dockerfile for Nym localnet
|
||||
# Stage 1: Build binaries
|
||||
# Stage 2: Slim runtime with only the final binaries
|
||||
# Single-stage Dockerfile for Nym localnet
|
||||
# Builds: nym-node, nym-network-requester, nym-socks5-client
|
||||
# Target: Apple Container Runtime with host networking
|
||||
|
||||
# --- Build stage ---
|
||||
FROM rust:latest AS builder
|
||||
FROM rust:latest
|
||||
|
||||
WORKDIR /usr/src/nym
|
||||
COPY ./ ./
|
||||
|
||||
ENV CARGO_BUILD_JOBS=8
|
||||
|
||||
RUN cargo build --release --locked -p nym-node --features otel && \
|
||||
cargo build --release --locked -p nym-network-requester -p nym-socks5-client
|
||||
# Build all required binaries in release mode
|
||||
RUN cargo build --release --locked \
|
||||
-p nym-node \
|
||||
-p nym-network-requester \
|
||||
-p nym-socks5-client
|
||||
|
||||
# --- Runtime stage ---
|
||||
FROM debian:trixie-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
build-essential \
|
||||
# Install runtime dependencies including Go for wireguard-go
|
||||
RUN apt update && apt install -y \
|
||||
python3 \
|
||||
python3-pip \
|
||||
netcat-openbsd \
|
||||
@@ -26,33 +24,31 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
iproute2 \
|
||||
net-tools \
|
||||
wireguard-tools \
|
||||
golang-go \
|
||||
git \
|
||||
iptables \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Go and build wireguard-go, then clean up
|
||||
ARG TARGETARCH
|
||||
RUN curl -fsSL "https://go.dev/dl/go1.23.6.linux-${TARGETARCH}.tar.gz" \
|
||||
| tar -C /usr/local -xz && \
|
||||
export PATH="/usr/local/go/bin:$PATH" && \
|
||||
git clone https://git.zx2c4.com/wireguard-go && \
|
||||
# Install wireguard-go (userspace WireGuard implementation)
|
||||
RUN git clone https://git.zx2c4.com/wireguard-go && \
|
||||
cd wireguard-go && \
|
||||
make && \
|
||||
cp wireguard-go /usr/local/bin/ && \
|
||||
cd .. && \
|
||||
rm -rf wireguard-go /usr/local/go && \
|
||||
apt-get purge -y --auto-remove build-essential curl
|
||||
rm -rf wireguard-go
|
||||
|
||||
# Install Python dependencies for build_topology.py
|
||||
RUN pip3 install --break-system-packages base58
|
||||
|
||||
# Copy only the compiled binaries from the builder stage
|
||||
COPY --from=builder /usr/src/nym/target/release/nym-node /usr/local/bin/
|
||||
COPY --from=builder /usr/src/nym/target/release/nym-network-requester /usr/local/bin/
|
||||
COPY --from=builder /usr/src/nym/target/release/nym-socks5-client /usr/local/bin/
|
||||
# Move binaries to /usr/local/bin for easy access
|
||||
RUN cp target/release/nym-node /usr/local/bin/ && \
|
||||
cp target/release/nym-network-requester /usr/local/bin/ && \
|
||||
cp target/release/nym-socks5-client /usr/local/bin/
|
||||
|
||||
# Copy supporting scripts
|
||||
COPY ./docker/localnet/build_topology.py /usr/local/bin/
|
||||
|
||||
WORKDIR /nym
|
||||
|
||||
# Default command
|
||||
CMD ["nym-node", "--help"]
|
||||
|
||||
+37
-128
@@ -1,71 +1,35 @@
|
||||
# Nym Localnet
|
||||
# Nym Localnet for Kata Container Runtimes
|
||||
|
||||
A complete Nym mixnet test environment with OpenTelemetry instrumentation.
|
||||
Supports both Docker Desktop and Apple Container Runtime on macOS.
|
||||
A complete Nym mixnet test environment running on Apple's container runtime for macOS (for now).
|
||||
|
||||
## Overview
|
||||
|
||||
This localnet setup provides a fully functional Nym mixnet for local development and testing:
|
||||
- **3 mixnodes** (layer 1, 2, 3)
|
||||
- **2 gateways** (entry + exit mode)
|
||||
- **1 gateway** (entry + exit mode)
|
||||
- **1 network-requester** (service provider)
|
||||
- **1 SOCKS5 client**
|
||||
- **OpenTelemetry tracing** via OTLP/gRPC to SigNoz (or any OTLP collector)
|
||||
|
||||
All components run in isolated containers with proper networking and dynamic IP resolution.
|
||||
When the `otel` feature is enabled (default), every nym-node exports traces covering
|
||||
the full packet lifecycle: ingress, Sphinx processing, forwarding, and final-hop delivery.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required
|
||||
- **macOS** (tested on macOS Sequoia 15.0+)
|
||||
- **Docker Desktop** (recommended) or **Apple Container Runtime**
|
||||
- **Apple Container Runtime** - Built into macOS
|
||||
- **Docker Desktop** (for building images only)
|
||||
- **Python 3** with `base58` library
|
||||
|
||||
### SigNoz (for trace viewing)
|
||||
|
||||
SigNoz is an open-source APM that receives and visualises OpenTelemetry data.
|
||||
Install it locally with Docker Compose -- this takes about 2 minutes:
|
||||
|
||||
```bash
|
||||
# Clone the SigNoz repository
|
||||
git clone -b main https://github.com/SigNoz/signoz.git ~/signoz
|
||||
cd ~/signoz/deploy
|
||||
|
||||
# Start SigNoz (runs ClickHouse, otel-collector, query-service, frontend)
|
||||
docker compose up -d
|
||||
|
||||
# Verify it is running
|
||||
docker ps --filter "name=signoz" --format "table {{.Names}}\t{{.Status}}"
|
||||
```
|
||||
|
||||
Once running:
|
||||
- **SigNoz UI**: http://localhost:8080
|
||||
- **OTLP gRPC collector**: localhost:4317 (used by nym-nodes)
|
||||
- **OTLP HTTP collector**: localhost:4318
|
||||
|
||||
The localnet script auto-detects the SigNoz Docker network (`signoz-net`) and
|
||||
routes OTel traffic directly to the collector container -- no manual endpoint
|
||||
configuration needed.
|
||||
|
||||
To stop SigNoz later:
|
||||
```bash
|
||||
cd ~/signoz/deploy && docker compose down
|
||||
```
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Install Python dependencies
|
||||
pip3 install --break-system-packages base58
|
||||
|
||||
# Verify Docker is installed
|
||||
docker --version
|
||||
```
|
||||
|
||||
If using Apple Container Runtime instead of Docker:
|
||||
```bash
|
||||
# Verify container runtime is available
|
||||
container --version
|
||||
|
||||
# Verify Docker is installed (for building)
|
||||
docker --version
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
@@ -118,17 +82,7 @@ Ports published to host:
|
||||
- 20001-20005 → Verloc ports
|
||||
- 30001-30005 → HTTP APIs
|
||||
- 41264/41265 → LP control ports (registration)
|
||||
- 51822/51823 → WireGuard tunnel ports (gateway/gateway2; only used when WireGuard is enabled)
|
||||
|
||||
### WireGuard and privileges
|
||||
|
||||
By default, gateways run with **WireGuard disabled** (`--wireguard-enabled false`). No elevated capabilities are required: the script does not use `--cap-add=NET_ADMIN` or `--device /dev/net/tun`, so localnet runs without net admin privileges and is suitable for mixnet packet testing and SOCKS5 over the mixnet.
|
||||
|
||||
To enable WireGuard VPN routing in localnet (e.g. for two-hop VPN tests), set `WIREGUARD_ENABLED=1` before starting. The script will then add `--cap-add=NET_ADMIN` and `--device /dev/net/tun` to the gateway containers and configure IP forwarding and NAT. This may not work in all Docker environments (e.g. some hosted runners restrict capabilities).
|
||||
|
||||
```bash
|
||||
WIREGUARD_ENABLED=1 ./localnet.sh start
|
||||
```
|
||||
- 51822/51823 → WireGuard tunnel ports (gateway/gateway2)
|
||||
|
||||
### Startup Flow
|
||||
|
||||
@@ -244,99 +198,54 @@ container logs nym-gateway --follow
|
||||
### Status
|
||||
```bash
|
||||
# List all containers
|
||||
docker ps --filter "name=nym-" --format "table {{.Names}}\t{{.Status}}"
|
||||
container list
|
||||
|
||||
# Check specific container
|
||||
docker logs nym-gateway
|
||||
container logs nym-gateway
|
||||
|
||||
# Inspect network
|
||||
docker network inspect nym-localnet-network
|
||||
container network inspect nym-localnet-network
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Basic SOCKS5 Test
|
||||
```bash
|
||||
# Simple HTTP request through the mixnet
|
||||
curl -x socks5h://127.0.0.1:1080 https://httpbin.org/get
|
||||
# Simple HTTP request with redirect following
|
||||
curl -L --socks5 localhost:1080 http://example.com
|
||||
|
||||
# HTTPS request
|
||||
curl -x socks5h://127.0.0.1:1080 https://nymtech.net
|
||||
curl -L --socks5 localhost:1080 https://nymtech.net
|
||||
|
||||
# Download a file
|
||||
curl -x socks5h://127.0.0.1:1080 \
|
||||
curl -L --socks5 localhost:1080 \
|
||||
https://test-download-files-nym.s3.amazonaws.com/download-files/1MB.zip \
|
||||
--output /tmp/test.zip
|
||||
```
|
||||
|
||||
### Load Testing
|
||||
|
||||
A load test script is included to generate sustained traffic and populate SigNoz
|
||||
with meaningful trace data:
|
||||
|
||||
```bash
|
||||
# Default: 10 concurrent workers, 60 seconds
|
||||
./loadtest.sh
|
||||
|
||||
# Heavier load: 20 workers for 2 minutes
|
||||
./loadtest.sh -c 20 -d 120
|
||||
|
||||
# Light single-threaded test
|
||||
./loadtest.sh -c 1 -d 10
|
||||
|
||||
# Target a specific URL
|
||||
./loadtest.sh -c 5 -d 30 -u https://httpbin.org/bytes/4096
|
||||
```
|
||||
|
||||
The script reports live progress, then prints a summary with request counts,
|
||||
throughput, and latency percentiles (p50/p95/p99).
|
||||
|
||||
### Verify Network Topology
|
||||
```bash
|
||||
# View the generated topology
|
||||
docker exec nym-gateway cat /localnet/network.json | jq .
|
||||
container exec nym-gateway cat /localnet/network.json | jq .
|
||||
|
||||
# Check container status
|
||||
docker ps --filter "name=nym-" --format "table {{.Names}}\t{{.Status}}"
|
||||
# Check container IPs
|
||||
container list | grep nym-
|
||||
|
||||
# Verify all bonding files exist
|
||||
docker exec nym-gateway ls -la /localnet/
|
||||
container exec nym-gateway ls -la /localnet/
|
||||
```
|
||||
|
||||
### Test Mixnet Routing
|
||||
```bash
|
||||
# All traffic flows through: client -> gateway -> mix1 -> mix2 -> mix3 -> gateway -> internet
|
||||
# All traffic flows through: client → mix1 → mix2 → mix3 → gateway → internet
|
||||
# Watch logs to verify routing:
|
||||
docker logs nym-mixnode1 --follow &
|
||||
docker logs nym-mixnode2 --follow &
|
||||
docker logs nym-mixnode3 --follow &
|
||||
docker logs nym-gateway --follow &
|
||||
container logs nym-mixnode1 --follow &
|
||||
container logs nym-mixnode2 --follow &
|
||||
container logs nym-mixnode3 --follow &
|
||||
container logs nym-gateway --follow &
|
||||
|
||||
# Make a request
|
||||
curl -x socks5h://127.0.0.1:1080 https://nymtech.net
|
||||
```
|
||||
|
||||
## OpenTelemetry
|
||||
|
||||
OTel is enabled by default. Each nym-node exports traces via OTLP/gRPC covering
|
||||
packet ingress, Sphinx processing, forwarding, and final-hop delivery.
|
||||
|
||||
### Viewing Traces
|
||||
|
||||
- **SigNoz UI**: http://localhost:8080 -- filter by `serviceName = nym-node`
|
||||
- **Terminal report** (queries ClickHouse directly, no login needed):
|
||||
|
||||
```bash
|
||||
./otel-report.sh # last 15 minutes
|
||||
./otel-report.sh 60 # last 60 minutes
|
||||
./otel-report.sh live # auto-refresh every 10s
|
||||
```
|
||||
|
||||
### Disabling OTel
|
||||
|
||||
```bash
|
||||
OTEL_ENABLE=0 ./localnet.sh start # disable
|
||||
OTEL_ENDPOINT=http://my-collector:4317 ./localnet.sh start # custom collector
|
||||
curl -L --socks5 localhost:1080 https://nymtech.com
|
||||
```
|
||||
|
||||
### LP (Lewes Protocol) Testing
|
||||
@@ -380,11 +289,8 @@ This makes localnet perfect for rapid LP protocol development and testing.
|
||||
docker/localnet/
|
||||
├── README.md # This file
|
||||
├── localnet.sh # Main orchestration script
|
||||
├── loadtest.sh # Load test / traffic generator
|
||||
├── otel-report.sh # Terminal-based OTel metrics report
|
||||
├── Dockerfile.localnet # Multi-stage Docker image (builder + slim runtime)
|
||||
├── build_topology.py # Topology generator
|
||||
└── localnet-logs.sh # Tmux-based multi-container log viewer
|
||||
├── Dockerfile.localnet # Docker image definition
|
||||
└── build_topology.py # Topology generator
|
||||
```
|
||||
|
||||
## How It Works
|
||||
@@ -674,11 +580,14 @@ start_mixnode 4 "$MIXNODE4_CONTAINER"
|
||||
|
||||
### Container Runtime
|
||||
|
||||
**Docker Desktop** is the default and recommended runtime; no extra setup is required for mixnet testing.
|
||||
Apple's container runtime is a native macOS container system:
|
||||
- Uses Virtualization.framework for isolation
|
||||
- Lightweight VMs for each container
|
||||
- Native macOS integration
|
||||
- Separate image store from Docker
|
||||
- Natively uses [Kata Containers](https://github.com/kata-containers/kata-containers) images
|
||||
|
||||
**Apple Container Runtime** is an optional alternative on macOS. It natively uses [Kata Containers](https://github.com/kata-containers/kata-containers) images and is only required if you use `container` instead of Docker (e.g. for consistency with other Apple tooling). Kata is also the path that provides a kernel with `CONFIG_TUN=y` if you need TUN/WireGuard inside containers under the Apple runtime.
|
||||
|
||||
### Initial setup for [Container Runtime](https://github.com/apple/container) (optional)
|
||||
### Initial setup for [Container Runtime](https://github.com/apple/container)
|
||||
|
||||
- **MUST** have MacOS Tahoe for inter-container networking
|
||||
- `brew install --cask container`
|
||||
@@ -722,7 +631,7 @@ Both are ephemeral by default (cleaned up on stop).
|
||||
- **No Docker Compose**: Uses custom orchestration script
|
||||
- **Dynamic IPs**: Container IPs may change between restarts
|
||||
- **Port conflicts**: Cannot run alongside services using same ports
|
||||
- **TUN device**: Only required when `WIREGUARD_ENABLED=1`; otherwise gateways run without it
|
||||
- **TUN device**: Gateway requires `ip` command for network interfaces
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Nym Localnet Load Test
|
||||
# Generates sustained traffic through the mixnet SOCKS5 proxy to produce
|
||||
# OTel traces and exercise the packet pipeline end-to-end.
|
||||
#
|
||||
# Usage:
|
||||
# ./loadtest.sh # defaults: 10 concurrent, 60s, mixed sizes
|
||||
# ./loadtest.sh -c 20 -d 120 # 20 concurrent, 120s
|
||||
# ./loadtest.sh -s 64k # fixed 64KB responses (many Sphinx fragments)
|
||||
# ./loadtest.sh -s 1k -c 5 -d 30 # small payloads, 5 workers
|
||||
#
|
||||
# Payload sizes (-s flag) map to Sphinx packet fragmentation:
|
||||
# 1k = ~1 Sphinx packet (sub-MTU, minimal fragmentation)
|
||||
# 4k = ~2-3 packets (small payload)
|
||||
# 16k = ~8-10 packets (medium payload)
|
||||
# 64k = ~32-35 packets (large payload, stresses forwarding)
|
||||
# 256k = ~128-130 packets (heavy payload, stresses queues)
|
||||
# 1m = ~512 packets (very heavy, potential backpressure)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Localnet running (./localnet.sh start)
|
||||
# - SOCKS5 proxy available on localhost:1080
|
||||
|
||||
set -e
|
||||
|
||||
CONCURRENCY=10
|
||||
DURATION=60
|
||||
PROXY="socks5h://127.0.0.1:1080"
|
||||
PAYLOAD_SIZE=""
|
||||
CUSTOM_URL=""
|
||||
STATS_INTERVAL=5
|
||||
|
||||
# Default targets: mixed sizes for general testing
|
||||
TARGETS=(
|
||||
"https://httpbin.org/get"
|
||||
"https://httpbin.org/bytes/1024"
|
||||
"https://httpbin.org/delay/1"
|
||||
"https://example.com"
|
||||
"https://nym.com"
|
||||
)
|
||||
|
||||
# Convert human-readable size to bytes for httpbin
|
||||
parse_size() {
|
||||
local s
|
||||
s=$(echo "$1" | tr '[:upper:]' '[:lower:]')
|
||||
local num
|
||||
num=$(echo "$s" | sed 's/[a-z]*$//')
|
||||
case "$s" in
|
||||
*m|*mb) echo $(( num * 1024 * 1024 )) ;;
|
||||
*k|*kb) echo $(( num * 1024 )) ;;
|
||||
*) echo "$num" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [-c concurrency] [-d duration_secs] [-s payload_size] [-u url] [-p proxy]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -c Number of concurrent workers (default: $CONCURRENCY)"
|
||||
echo " -d Test duration in seconds (default: $DURATION)"
|
||||
echo " -s Response payload size: 1k, 4k, 16k, 64k, 256k, 1m (default: mixed)"
|
||||
echo " -u Custom target URL (overrides -s and default targets)"
|
||||
echo " -p SOCKS5 proxy address (default: $PROXY)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # 10 workers, 60s, mixed targets/sizes"
|
||||
echo " $0 -s 1k # small payloads (~1 Sphinx packet each)"
|
||||
echo " $0 -s 64k -c 5 # large payloads, 5 workers"
|
||||
echo " $0 -s 256k -c 2 -d 30 # very large payloads, observe queue pressure"
|
||||
echo " $0 -c 20 -d 120 # heavier concurrency, 2 minutes"
|
||||
exit 0
|
||||
}
|
||||
|
||||
while getopts "c:d:s:u:p:h" opt; do
|
||||
case $opt in
|
||||
c) CONCURRENCY=$OPTARG ;;
|
||||
d) DURATION=$OPTARG ;;
|
||||
s) PAYLOAD_SIZE=$OPTARG ;;
|
||||
u) CUSTOM_URL=$OPTARG ;;
|
||||
p) PROXY=$OPTARG ;;
|
||||
h) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_err() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
# Build sized URL if -s was specified
|
||||
SIZED_URL=""
|
||||
SIZE_LABEL="mixed"
|
||||
if [ -n "$PAYLOAD_SIZE" ]; then
|
||||
PAYLOAD_BYTES=$(parse_size "$PAYLOAD_SIZE")
|
||||
SIZED_URL="https://httpbin.org/bytes/${PAYLOAD_BYTES}"
|
||||
SIZE_LABEL="${PAYLOAD_SIZE} (~${PAYLOAD_BYTES} bytes)"
|
||||
fi
|
||||
|
||||
# Preflight checks
|
||||
if ! nc -z 127.0.0.1 1080 2>/dev/null; then
|
||||
log_err "SOCKS5 proxy not reachable on localhost:1080. Is the localnet running?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Counters (written to temp files for cross-process aggregation)
|
||||
STATS_DIR=$(mktemp -d)
|
||||
cleanup() {
|
||||
kill $(jobs -p) 2>/dev/null || true
|
||||
rm -rf "$STATS_DIR"
|
||||
}
|
||||
trap cleanup INT TERM EXIT
|
||||
|
||||
pick_url() {
|
||||
if [ -n "$CUSTOM_URL" ]; then
|
||||
echo "$CUSTOM_URL"
|
||||
elif [ -n "$PAYLOAD_SIZE" ]; then
|
||||
echo "$SIZED_URL"
|
||||
else
|
||||
local idx=$((RANDOM % ${#TARGETS[@]}))
|
||||
echo "${TARGETS[$idx]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Millisecond timestamp (works on both GNU and BSD/macOS date)
|
||||
now_ms() {
|
||||
python3 -c 'import time; print(int(time.time()*1000))'
|
||||
}
|
||||
|
||||
# Worker function: runs requests in a loop until duration expires
|
||||
worker() {
|
||||
local id=$1
|
||||
local end_time=$2
|
||||
local ok=0
|
||||
local fail=0
|
||||
|
||||
while [ "$(date +%s)" -lt "$end_time" ]; do
|
||||
local url
|
||||
url=$(pick_url)
|
||||
local start_ms
|
||||
start_ms=$(now_ms)
|
||||
|
||||
if curl -x "$PROXY" -m 15 -sf -o /dev/null -w "" "$url" 2>/dev/null; then
|
||||
ok=$((ok + 1))
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
|
||||
local end_ms
|
||||
end_ms=$(now_ms)
|
||||
local latency=$((end_ms - start_ms))
|
||||
|
||||
echo "$latency" >> "$STATS_DIR/latencies_${id}.txt"
|
||||
done
|
||||
|
||||
echo "$ok" > "$STATS_DIR/ok_${id}.txt"
|
||||
echo "$fail" > "$STATS_DIR/fail_${id}.txt"
|
||||
}
|
||||
|
||||
echo ""
|
||||
log_info "=== Nym Localnet Load Test ==="
|
||||
log_info "Concurrency: $CONCURRENCY workers"
|
||||
log_info "Duration: ${DURATION}s"
|
||||
log_info "Payload: $SIZE_LABEL"
|
||||
if [ -n "$CUSTOM_URL" ]; then
|
||||
log_info "Target: $CUSTOM_URL"
|
||||
elif [ -n "$PAYLOAD_SIZE" ]; then
|
||||
log_info "Target: $SIZED_URL"
|
||||
else
|
||||
log_info "Targets: ${#TARGETS[@]} rotating URLs"
|
||||
fi
|
||||
log_info "Proxy: $PROXY"
|
||||
echo ""
|
||||
|
||||
# Quick connectivity check
|
||||
log_info "Preflight: testing SOCKS5 proxy..."
|
||||
if curl -x "$PROXY" -m 15 -sf -o /dev/null "https://httpbin.org/get"; then
|
||||
log_ok "SOCKS5 proxy is working"
|
||||
else
|
||||
log_err "SOCKS5 proxy test failed. Check localnet status."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
END_TIME=$(( $(date +%s) + DURATION ))
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
log_info "Starting $CONCURRENCY workers for ${DURATION}s..."
|
||||
echo ""
|
||||
|
||||
for i in $(seq 1 "$CONCURRENCY"); do
|
||||
worker "$i" "$END_TIME" &
|
||||
done
|
||||
|
||||
# Progress reporter (counts completed latency entries as a proxy for request count)
|
||||
while [ "$(date +%s)" -lt "$END_TIME" ]; do
|
||||
sleep "$STATS_INTERVAL"
|
||||
elapsed=$(( $(date +%s) - START_TIME ))
|
||||
remaining=$(( END_TIME - $(date +%s) ))
|
||||
if [ "$remaining" -lt 0 ]; then remaining=0; fi
|
||||
|
||||
total=0
|
||||
for f in "$STATS_DIR"/latencies_*.txt; do
|
||||
if [ -f "$f" ]; then
|
||||
count=$(wc -l < "$f" 2>/dev/null || echo 0)
|
||||
total=$((total + count))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$elapsed" -gt 0 ]; then
|
||||
rps=$(echo "scale=1; $total / $elapsed" | bc 2>/dev/null || echo "?")
|
||||
else
|
||||
rps="?"
|
||||
fi
|
||||
|
||||
printf "\r [%3ds / %3ds] requests: %d | ~%s req/s | remaining: %ds " \
|
||||
"$elapsed" "$DURATION" "$total" "$rps" "$remaining"
|
||||
done
|
||||
|
||||
echo ""
|
||||
log_info "Waiting for workers to finish..."
|
||||
wait 2>/dev/null || true
|
||||
|
||||
# Final stats
|
||||
echo ""
|
||||
log_info "=== Results ==="
|
||||
total_ok=0
|
||||
total_fail=0
|
||||
all_latencies=""
|
||||
|
||||
for f in "$STATS_DIR"/ok_*.txt; do
|
||||
[ -f "$f" ] && total_ok=$((total_ok + $(cat "$f" 2>/dev/null || echo 0)))
|
||||
done
|
||||
for f in "$STATS_DIR"/fail_*.txt; do
|
||||
[ -f "$f" ] && total_fail=$((total_fail + $(cat "$f" 2>/dev/null || echo 0)))
|
||||
done
|
||||
for f in "$STATS_DIR"/latencies_*.txt; do
|
||||
[ -f "$f" ] && all_latencies="$all_latencies $(cat "$f" 2>/dev/null | tr '\n' ' ')"
|
||||
done
|
||||
|
||||
total=$((total_ok + total_fail))
|
||||
actual_duration=$(( $(date +%s) - START_TIME ))
|
||||
|
||||
echo ""
|
||||
echo " Total requests: $total"
|
||||
echo " Successful: $total_ok"
|
||||
echo " Failed: $total_fail"
|
||||
if [ "$actual_duration" -gt 0 ]; then
|
||||
rps=$(echo "scale=2; $total / $actual_duration" | bc 2>/dev/null || echo "?")
|
||||
echo " Duration: ${actual_duration}s"
|
||||
echo " Throughput: ~${rps} req/s"
|
||||
fi
|
||||
|
||||
if [ -n "$all_latencies" ]; then
|
||||
sorted=$(echo "$all_latencies" | tr ' ' '\n' | sort -n | grep -v '^$')
|
||||
count=$(echo "$sorted" | wc -l | tr -d ' ')
|
||||
if [ "$count" -gt 0 ]; then
|
||||
p50_idx=$(( count * 50 / 100 ))
|
||||
p95_idx=$(( count * 95 / 100 ))
|
||||
p99_idx=$(( count * 99 / 100 ))
|
||||
[ "$p50_idx" -lt 1 ] && p50_idx=1
|
||||
[ "$p95_idx" -lt 1 ] && p95_idx=1
|
||||
[ "$p99_idx" -lt 1 ] && p99_idx=1
|
||||
|
||||
min_lat=$(echo "$sorted" | head -1)
|
||||
max_lat=$(echo "$sorted" | tail -1)
|
||||
p50=$(echo "$sorted" | sed -n "${p50_idx}p")
|
||||
p95=$(echo "$sorted" | sed -n "${p95_idx}p")
|
||||
p99=$(echo "$sorted" | sed -n "${p99_idx}p")
|
||||
|
||||
echo ""
|
||||
echo " Latency (ms):"
|
||||
echo " min: ${min_lat}ms"
|
||||
echo " p50: ${p50}ms"
|
||||
echo " p95: ${p95}ms"
|
||||
echo " p99: ${p99}ms"
|
||||
echo " max: ${max_lat}ms"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ "$total_fail" -gt 0 ] && [ "$total" -gt 0 ]; then
|
||||
fail_pct=$(echo "scale=1; $total_fail * 100 / $total" | bc 2>/dev/null || echo "?")
|
||||
log_warn "Failure rate: ${fail_pct}% -- ${total_fail} of ${total} failed"
|
||||
else
|
||||
log_ok "All requests succeeded"
|
||||
fi
|
||||
echo ""
|
||||
log_info "View traces in SigNoz: http://localhost:8080/traces"
|
||||
log_info "Filter by service: nym-node"
|
||||
echo ""
|
||||
@@ -5,13 +5,6 @@
|
||||
|
||||
SESSION_NAME="nym-localnet-logs"
|
||||
|
||||
# Detect runtime
|
||||
if command -v container &> /dev/null; then
|
||||
RUNTIME="container"
|
||||
else
|
||||
RUNTIME="docker"
|
||||
fi
|
||||
|
||||
# Container names
|
||||
CONTAINERS=(
|
||||
"nym-mixnode1"
|
||||
@@ -24,9 +17,9 @@ CONTAINERS=(
|
||||
|
||||
# Check if containers are running
|
||||
running_containers=()
|
||||
for ctr in "${CONTAINERS[@]}"; do
|
||||
if $RUNTIME inspect "$ctr" &>/dev/null; then
|
||||
running_containers+=("$ctr")
|
||||
for container in "${CONTAINERS[@]}"; do
|
||||
if container inspect "$container" &>/dev/null; then
|
||||
running_containers+=("$container")
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -39,11 +32,11 @@ fi
|
||||
# Check if we're already in tmux
|
||||
if [ -n "$TMUX" ]; then
|
||||
# Inside tmux - create new window
|
||||
tmux new-window -n "logs" "$RUNTIME logs -f ${running_containers[0]}"
|
||||
tmux new-window -n "logs" "container logs -f ${running_containers[0]}"
|
||||
|
||||
# Split for remaining containers
|
||||
for ((i=1; i<${#running_containers[@]}; i++)); do
|
||||
tmux split-window -t logs "$RUNTIME logs -f ${running_containers[$i]}"
|
||||
tmux split-window -t logs "container logs -f ${running_containers[$i]}"
|
||||
tmux select-layout -t logs tiled
|
||||
done
|
||||
|
||||
@@ -55,11 +48,11 @@ else
|
||||
exec tmux attach-session -t "$SESSION_NAME"
|
||||
else
|
||||
# Create new session
|
||||
tmux new-session -d -s "$SESSION_NAME" -n "logs" "$RUNTIME logs -f ${running_containers[0]}"
|
||||
tmux new-session -d -s "$SESSION_NAME" -n "logs" "container logs -f ${running_containers[0]}"
|
||||
|
||||
# Split for remaining containers
|
||||
for ((i=1; i<${#running_containers[@]}; i++)); do
|
||||
tmux split-window -t "$SESSION_NAME:logs" "$RUNTIME logs -f ${running_containers[$i]}"
|
||||
tmux split-window -t "$SESSION_NAME:logs" "container logs -f ${running_containers[$i]}"
|
||||
tmux select-layout -t "$SESSION_NAME:logs" tiled
|
||||
done
|
||||
|
||||
|
||||
+77
-140
@@ -2,8 +2,8 @@
|
||||
|
||||
set -ex
|
||||
|
||||
# Nym Localnet Orchestration Script
|
||||
# Supports both Docker and Apple Container Runtime
|
||||
# Nym Localnet Orchestration Script for Apple Container Runtime
|
||||
# Emulates docker-compose functionality
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
@@ -14,54 +14,6 @@ NYM_VOLUME_PATH="/tmp/nym-localnet-home-$$"
|
||||
|
||||
SUFFIX=${NYM_NODE_SUFFIX:-localnet}
|
||||
|
||||
# Detect container runtime: prefer Apple 'container' if available, fall back to docker
|
||||
if command -v container &> /dev/null; then
|
||||
RUNTIME="container"
|
||||
HOST_INTERNAL="host.containers.internal"
|
||||
else
|
||||
RUNTIME="docker"
|
||||
HOST_INTERNAL="host.docker.internal"
|
||||
fi
|
||||
|
||||
# WireGuard: set to 1 only if you need VPN routing in localnet (requires NET_ADMIN and /dev/net/tun).
|
||||
# Default 0: mixnet-only, no elevated capabilities required.
|
||||
WIREGUARD_ENABLED=${WIREGUARD_ENABLED:-0}
|
||||
|
||||
# OpenTelemetry configuration
|
||||
# Set OTEL_ENABLE=1 to enable OTel tracing on all nym-node instances.
|
||||
# OTEL_ENDPOINT should point to the OTLP gRPC collector reachable from containers.
|
||||
# When SigNoz runs in Docker (signoz-net), we route to its collector directly.
|
||||
OTEL_ENABLE=${OTEL_ENABLE:-1}
|
||||
if [ -z "${OTEL_ENDPOINT:-}" ]; then
|
||||
SIGNOZ_NET=$(docker network ls --filter name=signoz-net --format '{{.Name}}' 2>/dev/null || true)
|
||||
if [ "$RUNTIME" = "docker" ] && [ -n "$SIGNOZ_NET" ]; then
|
||||
OTEL_ENDPOINT="http://signoz-otel-collector:4317"
|
||||
OTEL_SIGNOZ_NET="$SIGNOZ_NET"
|
||||
else
|
||||
OTEL_ENDPOINT="http://${HOST_INTERNAL}:4317"
|
||||
OTEL_SIGNOZ_NET=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build OTel flags for nym-node run commands
|
||||
otel_flags() {
|
||||
if [ "$OTEL_ENABLE" = "1" ]; then
|
||||
echo "--otel --otel-endpoint $OTEL_ENDPOINT"
|
||||
fi
|
||||
}
|
||||
|
||||
# WireGuard capability flags for gateway containers (only when WIREGUARD_ENABLED=1)
|
||||
wireguard_cap_args() {
|
||||
if [ "$WIREGUARD_ENABLED" = "1" ]; then
|
||||
echo "--cap-add=NET_ADMIN --device /dev/net/tun"
|
||||
fi
|
||||
}
|
||||
|
||||
# --wireguard-enabled value for nym-node
|
||||
wireguard_flag() {
|
||||
[ "$WIREGUARD_ENABLED" = "1" ] && echo "true" || echo "false"
|
||||
}
|
||||
|
||||
# Container names
|
||||
INIT_CONTAINER="nym-localnet-init"
|
||||
MIXNODE1_CONTAINER="nym-mixnode1"
|
||||
@@ -112,13 +64,13 @@ cleanup_host_state() {
|
||||
done
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
# Check if container command exists
|
||||
check_prerequisites() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker not found"
|
||||
if ! command -v container &> /dev/null; then
|
||||
log_error "Apple 'container' command not found"
|
||||
log_error "Install from: https://github.com/apple/container"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Using runtime: $RUNTIME"
|
||||
}
|
||||
|
||||
# Build the Docker image
|
||||
@@ -128,6 +80,7 @@ build_image() {
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Build with Docker
|
||||
log_info "Building with Docker..."
|
||||
if ! docker build \
|
||||
-f "$SCRIPT_DIR/Dockerfile.localnet" \
|
||||
@@ -137,24 +90,30 @@ build_image() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If using Apple container runtime, transfer image from Docker
|
||||
if [ "$RUNTIME" = "container" ]; then
|
||||
log_info "Transferring image to Apple container runtime..."
|
||||
TEMP_IMAGE="/tmp/nym-localnet-image-$$.tar"
|
||||
if ! docker save -o "$TEMP_IMAGE" "$IMAGE_NAME"; then
|
||||
log_error "Failed to save Docker image"
|
||||
exit 1
|
||||
fi
|
||||
if ! container image load --input "$TEMP_IMAGE"; then
|
||||
rm -f "$TEMP_IMAGE"
|
||||
log_error "Failed to load image into container runtime"
|
||||
exit 1
|
||||
fi
|
||||
# Transfer image to container runtime
|
||||
log_info "Transferring image to container runtime..."
|
||||
|
||||
# Save to temporary file (container image load doesn't support stdin)
|
||||
TEMP_IMAGE="/tmp/nym-localnet-image-$$.tar"
|
||||
if ! docker save -o "$TEMP_IMAGE" "$IMAGE_NAME"; then
|
||||
log_error "Failed to save Docker image"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load into container runtime from file
|
||||
if ! container image load --input "$TEMP_IMAGE"; then
|
||||
rm -f "$TEMP_IMAGE"
|
||||
if ! container image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||
log_error "Image not found in container runtime after load"
|
||||
exit 1
|
||||
fi
|
||||
log_error "Failed to load image into container runtime"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up temporary file
|
||||
rm -f "$TEMP_IMAGE"
|
||||
|
||||
# Verify image is available
|
||||
if ! container image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||
log_error "Image not found in container runtime after load"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Image built and loaded: $IMAGE_NAME"
|
||||
@@ -196,7 +155,7 @@ NETWORK_NAME="nym-localnet-network"
|
||||
# Create container network
|
||||
create_network() {
|
||||
log_info "Creating container network: $NETWORK_NAME"
|
||||
if $RUNTIME network create "$NETWORK_NAME" 2>/dev/null; then
|
||||
if container network create "$NETWORK_NAME" 2>/dev/null; then
|
||||
log_success "Network created: $NETWORK_NAME"
|
||||
else
|
||||
log_info "Network $NETWORK_NAME already exists or creation failed"
|
||||
@@ -205,9 +164,9 @@ create_network() {
|
||||
|
||||
# Remove container network
|
||||
remove_network() {
|
||||
if $RUNTIME network list | grep -q "$NETWORK_NAME"; then
|
||||
if container network list | grep -q "$NETWORK_NAME"; then
|
||||
log_info "Removing network: $NETWORK_NAME"
|
||||
$RUNTIME network rm "$NETWORK_NAME" 2>/dev/null || true
|
||||
container network rm "$NETWORK_NAME" 2>/dev/null || true
|
||||
log_success "Network removed"
|
||||
fi
|
||||
}
|
||||
@@ -224,10 +183,7 @@ start_mixnode() {
|
||||
local verloc_port="2000${node_id}"
|
||||
local http_port="3000${node_id}"
|
||||
|
||||
local otel_args
|
||||
otel_args=$(otel_flags)
|
||||
|
||||
$RUNTIME run \
|
||||
container run \
|
||||
--name "$container_name" \
|
||||
-m 2G \
|
||||
--network "$NETWORK_NAME" \
|
||||
@@ -259,7 +215,7 @@ start_mixnode() {
|
||||
sleep 2;
|
||||
done;
|
||||
echo "Starting mix'"${node_id}"'...";
|
||||
exec nym-node '"${otel_args}"' run --id mix'"${node_id}"'-localnet --unsafe-disable-replay-protection --local
|
||||
exec nym-node run --id mix'"${node_id}"'-localnet --unsafe-disable-replay-protection --local
|
||||
'
|
||||
|
||||
log_success "$container_name started"
|
||||
@@ -268,14 +224,9 @@ start_mixnode() {
|
||||
start_gateway() {
|
||||
log_info "Starting $GATEWAY_CONTAINER..."
|
||||
|
||||
local otel_args wg_flag
|
||||
otel_args=$(otel_flags)
|
||||
wg_flag=$(wireguard_flag)
|
||||
|
||||
$RUNTIME run \
|
||||
container run \
|
||||
--name "$GATEWAY_CONTAINER" \
|
||||
-m 2G \
|
||||
$(wireguard_cap_args) \
|
||||
--network "$NETWORK_NAME" \
|
||||
-p 9000:9000 \
|
||||
-p 10004:10004 \
|
||||
@@ -304,9 +255,11 @@ start_gateway() {
|
||||
--http-bind-address=0.0.0.0:30004 \
|
||||
--http-access-token=lala \
|
||||
--public-ips $CONTAINER_IP \
|
||||
--enable-lp true \
|
||||
--lp-use-mock-ecash true \
|
||||
--output=json \
|
||||
--wireguard-enabled '"$wg_flag"' \
|
||||
--wireguard-enabled true \
|
||||
--wireguard-userspace true \
|
||||
--bonding-information-output="/localnet/gateway.json";
|
||||
|
||||
echo "Waiting for network.json...";
|
||||
@@ -314,7 +267,7 @@ start_gateway() {
|
||||
sleep 2;
|
||||
done;
|
||||
echo "Starting gateway with LP listener (mock ecash)...";
|
||||
exec nym-node '"${otel_args}"' run --id gateway-localnet --unsafe-disable-replay-protection --local --wireguard-enabled '"$wg_flag"' --lp-use-mock-ecash true
|
||||
exec nym-node run --id gateway-localnet --unsafe-disable-replay-protection --local --wireguard-enabled true --wireguard-userspace true --lp-use-mock-ecash true
|
||||
'
|
||||
|
||||
log_success "$GATEWAY_CONTAINER started"
|
||||
@@ -338,14 +291,9 @@ start_gateway() {
|
||||
start_gateway2() {
|
||||
log_info "Starting $GATEWAY2_CONTAINER..."
|
||||
|
||||
local otel_args wg_flag
|
||||
otel_args=$(otel_flags)
|
||||
wg_flag=$(wireguard_flag)
|
||||
|
||||
$RUNTIME run \
|
||||
container run \
|
||||
--name "$GATEWAY2_CONTAINER" \
|
||||
-m 2G \
|
||||
$(wireguard_cap_args) \
|
||||
--network "$NETWORK_NAME" \
|
||||
-p 9001:9001 \
|
||||
-p 10005:10005 \
|
||||
@@ -374,9 +322,11 @@ start_gateway2() {
|
||||
--http-bind-address=0.0.0.0:30005 \
|
||||
--http-access-token=lala \
|
||||
--public-ips $CONTAINER_IP \
|
||||
--enable-lp true \
|
||||
--lp-use-mock-ecash true \
|
||||
--output=json \
|
||||
--wireguard-enabled '"$wg_flag"' \
|
||||
--wireguard-enabled true \
|
||||
--wireguard-userspace true \
|
||||
--bonding-information-output="/localnet/gateway2.json";
|
||||
|
||||
echo "Waiting for network.json...";
|
||||
@@ -384,7 +334,7 @@ start_gateway2() {
|
||||
sleep 2;
|
||||
done;
|
||||
echo "Starting gateway2 with LP listener (mock ecash)...";
|
||||
exec nym-node '"${otel_args}"' run --id gateway2-localnet --unsafe-disable-replay-protection --local --wireguard-enabled '"$wg_flag"' --lp-use-mock-ecash true
|
||||
exec nym-node run --id gateway2-localnet --unsafe-disable-replay-protection --local --wireguard-enabled true --wireguard-userspace true --lp-use-mock-ecash true
|
||||
'
|
||||
|
||||
log_success "$GATEWAY2_CONTAINER started"
|
||||
@@ -408,12 +358,12 @@ start_gateway2() {
|
||||
start_network_requester() {
|
||||
log_info "Starting $REQUESTER_CONTAINER..."
|
||||
|
||||
# Get gateway IP address (first IP only, in case container has multiple networks)
|
||||
# Get gateway IP address
|
||||
log_info "Getting gateway IP address..."
|
||||
GATEWAY_IP=$($RUNTIME exec "$GATEWAY_CONTAINER" hostname -i | awk '{print $1}')
|
||||
GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i)
|
||||
log_info "Gateway IP: $GATEWAY_IP"
|
||||
|
||||
$RUNTIME run \
|
||||
container run \
|
||||
--name "$REQUESTER_CONTAINER" \
|
||||
--network "$NETWORK_NAME" \
|
||||
-v "$VOLUME_PATH:/localnet" \
|
||||
@@ -448,7 +398,7 @@ start_network_requester() {
|
||||
start_socks5_client() {
|
||||
log_info "Starting $SOCKS5_CONTAINER..."
|
||||
|
||||
$RUNTIME run \
|
||||
container run \
|
||||
--name "$SOCKS5_CONTAINER" \
|
||||
--network "$NETWORK_NAME" \
|
||||
-p 1080:1080 \
|
||||
@@ -501,15 +451,15 @@ stop_containers() {
|
||||
log_info "Stopping all containers..."
|
||||
|
||||
for container_name in "${ALL_CONTAINERS[@]}"; do
|
||||
if $RUNTIME inspect "$container_name" &>/dev/null; then
|
||||
if container inspect "$container_name" &>/dev/null; then
|
||||
log_info "Stopping $container_name"
|
||||
$RUNTIME stop "$container_name" 2>/dev/null || true
|
||||
$RUNTIME rm "$container_name" 2>/dev/null || true
|
||||
container stop "$container_name" 2>/dev/null || true
|
||||
container rm "$container_name" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Also clean up init container if it exists
|
||||
$RUNTIME rm "$INIT_CONTAINER" 2>/dev/null || true
|
||||
container rm "$INIT_CONTAINER" 2>/dev/null || true
|
||||
|
||||
log_success "All containers stopped"
|
||||
|
||||
@@ -517,7 +467,7 @@ stop_containers() {
|
||||
remove_network
|
||||
}
|
||||
|
||||
# Show $RUNTIME logs
|
||||
# Show container logs
|
||||
show_logs() {
|
||||
local container_name=${1:-}
|
||||
|
||||
@@ -528,8 +478,8 @@ show_logs() {
|
||||
fi
|
||||
|
||||
# Show logs for specific container
|
||||
if $RUNTIME inspect "$container_name" &>/dev/null; then
|
||||
$RUNTIME logs -f "$container_name"
|
||||
if container inspect "$container_name" &>/dev/null; then
|
||||
container logs -f "$container_name"
|
||||
else
|
||||
log_error "Container not found: $container_name"
|
||||
log_info "Available containers:"
|
||||
@@ -546,8 +496,8 @@ show_status() {
|
||||
echo ""
|
||||
|
||||
for container_name in "${ALL_CONTAINERS[@]}"; do
|
||||
if $RUNTIME inspect "$container_name" &>/dev/null; then
|
||||
local status=$($RUNTIME inspect "$container_name" 2>/dev/null | grep -o '"Status":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
|
||||
if container inspect "$container_name" &>/dev/null; then
|
||||
local status=$(container inspect "$container_name" 2>/dev/null | grep -o '"Status":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
|
||||
echo -e " ${GREEN}●${NC} $container_name - $status"
|
||||
else
|
||||
echo -e " ${RED}○${NC} $container_name - not running"
|
||||
@@ -602,13 +552,13 @@ build_topology() {
|
||||
log_success " $file created"
|
||||
done
|
||||
|
||||
# Get container IPs (first IP only, containers may be on multiple networks)
|
||||
# Get container IPs
|
||||
log_info "Getting container IP addresses..."
|
||||
MIX1_IP=$($RUNTIME exec "$MIXNODE1_CONTAINER" hostname -i | awk '{print $1}')
|
||||
MIX2_IP=$($RUNTIME exec "$MIXNODE2_CONTAINER" hostname -i | awk '{print $1}')
|
||||
MIX3_IP=$($RUNTIME exec "$MIXNODE3_CONTAINER" hostname -i | awk '{print $1}')
|
||||
GATEWAY_IP=$($RUNTIME exec "$GATEWAY_CONTAINER" hostname -i | awk '{print $1}')
|
||||
GATEWAY2_IP=$($RUNTIME exec "$GATEWAY2_CONTAINER" hostname -i | awk '{print $1}')
|
||||
MIX1_IP=$(container exec "$MIXNODE1_CONTAINER" hostname -i)
|
||||
MIX2_IP=$(container exec "$MIXNODE2_CONTAINER" hostname -i)
|
||||
MIX3_IP=$(container exec "$MIXNODE3_CONTAINER" hostname -i)
|
||||
GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i)
|
||||
GATEWAY2_IP=$(container exec "$GATEWAY2_CONTAINER" hostname -i)
|
||||
|
||||
log_info "Container IPs:"
|
||||
echo " mix1: $MIX1_IP"
|
||||
@@ -618,7 +568,7 @@ build_topology() {
|
||||
echo " gateway2: $GATEWAY2_IP"
|
||||
|
||||
# Run build_topology.py in a container with access to the volumes
|
||||
$RUNTIME run \
|
||||
container run \
|
||||
--name "nym-localnet-topology-builder" \
|
||||
--network "$NETWORK_NAME" \
|
||||
-v "$VOLUME_PATH:/localnet" \
|
||||
@@ -657,33 +607,20 @@ start_all() {
|
||||
start_mixnode 3 "$MIXNODE3_CONTAINER"
|
||||
start_gateway
|
||||
start_gateway2
|
||||
|
||||
# Connect nym containers to SigNoz network for direct OTLP routing
|
||||
if [ -n "${OTEL_SIGNOZ_NET:-}" ]; then
|
||||
log_info "Connecting containers to SigNoz network ($OTEL_SIGNOZ_NET)..."
|
||||
for c in "$MIXNODE1_CONTAINER" "$MIXNODE2_CONTAINER" "$MIXNODE3_CONTAINER" \
|
||||
"$GATEWAY_CONTAINER" "$GATEWAY2_CONTAINER"; do
|
||||
docker network connect "$OTEL_SIGNOZ_NET" "$c" 2>/dev/null && \
|
||||
log_success " $c connected to $OTEL_SIGNOZ_NET" || true
|
||||
done
|
||||
fi
|
||||
|
||||
build_topology
|
||||
|
||||
# Configure networking for WireGuard VPN routing only when WIREGUARD_ENABLED=1
|
||||
if [ "$WIREGUARD_ENABLED" = "1" ]; then
|
||||
log_info "Configuring gateway networking (IP forwarding, NAT) for WireGuard..."
|
||||
for gw in "$GATEWAY_CONTAINER" "$GATEWAY2_CONTAINER"; do
|
||||
if $RUNTIME exec "$gw" sh -c "
|
||||
echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null
|
||||
iptables-legacy -t nat -A POSTROUTING -o eth0 -j MASQUERADE 2>/dev/null
|
||||
" 2>/dev/null; then
|
||||
log_success "Configured $gw"
|
||||
else
|
||||
log_warn "Could not configure NAT on $gw. WireGuard VPN routing may not work."
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Configure networking for two-hop WireGuard routing on both gateways
|
||||
# Note: Runs after build_topology to ensure gateways have finished WireGuard setup
|
||||
log_info "Configuring gateway networking (IP forwarding, NAT)..."
|
||||
for gw in "$GATEWAY_CONTAINER" "$GATEWAY2_CONTAINER"; do
|
||||
container exec "$gw" sh -c "
|
||||
# Enable IP forwarding
|
||||
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||
# Add NAT masquerade for outbound traffic
|
||||
iptables-legacy -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
"
|
||||
log_success "Configured $gw"
|
||||
done
|
||||
|
||||
start_network_requester
|
||||
start_socks5_client
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Nym Localnet OTel Report
|
||||
# Queries ClickHouse directly to produce a terminal-based summary of
|
||||
# the core metrics captured by the OTel-instrumented nym-nodes.
|
||||
#
|
||||
# Usage:
|
||||
# ./otel-report.sh # last 15 minutes
|
||||
# ./otel-report.sh 60 # last 60 minutes
|
||||
# ./otel-report.sh live # live mode: refresh every 10s
|
||||
#
|
||||
# Prerequisites: localnet + SigNoz running
|
||||
|
||||
set -e
|
||||
|
||||
CH_CONTAINER="signoz-clickhouse"
|
||||
TRACES_TABLE="signoz_traces.distributed_signoz_index_v3"
|
||||
LOOKBACK_MIN=${1:-15}
|
||||
LIVE=false
|
||||
|
||||
if [ "$1" = "live" ]; then
|
||||
LIVE=true
|
||||
LOOKBACK_MIN=5
|
||||
fi
|
||||
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
|
||||
ch() {
|
||||
docker exec "$CH_CONTAINER" clickhouse-client --query "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
divider() {
|
||||
echo -e "${DIM}$(printf '%.0s-' {1..78})${NC}"
|
||||
}
|
||||
|
||||
print_report() {
|
||||
local window="$1"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD} Nym Localnet -- OTel Packet Pipeline Report${NC}"
|
||||
echo -e " ${DIM}Window: last ${window} minutes | $(date '+%Y-%m-%d %H:%M:%S')${NC}"
|
||||
divider
|
||||
|
||||
# 1. Throughput per operation
|
||||
echo -e "\n${BOLD} [1] Packet Throughput (packets/sec by operation)${NC}\n"
|
||||
ch "
|
||||
SELECT
|
||||
name AS operation,
|
||||
count(*) AS total,
|
||||
round(count(*) / (${window} * 60), 1) AS per_sec
|
||||
FROM ${TRACES_TABLE}
|
||||
WHERE timestamp >= now() - INTERVAL ${window} MINUTE
|
||||
AND serviceName = 'nym-node'
|
||||
AND name IN (
|
||||
'handle_received_nym_packet',
|
||||
'mixnode.sphinx_full_unwrap',
|
||||
'mixnode.forward_packet',
|
||||
'mixnode.final_hop'
|
||||
)
|
||||
GROUP BY name
|
||||
ORDER BY total DESC
|
||||
FORMAT PrettyCompactNoEscapes
|
||||
"
|
||||
|
||||
divider
|
||||
|
||||
# 2. Latency per operation
|
||||
echo -e "\n${BOLD} [2] Processing Latency (milliseconds)${NC}\n"
|
||||
ch "
|
||||
SELECT
|
||||
name AS operation,
|
||||
round(quantile(0.50)(duration_nano / 1e6), 3) AS p50_ms,
|
||||
round(quantile(0.95)(duration_nano / 1e6), 3) AS p95_ms,
|
||||
round(quantile(0.99)(duration_nano / 1e6), 3) AS p99_ms,
|
||||
round(quantile(0.999)(duration_nano / 1e6), 3) AS p999_ms,
|
||||
round(avg(duration_nano / 1e6), 3) AS avg_ms
|
||||
FROM ${TRACES_TABLE}
|
||||
WHERE timestamp >= now() - INTERVAL ${window} MINUTE
|
||||
AND serviceName = 'nym-node'
|
||||
AND name IN (
|
||||
'handle_received_nym_packet',
|
||||
'mixnode.sphinx_full_unwrap',
|
||||
'mixnode.forward_packet',
|
||||
'mixnode.final_hop'
|
||||
)
|
||||
AND duration_nano < 60000000000
|
||||
GROUP BY name
|
||||
ORDER BY p50_ms DESC
|
||||
FORMAT PrettyCompactNoEscapes
|
||||
"
|
||||
|
||||
divider
|
||||
|
||||
# 3. Error rate
|
||||
echo -e "\n${BOLD} [3] Error Rate${NC}\n"
|
||||
local errors
|
||||
errors=$(ch "
|
||||
SELECT
|
||||
name,
|
||||
countIf(has_error = true) AS errors,
|
||||
count(*) AS total,
|
||||
round(100.0 * countIf(has_error = true) / count(*), 3) AS error_pct
|
||||
FROM ${TRACES_TABLE}
|
||||
WHERE timestamp >= now() - INTERVAL ${window} MINUTE
|
||||
AND serviceName = 'nym-node'
|
||||
AND name IN (
|
||||
'handle_received_nym_packet',
|
||||
'mixnode.sphinx_full_unwrap',
|
||||
'mixnode.forward_packet',
|
||||
'mixnode.final_hop'
|
||||
)
|
||||
GROUP BY name
|
||||
HAVING errors > 0
|
||||
ORDER BY errors DESC
|
||||
FORMAT PrettyCompactNoEscapes
|
||||
")
|
||||
|
||||
if [ -z "$errors" ]; then
|
||||
echo -e " ${GREEN}No errors detected across all operations${NC}"
|
||||
else
|
||||
echo "$errors"
|
||||
fi
|
||||
|
||||
divider
|
||||
|
||||
# 4. Forwarding ratio (are packets being dropped between stages?)
|
||||
echo -e "\n${BOLD} [4] Pipeline Funnel (packet drop detection)${NC}\n"
|
||||
ch "
|
||||
SELECT
|
||||
name AS stage,
|
||||
count(*) AS packets,
|
||||
round(100.0 * count(*) / max(total_ingress), 1) AS pct_of_ingress
|
||||
FROM ${TRACES_TABLE}
|
||||
CROSS JOIN (
|
||||
SELECT count(*) AS total_ingress
|
||||
FROM ${TRACES_TABLE}
|
||||
WHERE timestamp >= now() - INTERVAL ${window} MINUTE
|
||||
AND serviceName = 'nym-node'
|
||||
AND name = 'handle_received_nym_packet'
|
||||
) AS t
|
||||
WHERE timestamp >= now() - INTERVAL ${window} MINUTE
|
||||
AND serviceName = 'nym-node'
|
||||
AND name IN (
|
||||
'handle_received_nym_packet',
|
||||
'mixnode.sphinx_full_unwrap',
|
||||
'mixnode.forward_packet',
|
||||
'mixnode.final_hop'
|
||||
)
|
||||
GROUP BY name
|
||||
ORDER BY packets DESC
|
||||
FORMAT PrettyCompactNoEscapes
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo -e " ${DIM}Expected ratios: sphinx_unwrap ~ 100%, forward ~ 75% (3 of 4 hops forward),${NC}"
|
||||
echo -e " ${DIM}final_hop ~ 25% (1 of 4 hops is the last one). Significantly lower = drops.${NC}"
|
||||
|
||||
divider
|
||||
|
||||
# 5. Throughput over time (1-minute buckets)
|
||||
echo -e "\n${BOLD} [5] Throughput Timeline (1-min buckets, ingress packets)${NC}\n"
|
||||
ch "
|
||||
SELECT
|
||||
toStartOfMinute(timestamp) AS minute,
|
||||
count(*) AS packets,
|
||||
round(count(*) / 60, 1) AS per_sec
|
||||
FROM ${TRACES_TABLE}
|
||||
WHERE timestamp >= now() - INTERVAL ${window} MINUTE
|
||||
AND serviceName = 'nym-node'
|
||||
AND name = 'handle_received_nym_packet'
|
||||
GROUP BY minute
|
||||
ORDER BY minute
|
||||
FORMAT PrettyCompactNoEscapes
|
||||
"
|
||||
|
||||
divider
|
||||
|
||||
# 6. Latency spikes (potential TCP congestion / backpressure indicators)
|
||||
echo -e "\n${BOLD} [6] Latency Spikes (sphinx_unwrap p99 per minute)${NC}\n"
|
||||
ch "
|
||||
SELECT
|
||||
toStartOfMinute(timestamp) AS minute,
|
||||
round(quantile(0.99)(duration_nano / 1e6), 3) AS p99_ms,
|
||||
round(quantile(0.50)(duration_nano / 1e6), 3) AS p50_ms,
|
||||
round(p99_ms / greatest(p50_ms, 0.001), 1) AS spike_ratio,
|
||||
count(*) AS samples
|
||||
FROM ${TRACES_TABLE}
|
||||
WHERE timestamp >= now() - INTERVAL ${window} MINUTE
|
||||
AND serviceName = 'nym-node'
|
||||
AND name = 'mixnode.sphinx_full_unwrap'
|
||||
GROUP BY minute
|
||||
ORDER BY minute
|
||||
FORMAT PrettyCompactNoEscapes
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo -e " ${DIM}spike_ratio > 10x suggests backpressure or queue buildup.${NC}"
|
||||
echo -e " ${DIM}Sustained high p99 across minutes may indicate TCP meltdown.${NC}"
|
||||
|
||||
divider
|
||||
echo ""
|
||||
echo -e " ${BLUE}SigNoz UI:${NC} http://localhost:8080"
|
||||
echo -e " ${DIM}Traces tab -> Filter: serviceName = nym-node${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
if [ "$LIVE" = "true" ]; then
|
||||
while true; do
|
||||
clear
|
||||
print_report "$LOOKBACK_MIN"
|
||||
echo -e " ${DIM}Refreshing in 10s... (Ctrl+C to stop)${NC}"
|
||||
sleep 10
|
||||
done
|
||||
else
|
||||
print_report "$LOOKBACK_MIN"
|
||||
fi
|
||||
@@ -56,6 +56,39 @@ pnpm run build
|
||||
## CI/CD
|
||||
- **Link checking**: Runs on every push to `documentation/docs/` via `.github/workflows/ci-docs-linkcheck.yml`
|
||||
|
||||
## SEO & Structured Data
|
||||
### Frontmatter
|
||||
Every `.mdx` page supports frontmatter fields that control meta tags, Open Graph, and JSON-LD schema:
|
||||
```yaml
|
||||
---
|
||||
title: "Page Title for Search Engines"
|
||||
description: "Unique meta description for this page."
|
||||
schemaType: "TechArticle" # TechArticle (default), HowTo, or FAQPage
|
||||
section: "Operators" # Operators, Developers, Network, APIs
|
||||
lastUpdated: "2026-02-11" # Feeds dateModified schema
|
||||
breadcrumbLabel: "Custom Label" # Optional, overrides URL slug in breadcrumbs
|
||||
---
|
||||
```
|
||||
|
||||
### Sitemap
|
||||
```bash
|
||||
npx next-sitemap
|
||||
```
|
||||
Outputs `sitemap.xml` and `robots.txt` to `/public`.
|
||||
|
||||
### Environment Variable
|
||||
Set in production:
|
||||
```
|
||||
NEXT_PUBLIC_SITE_URL=https://nymtech.net/docs
|
||||
```
|
||||
|
||||
### Schema Types
|
||||
| Type | Use When |
|
||||
|------|----------|
|
||||
| TechArticle | Reference docs, config guides, overviews (default) |
|
||||
| HowTo | Step-by-step install/setup guides |
|
||||
| FAQPage | Question-answer pages |
|
||||
|
||||
## Licensing and copyright information
|
||||
This is a monorepo and components that make up Nym as a system are licensed individually, so for accurate information, please check individual files.
|
||||
|
||||
|
||||
@@ -1,86 +1,273 @@
|
||||
```tsx copy filename="mixFetchExample.tsx"
|
||||
import React, { useState } from "react";
|
||||
```tsx
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { mixFetch } from "@nymproject/mix-fetch-full-fat";
|
||||
import { mixFetch, createMixFetch } from "@nymproject/mix-fetch-full-fat";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat";
|
||||
|
||||
const defaultUrl = "https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
|
||||
const defaultUrl =
|
||||
"https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
|
||||
const args = { mode: "unsafe-ignore-cors" };
|
||||
|
||||
const mixFetchOptions: SetupMixFetchOps = {
|
||||
preferredGateway: "2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW", // with WSS
|
||||
// preferredNetworkRequester:
|
||||
// "CTDxrcXgrZHWyCWnuCgjpJPghQUcEVz1HkhUr5mGdFnT.3UAww1YWNyVNYNWFQL1LaHYouQtDiXBGK5GiDZgpXkTK@2RFtU5BwxvJJXagAWAEuaPgb5ZVPRoy2542TT93Edw6v",
|
||||
clientId: "docs-mixfetch-demo", // explicit ID
|
||||
preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1",
|
||||
mixFetchOverride: {
|
||||
requestTimeoutMs: 60_000,
|
||||
},
|
||||
forceTls: true, // force WSS
|
||||
};
|
||||
|
||||
// Log entry type for the visible log panel
|
||||
type LogLevel = "info" | "error" | "send" | "receive";
|
||||
type LogEntry = { timestamp: string; message: string; level: LogLevel };
|
||||
|
||||
const logColors: Record<LogLevel, string> = {
|
||||
info: "gray",
|
||||
error: "red",
|
||||
send: "blue",
|
||||
receive: "green",
|
||||
};
|
||||
|
||||
const logLabels: Record<LogLevel, string> = {
|
||||
info: "INFO",
|
||||
error: "ERROR",
|
||||
send: "SEND",
|
||||
receive: "RECV",
|
||||
};
|
||||
|
||||
export const MixFetch = () => {
|
||||
// MixFetch initialization state
|
||||
const [status, setStatus] = useState<"idle" | "starting" | "ready" | "error">("idle");
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
// Log panel state
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const logEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Single fetch state
|
||||
const [url, setUrl] = useState<string>(defaultUrl);
|
||||
const [html, setHtml] = useState<string>();
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
|
||||
// Concurrent fetch state
|
||||
const [concurrentResults, setConcurrentResults] = useState<string[]>([]);
|
||||
const [concurrentBusy, setConcurrentBusy] = useState<boolean>(false);
|
||||
|
||||
// Auto-scroll log panel to bottom
|
||||
useEffect(() => {
|
||||
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [logs]);
|
||||
|
||||
// Helper to add a timestamped log entry
|
||||
const addLog = (message: string, level: LogLevel) => {
|
||||
const timestamp = new Date().toISOString().substring(11, 23);
|
||||
setLogs((prev) => [...prev, { timestamp, message, level }]);
|
||||
};
|
||||
|
||||
// Initialize MixFetch explicitly via createMixFetch
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
setStatus("starting");
|
||||
setErrorMsg(null);
|
||||
addLog("Starting MixFetch...", "info");
|
||||
await createMixFetch(mixFetchOptions);
|
||||
setStatus("ready");
|
||||
addLog("MixFetch is ready!", "info");
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setStatus("error");
|
||||
setErrorMsg(msg);
|
||||
addLog(`Error: ${msg}`, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Single URL fetch — reuses the existing MixFetch singleton
|
||||
const handleFetch = async () => {
|
||||
try {
|
||||
setBusy(true);
|
||||
setHtml(undefined);
|
||||
addLog(`Sending request to ${url}...`, "send");
|
||||
const response = await mixFetch(url, args, mixFetchOptions);
|
||||
console.log(response);
|
||||
const resHtml = await response.text();
|
||||
setHtml(resHtml);
|
||||
addLog(`Response received (${resHtml.length} bytes)`, "receive");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
addLog(`Fetch error: ${msg}`, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Send 5 concurrent requests to different URLs on the same domain
|
||||
const handleConcurrentFetch = async () => {
|
||||
const baseUrl = "https://jsonplaceholder.typicode.com/posts/";
|
||||
const count = 5;
|
||||
try {
|
||||
setConcurrentBusy(true);
|
||||
setConcurrentResults([]);
|
||||
addLog(
|
||||
`Starting ${count} concurrent requests to ${baseUrl}1-${count}...`,
|
||||
"send",
|
||||
);
|
||||
// Fire off all requests concurrently using Promise.all
|
||||
const requests = Array.from({ length: count }, (_, i) => {
|
||||
const targetUrl = `${baseUrl}${i + 1}`;
|
||||
return mixFetch(targetUrl, args, mixFetchOptions)
|
||||
.then((res) => res.json())
|
||||
.then((json: { id: number; title: string }) => {
|
||||
const entry = `[${json.id}] ${json.title}`;
|
||||
addLog(entry, "receive");
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
const results = await Promise.all(requests);
|
||||
setConcurrentResults(results);
|
||||
addLog(`All ${count} concurrent requests completed!`, "info");
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
addLog(`Concurrent fetch error: ${msg}`, "error");
|
||||
} finally {
|
||||
setConcurrentBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isReady = status === "ready";
|
||||
|
||||
const statusText = {
|
||||
idle: "Not started",
|
||||
starting: "Starting...",
|
||||
ready: "Ready",
|
||||
error: `Error: ${errorMsg}`,
|
||||
};
|
||||
const statusColor = {
|
||||
idle: "gray",
|
||||
starting: "orange",
|
||||
ready: "green",
|
||||
error: "red",
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Stack direction="row">
|
||||
<TextField
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
label="URL"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
defaultValue={defaultUrl}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={busy}
|
||||
sx={{ marginLeft: "1rem" }}
|
||||
onClick={handleFetch}
|
||||
>
|
||||
Fetch
|
||||
</Button>
|
||||
</Stack>
|
||||
{/* Start MixFetch */}
|
||||
<Paper sx={{ p: 2, mb: 2 }} variant="outlined">
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={status === "starting" || status === "ready"}
|
||||
onClick={handleStart}
|
||||
>
|
||||
Start MixFetch
|
||||
</Button>
|
||||
{status === "starting" && <CircularProgress size={20} />}
|
||||
<Typography
|
||||
fontFamily="monospace"
|
||||
fontSize="small"
|
||||
sx={{ color: statusColor[status] }}
|
||||
>
|
||||
{statusText[status]}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{busy && (
|
||||
<Box mt={4}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{html && (
|
||||
<>
|
||||
<Box mt={4}>
|
||||
<strong>Response</strong>
|
||||
{/* Fetch controls — disabled until MixFetch is ready */}
|
||||
<Box
|
||||
sx={{
|
||||
opacity: isReady ? 1 : 0.5,
|
||||
pointerEvents: isReady ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
{/* Single fetch */}
|
||||
<Stack direction="row">
|
||||
<TextField
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
label="URL"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
defaultValue={defaultUrl}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={busy}
|
||||
sx={{ marginLeft: "1rem" }}
|
||||
onClick={handleFetch}
|
||||
>
|
||||
Fetch
|
||||
</Button>
|
||||
</Stack>
|
||||
{busy && (
|
||||
<Box mt={2}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
|
||||
<Typography fontFamily="monospace" fontSize="small">
|
||||
{html}
|
||||
</Typography>
|
||||
)}
|
||||
{html && (
|
||||
<>
|
||||
<Box mt={2}>
|
||||
<strong>Response</strong>
|
||||
</Box>
|
||||
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
|
||||
<Typography fontFamily="monospace" fontSize="small">
|
||||
{html}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Concurrent fetch */}
|
||||
<Box mt={3}>
|
||||
<strong>Concurrent Requests</strong>
|
||||
<Box mt={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={concurrentBusy}
|
||||
onClick={handleConcurrentFetch}
|
||||
>
|
||||
Send 5 Concurrent Requests (posts/1-5)
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{concurrentBusy && (
|
||||
<Box mt={2}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{concurrentResults.length > 0 && (
|
||||
<Paper sx={{ p: 2, mt: 2 }} elevation={4}>
|
||||
{concurrentResults.map((result, i) => (
|
||||
<Typography key={i} fontFamily="monospace" fontSize="small">
|
||||
{result}
|
||||
</Typography>
|
||||
))}
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Log Panel */}
|
||||
{logs.length > 0 && (
|
||||
<Paper
|
||||
sx={{ p: 2, mt: 3, maxHeight: 200, overflow: "auto" }}
|
||||
variant="outlined"
|
||||
>
|
||||
<strong>Log</strong>
|
||||
{logs.map((entry, i) => (
|
||||
<Typography
|
||||
key={i}
|
||||
fontFamily="monospace"
|
||||
fontSize="small"
|
||||
sx={{ color: logColors[entry.level] }}
|
||||
>
|
||||
{entry.timestamp} [{logLabels[entry.level]}] {entry.message}
|
||||
</Typography>
|
||||
))}
|
||||
<div ref={logEndRef} />
|
||||
</Paper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -51,8 +51,8 @@ export const LandingPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box margin={"0 auto"}>
|
||||
<Typography variant="h2" mb={6}>
|
||||
<Box margin={"0 auto"} textAlign="center">
|
||||
{/*<Typography variant="h2" mb={6}>
|
||||
Nym Docs
|
||||
</Typography>
|
||||
|
||||
@@ -62,70 +62,54 @@ export const LandingPage = () => {
|
||||
using blinded, re-randomizable, decentralized credentials. Our goal is
|
||||
to allow developers to build new applications, or upgrade existing apps,
|
||||
with privacy features unavailable in other systems.
|
||||
</Typography>
|
||||
<Grid container border={"1px solid #262626"}>
|
||||
</Typography>*/}
|
||||
<Grid container border={"1px solid #2E3538"}>
|
||||
{squares.map((square, index) => (
|
||||
<Grid
|
||||
item
|
||||
key={index}
|
||||
xs={12}
|
||||
lg={6}
|
||||
sm={6}
|
||||
padding={{ xs: 3, xl: 4 }}
|
||||
width={"100%"}
|
||||
sx={{
|
||||
borderBottom: {
|
||||
xs: index < 3 ? "1px solid #262626" : "none",
|
||||
lg: index === 0 || index === 1 ? "1px solid #262626" : "none",
|
||||
xs: index < 3 ? "1px solid #2E3538" : "none",
|
||||
sm: index === 0 || index === 1 ? "1px solid #2E3538" : "none",
|
||||
},
|
||||
borderRight: {
|
||||
md: index === 0 || index === 2 ? "1px solid #262626" : "none",
|
||||
xs: "none",
|
||||
sm: index === 0 || index === 2 ? "1px solid #2E3538" : "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Link href={square.href} target="_blank" rel="noopener noreferrer">
|
||||
<Link href={square.href}>
|
||||
<Box
|
||||
display={"flex"}
|
||||
gap={{ xs: 3, xl: 4 }}
|
||||
height={"100%"}
|
||||
flexDirection={{ xs: "column", sm: "row" }}
|
||||
alignItems={{ xs: "center" }}
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||||
{square.text}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
textAlign="center"
|
||||
sx={{
|
||||
color: "#909195",
|
||||
}}
|
||||
>
|
||||
{square.description}
|
||||
</Typography>
|
||||
|
||||
<Image
|
||||
src={square.icon}
|
||||
alt={square.text}
|
||||
width={isDesktop ? 180 : isTablet ? 140 : 180}
|
||||
height={isDesktop ? 134 : isTablet ? 90 : 134}
|
||||
/>
|
||||
<Box
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
justifyContent={"space-between"}
|
||||
alignItems={{ xs: "center", sm: "flex-start" }}
|
||||
flexGrow={1}
|
||||
height={"100%"}
|
||||
>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||||
{square.text}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
textAlign={{ xs: "center", sm: "left" }}
|
||||
sx={{
|
||||
color: "#909195",
|
||||
display: {
|
||||
lg: "none",
|
||||
xl: "block",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{square.description}
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ color: "#14E76F", fontWeight: 600 }}>
|
||||
Open
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
</Grid>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { mixFetch } from "@nymproject/mix-fetch-full-fat";
|
||||
import { mixFetch, createMixFetch } from "@nymproject/mix-fetch-full-fat";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat";
|
||||
@@ -12,8 +12,8 @@ import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat";
|
||||
const defaultUrl =
|
||||
"https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
|
||||
const args = { mode: "unsafe-ignore-cors" };
|
||||
|
||||
const mixFetchOptions: SetupMixFetchOps = {
|
||||
clientId: "docs-mixfetch-demo", // explicit ID
|
||||
preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1",
|
||||
mixFetchOverride: {
|
||||
requestTimeoutMs: 60_000,
|
||||
@@ -21,64 +21,260 @@ const mixFetchOptions: SetupMixFetchOps = {
|
||||
forceTls: true, // force WSS
|
||||
};
|
||||
|
||||
// Log entry type for the visible log panel
|
||||
type LogLevel = "info" | "error" | "send" | "receive";
|
||||
type LogEntry = { timestamp: string; message: string; level: LogLevel };
|
||||
|
||||
// Color map for log levels
|
||||
const logColors: Record<LogLevel, string> = {
|
||||
info: "gray",
|
||||
error: "red",
|
||||
send: "blue",
|
||||
receive: "green",
|
||||
};
|
||||
|
||||
// Label map for log levels
|
||||
const logLabels: Record<LogLevel, string> = {
|
||||
info: "INFO",
|
||||
error: "ERROR",
|
||||
send: "SEND",
|
||||
receive: "RECV",
|
||||
};
|
||||
|
||||
export const MixFetch = () => {
|
||||
// MixFetch initialization state
|
||||
const [status, setStatus] = useState<"idle" | "starting" | "ready" | "error">(
|
||||
"idle"
|
||||
);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
// Log panel state
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
|
||||
// Single fetch state
|
||||
const [url, setUrl] = useState<string>(defaultUrl);
|
||||
const [html, setHtml] = useState<string>();
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
|
||||
// Concurrent fetch state
|
||||
const [concurrentResults, setConcurrentResults] = useState<string[]>([]);
|
||||
const [concurrentBusy, setConcurrentBusy] = useState<boolean>(false);
|
||||
|
||||
// Auto-scroll within the log panel when new entries are added (without scrolling the page)
|
||||
const logContainerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (logContainerRef.current) {
|
||||
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
// Helper to add a timestamped log entry
|
||||
const addLog = (message: string, level: LogLevel) => {
|
||||
const timestamp = new Date().toISOString().substring(11, 23); // HH:MM:SS.mmm
|
||||
setLogs((prev) => [...prev, { timestamp, message, level }]);
|
||||
};
|
||||
|
||||
// Initialize MixFetch explicitly via createMixFetch
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
setStatus("starting");
|
||||
setErrorMsg(null);
|
||||
addLog("Starting MixFetch...", "info");
|
||||
await createMixFetch(mixFetchOptions);
|
||||
setStatus("ready");
|
||||
addLog("MixFetch is ready!", "info");
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setStatus("error");
|
||||
setErrorMsg(msg);
|
||||
addLog(`Error: ${msg}`, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Single URL fetch — mixFetch reuses the existing singleton
|
||||
const handleFetch = async () => {
|
||||
try {
|
||||
setBusy(true);
|
||||
setHtml(undefined);
|
||||
addLog(`Sending request to ${url}...`, "send");
|
||||
const response = await mixFetch(url, args, mixFetchOptions);
|
||||
console.log(response);
|
||||
const resHtml = await response.text();
|
||||
setHtml(resHtml);
|
||||
addLog(`Response received (${resHtml.length} bytes)`, "receive");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
addLog(`Fetch error: ${msg}`, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Send 5 concurrent requests to different URLs on the same domain
|
||||
const handleConcurrentFetch = async () => {
|
||||
const baseUrl = "https://jsonplaceholder.typicode.com/posts/";
|
||||
const count = 5;
|
||||
try {
|
||||
setConcurrentBusy(true);
|
||||
setConcurrentResults([]);
|
||||
addLog(
|
||||
`Starting ${count} concurrent requests to ${baseUrl}1-${count}...`,
|
||||
"send"
|
||||
);
|
||||
// Fire off all requests concurrently using Promise.all
|
||||
const requests = Array.from({ length: count }, (_, i) => {
|
||||
const targetUrl = `${baseUrl}${i + 1}`;
|
||||
return mixFetch(targetUrl, args, mixFetchOptions)
|
||||
.then((res) => res.json())
|
||||
.then((json: { id: number; title: string }) => {
|
||||
const entry = `[${json.id}] ${json.title}`;
|
||||
addLog(entry, "receive");
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
const results = await Promise.all(requests);
|
||||
setConcurrentResults(results);
|
||||
addLog(`All ${count} concurrent requests completed!`, "info");
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
addLog(`Concurrent fetch error: ${msg}`, "error");
|
||||
} finally {
|
||||
setConcurrentBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Are fetch controls enabled?
|
||||
const isReady = status === "ready";
|
||||
|
||||
// Status text + color for the startup indicator
|
||||
const statusText: Record<typeof status, string> = {
|
||||
idle: "Not started",
|
||||
starting: "Starting...",
|
||||
ready: "Ready",
|
||||
error: `Error: ${errorMsg}`,
|
||||
};
|
||||
const statusColor: Record<typeof status, string> = {
|
||||
idle: "gray",
|
||||
starting: "orange",
|
||||
ready: "green",
|
||||
error: "red",
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Stack direction="row">
|
||||
<TextField
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
label="URL"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
defaultValue={defaultUrl}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={busy}
|
||||
sx={{ marginLeft: "1rem" }}
|
||||
onClick={handleFetch}
|
||||
>
|
||||
Fetch
|
||||
</Button>
|
||||
</Stack>
|
||||
{/* --- Start MixFetch Section --- */}
|
||||
<Paper sx={{ p: 2, mb: 2 }} variant="outlined">
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={status === "starting" || status === "ready"}
|
||||
onClick={handleStart}
|
||||
>
|
||||
Start MixFetch
|
||||
</Button>
|
||||
{status === "starting" && <CircularProgress size={20} />}
|
||||
<Typography
|
||||
fontFamily="monospace"
|
||||
fontSize="small"
|
||||
sx={{ color: statusColor[status] }}
|
||||
>
|
||||
{statusText[status]}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{busy && (
|
||||
<Box mt={4}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{html && (
|
||||
<>
|
||||
<Box mt={4}>
|
||||
<strong>Response</strong>
|
||||
{/* --- Fetch Controls (disabled until ready) --- */}
|
||||
<Box
|
||||
sx={{
|
||||
opacity: isReady ? 1 : 0.5,
|
||||
pointerEvents: isReady ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
{/* Single fetch */}
|
||||
<Stack direction="row">
|
||||
<TextField
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
label="URL"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
defaultValue={defaultUrl}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={busy}
|
||||
sx={{ marginLeft: "1rem" }}
|
||||
onClick={handleFetch}
|
||||
>
|
||||
Fetch
|
||||
</Button>
|
||||
</Stack>
|
||||
{busy && (
|
||||
<Box mt={2}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
|
||||
<Typography fontFamily="monospace" fontSize="small">
|
||||
{html}
|
||||
</Typography>
|
||||
)}
|
||||
{html && (
|
||||
<>
|
||||
<Box mt={2}>
|
||||
<strong>Response</strong>
|
||||
</Box>
|
||||
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
|
||||
<Typography fontFamily="monospace" fontSize="small">
|
||||
{html}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Concurrent fetch demo */}
|
||||
<Box mt={3}>
|
||||
<strong>Concurrent Requests</strong>
|
||||
<Box mt={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={concurrentBusy}
|
||||
onClick={handleConcurrentFetch}
|
||||
>
|
||||
Send 5 Concurrent Requests (posts/1-5)
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{concurrentBusy && (
|
||||
<Box mt={2}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{concurrentResults.length > 0 && (
|
||||
<Paper sx={{ p: 2, mt: 2 }} elevation={4}>
|
||||
{concurrentResults.map((result, i) => (
|
||||
<Typography key={i} fontFamily="monospace" fontSize="small">
|
||||
{result}
|
||||
</Typography>
|
||||
))}
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* --- Log Panel --- */}
|
||||
{logs.length > 0 && (
|
||||
<Paper
|
||||
ref={logContainerRef}
|
||||
sx={{ p: 2, mt: 3, maxHeight: 200, overflow: "auto" }}
|
||||
variant="outlined"
|
||||
>
|
||||
<strong>Log</strong>
|
||||
{logs.map((entry, i) => (
|
||||
<Typography
|
||||
key={i}
|
||||
fontFamily="monospace"
|
||||
fontSize="small"
|
||||
sx={{ color: logColors[entry.level] }}
|
||||
>
|
||||
{entry.timestamp} [{logLabels[entry.level]}] {entry.message}
|
||||
</Typography>
|
||||
))}
|
||||
</Paper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.87%
|
||||
0.90%
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
33.087
|
||||
31.932
|
||||
|
||||
@@ -1 +1 @@
|
||||
Wednesday, February 11th 2026, 11:35:05 UTC
|
||||
Tuesday, February 24th 2026, 10:08:37 UTC
|
||||
|
||||
@@ -11,7 +11,7 @@ positional arguments:
|
||||
version_count (v, version)
|
||||
Sum of nodes in given version(s)
|
||||
|
||||
options:
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-V, --version show program's version number and exit
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@ usage: Nym-node API check query_stats [-h] [--no_routing_history]
|
||||
positional arguments:
|
||||
id supply nym-node identity key
|
||||
|
||||
options:
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--no_routing_history Display node stats without routing history
|
||||
--no_verloc_metrics Display node stats without verloc metrics
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/** @type {import('next-sitemap').IConfig} */
|
||||
module.exports = {
|
||||
siteUrl: 'https://nymtech.net/docs',
|
||||
generateRobotsTxt: true,
|
||||
outDir: './public',
|
||||
exclude: ['/api/*', '/docs/_*', '/404'],
|
||||
robotsTxtOptions: {
|
||||
policies: [
|
||||
{ userAgent: '*', allow: '/' },
|
||||
{ userAgent: '*', disallow: ['/api/', '/_next/'] },
|
||||
],
|
||||
additionalSitemaps: [
|
||||
'https://nymtech.net/docs/sitemap-docs.xml',
|
||||
],
|
||||
},
|
||||
transform: async (config, path) => ({
|
||||
loc: path,
|
||||
changefreq: path.includes('/changelog')
|
||||
? 'weekly'
|
||||
: path.includes('/docs/operators') || path.includes('/docs/developers')
|
||||
? 'monthly'
|
||||
: 'yearly',
|
||||
priority: path === '/docs' ? 1.0
|
||||
: path.includes('/operators/nodes') || path.includes('/developers') ? 0.8
|
||||
: 0.6,
|
||||
lastmod: new Date().toISOString(),
|
||||
}),
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
"@nextui-org/accordion": "^2.0.40",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
"@nymproject/contract-clients": ">=1.2.4-rc.2 || ^1",
|
||||
"@nymproject/mix-fetch-full-fat": ">=1.5.1-rc.0 || ^1.4.1",
|
||||
"@nymproject/mix-fetch-full-fat": "^1.4.2",
|
||||
"@nymproject/sdk-full-fat": ">=1.5.1-rc.0 || ^1.4.1",
|
||||
"@redocly/cli": "^1.25.15",
|
||||
"@types/mdx": "^2.0.13",
|
||||
@@ -61,6 +61,7 @@
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-next": "13.4.13",
|
||||
"next-sitemap": "4.2.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,11 @@ const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#e67300',
|
||||
main: '#85E89D',
|
||||
},
|
||||
background: {
|
||||
default: '#242B2D',
|
||||
paper: '#2A3235',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym API Reference: Network Infrastructure"
|
||||
description: "Interactive API documentation for Nym network infrastructure. Query node status, network topology, blockchain state & mixnet performance programmatically."
|
||||
schemaType: "TechArticle"
|
||||
section: "APIs"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
This site contains interactive APIs generated from the OpenAPI specs of various API endpoints offered by bits of Nym infrastructure run both by Nym and community operators for both Mainnet and the Sandbox testnet.
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nyx Blockchain & Nym Smart Contracts"
|
||||
description: "Developer guide for interacting with the Nyx blockchain via Cosmos SDK. Covers CLI wallet setup, Cosmos Registry, Ledger Live, and RPC node deployment."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-11"
|
||||
---
|
||||
|
||||
# Interacting with Nyx Chain and Smart Contracts
|
||||
|
||||
There are two options for interacting with the blockchain to send tokens or interact with deployed smart contracts:
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Run a Nyx RPC Node for the Nym Network"
|
||||
description: "Set up and run a dedicated RPC node for the Nyx blockchain. Query network state, serve chain data, and interact with Nym smart contracts programmatically."
|
||||
schemaType: "HowTo"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
# RPC Nodes
|
||||
|
||||
RPC Nodes (which might otherwise be referred to as 'Lite Nodes' or just 'Full Nodes') differ from Validators in that they hold a copy of the Nyx blockchain, but do **not** participate in consensus / block-production.
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
---
|
||||
title: "Nym Developer Portal: SDKs & Tools"
|
||||
description: "Developer documentation for building privacy-enhanced applications on the Nym mixnet. Covers Rust SDK, TypeScript SDK, blockchain interaction & CLI tools."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
Nym's developer documentation covering core concepts of integrating with the Mixnet, interacting with the Nyx blockchain, an overview of the avaliable tools, and our SDK docs.
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "NymVPN CLI: Run NymVPN from the Command Line"
|
||||
description: "Install and run NymVPN from the terminal on Linux, macOS, and Windows. Requires Rust and Go. Includes mnemonic generation and testnet configuration."
|
||||
schemaType: "HowTo"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-11"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Nym VPN CLI
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Rust SDK: Privacy Apps for the Mixnet"
|
||||
description: "Rust SDK reference for building privacy applications on the Nym mixnet. Covers TcpProxy, Mixnet module, Client Pool, FFI bindings, and code examples."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
The Rust SDK allows exposes a few different modules, some more plug and play than others. Each of which handles exposes a Nym Client, which handles finding and using a route for packets through the Mixnet, encryption, and cover traffic, all under the hood.
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Rust SDK: Mixnet Messaging Module"
|
||||
description: "Use the Nym Rust SDK Mixnet module to send messages through the mixnet. Covers builder patterns, custom topologies, SOCKS proxy, and anonymous replies."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
# Mixnet Module
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym TcpProxy: Route TCP via the Mixnet"
|
||||
description: "Route TCP traffic through the Nym mixnet using the TcpProxy Rust module. Covers architecture, single and multi-connection patterns, and troubleshooting."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-11"
|
||||
---
|
||||
|
||||
# TcpProxy Module
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Developer Tools: CLI, Echo & TcpProxy"
|
||||
description: "Overview of Nym developer tools including nym-cli for blockchain interaction, echo server for traffic testing, and standalone TcpProxy binary downloads."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
# Tools
|
||||
|
||||
There are a few tools available to developers for chain interaction: the `nym-cli` tool, which operates as an easier-to-use wrapper around `nyxd`, to allow operators to script interactions with their infrastructure (and those who prefer CLI tools).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"nym-cli": "Nym-cli",
|
||||
"diagnostic-tool": "Diagnostic Tool",
|
||||
"echo-server": "Echo Server",
|
||||
"standalone-tcpproxy": "TcpProxy Binaries (Standalone)"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Steps } from 'nextra/components';
|
||||
|
||||
# Diagnostic Tool
|
||||
|
||||
The Diagnostic Tool is a standalone binary designed to perform various network tests, including DNS, HTTP, and gateway connectivity tests. This tool helps diagnose connectivity issues and provides insights into network performance.
|
||||
|
||||
It’s also possible to run it within the daemon with the same CLI interface.
|
||||
|
||||
## Download Binary
|
||||
|
||||
To get `nym-diagnostic` follow these steps:
|
||||
<Steps>
|
||||
###### 1. Download `nym-vpn-core`
|
||||
- Navigate to [github.com/nymtech/nym-vpn-client/releases](https://github.com/nymtech/nym-vpn-client/releases)
|
||||
- Find latest `nym-vpn-core-<VERSION>`
|
||||
- Download version for your system
|
||||
|
||||
###### 2. Install or extract and make executable
|
||||
|
||||
- If you downloaded `.deb` installer, install it with this command:
|
||||
```sh
|
||||
sudo dpkg -i <FILE_NAME>
|
||||
```
|
||||
|
||||
- If you downloaded `.tar.gz`, in terminal you can extract the file with
|
||||
```sh
|
||||
tar -xvf <FILE_NAME>
|
||||
```
|
||||
|
||||
- Navigate inside the directory and make executable:
|
||||
```sh
|
||||
cd nym-vpn-core-<VERSION>
|
||||
chmod +x ./*
|
||||
```
|
||||
</ Steps>
|
||||
|
||||
## CLI Usage
|
||||
|
||||
The Diagnostic Tool can be executed from the command line interface (CLI). Below are the usage instructions and options available. Read in the chapter [*Tests Performed*](#tests-performed) about the purpose and outcome of these commands.
|
||||
|
||||
### Command Syntax
|
||||
|
||||
```sh
|
||||
./nym-diagnostic [command] [options]
|
||||
./nym-vpnc diagnostic [command] [options]
|
||||
```
|
||||
|
||||
#### `run` command arguments
|
||||
|
||||
The most useful command is `run`, here are the options for that command:
|
||||
|
||||
```sh
|
||||
-h, --help Display help information and exit.
|
||||
--skip-dns Skip the DNS tests
|
||||
--skip-http Skip the HTTP tests
|
||||
--gateway <ID_KEY> Run the gateway connectivity test on the given gateway. Skip those tests if not provided
|
||||
-v, --verbose Enable verbose output for detailed logging.
|
||||
```
|
||||
|
||||
#### `register` command arguments
|
||||
|
||||
Command `register` requires valid credential. Here are the options for that command:
|
||||
|
||||
```sh
|
||||
--gateway <ID_KEY> Register to the given gateway
|
||||
--storage-path Path to the directory containing the credentials database. If it is not valid registration will be skipped.
|
||||
--skip-wireguard Skip Wireguard tests
|
||||
```
|
||||
|
||||
### Command Examples
|
||||
|
||||
|
||||
- Run all tests on a gateway:
|
||||
```sh
|
||||
./nym-diagnostic run --gateway <ID_KEY>
|
||||
```
|
||||
|
||||
- Run the DNS tests only:
|
||||
```sh
|
||||
./nym-diagnostic run --skip-http
|
||||
```
|
||||
|
||||
- Register to a gateway:
|
||||
```sh
|
||||
sudo ./nym-diagnostic register --gateway <ID_KEY> --storage-path /var/lib/nym-vpnd/mainnet
|
||||
# sudo is required to read the database
|
||||
```
|
||||
|
||||
- You can also run DNS and HTTP tests from `nym-vpnc` (installation [here](/developers/nymvpncli)):
|
||||
```sh
|
||||
./nym-vpnc diagnostic run
|
||||
```
|
||||
|
||||
|
||||
## Tests Performed
|
||||
|
||||
The Diagnostic Tool runs the following tests:
|
||||
|
||||
|
||||
### 1. DNS Test
|
||||
|
||||
- **Purpose**: To check the resolution DNS availability.
|
||||
- **Process**: We try to resolve all the domain names present in a given nym network environment with different DNS configurations
|
||||
- **Output**: Displays the resolved IP address and the time taken for the resolution.
|
||||
|
||||
|
||||
### 2. HTTP Test
|
||||
|
||||
- **Purpose**: To verify the accessibility of the NymVPN API.
|
||||
- **Process**: The tool query the `health` endpoint as well as the `nodes/described` endpoint.
|
||||
- **Output**: Displays the response of the `health` endpoint, the time skew and the number of nodes in the network (sanity check)
|
||||
|
||||
### 3. Gateway Test
|
||||
|
||||
- **Purpose**: To check the connectivity to a given gateway.
|
||||
- **Process**: The tool fetches information about the gateway, then establishes a TCP connection, upgrades it to WS and sends a request
|
||||
- **Output**: Display the gateway reported information, the status of the connections and the WS response.
|
||||
|
||||
### 4. Registration Test
|
||||
|
||||
- **Purpose:** To check the correctness of the registration process.
|
||||
- **Process:** The tool tries to build a mixnet client to the provided gateway and then tries to register to the entry authenticator
|
||||
- **Output:** Display the status of the different steps
|
||||
- **Caveat:** This test requires a credential to be spent, which is why it is available as a separate command only
|
||||
|
||||
### 5. Wireguard Test
|
||||
|
||||
- **Purpose:** To check the soundness of a wireguard connection
|
||||
- **Process:** The tool uses the registration data from the previous step to establish a wireguard connection and ping an IP.
|
||||
- **Output:** Display the ping RTTs and any error that might have happened
|
||||
|
||||
## Reports
|
||||
|
||||
Reports are logged in a JSON format and also returned by the commands for a future use
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym CLI: Mixnet & Blockchain Commands"
|
||||
description: "Use nym-cli to interact with the Nym mixnet and Nyx blockchain. Manage nodes, delegate stake, and query network state from the command line."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-11"
|
||||
---
|
||||
|
||||
# Nym-CLI
|
||||
|
||||
This is a CLI tool for interacting with:
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
---
|
||||
title: "Nym TypeScript SDK: Privacy for Web Apps"
|
||||
description: "TypeScript SDK for integrating web apps with the Nym mixnet. Covers mixFetch, Mixnet Client, Smart Contracts, and Cosmos Kit with live playground examples."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-02-11"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Sounds great, are there any catches? Well, there are a few (for now):
|
||||
|
||||
- For now, `mixfetch` doesn't work with SURBS, although this will change in the future.
|
||||
|
||||
- For now, `mixFetch` cannot deal with concurrent requests with the same base URL.
|
||||
- `mixFetch` supports concurrent requests (up to 10) to either different URLs on the same domain or different domains. Duplicate requests to the exact same URL will be deduplicated.
|
||||
|
||||
<Callout type="info" emoji="ℹ️">
|
||||
Right now Gateways are not required to run a Secure Websocket (WSS) listener, so only a subset of nodes running in Gateway mode have configured their nodes to do so.
|
||||
@@ -33,6 +33,7 @@ curl -X 'GET' \
|
||||
import type { SetupMixFetchOps } from '@nymproject/mix-fetch';
|
||||
|
||||
const mixFetchOptions: SetupMixFetchOps = {
|
||||
clientId: "my-mixfetch-client", // explicit ID to avoid stale default IndexedDB storage
|
||||
preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1", // with WSS
|
||||
mixFetchOverride: {
|
||||
requestTimeoutMs: 60_000,
|
||||
@@ -78,7 +79,7 @@ import { mixFetch } from '@nymproject/mix-fetch-full-fat';
|
||||
|
||||
##### Example: using the `mixFetch` client:
|
||||
|
||||
`Get` and `Post` outputs will be observable from your console.
|
||||
`Get`, `Post`, and `Concurrent` outputs will be observable from your console. MixFetch auto-initializes on the first request. Individual concurrent results are logged as they arrive.
|
||||
|
||||
```ts
|
||||
import './App.css';
|
||||
@@ -86,6 +87,7 @@ import { mixFetch, SetupMixFetchOps } from '@nymproject/mix-fetch-full-fat';
|
||||
import React from 'react';
|
||||
|
||||
const mixFetchOptions: SetupMixFetchOps = {
|
||||
clientId: "my-mixfetch-client", // explicit ID to avoid stale default IndexedDB storage
|
||||
preferredGateway: '23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb', // with WSS
|
||||
preferredNetworkRequester:
|
||||
'HuNL1pFprNSKW6jdqppibXP5KNKCNJxDh7ivpYcoULN9.C62NahRTUf6kqpNtDVHXoVriQr6yyaU5LtxdgpbsGrtA@23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb',
|
||||
@@ -103,7 +105,7 @@ export function HttpGET() {
|
||||
const response = await mixFetch('https://nym.com/favicon.svg', { mode: 'unsafe-ignore-cors' }, mixFetchOptions);
|
||||
const text = await response.text();
|
||||
console.log('response was', text);
|
||||
setHtml(html);
|
||||
setHtml(text);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -146,11 +148,63 @@ export function HttpPOST() {
|
||||
);
|
||||
}
|
||||
|
||||
// Send 5 concurrent requests to different URLs on the same domain using Promise.all
|
||||
export function HttpConcurrent() {
|
||||
const [results, setResults] = React.useState<string[]>([]);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
|
||||
async function fetchConcurrent() {
|
||||
const baseUrl = 'https://jsonplaceholder.typicode.com/posts/';
|
||||
const count = 5;
|
||||
setBusy(true);
|
||||
setResults([]);
|
||||
console.log(`Starting ${count} concurrent requests to ${baseUrl}1-${count}...`);
|
||||
|
||||
try {
|
||||
// Fire off all requests at once with Promise.all
|
||||
const requests = Array.from({ length: count }, (_, i) => {
|
||||
const url = `${baseUrl}${i + 1}`;
|
||||
return mixFetch(url, { mode: 'unsafe-ignore-cors' }, mixFetchOptions)
|
||||
.then((res) => res.json())
|
||||
.then((json: { id: number; title: string }) => {
|
||||
const entry = `[${json.id}] ${json.title}`;
|
||||
console.log(entry);
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
|
||||
const allResults = await Promise.all(requests);
|
||||
setResults(allResults);
|
||||
console.log('All concurrent requests completed!', allResults);
|
||||
} catch (err) {
|
||||
console.error('Concurrent fetch error:', err);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={fetchConcurrent} disabled={busy}>
|
||||
{busy ? 'Fetching...' : 'Send 5 Concurrent Requests'}
|
||||
</button>
|
||||
{results.length > 0 && (
|
||||
<ul>
|
||||
{results.map((r, i) => (
|
||||
<li key={i}>{r}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
<HttpGET />
|
||||
<HttpPOST />
|
||||
<HttpConcurrent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
---
|
||||
title: "Nym Docs: Guides, SDKs & Architecture"
|
||||
description: "Official Nym documentation hub. Build privacy-enhanced applications, run mixnet nodes, and explore the network architecture and protocols powering NymVPN."
|
||||
schemaType: "TechArticle"
|
||||
section: "Documentation"
|
||||
lastUpdated: "2026-02-11"
|
||||
layout: raw
|
||||
---
|
||||
|
||||
import { LandingPage } from '../components/landing-page.tsx'
|
||||
|
||||
<LandingPage />
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Network Architecture: How the Mixnet Works"
|
||||
description: "Deep dive into Nym network architecture, cryptographic systems, and how the mixnet provides network-level privacy against end-to-end attackers."
|
||||
schemaType: "TechArticle"
|
||||
section: "Network"
|
||||
lastUpdated: "2026-02-11"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
Nym's network documentation covering network architecture, node types, tokenomics, and crypto systems.
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
---
|
||||
title: "Building Nym from Source: Linux, macOS & Windows"
|
||||
description: "How to build Nym platform binaries from source code. Covers dependencies for Debian, Arch, macOS, and Windows. Requires Rust toolchain and Git."
|
||||
schemaType: "HowTo"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
breadcrumbLabel: "Building from Source"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { VarInfo } from 'components/variable-info.tsx';
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Node Changelog & Release History"
|
||||
description: "Complete changelog for Nym node releases, binary updates, SDK changes, and network upgrades. Sorted newest first with links to relevant documentation."
|
||||
schemaType: "TechArticle"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { MyTab } from 'components/generic-tabs.tsx';
|
||||
@@ -49,6 +57,37 @@ This page displays a full list of all the changes during our release cycle from
|
||||
|
||||
<VarInfo />
|
||||
|
||||
## `v2026.4-quark`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2026.4-quark)
|
||||
- [`nym-node`](nodes/nym-node.mdx) version `1.26.0`
|
||||
|
||||
```sh
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2026-02-24T13:43:24.098285047Z
|
||||
Build Version: 1.26.0
|
||||
Commit SHA: a2081af6038ef3ef40b3d9368299d2676a2fbb6a
|
||||
Commit Date: 2026-02-24T12:02:35.000000000+01:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.91.1
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
|
||||
### Operator & Developer Updates
|
||||
|
||||
### Features
|
||||
|
||||
- [Stateless handshake improvements for LP Gateway](https://github.com/nymtech/nym/pull/6437)
|
||||
- [HTTP & DNS improvements](https://github.com/nymtech/nym/pull/6423)
|
||||
- [Endpoint support for exit gateway IPs](https://github.com/nymtech/nym/pull/6418)
|
||||
|
||||
### Tools
|
||||
|
||||
- **Diagnostic Tool** - a standalone binary for network diagnostics. It performs DNS, HTTP, and gateway connectivity tests, helping developers identify connectivity issues and monitor network performance. It can also be run via the daemon CLI. Read the full guide [here](https://nym.com/docs/developers/tools/diagnostic-tool).
|
||||
- **Socks5 Score Calculation** - performed by the Gateway probe, which tests `nym-node --mode exit-gateway` instances over Socks5. The probe assigns a latency-based rating: high, medium, low, or offline. Full guide [here](https://nym.com/docs/operators/performance-and-testing#socks5-score-calculation-process).
|
||||
|
||||
## `v2026.3-parmigiano`
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2026.3-parmigiano)
|
||||
- [`nym-node`](nodes/nym-node.mdx) version `1.25.0`
|
||||
@@ -66,9 +105,9 @@ rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
|
||||
### Key updates for operators include:
|
||||
### Operator & Developer Updates
|
||||
|
||||
LP Gateway and Client fixes:
|
||||
### Features
|
||||
|
||||
- [Registration client now properly supports fallback](https://github.com/nymtech/nym/pull/6419)
|
||||
- [Exposed WireGuard PSK for vpn-client](https://github.com/nymtech/nym/pull/6411)
|
||||
@@ -77,28 +116,14 @@ LP Gateway and Client fixes:
|
||||
|
||||
Note: This code is currently deactivated and doesn’t involve any changes for operators right now, but it will in the future.
|
||||
|
||||
Mixnet & Networking Enhancements:
|
||||
|
||||
- [NS API Socks5 support](https://github.com/nymtech/nym/pull/6361)
|
||||
- [Two-step dvpn registration flow](https://github.com/nymtech/nym/pull/6386)
|
||||
- [DVPN PSK injection](https://github.com/nymtech/nym/pull/6378)
|
||||
|
||||
Security & Encoding Improvements:
|
||||
|
||||
- [Hex-encoding for LP key digests](https://github.com/nymtech/nym/pull/6394)
|
||||
- [Encrypted KKT](https://github.com/nymtech/nym/pull/6331)
|
||||
- [Reject packets with incompatible versions](https://github.com/nymtech/nym/pull/6326)
|
||||
|
||||
Bugfixes & Quality-of-Life:
|
||||
### Bugfix
|
||||
|
||||
- [Share IP allocation fixes](https://github.com/nymtech/nym/pull/6395)
|
||||
- [Mixnet registration fixes](https://github.com/nymtech/nym/pull/6356)
|
||||
- [Small QoL changes](https://github.com/nymtech/nym/pull/6340)
|
||||
|
||||
Chores & Maintenance:
|
||||
### Refactors & Maintenance
|
||||
|
||||
- [Cleanup x25519/ed22519 usage](https://github.com/nymtech/nym/pull/6335)
|
||||
- [Upgrade to def_guard_wireguard v0.8.0](https://github.com/nymtech/nym/pull/6315)
|
||||
|
||||
## `v2026.2-oscypek`
|
||||
|
||||
@@ -130,7 +155,7 @@ Secondly, the outcome of [NIP-7: Nym Exit Policy Update - Opening Ports for Stea
|
||||
|
||||
This release brings changes which would lead into a *foreign constraint bug* if operators just switched binaries and restarted the node. To prevent it we need to do a little `sqlite` tweak on the node database.
|
||||
|
||||
To simplify this, we made **a build in command, which operators must run after getting the new binary, but beofre restarting the node.**
|
||||
To simplify this, we made **a build in command, which operators must run after getting the new binary, but before restarting the node.**
|
||||
|
||||
These are the steps to follow:
|
||||
|
||||
@@ -154,7 +179,7 @@ chmod +x nym-node
|
||||
```sh
|
||||
systemctl restart nym-node
|
||||
```
|
||||
- Additionaly look for starus or serivice journal
|
||||
- Additionally look for status or service journal
|
||||
```sh
|
||||
service nym-node status
|
||||
# or
|
||||
@@ -1294,7 +1319,7 @@ cargo Profile: release
|
||||
|
||||
- [Listen for shutdown signals during nym-node startup](https://github.com/nymtech/nym/pull/5879): This is to avoid situation where the process can't be killed without 'kill -9' because the logic to listen to shutdown signals hasn't been hit yet
|
||||
|
||||
### Bugfixes
|
||||
### Bugfix
|
||||
|
||||
- [Don't allow mixnode running in exit mode](https://github.com/nymtech/nym/pull/5898)
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Legal Guide for Nym Exit Gateway Operators"
|
||||
description: "Legal considerations for running a Nym exit gateway. Covers ISP communication templates, jurisdiction guidance, and abuse report response strategies."
|
||||
schemaType: "TechArticle"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { RunTabs } from 'components/operators/nodes/node-run-command-tabs';
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Email & Legal Templates for Nym Node Operators"
|
||||
description: "Ready-to-use templates for communicating with VPS providers about running Nym nodes. Includes introduction emails and abuse report response templates."
|
||||
schemaType: "TechArticle"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { RunTabs } from 'components/operators/nodes/node-run-command-tabs';
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Node Operator FAQ: Questions Answered"
|
||||
description: "Frequently asked questions about running Nym nodes. Covers VPS selection, staking requirements, node configuration, rewards, and community resources."
|
||||
schemaType: "FAQPage"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
# General Operators FAQ
|
||||
|
||||
## Nym Network
|
||||
@@ -16,8 +24,8 @@ Yes, there are..
|
||||
|
||||
**Built by community**
|
||||
|
||||
* [ExploreNYM](https://explorenym.net/)
|
||||
* [Mixplorer](https://mixplorer.xyz/)
|
||||
* [SpectreDAO Explorer](https://explorer.nym.spectredao.net/dashboard)
|
||||
* [Nymesis](https://nymesis.vercel.app)
|
||||
|
||||
|
||||
### Which VPS providers would you recommend?
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Node Operator Guide & Prerequisites"
|
||||
description: "Introduction to running Nym nodes. Learn requirements, skill expectations, time commitment, and how to get started operating a node on the Nym mixnet."
|
||||
schemaType: "TechArticle"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-11"
|
||||
---
|
||||
|
||||
import QuickStart from 'components/operators/snippets/quick-start.mdx'
|
||||
|
||||
# Introduction
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "How to Set Up & Run a Nym Node on the Mixnet"
|
||||
description: "Step-by-step guide to installing, configuring, and running a nym-node on the Nym mixnet. Covers prerequisites, staking requirements, and CLI setup."
|
||||
schemaType: "HowTo"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Steps } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Node Configuration & Systemd Setup"
|
||||
description: "Configure your nym-node with systemd automation, reverse proxy, IPv6, and custom settings. Includes service file templates and maintenance tips."
|
||||
schemaType: "TechArticle"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { VarInfo } from 'components/variable-info.tsx';
|
||||
|
||||
@@ -21,17 +21,16 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
|
||||
```sh
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2026-01-27T14:54:15.579821601Z
|
||||
Build Version: 1.24.0
|
||||
Commit SHA: 83bf9dc7cc2b01f65cab671733f2bf6c3abd471d
|
||||
Commit Date: 2026-01-27T15:46:52.000000000+01:00
|
||||
Build Timestamp: 2026-02-24T13:43:24.098285047Z
|
||||
Build Version: 1.26.0
|
||||
Commit SHA: a2081af6038ef3ef40b3d9368299d2676a2fbb6a
|
||||
Commit Date: 2026-02-24T12:02:35.000000000+01:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.91.1
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
|
||||
|
||||
Detailed version archive and release notes is documented [here](../../changelog.mdx).
|
||||
|
||||
{/* COMMENTING THIS OUT ASS WE HAVE TO FIGURE OUT HOW TO SHOW THE LATEST VERSION FROM MASTER BRANCH
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
---
|
||||
title: "Nym Node Performance Monitoring & Testing Guide"
|
||||
description: "Monitor your Nym node performance with Prometheus, Grafana, and community tools. Covers key metrics, routing score analysis, and testing best practices."
|
||||
schemaType: "TechArticle"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Steps } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { MyTab } from 'components/generic-tabs.tsx';
|
||||
import { AccordionTemplate } from 'components/accordion-template.tsx';
|
||||
@@ -7,11 +16,11 @@ import NodePerfMixnet from 'components/operators/snippets/node-perf-mixnet.mdx';
|
||||
|
||||
# Performance Monitoring & Testing
|
||||
|
||||
As Nym developers constantly improve the software, the role of Node Operators is to keep their nodes up to date, monitor their performance and share feedback with the rest of the community and Nym team. Node performance measurements and [server monitoring](#monitoring) are an essential pillar of our common work.
|
||||
As Nym developers constantly improve the software, the role of Node Operators is to keep their nodes up to date, monitor their performance and share feedback with the rest of the community and Nym team. Node performance measurements and [server monitoring](#monitoring) are an essential pillar of our common work.
|
||||
|
||||
Nym Network is routed either through the Mixnet (5-hop) or through Wireguard (2-hop). In all cases Nym node operators always employ only one binary called [`nym-node`](/operators/nodes/nym-node). Through provided arguments (or changes in the config file), `nym-node` can be utilised for different [functionalities](/operators/nodes/nym-node/setup#functionality-mode). However, once it's [registered to Nym Network](/operators/nodes/nym-node/bonding) it's by default available for Nym Mixnet not for Wireguard routing. Only nodes with Wireguard enabled, are also available for Wireguard routing. This creates a situation where every Wireguard enabled `nym-node` is required to have a solid performance score in Mixnet to begin with, but not every Mixnet routing `nym-node` must have Wireguard enabled.
|
||||
Nym Network is routed either through the Mixnet (5-hop) or through Wireguard (2-hop). In all cases Nym node operators always employ only one binary called [`nym-node`](/operators/nodes/nym-node). Through provided arguments (or changes in the config file), `nym-node` can be utilised for different [functionalities](/operators/nodes/nym-node/setup#functionality-mode). However, once it's [registered to Nym Network](/operators/nodes/nym-node/bonding) it's by default available for Nym Mixnet not for Wireguard routing. Only nodes with Wireguard enabled, are also available for Wireguard routing. This creates a situation where every Wireguard enabled `nym-node` is required to have a solid performance score in Mixnet to begin with, but not every Mixnet routing `nym-node` must have Wireguard enabled.
|
||||
|
||||
Given this complexity, we divided the part below about perfromance calculation logic and node selection into two parallel tabs: Mixnet and Wireguard.
|
||||
Given this complexity, we divided the part below about performance calculation logic and node selection into two parallel tabs: Mixnet and Wireguard.
|
||||
|
||||
<div>
|
||||
<Tabs items={[
|
||||
@@ -56,18 +65,53 @@ https://<HOSTNAME>/api/v1/roles
|
||||
```
|
||||
</AccordionTemplate>
|
||||
|
||||
## Socks5 Score Calculation
|
||||
|
||||
Gateway probe also runs tests through a Network requester - a module build as a part of `nym-node`, active only in mode Exit Gateway, used for [Socks5](/developers/clients/socks5) proxy TCP connection.
|
||||
|
||||
Socks5 score is displayed in [Nym Node Status Observatory](https://harbourmaster.nymtech.net) (if you open a page with a particular gateway) and in detail it can be previewed at [mainnet-node-status-api.nymtech.cc/dvpn/v1/directory/gateways](https://mainnet-node-status-api.nymtech.cc/dvpn/v1/directory/gateways) or when running own instance of [Gateway probe](/operators/performance-and-testing/gateway-probe).
|
||||
|
||||
### Socks5 Score Calculation Process
|
||||
|
||||
Socks5 score is defined in the json output of Gateway probe as `"socks5"` key. Here is an example of the dictionary:
|
||||
|
||||
```json
|
||||
"socks5": {
|
||||
"can_proxy_https": true,
|
||||
"score": "medium",
|
||||
"errors": null
|
||||
}
|
||||
```
|
||||
|
||||
> Note: When we write *gateway* we refer to a `nym-node --mode exit-gateway` in this sub-chapter.
|
||||
|
||||
<Steps>
|
||||
1. Gateway gets probed as part of a Gateway probe test where other components get tested as well
|
||||
|
||||
2. Probe tries to connect to the Gateway through Socks5 10 times per testing instance
|
||||
|
||||
3. Latency is calculated as an average of the successful attempts
|
||||
|
||||
4. Gateway is scored as `"low"`, `"medium"`, `"high"` or `"offline"`, in numbers it means:
|
||||
- `"offline"`: Gateway failed the test 3 or more times (out of 10 attempts)
|
||||
- `"high"`: Top 50% of nodes with lowest average latency
|
||||
- `"medium"`: Following 25% of nodes with lowest average latency below top 50% nodes
|
||||
- `"low"`: Remaining 25% of nodes with the highest average latency time
|
||||
</Steps>
|
||||
|
||||
## Monitoring
|
||||
|
||||
There are multiple ways to monitor performance of nodes and the machines on which they run. For the purpose of maximal privacy and decentralisation of the data - preventing Nym Mixnet from any global adversary takeover - we created these pages as a source of mutual empowerment, a place where operators can share and learn new skills to **setup metrics monitors on their own infrastructure**.
|
||||
|
||||
### Guides to Setup Own Metrics
|
||||
|
||||
A list of different scripts, templates and guides for easier navigation:
|
||||
A list of different tools, templates and guides for easier navigation:
|
||||
|
||||
* [`nym-gateway-probe`](performance-and-testing/gateway-probe.mdx): a useful tool used under the hood of [Node Status Observatory](https://harbourmaster.nymtech.net)
|
||||
|
||||
* [Diagnostic Tool](/developers/tools/diagnostic-tool): diagnose connectivity issues and provides insights into network performance
|
||||
|
||||
* [`nym-gateway-probe`](performance-and-testing/gateway-probe.mdx) - a useful tool used under the hood of [Node Status Observatory](https://harbourmaster.nymtech.net)
|
||||
* [Prometheus and Grafana](performance-and-testing/prometheus-grafana.mdx) self-hosted setup
|
||||
* [Nym-node CPU cron service](https://gist.github.com/tommyv1987/97e939a7adf491333d686a8eaa68d4bd) - an easy bash script by Nym core developer [@tommy1987](https://gist.github.com/tommyv1987), designed to monitor a CPU usage of your node, running locally
|
||||
* Nym's script [`prom_targets.py`](https://github.com/nymtech/nym/blob/develop/scripts/prom_targets.py) - a useful python program to request data from API and can be run on its own or plugged to more sophisticated flows
|
||||
|
||||
### Collecting Testing Metrics
|
||||
|
||||
@@ -93,4 +137,4 @@ We do testing in order to **understand and increase the overall quality of the N
|
||||
7. Adjust rewarding based on the machine specs and server pricing
|
||||
|
||||
Visit [Nym Harbour Master](https://harbourmaster.nymtech.net/) monitoring page to monitor network components (nodes) performance.
|
||||
*/}
|
||||
*/}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Sandbox Testnet for Node Operators"
|
||||
description: "Run your Nym node in the Sandbox testnet environment. Test configurations, try new features, and experiment safely before deploying to mainnet."
|
||||
schemaType: "HowTo"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { VarInfo } from 'components/variable-info.tsx';
|
||||
import { Steps } from 'nextra/components';import { Tabs } from 'nextra/components'
|
||||
|
||||
@@ -202,3 +202,13 @@ PING_RETRIES=10 PING_TIMEOUT=5 CONCURRENCY=16 ./test-nodes-pings.sh
|
||||
You can look up the IPs from `ping_not_working.csv`, using some online database, like [ipinfo.io](https://ipinfo.io).
|
||||
|
||||
Feel invited to share the outcome with Nym team, mentors and the rest of the operators in our [Matrix Node Operators channel](https://matrix.to/#/#operators:nymtech.chat).
|
||||
|
||||
## Guides to Setup Own Metrics
|
||||
|
||||
A list of different tools, templates and guides for easier navigation:
|
||||
|
||||
* [`nym-gateway-probe`](performance-and-testing/gateway-probe.mdx): a useful tool used under the hood of [Node Status Observatory](https://harbourmaster.nymtech.net)
|
||||
|
||||
* [Diagnostic Tool](/developers/tools/diagnostic-tool): diagnose connectivity issues and provides insights into network performance
|
||||
|
||||
* [Prometheus and Grafana](performance-and-testing/prometheus-grafana.mdx) self-hosted setup
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Nym Node Troubleshooting: Common Errors & Fixes"
|
||||
description: "Solutions for common nym-node issues including build failures, connectivity problems, and configuration errors. Includes error messages and fix steps."
|
||||
schemaType: "TechArticle"
|
||||
section: "Operators"
|
||||
lastUpdated: "2026-02-01"
|
||||
---
|
||||
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { Callout } from 'nextra/components';
|
||||
import { VarInfo } from 'components/variable-info.tsx';
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
/* nym.com-aligned colour tokens */
|
||||
:root {
|
||||
--colorPrimary: #85E89D;
|
||||
--textPrimary: #FFFFFF;
|
||||
--bg-dark: #1E2426;
|
||||
--border-dark: #2E3538;
|
||||
}
|
||||
|
||||
/* dark mode background override */
|
||||
html.dark {
|
||||
background-color: var(--bg-dark);
|
||||
}
|
||||
|
||||
html.dark body {
|
||||
background-color: var(--bg-dark);
|
||||
}
|
||||
|
||||
/* nextra main content area bg */
|
||||
html.dark .nextra-nav-container,
|
||||
html.dark .nextra-sidebar-container,
|
||||
html.dark .nextra-content,
|
||||
html.dark .nx-bg-white,
|
||||
html.dark .dark\:nx-bg-dark {
|
||||
background-color: var(--bg-dark) !important;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -19,7 +45,7 @@ footer {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
/* text-align: right; */
|
||||
border-left: 1px solid #262626;
|
||||
border-left: 1px solid var(--border-dark);
|
||||
}
|
||||
|
||||
.nextra-content {
|
||||
@@ -34,14 +60,21 @@ footer {
|
||||
background-color: hsl(var(black) 100% 77%/0.1) !important;
|
||||
}
|
||||
|
||||
/* Sidebar buttons */
|
||||
/* Sidebar active item */
|
||||
:is(html .dark\:nx-bg-primary-400\/10) {
|
||||
background: var(--colorPrimary) !important;
|
||||
color: var(--textPrimary) !important;
|
||||
background: transparent !important;
|
||||
border-left: 2px solid #85E89D;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
:is(html:not(.dark) .dark\:nx-bg-primary-400\/10) {
|
||||
background: transparent !important;
|
||||
border-left: 2px solid #4A9E5C;
|
||||
color: #242B2D !important;
|
||||
}
|
||||
|
||||
.nextra-sidebar-container {
|
||||
border-right: 1px solid #262626;
|
||||
border-right: 1px solid var(--border-dark);
|
||||
width: 300px !important;
|
||||
}
|
||||
|
||||
|
||||
Generated
+68
-27
@@ -78,8 +78,8 @@ importers:
|
||||
specifier: '>=1.2.4-rc.2 || ^1'
|
||||
version: 1.4.1
|
||||
'@nymproject/mix-fetch-full-fat':
|
||||
specifier: '>=1.5.1-rc.0 || ^1.4.1'
|
||||
version: 1.4.1
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2
|
||||
'@nymproject/sdk-full-fat':
|
||||
specifier: '>=1.5.1-rc.0 || ^1.4.1'
|
||||
version: 1.4.1
|
||||
@@ -141,6 +141,9 @@ importers:
|
||||
eslint-config-next:
|
||||
specifier: 13.4.13
|
||||
version: 13.4.13(eslint@8.46.0)(typescript@5.9.3)
|
||||
next-sitemap:
|
||||
specifier: 4.2.3
|
||||
version: 4.2.3(next@15.5.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
raw-loader:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2(webpack@5.101.3)
|
||||
@@ -247,6 +250,9 @@ packages:
|
||||
react: '>=17'
|
||||
react-dom: '>=17'
|
||||
|
||||
'@corex/deepmerge@4.0.43':
|
||||
resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==}
|
||||
|
||||
'@cosmjs/amino@0.25.6':
|
||||
resolution: {integrity: sha512-9dXN2W7LHjDtJUGNsQ9ok0DfxeN3ca/TXnxCR3Ikh/5YqBqxI8Gel1J9PQO9L6EheYyh045Wff4bsMaLjyEeqQ==}
|
||||
|
||||
@@ -1029,6 +1035,9 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@next/env@13.5.11':
|
||||
resolution: {integrity: sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==}
|
||||
|
||||
'@next/env@15.5.10':
|
||||
resolution: {integrity: sha512-plg+9A/KoZcTS26fe15LHg+QxReTazrIOoKKUC3Uz4leGGeNPgLHdevVraAAOX0snnUs3WkRx3eUQpj9mreG6A==}
|
||||
|
||||
@@ -1705,8 +1714,8 @@ packages:
|
||||
'@nymproject/contract-clients@1.4.1':
|
||||
resolution: {integrity: sha512-HuJZ4Hv+Rl6ZZEtCHKgurNLJapM+QQRJlGkevFH2a4UdqUqF9omUkUi3AVes4679dPoSFgvA7plyVSDBdbgV6w==}
|
||||
|
||||
'@nymproject/mix-fetch-full-fat@1.4.1':
|
||||
resolution: {integrity: sha512-AMa21sEd9FELqAJe1lCyHPqxxbc13ApiJ1P/exAslQjiFPb/de/3Ow0FHqKGNPrwyVRS/T2pSzjQ3l8TddiEBA==}
|
||||
'@nymproject/mix-fetch-full-fat@1.4.2':
|
||||
resolution: {integrity: sha512-QHPwa7A+c/2VUm4Imq2I21toFiZhbZxcjHud1sFsE9hN5BWxZ+QJKV2bg9oBUzulzoQabsk48RA13/hqU7c4KA==}
|
||||
|
||||
'@nymproject/sdk-full-fat@1.4.1':
|
||||
resolution: {integrity: sha512-dh5bvMUj3m8nEssvO8Nl66WpcJAjwRZrGNwqfczJWLG4nX3Vt95tPLv4v0/Z1W3DQWQFW6WmEPPYHNjl18V/fA==}
|
||||
@@ -3215,6 +3224,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -3390,8 +3404,9 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
baseline-browser-mapping@2.9.19:
|
||||
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
||||
baseline-browser-mapping@2.10.0:
|
||||
resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
bech32@1.1.4:
|
||||
@@ -3510,6 +3525,9 @@ packages:
|
||||
caniuse-lite@1.0.30001769:
|
||||
resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
|
||||
|
||||
caniuse-lite@1.0.30001774:
|
||||
resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==}
|
||||
|
||||
cardinal@2.1.1:
|
||||
resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==}
|
||||
hasBin: true
|
||||
@@ -4022,8 +4040,8 @@ packages:
|
||||
duplexify@4.1.3:
|
||||
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
|
||||
|
||||
electron-to-chromium@1.5.286:
|
||||
resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
|
||||
electron-to-chromium@1.5.302:
|
||||
resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==}
|
||||
|
||||
elkjs@0.9.3:
|
||||
resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==}
|
||||
@@ -5355,8 +5373,8 @@ packages:
|
||||
modern-ahocorasick@1.1.0:
|
||||
resolution: {integrity: sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==}
|
||||
|
||||
motion-dom@12.34.0:
|
||||
resolution: {integrity: sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==}
|
||||
motion-dom@12.34.3:
|
||||
resolution: {integrity: sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==}
|
||||
|
||||
motion-utils@12.29.2:
|
||||
resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==}
|
||||
@@ -5404,6 +5422,13 @@ packages:
|
||||
react: '>=16.0.0'
|
||||
react-dom: '>=16.0.0'
|
||||
|
||||
next-sitemap@4.2.3:
|
||||
resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==}
|
||||
engines: {node: '>=14.18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
next: '*'
|
||||
|
||||
next-themes@0.2.1:
|
||||
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
|
||||
peerDependencies:
|
||||
@@ -6709,8 +6734,8 @@ packages:
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
webpack-sources@3.3.3:
|
||||
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
|
||||
webpack-sources@3.3.4:
|
||||
resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
webpack@5.101.3:
|
||||
@@ -6982,6 +7007,8 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@corex/deepmerge@4.0.43': {}
|
||||
|
||||
'@cosmjs/amino@0.25.6':
|
||||
dependencies:
|
||||
'@cosmjs/crypto': 0.25.6
|
||||
@@ -8213,6 +8240,8 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.0
|
||||
optional: true
|
||||
|
||||
'@next/env@13.5.11': {}
|
||||
|
||||
'@next/env@15.5.10': {}
|
||||
|
||||
'@next/eslint-plugin-next@13.4.13':
|
||||
@@ -9307,7 +9336,7 @@ snapshots:
|
||||
|
||||
'@nymproject/contract-clients@1.4.1': {}
|
||||
|
||||
'@nymproject/mix-fetch-full-fat@1.4.1': {}
|
||||
'@nymproject/mix-fetch-full-fat@1.4.2': {}
|
||||
|
||||
'@nymproject/sdk-full-fat@1.4.1': {}
|
||||
|
||||
@@ -11810,9 +11839,9 @@ snapshots:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
|
||||
acorn-import-phases@1.0.4(acorn@8.15.0):
|
||||
acorn-import-phases@1.0.4(acorn@8.16.0):
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn: 8.16.0
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
dependencies:
|
||||
@@ -11820,6 +11849,8 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv-draft-04@1.0.0(ajv@8.17.1):
|
||||
@@ -12009,7 +12040,7 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.9.19: {}
|
||||
baseline-browser-mapping@2.10.0: {}
|
||||
|
||||
bech32@1.1.4: {}
|
||||
|
||||
@@ -12079,9 +12110,9 @@ snapshots:
|
||||
|
||||
browserslist@4.28.1:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.9.19
|
||||
caniuse-lite: 1.0.30001769
|
||||
electron-to-chromium: 1.5.286
|
||||
baseline-browser-mapping: 2.10.0
|
||||
caniuse-lite: 1.0.30001774
|
||||
electron-to-chromium: 1.5.302
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
|
||||
@@ -12135,6 +12166,8 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001769: {}
|
||||
|
||||
caniuse-lite@1.0.30001774: {}
|
||||
|
||||
cardinal@2.1.1:
|
||||
dependencies:
|
||||
ansicolors: 0.3.2
|
||||
@@ -12672,7 +12705,7 @@ snapshots:
|
||||
readable-stream: 3.6.2
|
||||
stream-shift: 1.0.3
|
||||
|
||||
electron-to-chromium@1.5.286: {}
|
||||
electron-to-chromium@1.5.302: {}
|
||||
|
||||
elkjs@0.9.3: {}
|
||||
|
||||
@@ -13171,7 +13204,7 @@ snapshots:
|
||||
|
||||
framer-motion@12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
motion-dom: 12.34.0
|
||||
motion-dom: 12.34.3
|
||||
motion-utils: 12.29.2
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
@@ -14491,7 +14524,7 @@ snapshots:
|
||||
|
||||
modern-ahocorasick@1.1.0: {}
|
||||
|
||||
motion-dom@12.34.0:
|
||||
motion-dom@12.34.3:
|
||||
dependencies:
|
||||
motion-utils: 12.29.2
|
||||
|
||||
@@ -14530,6 +14563,14 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
next-sitemap@4.2.3(next@15.5.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
dependencies:
|
||||
'@corex/deepmerge': 4.0.43
|
||||
'@next/env': 13.5.11
|
||||
fast-glob: 3.3.3
|
||||
minimist: 1.2.8
|
||||
next: 15.5.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
next-themes@0.2.1(next@15.5.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
next: 15.5.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -15781,7 +15822,7 @@ snapshots:
|
||||
terser@5.46.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.15.0
|
||||
acorn: 8.16.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
@@ -16171,7 +16212,7 @@ snapshots:
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webpack-sources@3.3.3: {}
|
||||
webpack-sources@3.3.4: {}
|
||||
|
||||
webpack@5.101.3:
|
||||
dependencies:
|
||||
@@ -16181,8 +16222,8 @@ snapshots:
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@webassemblyjs/wasm-edit': 1.14.1
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.15.0)
|
||||
acorn: 8.16.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
||||
browserslist: 4.28.1
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.19.0
|
||||
@@ -16199,7 +16240,7 @@ snapshots:
|
||||
tapable: 2.3.0
|
||||
terser-webpack-plugin: 5.3.16(webpack@5.101.3)
|
||||
watchpack: 2.5.1
|
||||
webpack-sources: 3.3.3
|
||||
webpack-sources: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# *
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# *
|
||||
User-agent: *
|
||||
Disallow: /api/
|
||||
Disallow: /_next/
|
||||
|
||||
# Host
|
||||
Host: https://nymtech.net/docs
|
||||
|
||||
# Sitemaps
|
||||
Sitemap: https://nymtech.net/docs/sitemap.xml
|
||||
Sitemap: https://nymtech.net/docs/sitemap-docs.xml
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||
<url><loc>https://nymtech.net/docs</loc><lastmod>2026-02-25T10:35:41.122Z</lastmod><changefreq>yearly</changefreq><priority>0.6</priority></url>
|
||||
<url><loc>https://nymtech.net/docs/network</loc><lastmod>2026-02-25T10:35:41.122Z</lastmod><changefreq>yearly</changefreq><priority>0.6</priority></url>
|
||||
</urlset>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap><loc>https://nymtech.net/docs/sitemap-0.xml</loc></sitemap>
|
||||
<sitemap><loc>https://nymtech.net/docs/sitemap-docs.xml</loc></sitemap>
|
||||
</sitemapindex>
|
||||
@@ -13,52 +13,129 @@ const config: DocsThemeConfig = {
|
||||
const image = url + "/images/Nym_meta_Image.png";
|
||||
const favicon = url + "/favicon.svg";
|
||||
|
||||
// Define descriptions for different "books"
|
||||
const bookDescriptions: Record<string, string> = {
|
||||
"/developers":
|
||||
"Nym's developer documentation covering core concepts of integrating with the Mixnet, interacting with the Nyx blockchain, an overview of the avaliable tools, and our SDK docs.",
|
||||
"/network":
|
||||
"Nym's network documentation covering network architecture, node types, tokenomics, and cryptography.",
|
||||
"/operators":
|
||||
"Nym's Operators guide containing information and setup guides for the various components of Nym network and Nyx blockchain validators.",
|
||||
"/apis":
|
||||
"Interactive APIs generated from the OpenAPI specs of various API endpoints offered by bits of Nym infrastructure run both by Nym and community operators for both Mainnet and the Sandbox testnet.",
|
||||
};
|
||||
|
||||
const defaultDescription =
|
||||
"Nym is a privacy platform. It provides strong network-level privacy against sophisticated end-to-end attackers, and anonymous access control using blinded, re-randomizable, decentralized credentials.";
|
||||
|
||||
const topLevel = "/" + route.split("/")[1];
|
||||
const description =
|
||||
config.frontMatter.description ||
|
||||
bookDescriptions[topLevel] ||
|
||||
defaultDescription;
|
||||
// Frontmatter-first description
|
||||
const description = config.frontMatter.description || defaultDescription;
|
||||
|
||||
const title = (route === "/" ? "Nym docs" : config.title + " - Nym docs");
|
||||
const baseTitle = config.frontMatter.title || config.title || "";
|
||||
const title =
|
||||
route === "/"
|
||||
? "Nym Docs: Privacy Network Documentation"
|
||||
: baseTitle.includes("| Nym Docs")
|
||||
? baseTitle
|
||||
: `${baseTitle} | Nym Docs`;
|
||||
|
||||
const pageUrl = `${url}${route}`;
|
||||
|
||||
// Frontmatter fields
|
||||
const section = config.frontMatter.section || "";
|
||||
const lastUpdated = config.frontMatter.lastUpdated || "";
|
||||
const schemaType = config.frontMatter.schemaType || "TechArticle";
|
||||
|
||||
// JSON-LD structured data
|
||||
const org = {
|
||||
"@id": "https://nym.com/#org",
|
||||
"@type": "Organization",
|
||||
name: "Nym Technologies SA",
|
||||
url: "https://nym.com",
|
||||
logo: {
|
||||
"@id": "https://nym.com/#logo",
|
||||
"@type": "ImageObject",
|
||||
url: "https://nym.com/apple-touch-icon.png",
|
||||
},
|
||||
sameAs: ["https://x.com/nymproject", "https://github.com/nymtech"],
|
||||
};
|
||||
|
||||
const website = {
|
||||
"@id": "https://nym.com/docs#website",
|
||||
"@type": "WebSite",
|
||||
name: "Nym Docs",
|
||||
url: "https://nym.com/docs",
|
||||
publisher: { "@id": "https://nym.com/#org" },
|
||||
};
|
||||
|
||||
const webpage = {
|
||||
"@id": `${pageUrl}#webpage`,
|
||||
"@type": "WebPage",
|
||||
url: pageUrl,
|
||||
name: title,
|
||||
description: description,
|
||||
inLanguage: "en",
|
||||
isPartOf: { "@id": "https://nym.com/docs#website" },
|
||||
breadcrumb: { "@id": `${pageUrl}#breadcrumb` },
|
||||
potentialAction: { "@type": "ReadAction", target: pageUrl },
|
||||
};
|
||||
|
||||
const articleSchema: Record<string, any> = {
|
||||
"@id": `${pageUrl}#article`,
|
||||
"@type": schemaType,
|
||||
...(schemaType === "HowTo"
|
||||
? { name: baseTitle }
|
||||
: { headline: baseTitle }),
|
||||
description: description,
|
||||
url: pageUrl,
|
||||
author: { "@id": "https://nym.com/#org" },
|
||||
publisher: { "@id": "https://nym.com/#org" },
|
||||
mainEntityOfPage: { "@id": `${pageUrl}#webpage` },
|
||||
...(lastUpdated && {
|
||||
datePublished: lastUpdated,
|
||||
dateModified: lastUpdated,
|
||||
}),
|
||||
};
|
||||
|
||||
const pathParts = route.split("/").filter(Boolean);
|
||||
const breadcrumb = {
|
||||
"@id": `${pageUrl}#breadcrumb`,
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: pathParts.map((part: string, i: number) => ({
|
||||
"@type": "ListItem",
|
||||
position: i + 1,
|
||||
name:
|
||||
config.frontMatter.breadcrumbLabel && i === pathParts.length - 1
|
||||
? config.frontMatter.breadcrumbLabel
|
||||
: part.charAt(0).toUpperCase() + part.slice(1).replace(/-/g, " "),
|
||||
item: `${url}/${pathParts.slice(0, i + 1).join("/")}`,
|
||||
})),
|
||||
};
|
||||
|
||||
const schema = {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [org, website, webpage, articleSchema, breadcrumb],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{title}</title>
|
||||
<meta name="author" content="Nym" />
|
||||
<link rel="canonical" href={url + route} />
|
||||
<link rel="canonical" href={pageUrl} />
|
||||
<link rel="icon" href={favicon} type="image/svg+xml" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:site_name" content="Nym docs"></meta>
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:site_name" content="Nym docs" />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={url + route}></meta>
|
||||
|
||||
<meta property="twitter:title" content={title}></meta>
|
||||
<meta property="twitter:description" content={description}></meta>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content={pageUrl} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
{section && <meta property="article:section" content={section} />}
|
||||
{lastUpdated && (
|
||||
<meta property="article:modified_time" content={lastUpdated} />
|
||||
)}
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:image" content={image}></meta>
|
||||
<meta property="twitter:image" content={image} />
|
||||
<meta name="twitter:site" content="@nymproject" />
|
||||
<meta name="twitter:site:domain" content={url} />
|
||||
<meta name="twitter:url" content={url + route} />
|
||||
|
||||
<meta name="twitter:url" content={pageUrl} />
|
||||
<meta name="apple-mobile-web-app-title" content="Nym docs" />
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -72,6 +149,8 @@ const config: DocsThemeConfig = {
|
||||
// text: Footer,
|
||||
// },
|
||||
darkMode: true,
|
||||
primaryHue: 135,
|
||||
primarySaturation: 64,
|
||||
nextThemes: {
|
||||
defaultTheme: "dark",
|
||||
},
|
||||
|
||||
@@ -5,29 +5,29 @@ set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
cd ../scripts &&
|
||||
python csv2md.py -s 1 ../docs/data/csv/variables.csv > ../docs/components/outputs/csv2md-outputs/variables.md &&
|
||||
python csv2md.py -s 0 ../docs/data/csv/isp-sheet.csv > ../docs/components/outputs/csv2md-outputs/isp-sheet.md &&
|
||||
python3 csv2md.py -s 1 ../docs/data/csv/variables.csv > ../docs/components/outputs/csv2md-outputs/variables.md &&
|
||||
python3 csv2md.py -s 0 ../docs/data/csv/isp-sheet.csv > ../docs/components/outputs/csv2md-outputs/isp-sheet.md &&
|
||||
|
||||
cd cmdrun &&
|
||||
./nyx-percent-stake.sh > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md &&
|
||||
./nyx-total-stake.sh > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md &&
|
||||
|
||||
cd ../api-scraping &&
|
||||
python api_targets.py validator --api mainnet --endpoint circulating-supply --value circulating_supply amount --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md &&
|
||||
python3 api_targets.py validator --api mainnet --endpoint circulating-supply --value circulating_supply amount --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md &&
|
||||
|
||||
python api_targets.py validator --api mainnet --endpoint circulating-supply --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md &&
|
||||
python3 api_targets.py validator --api mainnet --endpoint circulating-supply --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md &&
|
||||
|
||||
python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval staking_supply_scale_factor --format percent > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md &&
|
||||
python3 api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval staking_supply_scale_factor --format percent > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md &&
|
||||
|
||||
python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval stake_saturation_point --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md &&
|
||||
python3 api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval stake_saturation_point --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md &&
|
||||
|
||||
python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval staking_supply --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md &&
|
||||
python3 api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval staking_supply --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md &&
|
||||
|
||||
python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval epoch_reward_budget --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md &&
|
||||
python3 api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval epoch_reward_budget --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md &&
|
||||
|
||||
python api_targets.py time_now > ../../docs/components/outputs/api-scraping-outputs/time-now.md &&
|
||||
python3 api_targets.py time_now > ../../docs/components/outputs/api-scraping-outputs/time-now.md &&
|
||||
|
||||
python api_targets.py calculate --staking_target --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md &&
|
||||
python3 api_targets.py calculate --staking_target --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md &&
|
||||
|
||||
curl -L https://validator.nymtech.net/api/v1/circulating-supply | jq > ../../docs/components/outputs/api-scraping-outputs/circulating-supply.json &&
|
||||
|
||||
@@ -35,11 +35,11 @@ curl -L https://validator.nymtech.net/api/v1/epoch/reward_params | jq > ../../do
|
||||
|
||||
cd ../../../scripts &&
|
||||
echo '```python' > ../documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md &&
|
||||
python node_api_check.py query_stats --help >> ../documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md &&
|
||||
python3 node_api_check.py query_stats --help >> ../documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md &&
|
||||
echo '```' >> ../documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md &&
|
||||
|
||||
echo '```python' > ../documentation/docs/components/outputs/command-outputs/node-api-check-help.md &&
|
||||
python node_api_check.py --help >> ../documentation/docs/components/outputs/command-outputs/node-api-check-help.md &&
|
||||
python3 node_api_check.py --help >> ../documentation/docs/components/outputs/command-outputs/node-api-check-help.md &&
|
||||
echo '```' >> ../documentation/docs/components/outputs/command-outputs/node-api-check-help.md &&
|
||||
|
||||
cd ../target/release/ &&
|
||||
@@ -62,11 +62,11 @@ echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-ap
|
||||
cd ../../scripts/nym-node-setup
|
||||
|
||||
echo '```sh' > ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md &&
|
||||
python ./nym-node-cli.py install --help >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md &&
|
||||
python3 ./nym-node-cli.py install --help >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md &&
|
||||
echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md &&
|
||||
|
||||
echo '```sh' > ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md &&
|
||||
python ./nym-node-cli.py install --help >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md &&
|
||||
python3 ./nym-node-cli.py install --help >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md &&
|
||||
echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md &&
|
||||
|
||||
echo "prebuild finished"
|
||||
|
||||
+47
-424
@@ -9,30 +9,13 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@jsdevtools/rehype-url-inspector": "^2.0.2",
|
||||
"glob": "^10.5.0",
|
||||
"glob": "^13.0.6",
|
||||
"rehype-parse": "^9.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"to-vfile": "^8.0.0",
|
||||
"unified": "^11.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/rehype-url-inspector": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/rehype-url-inspector/-/rehype-url-inspector-2.0.2.tgz",
|
||||
@@ -45,16 +28,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.0.tgz",
|
||||
@@ -81,30 +54,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/bail": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||
@@ -115,18 +64,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
|
||||
"integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
|
||||
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
@@ -156,24 +111,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
@@ -183,19 +120,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
@@ -216,18 +140,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@@ -239,36 +151,18 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
|
||||
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
|
||||
"node_modules/glob": {
|
||||
"version": "13.0.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
"signal-exit": "^4.0.1"
|
||||
"minimatch": "^10.2.2",
|
||||
"minipass": "^7.1.3",
|
||||
"path-scurry": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@@ -532,15 +426,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
@@ -552,31 +437,14 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
"version": "11.2.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
|
||||
"integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.0.2",
|
||||
@@ -682,35 +550,29 @@
|
||||
]
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"license": "ISC",
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
|
||||
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"license": "ISC",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||
@@ -722,25 +584,17 @@
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@@ -783,36 +637,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/space-separated-tokens": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||||
@@ -822,65 +646,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/stringify-entities": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz",
|
||||
@@ -894,43 +659,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tlds": {
|
||||
"version": "1.242.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.242.0.tgz",
|
||||
@@ -1225,111 +953,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@jsdevtools/rehype-url-inspector": "^2.0.2",
|
||||
"glob": "^10.5.0",
|
||||
"glob": "^13.0.6",
|
||||
"rehype-parse": "^9.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"to-vfile": "^8.0.0",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.73"
|
||||
version = "1.1.74"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
+4
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node"
|
||||
version = "1.25.0"
|
||||
version = "1.26.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -40,6 +40,8 @@ thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-indicatif = { workspace = true }
|
||||
tracing-subscriber.workspace = true
|
||||
opentelemetry = { workspace = true, features = ["trace"], optional = true }
|
||||
opentelemetry_sdk = { workspace = true, features = ["trace"], optional = true }
|
||||
tokio = { workspace = true, features = ["macros", "sync", "rt-multi-thread"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
tokio-stream = { workspace = true }
|
||||
@@ -135,6 +137,7 @@ rand_chacha = { workspace = true }
|
||||
|
||||
[features]
|
||||
tokio-console = ["console-subscriber", "nym-task/tokio-tracing"]
|
||||
otel = ["nym-bin-common/otel-otlp", "dep:opentelemetry", "dep:opentelemetry_sdk"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -106,6 +106,8 @@ pub mod verloc {
|
||||
#[serde(with = "bs58_ed25519_pubkey")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub node_identity: ed25519::PublicKey,
|
||||
|
||||
pub latest_measurement: Option<VerlocMeasurement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
|
||||
+102
-28
@@ -8,7 +8,6 @@ use crate::cli::commands::{
|
||||
use crate::env::vars::{NYMNODE_CONFIG_ENV_FILE_ARG, NYMNODE_NO_BANNER_ARG};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use nym_bin_common::bin_info;
|
||||
use std::future::Future;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub(crate) mod commands;
|
||||
@@ -22,6 +21,43 @@ fn pretty_build_info_static() -> &'static str {
|
||||
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||
}
|
||||
|
||||
/// OpenTelemetry-related CLI arguments. Only present when built with the `otel` feature.
|
||||
#[cfg(feature = "otel")]
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub(crate) struct OtelArgs {
|
||||
/// Enable OpenTelemetry tracing export via OTLP/gRPC.
|
||||
#[clap(long, env = "NYMNODE_OTEL_ENABLE")]
|
||||
pub(crate) otel: bool,
|
||||
|
||||
/// OpenTelemetry OTLP collector endpoint (gRPC).
|
||||
/// Only used when --otel is enabled.
|
||||
/// For SigNoz Cloud use https://ingest.<region>.signoz.cloud:443
|
||||
#[clap(
|
||||
long,
|
||||
env = "NYMNODE_OTEL_ENDPOINT",
|
||||
default_value = "http://localhost:4317"
|
||||
)]
|
||||
pub(crate) otel_endpoint: String,
|
||||
|
||||
/// SigNoz Cloud ingestion key for authenticated OTLP export.
|
||||
/// Only needed for SigNoz Cloud (not self-hosted).
|
||||
#[clap(long, env = "NYMNODE_OTEL_KEY")]
|
||||
pub(crate) otel_key: Option<String>,
|
||||
|
||||
/// Deployment environment label attached to all exported traces.
|
||||
/// Used to distinguish sandbox / mainnet / canary in the OTel backend.
|
||||
#[clap(long, env = "NYMNODE_OTEL_ENV", default_value = "mainnet")]
|
||||
pub(crate) otel_env: String,
|
||||
|
||||
/// Trace sampling ratio (0.0 to 1.0). e.g. 0.1 = 10%% of traces exported. Reduces cost.
|
||||
#[clap(long, env = "NYMNODE_OTEL_SAMPLE_RATIO", default_value = "0.1")]
|
||||
pub(crate) otel_sample_ratio: f64,
|
||||
|
||||
/// Timeout in seconds for each OTLP export batch. Prevents unbounded blocking.
|
||||
#[clap(long, env = "NYMNODE_OTEL_EXPORT_TIMEOUT", default_value = "10")]
|
||||
pub(crate) otel_export_timeout: u64,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
||||
pub(crate) struct Cli {
|
||||
@@ -40,44 +76,82 @@ pub(crate) struct Cli {
|
||||
)]
|
||||
pub(crate) no_banner: bool,
|
||||
|
||||
#[cfg(feature = "otel")]
|
||||
#[clap(flatten)]
|
||||
pub(crate) otel: OtelArgs,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
fn execute_async<F: Future>(fut: F) -> anyhow::Result<F::Output> {
|
||||
Ok(tokio::runtime::Builder::new_multi_thread()
|
||||
pub(crate) fn execute(self) -> anyhow::Result<()> {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(fut))
|
||||
.build()?;
|
||||
|
||||
// Set up tracing inside the runtime so the OTel batch exporter (when enabled)
|
||||
// can spawn its background tasks on the tokio reactor.
|
||||
let use_otel = matches!(self.command, Commands::Run(..));
|
||||
let _otel_guard = runtime.block_on(async { self.setup_logging(use_otel) })?;
|
||||
|
||||
// `_otel_guard` is dropped at function exit, flushing pending spans via its Drop impl
|
||||
runtime.block_on(async {
|
||||
match self.command {
|
||||
Commands::BuildInfo(args) => build_info::execute(args)?,
|
||||
Commands::BondingInformation(args) => bonding_information::execute(args).await?,
|
||||
Commands::NodeDetails(args) => node_details::execute(args).await?,
|
||||
Commands::Run(args) => run::execute(*args).await?,
|
||||
Commands::Migrate(args) => migrate::execute(*args)?,
|
||||
Commands::Sign(args) => sign::execute(args).await?,
|
||||
Commands::TestThroughput(args) => test_throughput::execute(args)?,
|
||||
Commands::UnsafeResetSphinxKeys(args) => reset_sphinx_keys::execute(args).await?,
|
||||
Commands::Debug(debug) => match debug.command {
|
||||
DebugCommands::ResetProvidersGatewayDbs(args) => {
|
||||
debug::reset_providers_dbs::execute(args).await?
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn execute(self) -> anyhow::Result<()> {
|
||||
// NOTE: `test_throughput` sets up its own logger as it has to include additional layers
|
||||
if !matches!(self.command, Commands::TestThroughput(..)) {
|
||||
crate::logging::setup_tracing_logger()?;
|
||||
#[cfg(feature = "otel")]
|
||||
fn build_otel_config(&self) -> Option<crate::logging::OtelConfig> {
|
||||
if self.otel.otel {
|
||||
Some(crate::logging::OtelConfig {
|
||||
endpoint: self.otel.otel_endpoint.clone(),
|
||||
service_name: "nym-node".to_string(),
|
||||
ingestion_key: self.otel.otel_key.clone(),
|
||||
environment: self.otel.otel_env.clone(),
|
||||
sample_ratio: self.otel.otel_sample_ratio,
|
||||
export_timeout_secs: self.otel.otel_export_timeout,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
match self.command {
|
||||
Commands::BuildInfo(args) => build_info::execute(args)?,
|
||||
Commands::BondingInformation(args) => {
|
||||
{ Self::execute_async(bonding_information::execute(args))? }?
|
||||
}
|
||||
Commands::NodeDetails(args) => { Self::execute_async(node_details::execute(args))? }?,
|
||||
Commands::Run(args) => { Self::execute_async(run::execute(*args))? }?,
|
||||
Commands::Migrate(args) => migrate::execute(*args)?,
|
||||
Commands::Sign(args) => { Self::execute_async(sign::execute(args))? }?,
|
||||
Commands::TestThroughput(args) => test_throughput::execute(args)?,
|
||||
Commands::UnsafeResetSphinxKeys(args) => {
|
||||
{ Self::execute_async(reset_sphinx_keys::execute(args))? }?
|
||||
}
|
||||
Commands::Debug(debug) => match debug.command {
|
||||
DebugCommands::ResetProvidersGatewayDbs(args) => {
|
||||
{ Self::execute_async(debug::reset_providers_dbs::execute(args))? }?
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "otel")]
|
||||
fn setup_logging(&self, use_otel: bool) -> anyhow::Result<Option<crate::logging::OtelGuard>> {
|
||||
if matches!(self.command, Commands::TestThroughput(..)) {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(())
|
||||
let otel_config = if use_otel {
|
||||
self.build_otel_config()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
crate::logging::setup_tracing_logger(otel_config)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "otel"))]
|
||||
fn setup_logging(&self, _use_otel: bool) -> anyhow::Result<Option<()>> {
|
||||
if matches!(self.command, Commands::TestThroughput(..)) {
|
||||
return Ok(None);
|
||||
}
|
||||
crate::logging::setup_tracing_logger()?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+111
-1
@@ -7,6 +7,42 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::{EnvFilter, Layer};
|
||||
|
||||
/// Configuration for OpenTelemetry OTLP export.
|
||||
#[cfg(feature = "otel")]
|
||||
pub(crate) struct OtelConfig {
|
||||
/// OTLP/gRPC collector endpoint, e.g. `http://localhost:4317`
|
||||
/// or `https://ingest.eu.signoz.cloud:443` for SigNoz Cloud.
|
||||
pub endpoint: String,
|
||||
/// Service name reported to the collector (appears in SigNoz "Services" view).
|
||||
pub service_name: String,
|
||||
/// Optional SigNoz Cloud ingestion key for authenticated export.
|
||||
/// Sent as the `signoz-ingestion-key` gRPC metadata header.
|
||||
pub ingestion_key: Option<String>,
|
||||
/// Deployment environment label, e.g. `mainnet`, `sandbox`, `canary`.
|
||||
/// Attached as the `deployment.environment` OTel resource attribute.
|
||||
pub environment: String,
|
||||
/// Trace sampling ratio in 0.0..=1.0 (e.g. 0.1 = 10% of traces). Used to limit cost.
|
||||
pub sample_ratio: f64,
|
||||
/// Timeout in seconds for each OTLP export batch. Prevents unbounded blocking.
|
||||
pub export_timeout_secs: u64,
|
||||
}
|
||||
|
||||
/// Handle returned when OTel is active. Flushes pending spans on drop
|
||||
/// to prevent telemetry loss during panics or early exits.
|
||||
#[cfg(feature = "otel")]
|
||||
pub(crate) struct OtelGuard {
|
||||
pub provider: opentelemetry_sdk::trace::SdkTracerProvider,
|
||||
}
|
||||
|
||||
#[cfg(feature = "otel")]
|
||||
impl Drop for OtelGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.provider.shutdown() {
|
||||
eprintln!("OpenTelemetry shutdown error in Drop: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn granual_filtered_env() -> anyhow::Result<EnvFilter> {
|
||||
fn directive_checked(directive: impl Into<String>) -> anyhow::Result<Directive> {
|
||||
directive.into().parse().map_err(From::from)
|
||||
@@ -22,12 +58,86 @@ pub(crate) fn granual_filtered_env() -> anyhow::Result<EnvFilter> {
|
||||
Ok(filter)
|
||||
}
|
||||
|
||||
/// Initialise the tracing subscriber stack.
|
||||
///
|
||||
/// When the `otel` feature is enabled **and** an `OtelConfig` is supplied, an
|
||||
/// OTLP exporter layer is added and the returned `OtelGuard` must be used to
|
||||
/// flush pending spans on shutdown.
|
||||
#[cfg(feature = "otel")]
|
||||
pub(crate) fn setup_tracing_logger(otel: Option<OtelConfig>) -> anyhow::Result<Option<OtelGuard>> {
|
||||
let stderr_layer =
|
||||
default_tracing_fmt_layer(std::io::stderr).with_filter(granual_filtered_env()?);
|
||||
|
||||
cfg_if::cfg_if! {if #[cfg(feature = "tokio-console")] {
|
||||
let console_layer = console_subscriber::spawn();
|
||||
|
||||
if let Some(otel_config) = otel {
|
||||
let (otel_layer, provider) = nym_bin_common::logging::init_otel_layer(
|
||||
&otel_config.service_name,
|
||||
&otel_config.endpoint,
|
||||
otel_config.ingestion_key.as_deref(),
|
||||
&otel_config.environment,
|
||||
otel_config.sample_ratio,
|
||||
otel_config.export_timeout_secs,
|
||||
).map_err(|e| anyhow::anyhow!(
|
||||
"failed to initialise OpenTelemetry exporter (endpoint: {}, service: {}): {e}",
|
||||
otel_config.endpoint,
|
||||
otel_config.service_name,
|
||||
))?;
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(console_layer)
|
||||
.with(stderr_layer)
|
||||
.with(otel_layer)
|
||||
.init();
|
||||
|
||||
Ok(Some(OtelGuard { provider }))
|
||||
} else {
|
||||
tracing_subscriber::registry()
|
||||
.with(console_layer)
|
||||
.with(stderr_layer)
|
||||
.init();
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
if let Some(otel_config) = otel {
|
||||
let (otel_layer, provider) = nym_bin_common::logging::init_otel_layer(
|
||||
&otel_config.service_name,
|
||||
&otel_config.endpoint,
|
||||
otel_config.ingestion_key.as_deref(),
|
||||
&otel_config.environment,
|
||||
otel_config.sample_ratio,
|
||||
otel_config.export_timeout_secs,
|
||||
).map_err(|e| anyhow::anyhow!(
|
||||
"failed to initialise OpenTelemetry exporter (endpoint: {}, service: {}): {e}",
|
||||
otel_config.endpoint,
|
||||
otel_config.service_name,
|
||||
))?;
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(stderr_layer)
|
||||
.with(otel_layer)
|
||||
.init();
|
||||
|
||||
Ok(Some(OtelGuard { provider }))
|
||||
} else {
|
||||
tracing_subscriber::registry()
|
||||
.with(stderr_layer)
|
||||
.init();
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
/// Non-OTel variant -- identical subscriber stack without the OTLP layer.
|
||||
#[cfg(not(feature = "otel"))]
|
||||
pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> {
|
||||
let stderr_layer =
|
||||
default_tracing_fmt_layer(std::io::stderr).with_filter(granual_filtered_env()?);
|
||||
|
||||
cfg_if::cfg_if! {if #[cfg(feature = "tokio-console")] {
|
||||
// instrument tokio console subscriber needs RUSTFLAGS="--cfg tokio_unstable" at build time
|
||||
let console_layer = console_subscriber::spawn();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use axum::extract::{Query, State};
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_node_requests::api::v1::metrics::models::{
|
||||
VerlocNodeResult, VerlocResult, VerlocResultData, VerlocStats,
|
||||
VerlocMeasurement, VerlocNodeResult, VerlocResult, VerlocResultData, VerlocStats,
|
||||
};
|
||||
use nym_verloc::measurements::SharedVerlocStats;
|
||||
|
||||
@@ -43,6 +43,12 @@ async fn build_response(verloc_stats: &SharedVerlocStats) -> VerlocStats {
|
||||
.iter()
|
||||
.map(|r| VerlocNodeResult {
|
||||
node_identity: r.node_identity,
|
||||
latest_measurement: r.latest_measurement.map(|m| VerlocMeasurement {
|
||||
minimum: m.minimum,
|
||||
mean: m.mean,
|
||||
maximum: m.maximum,
|
||||
standard_deviation: m.standard_deviation,
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::key_rotation::active_keys::SphinxKeyGuard;
|
||||
use crate::node::mixnet::shared::SharedData;
|
||||
use futures::StreamExt;
|
||||
use nym_noise::connection::Connection;
|
||||
@@ -20,7 +21,10 @@ use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::codec::Framed;
|
||||
use tracing::{debug, error, instrument, trace, warn};
|
||||
use tracing::{Span, debug, error, instrument, trace, warn};
|
||||
|
||||
/// How often (in packets) the stream-level span updates its packet count.
|
||||
const SPAN_UPDATE_INTERVAL: u64 = 10_000;
|
||||
|
||||
struct PendingReplayCheckPackets {
|
||||
// map of rotation id used for packet creation to the packets
|
||||
@@ -51,6 +55,10 @@ impl PendingReplayCheckPackets {
|
||||
.push(packet.packet)
|
||||
}
|
||||
|
||||
fn total_count(&self) -> usize {
|
||||
self.packets.values().map(|v| v.len()).sum()
|
||||
}
|
||||
|
||||
fn replay_tags(&self) -> HashMap<u32, Vec<&[u8; REPLAY_TAG_SIZE]>> {
|
||||
let mut replay_tags = HashMap::with_capacity(self.packets.len());
|
||||
'outer: for (rotation_id, packets) in &self.packets {
|
||||
@@ -130,20 +138,54 @@ impl ConnectionHandler {
|
||||
Some(now + delay)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "mixnode.forward_packet",
|
||||
skip(self, mix_packet, delay),
|
||||
level = "debug",
|
||||
fields(
|
||||
remote_addr = %self.remote_address,
|
||||
delay_ms = tracing::field::Empty,
|
||||
)
|
||||
)]
|
||||
fn handle_forward_packet(&self, now: Instant, mix_packet: MixPacket, delay: Option<Delay>) {
|
||||
if !self.shared.processing_config.forward_hop_processing_enabled {
|
||||
trace!("this nym-node does not support forward hop packets");
|
||||
warn!(
|
||||
event = "packet.dropped.forward_disabled",
|
||||
remote_addr = %self.remote_address,
|
||||
"dropping packet: forward hop processing disabled"
|
||||
);
|
||||
self.shared.dropped_forward_packet(self.remote_address.ip());
|
||||
return;
|
||||
}
|
||||
|
||||
let forward_instant = self.create_delay_target(now, delay);
|
||||
if let Some(target) = forward_instant {
|
||||
Span::current().record(
|
||||
"delay_ms",
|
||||
target.saturating_duration_since(now).as_millis() as u64,
|
||||
);
|
||||
}
|
||||
self.shared.forward_mix_packet(mix_packet, forward_instant);
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "mixnode.final_hop",
|
||||
skip(self, final_hop_data),
|
||||
level = "debug",
|
||||
fields(
|
||||
remote_addr = %self.remote_address,
|
||||
client_online,
|
||||
disk_fallback = false,
|
||||
ack_forwarded = false,
|
||||
)
|
||||
)]
|
||||
async fn handle_final_hop(&self, final_hop_data: ProcessedFinalHop) {
|
||||
if !self.shared.processing_config.final_hop_processing_enabled {
|
||||
trace!("this nym-node does not support final hop packets");
|
||||
warn!(
|
||||
event = "packet.dropped.final_hop_disabled",
|
||||
remote_addr = %self.remote_address,
|
||||
"dropping packet: final hop processing disabled"
|
||||
);
|
||||
self.shared
|
||||
.dropped_final_hop_packet(self.remote_address.ip());
|
||||
return;
|
||||
@@ -151,11 +193,13 @@ impl ConnectionHandler {
|
||||
|
||||
let client = final_hop_data.destination;
|
||||
let message = final_hop_data.message;
|
||||
let has_ack = final_hop_data.forward_ack.is_some();
|
||||
|
||||
// if possible attempt to push message directly to the client
|
||||
match self.shared.try_push_message_to_client(client, message) {
|
||||
Err(unsent_plaintext) => {
|
||||
// if that failed, store it on disk (to be 🔥 soon...)
|
||||
// if that failed, store it on disk
|
||||
Span::current().record("client_online", false);
|
||||
match self
|
||||
.shared
|
||||
.store_processed_packet_payload(client, unsent_plaintext)
|
||||
@@ -163,6 +207,7 @@ impl ConnectionHandler {
|
||||
{
|
||||
Err(err) => error!("Failed to store client data - {err}"),
|
||||
Ok(_) => {
|
||||
Span::current().record("disk_fallback", true);
|
||||
self.shared
|
||||
.metrics
|
||||
.mixnet
|
||||
@@ -172,13 +217,18 @@ impl ConnectionHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(_) => trace!("Pushed received packet to {client}"),
|
||||
Ok(_) => {
|
||||
Span::current().record("client_online", true);
|
||||
trace!("Pushed received packet to {client}");
|
||||
}
|
||||
}
|
||||
|
||||
// if we managed to either push message directly to the [online] client or store it at
|
||||
// its inbox, it means that it must exist at this gateway, hence we can send the
|
||||
// received ack back into the network
|
||||
// disk, forward the ack
|
||||
self.shared.forward_ack_packet(final_hop_data.forward_ack);
|
||||
if has_ack {
|
||||
Span::current().record("ack_forwarded", true);
|
||||
}
|
||||
}
|
||||
|
||||
fn within_deferral_threshold(&self, now: Instant) -> bool {
|
||||
@@ -206,32 +256,86 @@ impl ConnectionHandler {
|
||||
|
||||
if !time_threshold {
|
||||
warn!(
|
||||
"{}: time failure - {}",
|
||||
event = "replay_detection.deferral_exceeded",
|
||||
threshold_type = "time",
|
||||
deferred_count = self.pending_packets.total_count(),
|
||||
deferral_ms = now.saturating_duration_since(self.pending_packets.last_acquired_mutex).as_millis() as u64,
|
||||
remote_addr = %self.remote_address,
|
||||
"{}: time deferral threshold exceeded with {} pending packets",
|
||||
self.remote_address,
|
||||
self.pending_packets.packets.len()
|
||||
self.pending_packets.total_count()
|
||||
)
|
||||
}
|
||||
|
||||
if !count_threshold {
|
||||
warn!("{}, count failure", self.remote_address)
|
||||
warn!(
|
||||
event = "replay_detection.deferral_exceeded",
|
||||
threshold_type = "count",
|
||||
deferred_count = self.pending_packets.total_count(),
|
||||
remote_addr = %self.remote_address,
|
||||
"{}: count deferral threshold exceeded",
|
||||
self.remote_address
|
||||
)
|
||||
}
|
||||
|
||||
time_threshold && count_threshold
|
||||
}
|
||||
|
||||
/// Resolve the sphinx key for the given rotation, recording the rotation
|
||||
/// label on the current tracing span. Returns `ExpiredKey` if the requested
|
||||
/// odd/even key has already been rotated out.
|
||||
fn resolve_rotation_key(
|
||||
&self,
|
||||
rotation: SphinxKeyRotation,
|
||||
) -> Result<SphinxKeyGuard, PacketProcessingError> {
|
||||
let rotation_label = match rotation {
|
||||
SphinxKeyRotation::Unknown => "unknown",
|
||||
SphinxKeyRotation::OddRotation => "odd",
|
||||
SphinxKeyRotation::EvenRotation => "even",
|
||||
};
|
||||
Span::current().record("key_rotation", rotation_label);
|
||||
|
||||
match rotation {
|
||||
SphinxKeyRotation::Unknown => Ok(self.shared.sphinx_keys.primary()),
|
||||
SphinxKeyRotation::OddRotation => self.shared.sphinx_keys.odd().ok_or_else(|| {
|
||||
warn!(
|
||||
event = "packet.dropped.expired_key",
|
||||
key_rotation = "odd",
|
||||
remote_addr = %self.remote_address,
|
||||
"dropping packet: odd key rotation expired"
|
||||
);
|
||||
PacketProcessingError::ExpiredKey
|
||||
}),
|
||||
SphinxKeyRotation::EvenRotation => self.shared.sphinx_keys.even().ok_or_else(|| {
|
||||
warn!(
|
||||
event = "packet.dropped.expired_key",
|
||||
key_rotation = "even",
|
||||
remote_addr = %self.remote_address,
|
||||
"dropping packet: even key rotation expired"
|
||||
);
|
||||
PacketProcessingError::ExpiredKey
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "mixnode.sphinx_partial_unwrap",
|
||||
skip(self, packet),
|
||||
level = "debug",
|
||||
fields(key_rotation, unwrap_result,)
|
||||
)]
|
||||
fn try_partially_unwrap_packet(
|
||||
&self,
|
||||
packet: FramedNymPacket,
|
||||
) -> Result<PartialyUnwrappedPacketWithKeyRotation, PacketProcessingError> {
|
||||
// based on the received sphinx key rotation information,
|
||||
// attempt to choose appropriate key for processing the packet
|
||||
match packet.header().key_rotation {
|
||||
let rotation = packet.header().key_rotation;
|
||||
|
||||
let result = match rotation {
|
||||
SphinxKeyRotation::Unknown => {
|
||||
let primary = self.shared.sphinx_keys.primary();
|
||||
// Unknown rotation: try primary, fallback to secondary
|
||||
let primary = self.resolve_rotation_key(rotation)?;
|
||||
let primary_rotation = primary.rotation_id();
|
||||
|
||||
// we have to try both keys, start with the primary as it has higher likelihood of being correct
|
||||
// if let Ok(partially_unwrapped) = PartiallyUnwrappedPacket::new()
|
||||
match PartiallyUnwrappedPacket::new(packet, primary.inner().as_ref()) {
|
||||
Ok(unwrapped_packet) => {
|
||||
Ok(unwrapped_packet.with_key_rotation(primary_rotation))
|
||||
@@ -248,25 +352,17 @@ impl ConnectionHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
SphinxKeyRotation::OddRotation => {
|
||||
let Some(odd_key) = self.shared.sphinx_keys.odd() else {
|
||||
return Err(PacketProcessingError::ExpiredKey);
|
||||
};
|
||||
let odd_rotation = odd_key.rotation_id();
|
||||
PartiallyUnwrappedPacket::new(packet, odd_key.inner().as_ref())
|
||||
_ => {
|
||||
let key = self.resolve_rotation_key(rotation)?;
|
||||
let rotation_id = key.rotation_id();
|
||||
PartiallyUnwrappedPacket::new(packet, key.inner().as_ref())
|
||||
.map_err(|(_, err)| err)
|
||||
.map(|p| p.with_key_rotation(odd_rotation))
|
||||
.map(|p| p.with_key_rotation(rotation_id))
|
||||
}
|
||||
SphinxKeyRotation::EvenRotation => {
|
||||
let Some(even_key) = self.shared.sphinx_keys.even() else {
|
||||
return Err(PacketProcessingError::ExpiredKey);
|
||||
};
|
||||
let even_rotation = even_key.rotation_id();
|
||||
PartiallyUnwrappedPacket::new(packet, even_key.inner().as_ref())
|
||||
.map_err(|(_, err)| err)
|
||||
.map(|p| p.with_key_rotation(even_rotation))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Span::current().record("unwrap_result", if result.is_ok() { "ok" } else { "err" });
|
||||
result
|
||||
}
|
||||
|
||||
async fn handle_received_packet_with_replay_detection(
|
||||
@@ -280,6 +376,12 @@ impl ConnectionHandler {
|
||||
Ok(unwrapped) => unwrapped,
|
||||
Err(err) => {
|
||||
trace!("failed to process received mix packet: {err}");
|
||||
warn!(
|
||||
event = "packet.dropped.malformed",
|
||||
error = %err,
|
||||
remote_addr = %self.remote_address,
|
||||
"dropping malformed packet"
|
||||
);
|
||||
self.shared
|
||||
.metrics
|
||||
.mixnet
|
||||
@@ -316,7 +418,9 @@ impl ConnectionHandler {
|
||||
|
||||
// 3. forward the packet to the relevant sink (if enabled)
|
||||
match unwrapped_packet {
|
||||
Err(err) => trace!("failed to process received mix packet: {err}"),
|
||||
Err(err) => {
|
||||
trace!("failed to process received mix packet: {err}");
|
||||
}
|
||||
Ok(processed_packet) => match processed_packet.processing_data {
|
||||
MixProcessingResultData::ForwardHop { packet, delay } => {
|
||||
self.handle_forward_packet(now, packet, delay);
|
||||
@@ -334,6 +438,7 @@ impl ConnectionHandler {
|
||||
packets: HashMap<u32, Vec<PartiallyUnwrappedPacket>>,
|
||||
replay_check_results: HashMap<u32, Vec<bool>>,
|
||||
) {
|
||||
let mut replays_detected: u64 = 0;
|
||||
for (rotation_id, packets) in packets {
|
||||
let Some(replay_checks) = replay_check_results.get(&rotation_id) else {
|
||||
// this should never happen, but if we messed up, and it does, don't panic, just drop the packets
|
||||
@@ -342,6 +447,13 @@ impl ConnectionHandler {
|
||||
};
|
||||
for (packet, &replayed) in packets.into_iter().zip(replay_checks) {
|
||||
let unwrapped_packet = if replayed {
|
||||
replays_detected += 1;
|
||||
warn!(
|
||||
event = "packet.dropped.replay",
|
||||
remote_addr = %self.remote_address,
|
||||
rotation_id,
|
||||
"dropping replayed packet"
|
||||
);
|
||||
Err(PacketProcessingError::PacketReplay)
|
||||
} else {
|
||||
packet.finalise_unwrapping()
|
||||
@@ -350,6 +462,13 @@ impl ConnectionHandler {
|
||||
self.handle_unwrapped_packet(now, unwrapped_packet).await;
|
||||
}
|
||||
}
|
||||
if replays_detected > 0 {
|
||||
debug!(
|
||||
replays_detected,
|
||||
remote_addr = %self.remote_address,
|
||||
"replay detection batch completed with replays"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_pending_packets_batch_no_locking(&mut self, now: Instant) -> bool {
|
||||
@@ -379,13 +498,22 @@ impl ConnectionHandler {
|
||||
true
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "mixnode.replay_check_batch",
|
||||
skip(self),
|
||||
level = "debug",
|
||||
fields(batch_size, mutex_wait_ms,)
|
||||
)]
|
||||
async fn handle_pending_packets_batch(&mut self, now: Instant) {
|
||||
let batch = self.pending_packets.reset(now);
|
||||
let replay_tags = self.pending_packets.replay_tags();
|
||||
if replay_tags.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let batch_size = self.pending_packets.total_count();
|
||||
Span::current().record("batch_size", batch_size as u64);
|
||||
|
||||
let mutex_start = Instant::now();
|
||||
let Ok(replay_check_results) = self
|
||||
.shared
|
||||
.replay_protection_filter
|
||||
@@ -396,37 +524,25 @@ impl ConnectionHandler {
|
||||
self.shared.shutdown_token.cancel();
|
||||
return;
|
||||
};
|
||||
Span::current().record("mutex_wait_ms", mutex_start.elapsed().as_millis() as u64);
|
||||
|
||||
let batch = self.pending_packets.reset(now);
|
||||
self.handle_post_replay_detection_packets(now, batch, replay_check_results)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "mixnode.sphinx_full_unwrap",
|
||||
skip(self, packet),
|
||||
level = "debug",
|
||||
fields(key_rotation)
|
||||
)]
|
||||
fn try_full_unwrap_packet(
|
||||
&self,
|
||||
packet: FramedNymPacket,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
// based on the received sphinx key rotation information,
|
||||
// attempt to choose appropriate key for processing the packet
|
||||
// NOTE: due to the function signatures, outfox packets will **only** attempt primary key
|
||||
// if no rotation information is available (but that's fine given outfox is not really in use,
|
||||
// and by the time we need it, the rotation info should be present)
|
||||
match packet.header().key_rotation {
|
||||
SphinxKeyRotation::Unknown => {
|
||||
process_framed_packet(packet, self.shared.sphinx_keys.primary().inner().as_ref())
|
||||
}
|
||||
SphinxKeyRotation::OddRotation => {
|
||||
let Some(odd_key) = self.shared.sphinx_keys.odd() else {
|
||||
return Err(PacketProcessingError::ExpiredKey);
|
||||
};
|
||||
process_framed_packet(packet, odd_key.inner().as_ref())
|
||||
}
|
||||
SphinxKeyRotation::EvenRotation => {
|
||||
let Some(even_key) = self.shared.sphinx_keys.even() else {
|
||||
return Err(PacketProcessingError::ExpiredKey);
|
||||
};
|
||||
process_framed_packet(packet, even_key.inner().as_ref())
|
||||
}
|
||||
}
|
||||
let key = self.resolve_rotation_key(packet.header().key_rotation)?;
|
||||
process_framed_packet(packet, key.inner().as_ref())
|
||||
}
|
||||
|
||||
async fn handle_received_packet_with_no_replay_detection(
|
||||
@@ -456,23 +572,36 @@ impl ConnectionHandler {
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
skip(self),
|
||||
name = "mixnode.connection",
|
||||
skip(self, socket),
|
||||
level = "debug",
|
||||
fields(
|
||||
remote = %self.remote_address
|
||||
remote = %self.remote_address,
|
||||
noise_handshake_ms = tracing::field::Empty,
|
||||
)
|
||||
)]
|
||||
pub(crate) async fn handle_connection(&mut self, socket: TcpStream) {
|
||||
let handshake_start = Instant::now();
|
||||
let noise_stream = match upgrade_noise_responder(socket, &self.shared.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to perform Noise handshake with {:?} - {err}",
|
||||
self.remote_address
|
||||
Span::current().record(
|
||||
"noise_handshake_ms",
|
||||
handshake_start.elapsed().as_millis() as u64,
|
||||
);
|
||||
warn!(
|
||||
event = "connection.failed.noise",
|
||||
remote_addr = %self.remote_address,
|
||||
error = %err,
|
||||
"Noise responder handshake failed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
Span::current().record(
|
||||
"noise_handshake_ms",
|
||||
handshake_start.elapsed().as_millis() as u64,
|
||||
);
|
||||
debug!(
|
||||
"Noise responder handshake completed for {:?}",
|
||||
self.remote_address
|
||||
@@ -481,26 +610,58 @@ impl ConnectionHandler {
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "mixnode.stream",
|
||||
skip(self, mixnet_connection),
|
||||
level = "debug",
|
||||
fields(
|
||||
remote = %self.remote_address,
|
||||
packets_processed = 0u64,
|
||||
exit_reason,
|
||||
)
|
||||
)]
|
||||
pub(crate) async fn handle_stream(
|
||||
&mut self,
|
||||
mut mixnet_connection: Framed<Connection<TcpStream>, NymCodec>,
|
||||
) {
|
||||
let mut packets_processed: u64 = 0;
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.shared.shutdown_token.cancelled() => {
|
||||
trace!("connection handler: received shutdown");
|
||||
Span::current().record("exit_reason", "shutdown");
|
||||
break
|
||||
}
|
||||
maybe_framed_nym_packet = mixnet_connection.next() => {
|
||||
match maybe_framed_nym_packet {
|
||||
Some(Ok(packet)) => self.handle_received_nym_packet(packet).await,
|
||||
Some(Ok(packet)) => {
|
||||
self.handle_received_nym_packet(packet).await;
|
||||
packets_processed += 1;
|
||||
if packets_processed.is_multiple_of(SPAN_UPDATE_INTERVAL) {
|
||||
Span::current().record("packets_processed", packets_processed);
|
||||
}
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
debug!("connection got corrupted with: {err}");
|
||||
warn!(
|
||||
event = "connection.corrupted",
|
||||
remote_addr = %self.remote_address,
|
||||
error = %err,
|
||||
packets_processed,
|
||||
"connection stream corrupted"
|
||||
);
|
||||
Span::current().record("exit_reason", "corrupted");
|
||||
Span::current().record("packets_processed", packets_processed);
|
||||
return
|
||||
}
|
||||
None => {
|
||||
debug!("connection got closed by the remote");
|
||||
debug!(
|
||||
remote_addr = %self.remote_address,
|
||||
packets_processed,
|
||||
"connection closed by remote"
|
||||
);
|
||||
Span::current().record("exit_reason", "closed_by_remote");
|
||||
Span::current().record("packets_processed", packets_processed);
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -508,6 +669,7 @@ impl ConnectionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
Span::current().record("packets_processed", packets_processed);
|
||||
debug!("exiting and closing connection");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +56,17 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
if let Err(err) = self.mixnet_client.send_without_response(packet) {
|
||||
if err.kind() == io::ErrorKind::WouldBlock {
|
||||
// we only know for sure if we dropped a packet if our sending queue was full
|
||||
// in any other case the connection might still be re-established (or created for the first time)
|
||||
// and the packet might get sent, but we won't know about it
|
||||
warn!(
|
||||
event = "packet.dropped.buffer_full",
|
||||
next_hop = %next_hop,
|
||||
"dropping packet: egress connection buffer full (WouldBlock)"
|
||||
);
|
||||
self.metrics.mixnet.egress_dropped_forward_packet(next_hop)
|
||||
} else if err.kind() == io::ErrorKind::NotConnected {
|
||||
// let's give the benefit of the doubt and assume we manage to establish connection
|
||||
debug!(
|
||||
next_hop = %next_hop,
|
||||
"packet queued for not-yet-connected peer"
|
||||
);
|
||||
self.metrics.mixnet.egress_sent_forward_packet(next_hop)
|
||||
}
|
||||
} else {
|
||||
@@ -86,7 +92,11 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
let next_hop = new_packet.packet.next_hop();
|
||||
|
||||
if !self.routing_filter.should_route(next_hop.as_ref().ip()) {
|
||||
debug!("dropping packet as the egress address does not belong to any known node");
|
||||
warn!(
|
||||
event = "packet.dropped.routing_filter",
|
||||
next_hop = %next_hop,
|
||||
"dropping packet: egress address does not belong to any known node"
|
||||
);
|
||||
self.metrics
|
||||
.mixnet
|
||||
.egress_dropped_forward_packet(next_hop.into());
|
||||
@@ -125,7 +135,7 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
let mut processed = 0;
|
||||
let mut processed: u64 = 0;
|
||||
trace!("starting PacketForwarder");
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -145,11 +155,29 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.handle_new_packet(new_packet.unwrap());
|
||||
let channel_len = self.packet_sender.len();
|
||||
if processed % 1000 == 0 {
|
||||
let delay_queue_len = self.delay_queue.len();
|
||||
if processed.is_multiple_of(1000) {
|
||||
match channel_len {
|
||||
n if n > 1000 => error!("there are currently {n} mix packets waiting to get forwarded - the node seems to be significantly overloaded!"),
|
||||
n if n > 500 => warn!("there are currently {n} mix packets waiting to get forwarded - is the node overloaded?"),
|
||||
n => trace!("there are currently {n} mix packets waiting to get forwarded"),
|
||||
n if n > 1000 => error!(
|
||||
event = "forwarder.queue_overload",
|
||||
channel_depth = n,
|
||||
delay_queue_depth = delay_queue_len,
|
||||
packets_processed = processed,
|
||||
"there are currently {n} mix packets waiting to get forwarded - the node seems to be significantly overloaded!"
|
||||
),
|
||||
n if n > 500 => warn!(
|
||||
event = "forwarder.queue_high",
|
||||
channel_depth = n,
|
||||
delay_queue_depth = delay_queue_len,
|
||||
packets_processed = processed,
|
||||
"there are currently {n} mix packets waiting to get forwarded - is the node overloaded?"
|
||||
),
|
||||
n => trace!(
|
||||
channel_depth = n,
|
||||
delay_queue_depth = delay_queue_len,
|
||||
packets_processed = processed,
|
||||
"forwarder queue status"
|
||||
),
|
||||
}
|
||||
}
|
||||
self.update_channel_size_metric(channel_len);
|
||||
|
||||
@@ -5,7 +5,8 @@ use nym_gateway::node::{
|
||||
ActiveClientsStore, GatewayStorage, GatewayStorageError, InboxGatewayStorage,
|
||||
};
|
||||
use nym_sphinx_types::DestinationAddressBytes;
|
||||
use tracing::debug;
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SharedFinalHopData {
|
||||
@@ -27,14 +28,37 @@ impl SharedFinalHopData {
|
||||
message: Vec<u8>,
|
||||
) -> Result<(), Vec<u8>> {
|
||||
match self.active_clients.get_sender(client_address) {
|
||||
None => Err(message),
|
||||
None => {
|
||||
debug!(
|
||||
event = "gateway.push_to_client",
|
||||
client_found = false,
|
||||
send_result = "client_not_found",
|
||||
"client {client_address} not found in active clients"
|
||||
);
|
||||
Err(message)
|
||||
}
|
||||
Some(sender_channel) => {
|
||||
let send_start = Instant::now();
|
||||
if let Err(unsent) = sender_channel.unbounded_send(vec![message]) {
|
||||
warn!(
|
||||
event = "gateway.push_to_client",
|
||||
client_found = true,
|
||||
send_result = "channel_closed",
|
||||
send_us = send_start.elapsed().as_micros() as u64,
|
||||
"client {client_address} channel closed, message not delivered"
|
||||
);
|
||||
// the unwrap here is fine as the original message got returned;
|
||||
// plus we're only ever sending 1 message at the time (for now)
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Err(unsent.into_inner().pop().unwrap())
|
||||
} else {
|
||||
debug!(
|
||||
event = "gateway.push_to_client",
|
||||
client_found = true,
|
||||
send_result = "ok",
|
||||
send_us = send_start.elapsed().as_micros() as u64,
|
||||
"pushed message to client {client_address}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -46,8 +70,21 @@ impl SharedFinalHopData {
|
||||
client_address: DestinationAddressBytes,
|
||||
message: Vec<u8>,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
let start = Instant::now();
|
||||
debug!("Storing received message for {client_address} on the disk...",);
|
||||
|
||||
self.storage.store_message(client_address, message).await
|
||||
let result = self.storage.store_message(client_address, message).await;
|
||||
let store_us = start.elapsed().as_micros() as u64;
|
||||
if result.is_ok() {
|
||||
debug!(
|
||||
event = "gateway.disk_store",
|
||||
store_us, "stored message for {client_address} on disk in {store_us}us"
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
event = "gateway.disk_store_failed",
|
||||
store_us, "failed to store message for {client_address} on disk after {store_us}us"
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ impl SharedData {
|
||||
}
|
||||
|
||||
pub(super) fn forward_mix_packet(&self, packet: MixPacket, delay_until: Option<Instant>) {
|
||||
let has_delay = delay_until.is_some();
|
||||
if self
|
||||
.mixnet_forwarder
|
||||
.forward_packet(PacketToForward::new(packet, delay_until))
|
||||
@@ -192,6 +193,8 @@ impl SharedData {
|
||||
&& !self.shutdown_token.is_cancelled()
|
||||
{
|
||||
error!(
|
||||
event = "forwarder.channel_send_failed",
|
||||
has_delay,
|
||||
"failed to forward sphinx packet on the channel while the process is not going through the shutdown!"
|
||||
);
|
||||
self.shutdown_token.cancel();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-node",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"description": "This package is a drop-in replacement for `fetch` in NodeJS to send HTTP requests over the Nym Mixnet.",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Nym Technologies SA",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"description": "This package is a drop-in replacement for `fetch` to send HTTP requests over the Nym Mixnet.",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Nym Technologies SA",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-network-requester"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.71"
|
||||
version = "1.1.72"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version = "1.85"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-cli"
|
||||
version = "1.1.70"
|
||||
version = "1.1.71"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nymvisor"
|
||||
version = "0.1.35"
|
||||
version = "0.1.36"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user