Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7587eafb7 | |||
| 6710e8df56 | |||
| a5ba5e57ad | |||
| 389d7d0b4e | |||
| 650c034f46 | |||
| 9ca0ba802b | |||
| 118f836690 | |||
| afbdc107bf | |||
| a66852c0ae | |||
| 04e4378352 | |||
| 2b056fdd6e | |||
| c20dcd7de9 | |||
| 53caaf3ac7 | |||
| 98c7b57468 | |||
| 77e1468aae | |||
| 00d509ca79 | |||
| db5692ffc7 | |||
| 8151f65963 | |||
| 8b24e426bf | |||
| 507f40a963 | |||
| e06db2f18e | |||
| 8b23552e77 | |||
| 74b2aab446 | |||
| fb874d699b | |||
| 764639971c | |||
| 0de4aea77b | |||
| b9339b8f0c | |||
| 43a7360399 | |||
| 5f9f7f0fac | |||
| df0e2fe489 | |||
| bc33cc4c8d | |||
| a31597aca9 | |||
| 378229b04e | |||
| fec196c097 | |||
| 1d7ffc1bb6 | |||
| 0caa627960 | |||
| d6b3d7fc0a | |||
| ac273480f8 | |||
| 79603d61d7 | |||
| e8e9a70ef4 | |||
| 0c52ee89c8 | |||
| f324d45721 | |||
| 470e88f46c | |||
| 42a5016822 | |||
| 579cff358d | |||
| f95dda0f2f | |||
| fc666fb984 | |||
| 1264fd9bfb | |||
| 3e8451f292 | |||
| 53f4582202 | |||
| c7c6dcab65 | |||
| 3422c49e85 | |||
| deee0b8e14 | |||
| 7243cb57b5 | |||
| 0276bd7b0b | |||
| 457759bb57 | |||
| de0f8ee2d3 | |||
| ebf97ece9b | |||
| 50a55f4bfb | |||
| 4ee5c6457b | |||
| d7b5fce7aa | |||
| c3d9c1131b | |||
| 5bdfb1ba5c | |||
| 94e51f0047 | |||
| f313e95e2f | |||
| 2b13ac99b4 | |||
| ef220882d4 | |||
| 59e26178ee | |||
| 0d420fb0a5 | |||
| fce195fdba | |||
| e52bd918fb | |||
| 9d82d6d111 | |||
| 3593631e4a | |||
| 3a528d3b89 | |||
| e9bb9792ab | |||
| f5846d5bc2 | |||
| 88d4a9b111 | |||
| d7779df1b7 | |||
| 7fcc188041 | |||
| b8c8d33c94 | |||
| 02909c03dd | |||
| 11262836d2 | |||
| f26fd5384d | |||
| 085103b333 | |||
| 574f7f1abd | |||
| 31e161604a | |||
| e4e349bea8 | |||
| 6391b7ed3a | |||
| c225511f95 | |||
| 4eedbb235a | |||
| 548b8717b2 | |||
| a215b3d0bf | |||
| 03d5a133eb | |||
| b323c62a6e | |||
| d511aac301 |
@@ -5,7 +5,6 @@ on:
|
||||
paths:
|
||||
- 'clients/**'
|
||||
- 'common/**'
|
||||
- 'explorer-api/**'
|
||||
- 'gateway/**'
|
||||
- 'integrations/**'
|
||||
- 'nym-api/**'
|
||||
@@ -39,7 +38,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ]
|
||||
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -44,8 +44,10 @@ jobs:
|
||||
echo "Tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
# first, list all tags for logging purposes
|
||||
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
# check if there's a matching tag
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
|
||||
if [[ $exists = "true" ]]; then
|
||||
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
|
||||
exit 1
|
||||
@@ -53,5 +55,5 @@ jobs:
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -44,8 +44,10 @@ jobs:
|
||||
echo "Tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
# first, list all tags for logging purposes
|
||||
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
# check if there's a matching tag
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
|
||||
if [[ $exists = "true" ]]; then
|
||||
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
|
||||
exit 1
|
||||
@@ -53,5 +55,5 @@ jobs:
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -66,7 +66,6 @@ jobs:
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
target/release/explorer-api
|
||||
target/release/nym-client
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
@@ -75,14 +74,13 @@ jobs:
|
||||
target/release/nymvisor
|
||||
target/release/nym-node
|
||||
retention-days: 30
|
||||
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/explorer-api
|
||||
target/release/nym-client
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
|
||||
@@ -40,7 +40,6 @@ validator-config
|
||||
validator-api-config.toml
|
||||
dist
|
||||
storybook-static
|
||||
envs/qwerty.env
|
||||
.parcel-cache
|
||||
**/.DS_Store
|
||||
cpu-cycles/libcpucycles/build
|
||||
|
||||
@@ -4,6 +4,30 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.11-cheddar] (2025-06-10)
|
||||
|
||||
- No autoremoval of peers ([#5831])
|
||||
- Set cached storage counters to 0 ([#5812])
|
||||
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
|
||||
- chore: resolve 1.87 clippy warnings ([#5802])
|
||||
- Nym Statistics API ([#5800])
|
||||
- QoL: RequestPath trait for http-api-client ([#5788])
|
||||
- Fix contains ticketbook function that always returned true ([#5787])
|
||||
- swap a decode into a fromrow to please future postgres feature ([#5785])
|
||||
- Make address cache configurable ([#5784])
|
||||
- Track wireguard credential retries ([#5783])
|
||||
|
||||
[#5831]: https://github.com/nymtech/nym/pull/5831
|
||||
[#5812]: https://github.com/nymtech/nym/pull/5812
|
||||
[#5805]: https://github.com/nymtech/nym/pull/5805
|
||||
[#5802]: https://github.com/nymtech/nym/pull/5802
|
||||
[#5800]: https://github.com/nymtech/nym/pull/5800
|
||||
[#5788]: https://github.com/nymtech/nym/pull/5788
|
||||
[#5787]: https://github.com/nymtech/nym/pull/5787
|
||||
[#5785]: https://github.com/nymtech/nym/pull/5785
|
||||
[#5784]: https://github.com/nymtech/nym/pull/5784
|
||||
[#5783]: https://github.com/nymtech/nym/pull/5783
|
||||
|
||||
## [2025.10-brie] (2025-05-27)
|
||||
|
||||
- Backport PR 5779 ([#5801])
|
||||
|
||||
Generated
+938
-286
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -64,6 +64,7 @@ members = [
|
||||
"common/node-tester-utils",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
"common/nym-id",
|
||||
"common/nym-psq-kkt",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
@@ -101,7 +102,6 @@ members = [
|
||||
"common/wireguard-types",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integrations/bity",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-browser-extension/storage",
|
||||
@@ -127,6 +127,7 @@ members = [
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"sqlx-pool-guard",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
@@ -288,6 +289,7 @@ petgraph = "0.6.5"
|
||||
pin-project = "1.1"
|
||||
pin-project-lite = "0.2.16"
|
||||
publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.56"
|
||||
version = "1.1.57"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.56"
|
||||
version = "1.1.57"
|
||||
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"
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::BadGateway;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -19,7 +18,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -32,7 +30,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::replies::reply_storage::{
|
||||
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
|
||||
use crate::{
|
||||
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
|
||||
config,
|
||||
config::Config,
|
||||
error::ClientCoreError,
|
||||
};
|
||||
use crate::config;
|
||||
use crate::config::Config;
|
||||
use crate::error::ClientCoreError;
|
||||
use log::{error, info, trace};
|
||||
use nym_bandwidth_controller::BandwidthController;
|
||||
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
|
||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||
use nym_validator_client::nyxd;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use std::path::Path;
|
||||
use std::{fs, io};
|
||||
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
|
||||
use std::{io, path::Path};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
@@ -22,11 +20,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
db_path: P,
|
||||
surb_config: &config::ReplySurbs,
|
||||
) -> Result<fs_backend::Backend, ClientCoreError> {
|
||||
info!("creating fresh surb database");
|
||||
info!("Creating fresh surb database");
|
||||
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
|
||||
Ok(backend) => backend,
|
||||
Err(err) => {
|
||||
error!("failed to setup persistent storage backend for our reply needs: {err}");
|
||||
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
|
||||
return Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
});
|
||||
@@ -40,14 +38,15 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
surb_config.minimum_reply_surb_storage_threshold,
|
||||
surb_config.maximum_reply_surb_storage_threshold,
|
||||
);
|
||||
storage_backend
|
||||
.init_fresh(&mem_store)
|
||||
.await
|
||||
.map_err(|err| ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})?;
|
||||
|
||||
Ok(storage_backend)
|
||||
match storage_backend.init_fresh(&mem_store).await {
|
||||
Ok(()) => Ok(storage_backend),
|
||||
Err(err) => {
|
||||
storage_backend.shutdown().await;
|
||||
Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
|
||||
@@ -58,12 +57,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
// )
|
||||
// }
|
||||
|
||||
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
let db_path = db_path.as_ref();
|
||||
debug_assert!(db_path.exists());
|
||||
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
let suffix = format!("_{now}.corrupted");
|
||||
|
||||
let new_extension =
|
||||
@@ -72,11 +70,15 @@ fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
} else {
|
||||
suffix
|
||||
};
|
||||
let renamed = db_path.with_extension(new_extension);
|
||||
|
||||
let mut renamed = db_path.to_owned();
|
||||
renamed.set_extension(new_extension);
|
||||
|
||||
fs::rename(db_path, renamed)
|
||||
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
|
||||
error!(
|
||||
"Failed to rename corrupt database file: {} to {}",
|
||||
db_path.display(),
|
||||
renamed.display()
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
@@ -87,13 +89,12 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
// the existing one
|
||||
let db_path = db_path.as_ref();
|
||||
if db_path.exists() {
|
||||
info!("loading existing surb database");
|
||||
info!("Loading existing surb database");
|
||||
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
|
||||
Ok(backend) => Ok(backend),
|
||||
Err(err) => {
|
||||
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
|
||||
|
||||
archive_corrupted_database(db_path)?;
|
||||
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
|
||||
archive_corrupted_database(db_path).await?;
|
||||
setup_fresh_backend(db_path, surb_config).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,26 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
nym-task = { path = "../../task" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
workspace = true
|
||||
features = ["fs"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
||||
optional = true
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
path = "../../../sqlx-pool-guard"
|
||||
|
||||
[build-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
|
||||
[features]
|
||||
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -30,7 +29,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -43,7 +41,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -15,9 +15,11 @@ use sqlx::{
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageManager {
|
||||
pub connection_pool: sqlx::SqlitePool,
|
||||
connection_pool: SqlitePoolGuard,
|
||||
}
|
||||
|
||||
// all SQL goes here
|
||||
@@ -37,7 +39,7 @@ impl StorageManager {
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(database_path)
|
||||
.filename(&database_path)
|
||||
.create_if_missing(fresh)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -49,11 +51,15 @@ impl StorageManager {
|
||||
}
|
||||
};
|
||||
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
|
||||
.run(&connection_pool)
|
||||
.run(&*connection_pool)
|
||||
.await
|
||||
{
|
||||
error!("Failed to initialize SQLx database: {err}");
|
||||
connection_pool.close().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
@@ -61,38 +67,43 @@ impl StorageManager {
|
||||
Ok(StorageManager { connection_pool })
|
||||
}
|
||||
|
||||
/// Close connection pool waiting for all connections to be closed.
|
||||
pub async fn close_pool(&self) {
|
||||
self.connection_pool.close().await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.is_some())
|
||||
}
|
||||
|
||||
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT flush_in_progress FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.flush_in_progress > 0)
|
||||
}
|
||||
|
||||
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
|
||||
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.previous_flush_timestamp)
|
||||
}
|
||||
@@ -100,14 +111,14 @@ impl StorageManager {
|
||||
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
|
||||
let in_progress_int = i64::from(in_progress);
|
||||
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT client_in_use FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.client_in_use > 0)
|
||||
}
|
||||
@@ -115,21 +126,21 @@ impl StorageManager {
|
||||
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
|
||||
let in_use_int = i64::from(in_use);
|
||||
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM sender_tag;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -141,21 +152,21 @@ impl StorageManager {
|
||||
stored_tag.recipient,
|
||||
stored_tag.tag
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM reply_key;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -171,14 +182,14 @@ impl StorageManager {
|
||||
stored_reply_key.reply_key,
|
||||
stored_reply_key.sent_at_timestamp
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -193,7 +204,7 @@ impl StorageManager {
|
||||
stored_surb_sender.tag,
|
||||
stored_surb_sender.last_sent_timestamp
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
.last_insert_rowid();
|
||||
Ok(id)
|
||||
@@ -211,17 +222,17 @@ impl StorageManager {
|
||||
"#,
|
||||
sender_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM reply_surb;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM reply_surb_sender;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -239,7 +250,7 @@ impl StorageManager {
|
||||
stored_reply_surb.reply_surb,
|
||||
stored_reply_surb.encoded_key_rotation
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -253,7 +264,7 @@ impl StorageManager {
|
||||
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -267,7 +278,7 @@ impl StorageManager {
|
||||
"#,
|
||||
metadata.min_reply_surb_threshold,
|
||||
metadata.max_reply_surb_threshold,
|
||||
).execute(&self.connection_pool).await?;
|
||||
).execute(&*self.connection_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::backend::fs_backend::manager::StorageManager;
|
||||
use crate::backend::fs_backend::models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
|
||||
};
|
||||
use crate::surb_storage::ReceivedReplySurbs;
|
||||
use crate::{
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
|
||||
backend::fs_backend::{
|
||||
manager::StorageManager,
|
||||
models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
|
||||
StoredSurbSender,
|
||||
},
|
||||
},
|
||||
surb_storage::ReceivedReplySurbs,
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
|
||||
UsedSenderTags,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, info, warn};
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -41,15 +44,17 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, true).await?;
|
||||
manager.create_status_table().await?;
|
||||
|
||||
let backend = Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
};
|
||||
|
||||
Ok(backend)
|
||||
match manager.create_status_table().await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
}),
|
||||
Err(err) => {
|
||||
manager.close_pool().await;
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_load<P: AsRef<Path>>(
|
||||
@@ -64,7 +69,28 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, false).await?;
|
||||
match Self::try_load_inner(&manager, fresh_sender_tags).await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
}),
|
||||
Err(e) => {
|
||||
manager.close_pool().await;
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gracefully close sqlite connection pool and drop backend.
|
||||
pub async fn shutdown(self) {
|
||||
self.manager.close_pool().await
|
||||
}
|
||||
|
||||
async fn try_load_inner(
|
||||
manager: &StorageManager,
|
||||
fresh_sender_tags: bool,
|
||||
) -> Result<(), StorageError> {
|
||||
// the database flush wasn't fully finished and thus the data is in inconsistent state
|
||||
// (we don't really know what's properly saved or what's not)
|
||||
if manager.get_flush_status().await? {
|
||||
@@ -126,20 +152,11 @@ impl Backend {
|
||||
manager.delete_all_tags().await?;
|
||||
}
|
||||
|
||||
Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
// manager: StorageManagerState::Storage(manager),
|
||||
manager,
|
||||
})
|
||||
}
|
||||
|
||||
async fn close_pool(&mut self) {
|
||||
self.manager.connection_pool.close().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rotate(&mut self) -> Result<(), StorageError> {
|
||||
self.close_pool().await;
|
||||
self.manager.close_pool().await;
|
||||
|
||||
let new_extension = if let Some(existing_extension) =
|
||||
self.database_path.extension().and_then(|ext| ext.to_str())
|
||||
@@ -152,7 +169,8 @@ impl Backend {
|
||||
let mut temp_old = self.database_path.clone();
|
||||
temp_old.set_extension(new_extension);
|
||||
|
||||
fs::rename(&self.database_path, &temp_old)
|
||||
tokio::fs::rename(&self.database_path, &temp_old)
|
||||
.await
|
||||
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
|
||||
self.manager = StorageManager::init(&self.database_path, true).await?;
|
||||
self.manager.create_status_table().await?;
|
||||
@@ -161,9 +179,10 @@ impl Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
async fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
if let Some(old_path) = self.temporary_old_path.take() {
|
||||
fs::remove_file(old_path)
|
||||
tokio::fs::remove_file(old_path)
|
||||
.await
|
||||
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
|
||||
} else {
|
||||
warn!("the old database file doesn't seem to exist!");
|
||||
@@ -335,7 +354,7 @@ impl ReplyStorageBackend for Backend {
|
||||
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
|
||||
self.dump_reply_surbs(surbs_ref).await?;
|
||||
|
||||
self.remove_old()?;
|
||||
self.remove_old().await?;
|
||||
self.end_storage_flush().await
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ where
|
||||
self.backend.load_surb_storage().await
|
||||
}
|
||||
|
||||
// this will have to get enabled after merging develop
|
||||
pub async fn flush_on_shutdown(
|
||||
mut self,
|
||||
mem_state: CombinedReplyStorage,
|
||||
@@ -50,7 +49,6 @@ where
|
||||
shutdown.recv().await;
|
||||
|
||||
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
|
||||
info!("you MUST NOT forcefully shutdown now or you risk data corruption!");
|
||||
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
|
||||
error!("failed to flush our reply-related data to the persistent storage: {err}")
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@ use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use std::io;
|
||||
@@ -250,7 +250,7 @@ impl Client {
|
||||
|
||||
connections_count.fetch_add(1, Ordering::SeqCst);
|
||||
ManagedConnection::new(
|
||||
address,
|
||||
address.into(),
|
||||
noise_config,
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
|
||||
@@ -16,8 +16,8 @@ async fn main() {
|
||||
let prefix = "n";
|
||||
let denom: Denom = "unym".parse().unwrap();
|
||||
let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap();
|
||||
let validator = "https://qwerty-validator.qa.nymte.ch";
|
||||
let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap();
|
||||
let validator = "https://rpc.sandbox.nymtech.net";
|
||||
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap();
|
||||
|
||||
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
|
||||
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
|
||||
|
||||
@@ -25,9 +25,7 @@ use nym_api_requests::models::{
|
||||
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
|
||||
};
|
||||
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode, SkimmedNode};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_http_api_client::UserAgent;
|
||||
use nym_mixnet_contract_common::EpochRewardedSet;
|
||||
@@ -567,6 +565,31 @@ impl NymApiClient {
|
||||
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
/// retrieve expanded information for all bonded nodes on the network
|
||||
pub async fn get_all_expanded_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SemiSkimmedNode>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_expanded_nodes(false, Some(page), None)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
|
||||
Ok(self.nym_api.health().await?)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ use nym_api_requests::models::{
|
||||
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
|
||||
PaginatedCachedNodesResponseV2,
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
|
||||
SemiSkimmedNode,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
pub use nym_api_requests::{
|
||||
@@ -676,6 +676,39 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_expanded_nodes(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedCachedNodesResponse<SemiSkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"semi-skimmed",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn send(args: Args, client: &SigningClient) {
|
||||
"Nodesguru: https://nym.explorers.guru/transaction/{}",
|
||||
&res.hash
|
||||
);
|
||||
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
|
||||
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
|
||||
println!("Transaction result code: {}", &res.tx_result.code.value());
|
||||
println!("Transaction hash: {}", &res.hash);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) {
|
||||
"Nodesguru: https://nym.explorers.guru/transaction/{}",
|
||||
&res.hash
|
||||
);
|
||||
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
|
||||
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
|
||||
println!("Transaction result code: {}", &res.tx_result.code.value());
|
||||
println!("Transaction hash: {}", &res.hash);
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ pub async fn execute(args: Args, client: SigningClient) {
|
||||
&res.transaction_hash
|
||||
);
|
||||
println!(
|
||||
"Mintscan: https://www.mintscan.io/nyx/txs/{}",
|
||||
"Mintscan: https://ping.pub/nyx/tx/{}",
|
||||
&res.transaction_hash
|
||||
);
|
||||
println!("Transaction hash: {}", &res.transaction_hash);
|
||||
|
||||
@@ -20,6 +20,8 @@ nym-credentials = { path = "../credentials" }
|
||||
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
|
||||
nym-ecash-time = { path = "../ecash-time" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
path = "../../sqlx-pool-guard"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
@@ -31,8 +33,13 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
persistent-storage = ["bincode", "serde"]
|
||||
persistent-storage = ["bincode", "serde"]
|
||||
|
||||
@@ -7,10 +7,11 @@ use crate::models::{
|
||||
};
|
||||
use nym_ecash_time::Date;
|
||||
use sqlx::{Executor, Sqlite, Transaction};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SqliteEcashTicketbookManager {
|
||||
connection_pool: sqlx::SqlitePool,
|
||||
connection_pool: SqlitePoolGuard,
|
||||
}
|
||||
|
||||
impl SqliteEcashTicketbookManager {
|
||||
@@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `connection_pool`: database connection pool to use.
|
||||
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
|
||||
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
|
||||
SqliteEcashTicketbookManager { connection_pool }
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
|
||||
deadline
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager {
|
||||
data,
|
||||
expiration_date,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager {
|
||||
epoch_id,
|
||||
total_tickets,
|
||||
used_tickets,
|
||||
).execute(&self.connection_pool).await?;
|
||||
).execute(&*self.connection_pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
)
|
||||
.bind(data)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await?
|
||||
.is_some();
|
||||
|
||||
@@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager {
|
||||
FROM ecash_ticketbook
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager {
|
||||
ticketbook_id,
|
||||
expected_current_total_spent
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
Ok(affected > 0)
|
||||
@@ -153,7 +154,7 @@ impl SqliteEcashTicketbookManager {
|
||||
&self,
|
||||
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
|
||||
sqlx::query_as("SELECT * FROM pending_issuance")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM pending_issuance WHERE deposit_id = ?",
|
||||
pending_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
expiration_date
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
expiration_date
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
use std::path::Path;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -54,15 +55,15 @@ impl PersistentStorage {
|
||||
/// * `database_path`: path to the database.
|
||||
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(database_path)
|
||||
.filename(&database_path)
|
||||
.create_if_missing(true)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -74,13 +75,17 @@ impl PersistentStorage {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
|
||||
error!("Failed to perform migration on the SQLx database: {err}");
|
||||
connection_pool.close().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
Ok(PersistentStorage {
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ impl PersistentStatsStorage {
|
||||
/// * `database_path`: path to the database.
|
||||
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -82,8 +82,8 @@ impl GatewayStorage {
|
||||
message_retrieval_limit: i64,
|
||||
) -> Result<Self, GatewayStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(feature = "network")]
|
||||
use crate::{DenomDetails, ValidatorDetails};
|
||||
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
|
||||
|
||||
pub const NETWORK_NAME: &str = "mainnet";
|
||||
|
||||
@@ -29,10 +29,36 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str =
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
|
||||
|
||||
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_API,
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-fronntdoor.vercel.app/api/",
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
];
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_VPN_API,
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
];
|
||||
|
||||
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
|
||||
pub const EXIT_POLICY_URL: &str =
|
||||
|
||||
@@ -37,6 +37,37 @@ pub struct NymNetworkDetails {
|
||||
pub contracts: NymContracts,
|
||||
pub explorer_api: Option<String>,
|
||||
pub nym_vpn_api_url: Option<String>,
|
||||
pub nym_api_urls: Option<Vec<ApiUrl>>,
|
||||
pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct ApiUrl {
|
||||
/// Expects a string formatted Url
|
||||
///
|
||||
/// see https://docs.rs/url/latest/url/struct.Url.html
|
||||
pub url: String,
|
||||
/// Optional alternative equivalent hostnames. Each entry must parse as valid Host
|
||||
///
|
||||
/// see https://docs.rs/url/latest/url/enum.Host.html
|
||||
pub front_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub struct ApiUrlConst<'a> {
|
||||
pub url: &'a str,
|
||||
pub front_hosts: Option<&'a [&'a str]>,
|
||||
}
|
||||
|
||||
impl From<ApiUrlConst<'_>> for ApiUrl {
|
||||
fn from(value: ApiUrlConst) -> Self {
|
||||
ApiUrl {
|
||||
url: value.url.to_string(),
|
||||
front_hosts: value
|
||||
.front_hosts
|
||||
.map(|slice| slice.iter().map(|s| s.to_string()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
|
||||
@@ -67,6 +98,8 @@ impl NymNetworkDetails {
|
||||
contracts: Default::default(),
|
||||
explorer_api: Default::default(),
|
||||
nym_vpn_api_url: Default::default(),
|
||||
nym_api_urls: Default::default(),
|
||||
nym_vpn_api_urls: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +187,8 @@ impl NymNetworkDetails {
|
||||
},
|
||||
explorer_api: parse_optional_str(mainnet::EXPLORER_API),
|
||||
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
|
||||
nym_api_urls: None,
|
||||
nym_vpn_api_urls: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "nym-psq-kkt"
|
||||
version = "0.1.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
arc-swap = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
snow = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"]}
|
||||
|
||||
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
|
||||
rand = "0.9.1"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = {workspace = true}
|
||||
|
||||
[[bench]]
|
||||
name = "benches"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use libcrux_psq::impls::MlKem768;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_psq_kkt::kkt::{
|
||||
KKTInitiator, KKTResponder, KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
|
||||
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate Ed25519 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
let mut s: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut s);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate MlKem768 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut secret_responder: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut secret_responder);
|
||||
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
|
||||
|
||||
// generate kem keypair
|
||||
let (_, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap();
|
||||
|
||||
// initialize parties
|
||||
let initiator: KKTInitiator<MlKem768> =
|
||||
KKTInitiator::init(initiator_ed25519_keypair.private_key());
|
||||
|
||||
c.bench_function("Initialize Initiator", |b| {
|
||||
b.iter(|| KKTInitiator::<MlKem768>::init(initiator_ed25519_keypair.private_key()));
|
||||
});
|
||||
|
||||
let responder: KKTResponder<MlKem768> = KKTResponder::init(
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
);
|
||||
|
||||
c.bench_function("Initialize Responder", |b| {
|
||||
b.iter(|| {
|
||||
KKTResponder::init(
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
// create buffers
|
||||
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
|
||||
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
|
||||
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
|
||||
|
||||
c.bench_function("Initiator: Generate Request", |b| {
|
||||
b.iter(|| {
|
||||
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
|
||||
});
|
||||
});
|
||||
|
||||
// generate request
|
||||
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
|
||||
|
||||
c.bench_function("Responder: Ingest Request and Generate Response", |b| {
|
||||
b.iter(|| {
|
||||
responder
|
||||
.respond_kem_pk(
|
||||
&mut response_buffer,
|
||||
initiator_ed25519_keypair.public_key(),
|
||||
&request_buffer,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
// ingest request, generate response
|
||||
responder
|
||||
.respond_kem_pk(
|
||||
&mut response_buffer,
|
||||
initiator_ed25519_keypair.public_key(),
|
||||
&request_buffer,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function("Initiator: Ingest Response and Store Key", |b| {
|
||||
b.iter(|| {
|
||||
let _ = initiator
|
||||
.ingest_response_kem_pk::<MlKem768>(
|
||||
&response_buffer,
|
||||
&tag_buffer,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
// ingest response
|
||||
let received_responder_key = initiator
|
||||
.ingest_response_kem_pk::<MlKem768>(
|
||||
&response_buffer,
|
||||
&tag_buffer,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// check if the public key received is the same one that we generated at the start
|
||||
assert_eq!(
|
||||
responder_kem_public_key.encode(),
|
||||
received_responder_key.encode()
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
gen_ed25519_keypair,
|
||||
gen_mlkem768_keypair,
|
||||
kkt_benchmark
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PSQError {
|
||||
#[error("encountered a decryption error")]
|
||||
DecryptionError,
|
||||
|
||||
#[error("encountered a KEM error")]
|
||||
KEMError,
|
||||
|
||||
#[error("encountered a PSQ error")]
|
||||
PSQError,
|
||||
|
||||
#[error("encountered an IO error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
#[error("Incorrect state")]
|
||||
IncorrectStateError,
|
||||
|
||||
#[error("Handshake did not complete")]
|
||||
HandshakeError,
|
||||
|
||||
#[error("Unknown noise version")]
|
||||
UnknownVersion,
|
||||
|
||||
#[error("Handshake timeout")]
|
||||
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
|
||||
}
|
||||
|
||||
impl From<libcrux_kem::Error> for PSQError {
|
||||
fn from(err: libcrux_kem::Error) -> Self {
|
||||
match err {
|
||||
// Error::Decrypt => PSQError::DecryptionError,
|
||||
err => PSQError::KEMError,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<libcrux_psq::Error> for PSQError {
|
||||
fn from(err: libcrux_psq::Error) -> Self {
|
||||
match err {
|
||||
// Error::Decrypt => PSQError::DecryptionError,
|
||||
err => PSQError::PSQError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KKTError {
|
||||
#[error("Signature constructor error")]
|
||||
SigConstructorError,
|
||||
|
||||
#[error("Signature verification error")]
|
||||
SigVerifError,
|
||||
// #[error("Protocol did not complete")]
|
||||
// ProtocolError,
|
||||
|
||||
// #[error("encountered an IO error: {0}")]
|
||||
// IoError(#[from] io::Error),
|
||||
|
||||
// #[error("Handshake timeout")]
|
||||
// HandshakeTimeout(#[from] tokio::time::error::Elapsed),
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::prelude::*;
|
||||
|
||||
use libcrux_kem::MlKem768PublicKey;
|
||||
use libcrux_psq::{impls::MlKem768, traits::PSQ};
|
||||
use libcrux_traits::kem::KEM;
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
const REQ_STR: &[u8] = "KEM_REQ".as_bytes();
|
||||
const RES_STR: &[u8] = "KEM_RES".as_bytes();
|
||||
|
||||
const REQ_LEN: usize = REQ_STR.len() + KKT_TAG_LEN;
|
||||
const RES_LEN_MLKEM768: usize = REQ_LEN + MlKem768PublicKey::len();
|
||||
|
||||
pub const KKT_TAG_LEN: usize = 16;
|
||||
pub const KKT_REQ_LEN: usize = REQ_LEN + ed25519::SIGNATURE_LENGTH;
|
||||
pub const KKT_RES_LEN_MLKEM768: usize = RES_LEN_MLKEM768 + ed25519::SIGNATURE_LENGTH;
|
||||
|
||||
pub struct KKTInitiator<'a, T: PSQ> {
|
||||
signing_key: &'a ed25519::PrivateKey,
|
||||
_t: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: PSQ> KKTInitiator<'a, T> {
|
||||
pub fn init(signing_key: &'a ed25519::PrivateKey) -> Self {
|
||||
Self {
|
||||
signing_key,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
pub fn request_kem_pk(
|
||||
&self,
|
||||
request_buffer: &mut [u8; KKT_REQ_LEN],
|
||||
tag_buffer: &mut [u8; KKT_TAG_LEN],
|
||||
) {
|
||||
// request_buffer[0..7] <- request string
|
||||
request_buffer[0..REQ_STR.len()].copy_from_slice(REQ_STR);
|
||||
|
||||
// tag_buffer <- generate tag
|
||||
rand::rng().fill_bytes(tag_buffer);
|
||||
|
||||
// request_buffer[7..23] <- tag
|
||||
request_buffer[REQ_STR.len()..REQ_LEN].copy_from_slice(tag_buffer);
|
||||
|
||||
// sig <- Sign request_buffer[0..23]
|
||||
let sig = self.signing_key.sign(&request_buffer[0..REQ_LEN]);
|
||||
|
||||
// request_buffer[23..87] <- sig
|
||||
request_buffer[REQ_LEN..].copy_from_slice(&sig.to_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> KKTInitiator<'a, MlKem768> {
|
||||
pub fn ingest_response_kem_pk<T: PSQ>(
|
||||
&self,
|
||||
response: &[u8],
|
||||
tag: &[u8; KKT_TAG_LEN],
|
||||
responder_verification_key: &ed25519::PublicKey,
|
||||
) -> Result<libcrux_kem::PublicKey, KKTError> {
|
||||
// TODO: Refactor asserts into errors
|
||||
// Check size of message
|
||||
assert_eq!(response.len(), KKT_RES_LEN_MLKEM768);
|
||||
// Check if the response string is there at the start of the message
|
||||
assert_eq!(&response[0..RES_STR.len()], RES_STR);
|
||||
// Check if the tag is the one we expect
|
||||
assert_eq!(
|
||||
&response[RES_STR.len()..RES_STR.len() + KKT_TAG_LEN],
|
||||
&tag[..]
|
||||
);
|
||||
|
||||
// Attempt to reconstruct a signature from the received bytes
|
||||
match ed25519::Signature::from_bytes(&response[RES_LEN_MLKEM768..]) {
|
||||
Ok(sig) => {
|
||||
// Attempt to verify signature
|
||||
match responder_verification_key.verify(&response[0..RES_LEN_MLKEM768], &sig) {
|
||||
Ok(()) => {
|
||||
// Extract key from bytes
|
||||
// (has to be an owned sized array, unless I'm missing some function that works with slices)
|
||||
let mut key_bytes: [u8; MlKem768PublicKey::len()] =
|
||||
[0u8; MlKem768PublicKey::len()];
|
||||
key_bytes.copy_from_slice(
|
||||
&response[RES_STR.len() + KKT_TAG_LEN..RES_LEN_MLKEM768],
|
||||
);
|
||||
|
||||
// Create key from bytes and return it
|
||||
Ok(libcrux_kem::PublicKey::MlKem768(MlKem768PublicKey::from(
|
||||
key_bytes,
|
||||
)))
|
||||
}
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KKTResponder<'a, T: PSQ> {
|
||||
signing_key: &'a ed25519::PrivateKey,
|
||||
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
|
||||
}
|
||||
|
||||
impl<'a> KKTResponder<'a, MlKem768> {
|
||||
pub fn init(
|
||||
signing_key: &'a ed25519::PrivateKey,
|
||||
kem_public_key: &'a libcrux_kem::PublicKey,
|
||||
) -> Self {
|
||||
Self {
|
||||
signing_key,
|
||||
kem_public_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn respond_kem_pk(
|
||||
&self,
|
||||
response_buffer: &mut [u8; KKT_RES_LEN_MLKEM768],
|
||||
initiator_verification_key: &ed25519::PublicKey,
|
||||
request: &[u8],
|
||||
) -> Result<(), KKTError> {
|
||||
// TODO: Refactor asserts into errors
|
||||
// Check request size
|
||||
assert_eq!(request.len(), KKT_REQ_LEN);
|
||||
// Check request string
|
||||
assert_eq!(&request[0..REQ_STR.len()], REQ_STR);
|
||||
|
||||
match ed25519::Signature::from_bytes(&request[REQ_LEN..]) {
|
||||
Ok(sig) => match initiator_verification_key.verify(&request[0..REQ_LEN], &sig) {
|
||||
Ok(()) => {
|
||||
// response_buffer[0..7] <- RES_STR (7 bytes)
|
||||
response_buffer[0..RES_STR.len()].copy_from_slice(RES_STR);
|
||||
|
||||
// response_buffer[7..23] <- tag (16 bytes, sent by initiator)
|
||||
response_buffer[RES_STR.len()..REQ_LEN]
|
||||
.copy_from_slice(&request[REQ_STR.len()..REQ_LEN]);
|
||||
|
||||
// response_buffer[23..1207] <- MlKem768 Public Key (1184 bytes)
|
||||
response_buffer[REQ_LEN..RES_LEN_MLKEM768]
|
||||
.copy_from_slice(self.kem_public_key.encode().as_slice());
|
||||
|
||||
// sign response_buffer[0..1207]
|
||||
let sig = self.signing_key.sign(&response_buffer[0..RES_LEN_MLKEM768]);
|
||||
|
||||
// response_buffer[1207..1271] <- signature (64 bytes)
|
||||
response_buffer[RES_LEN_MLKEM768..].copy_from_slice(&sig.to_bytes());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
},
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use crate::kkt::{KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN};
|
||||
|
||||
use super::{KKTInitiator, KKTResponder};
|
||||
use libcrux_psq::impls::MlKem768;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_kkt_e2e() {
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut secret_responder: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut secret_responder);
|
||||
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
|
||||
|
||||
// generate kem keypair
|
||||
let (_, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap();
|
||||
|
||||
// initialize parties
|
||||
let initiator: KKTInitiator<MlKem768> =
|
||||
KKTInitiator::init(initiator_ed25519_keypair.private_key());
|
||||
|
||||
let responder: KKTResponder<MlKem768> = KKTResponder::init(
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
);
|
||||
|
||||
// create buffers
|
||||
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
|
||||
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
|
||||
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
|
||||
|
||||
// generate request
|
||||
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
|
||||
|
||||
// ingest request, generate response
|
||||
responder
|
||||
.respond_kem_pk(
|
||||
&mut response_buffer,
|
||||
initiator_ed25519_keypair.public_key(),
|
||||
&request_buffer,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// ingest response
|
||||
let received_responder_key = initiator
|
||||
.ingest_response_kem_pk::<MlKem768>(
|
||||
&response_buffer,
|
||||
&tag_buffer,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// check if the public key received is the same one that we generated at the start
|
||||
assert_eq!(
|
||||
responder_kem_public_key.encode(),
|
||||
received_responder_key.encode()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod error;
|
||||
pub mod kkt;
|
||||
pub mod psq;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use libcrux_psq::impls::MlKem768;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::prelude::*;
|
||||
|
||||
use crate::{
|
||||
kkt::{KKTInitiator, KKTResponder, KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN},
|
||||
psq::{PSQInitiator, PSQResponder},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut secret_responder: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_responder);
|
||||
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
|
||||
|
||||
// generate kem keypair
|
||||
let (responder_kem_private_key, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap();
|
||||
|
||||
// initialize parties
|
||||
let kkt_initiator: KKTInitiator<MlKem768> =
|
||||
KKTInitiator::init(initiator_ed25519_keypair.private_key());
|
||||
|
||||
let kkt_responder: KKTResponder<MlKem768> = KKTResponder::init(
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
);
|
||||
|
||||
// create buffers
|
||||
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
|
||||
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
|
||||
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
|
||||
|
||||
// generate request
|
||||
kkt_initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
|
||||
|
||||
// ingest request, generate response
|
||||
kkt_responder
|
||||
.respond_kem_pk(
|
||||
&mut response_buffer,
|
||||
initiator_ed25519_keypair.public_key(),
|
||||
&request_buffer,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// ingest response
|
||||
let received_responder_key = kkt_initiator
|
||||
.ingest_response_kem_pk::<MlKem768>(
|
||||
&response_buffer,
|
||||
&tag_buffer,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// check if the public key received is the same one that we generated at the start
|
||||
assert_eq!(
|
||||
responder_kem_public_key.encode(),
|
||||
received_responder_key.encode()
|
||||
);
|
||||
|
||||
let mut psq_initiator: PSQInitiator<MlKem768> =
|
||||
PSQInitiator::init(&initiator_ed25519_keypair);
|
||||
let psq_responder: PSQResponder<MlKem768> =
|
||||
PSQResponder::init(&responder_kem_private_key, &responder_kem_public_key);
|
||||
|
||||
let initiator_psq_msg = psq_initiator
|
||||
.initiator_message(&mut rng, &received_responder_key)
|
||||
.unwrap();
|
||||
|
||||
let (responder_psk, responder_psq_msg) = psq_responder
|
||||
.responder_msg(initiator_ed25519_keypair.public_key(), &initiator_psq_msg)
|
||||
.unwrap();
|
||||
|
||||
let initiator_psk = psq_initiator.finalize(&responder_psq_msg).unwrap();
|
||||
|
||||
assert_eq!(initiator_psk, responder_psk);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::{marker::PhantomData, time::Duration};
|
||||
|
||||
use libcrux_psq::{
|
||||
cred::Ed25519,
|
||||
psk_registration::{Initiator, InitiatorMsg, Responder, ResponderMsg},
|
||||
traits::PSQ,
|
||||
};
|
||||
|
||||
use libcrux_traits::kem::KEM;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::CryptoRng;
|
||||
|
||||
use crate::error::PSQError;
|
||||
|
||||
pub const PSK_LENGTH: usize = 32;
|
||||
|
||||
pub struct PSQInitiator<'a, T: PSQ> {
|
||||
signing_keypair: &'a ed25519::KeyPair,
|
||||
state: Option<Initiator>,
|
||||
_t: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: PSQ> PSQInitiator<'a, T> {
|
||||
pub fn init(signing_keypair: &'a ed25519::KeyPair) -> Self {
|
||||
Self {
|
||||
signing_keypair,
|
||||
state: None,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initiator_message(
|
||||
&mut self,
|
||||
rng: &mut impl CryptoRng,
|
||||
responder_kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
|
||||
) -> Result<InitiatorMsg<T::InnerKEM>, PSQError> {
|
||||
let (state, message) = Initiator::send_initial_message::<Ed25519, T>(
|
||||
&[0u8],
|
||||
Duration::from_secs(3600),
|
||||
&responder_kem_public_key,
|
||||
&self.signing_keypair.private_key().to_bytes(),
|
||||
&self.signing_keypair.public_key().to_bytes(),
|
||||
rng,
|
||||
)?;
|
||||
self.state = Some(state);
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub fn finalize(&self, responder_message: &ResponderMsg) -> Result<[u8; PSK_LENGTH], PSQError> {
|
||||
match &self.state {
|
||||
Some(state) => match state.complete_handshake(responder_message) {
|
||||
Ok(psk) => Ok(psk.psk),
|
||||
Err(err) => Err(PSQError::KEMError),
|
||||
},
|
||||
None => Err(PSQError::IncorrectStateError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PSQResponder<'a, T: PSQ> {
|
||||
kem_private_key: &'a <T::InnerKEM as KEM>::DecapsulationKey,
|
||||
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
|
||||
_t: PhantomData<T>,
|
||||
}
|
||||
impl<'a, T: PSQ> PSQResponder<'a, T> {
|
||||
pub fn init(
|
||||
kem_private_key: &'a <T::InnerKEM as KEM>::DecapsulationKey,
|
||||
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
|
||||
) -> Self {
|
||||
Self {
|
||||
kem_private_key,
|
||||
kem_public_key,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn responder_msg(
|
||||
&self,
|
||||
initiator_verification_key: &'a ed25519::PublicKey,
|
||||
initiator_message: &InitiatorMsg<T::InnerKEM>,
|
||||
) -> Result<([u8; PSK_LENGTH], ResponderMsg), PSQError> {
|
||||
let (registered_psk, responder_msg) = Responder::send::<Ed25519, T>(
|
||||
&[0u8],
|
||||
Duration::from_secs(3600),
|
||||
&[0u8],
|
||||
self.kem_public_key,
|
||||
self.kem_private_key,
|
||||
&initiator_verification_key.to_bytes(),
|
||||
initiator_message,
|
||||
)?;
|
||||
|
||||
Ok((registered_psk.psk, responder_msg))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use libcrux_psq::impls::{MlKem768, XWingKemDraft06, X25519};
|
||||
use libcrux_psq::traits::PSQ;
|
||||
use libcrux_traits::kem::KEM;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::prelude::*;
|
||||
|
||||
use super::{PSQInitiator, PSQResponder};
|
||||
|
||||
fn test_helper<T: PSQ>(
|
||||
rng: &mut impl CryptoRng,
|
||||
responder_kem_private_key: <T::InnerKEM as KEM>::DecapsulationKey,
|
||||
responder_kem_public_key: <T::InnerKEM as KEM>::EncapsulationKey,
|
||||
) {
|
||||
// generate ed25519 keys
|
||||
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut initiator: PSQInitiator<T> = PSQInitiator::init(&initiator_ed25519_keypair);
|
||||
let responder: PSQResponder<T> =
|
||||
PSQResponder::init(&responder_kem_private_key, &responder_kem_public_key);
|
||||
|
||||
let initiator_msg = initiator
|
||||
.initiator_message(rng, &responder_kem_public_key)
|
||||
.unwrap();
|
||||
|
||||
let (responder_psk, responder_msg) = responder
|
||||
.responder_msg(initiator_ed25519_keypair.public_key(), &initiator_msg)
|
||||
.unwrap();
|
||||
|
||||
let initiator_psk = initiator.finalize(&responder_msg).unwrap();
|
||||
|
||||
assert_eq!(initiator_psk, responder_psk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_e2e_mlkem() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate mlkem keypair
|
||||
let (responder_kem_private_key, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap();
|
||||
|
||||
test_helper::<MlKem768>(
|
||||
&mut rng,
|
||||
responder_kem_private_key,
|
||||
responder_kem_public_key,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_e2e_xwing() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate mlkem keypair
|
||||
let (responder_kem_private_key, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft06, &mut rand::rng())
|
||||
.unwrap();
|
||||
|
||||
test_helper::<XWingKemDraft06>(
|
||||
&mut rng,
|
||||
responder_kem_private_key,
|
||||
responder_kem_public_key,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_e2e_dhkem() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate mlkem keypair
|
||||
let (responder_kem_private_key, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rand::rng()).unwrap();
|
||||
|
||||
test_helper::<X25519>(
|
||||
&mut rng,
|
||||
responder_kem_private_key,
|
||||
responder_kem_public_key,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,5 @@ tokio-util = { workspace = true, features = ["codec"] }
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-noise-keys = { path = "keys" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
rand_chacha = { workspace = true }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -8,25 +8,22 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(from = "u8", into = "u8")]
|
||||
pub enum NoiseVersion {
|
||||
V1,
|
||||
Unknown(u8), //Implies a newer version we don't know
|
||||
V1 = 1,
|
||||
Unknown, //Implies a newer version we don't know
|
||||
}
|
||||
|
||||
impl From<u8> for NoiseVersion {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
1 => NoiseVersion::V1,
|
||||
other => NoiseVersion::Unknown(other),
|
||||
_ => NoiseVersion::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NoiseVersion> for u8 {
|
||||
fn from(version: NoiseVersion) -> Self {
|
||||
match version {
|
||||
NoiseVersion::V1 => 1,
|
||||
NoiseVersion::Unknown(other) => other,
|
||||
}
|
||||
version as u8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +31,7 @@ impl From<NoiseVersion> for u8 {
|
||||
pub struct VersionedNoiseKey {
|
||||
#[schemars(with = "u8")]
|
||||
#[schema(value_type = u8)]
|
||||
pub supported_version: NoiseVersion,
|
||||
pub version: NoiseVersion,
|
||||
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
|
||||
@@ -13,19 +13,17 @@ use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
|
||||
use snow::params::NoiseParams;
|
||||
|
||||
use strum::{EnumIter, FromRepr};
|
||||
use strum::EnumIter;
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Default, Debug, Clone, Copy, EnumIter)]
|
||||
pub enum NoisePattern {
|
||||
#[default]
|
||||
XKpsk3 = 1,
|
||||
IKpsk2 = 2,
|
||||
XKpsk3,
|
||||
IKpsk2,
|
||||
}
|
||||
|
||||
impl NoisePattern {
|
||||
pub(crate) const fn as_str(&self) -> &'static str {
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::XKpsk3 => "Noise_XKpsk3_25519_AESGCM_SHA256",
|
||||
Self::IKpsk2 => "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s", //Wireguard handshake (not exactly though)
|
||||
@@ -81,7 +79,7 @@ impl NoiseNetworkView {
|
||||
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKey>) {
|
||||
let noise_support = new
|
||||
.iter()
|
||||
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
|
||||
.map(|(s_addr, key)| (s_addr.ip(), key.version))
|
||||
.collect::<HashMap<_, _>>();
|
||||
self.keys.inner.store(Arc::new(new));
|
||||
self.support.inner.store(Arc::new(noise_support));
|
||||
|
||||
@@ -4,21 +4,30 @@
|
||||
use std::io;
|
||||
|
||||
use pin_project::pin_project;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
net::TcpStream,
|
||||
};
|
||||
|
||||
use crate::stream::NoiseStream;
|
||||
|
||||
//SW once plain TCP support is dropped, this whole enum can be dropped, and we can only propagate NoiseStream
|
||||
#[pin_project(project = ConnectionProj)]
|
||||
pub enum Connection<C> {
|
||||
Raw(#[pin] C),
|
||||
Noise(#[pin] Box<NoiseStream<C>>),
|
||||
pub enum Connection {
|
||||
Tcp(#[pin] TcpStream),
|
||||
Noise(#[pin] NoiseStream),
|
||||
}
|
||||
|
||||
impl<C> AsyncRead for Connection<C>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
impl Connection {
|
||||
pub fn peer_addr(&self) -> Result<std::net::SocketAddr, io::Error> {
|
||||
match self {
|
||||
Self::Noise(stream) => stream.peer_addr(),
|
||||
Self::Tcp(stream) => stream.peer_addr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Connection {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
@@ -26,15 +35,12 @@ where
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
match self.project() {
|
||||
ConnectionProj::Noise(stream) => stream.poll_read(cx, buf),
|
||||
ConnectionProj::Raw(stream) => stream.poll_read(cx, buf),
|
||||
ConnectionProj::Tcp(stream) => stream.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsyncWrite for Connection<C>
|
||||
where
|
||||
C: AsyncWrite + AsyncRead + Unpin,
|
||||
{
|
||||
impl AsyncWrite for Connection {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
@@ -42,7 +48,7 @@ where
|
||||
) -> std::task::Poll<Result<usize, io::Error>> {
|
||||
match self.project() {
|
||||
ConnectionProj::Noise(stream) => stream.poll_write(cx, buf),
|
||||
ConnectionProj::Raw(stream) => stream.poll_write(cx, buf),
|
||||
ConnectionProj::Tcp(stream) => stream.poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
fn poll_flush(
|
||||
@@ -51,7 +57,7 @@ where
|
||||
) -> std::task::Poll<Result<(), io::Error>> {
|
||||
match self.project() {
|
||||
ConnectionProj::Noise(stream) => stream.poll_flush(cx),
|
||||
ConnectionProj::Raw(stream) => stream.poll_flush(cx),
|
||||
ConnectionProj::Tcp(stream) => stream.poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +67,7 @@ where
|
||||
) -> std::task::Poll<Result<(), io::Error>> {
|
||||
match self.project() {
|
||||
ConnectionProj::Noise(stream) => stream.poll_shutdown(cx),
|
||||
ConnectionProj::Raw(stream) => stream.poll_shutdown(cx),
|
||||
ConnectionProj::Tcp(stream) => stream.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use snow::Error;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
@@ -23,64 +22,13 @@ pub enum NoiseError {
|
||||
#[error("Handshake did not complete")]
|
||||
HandshakeError,
|
||||
|
||||
#[error("unknown noise version (encoded value: {encoded})")]
|
||||
UnknownVersion { encoded: u8 },
|
||||
|
||||
#[error("unknown noise pattern (encoded value: {encoded})")]
|
||||
UnknownPattern { encoded: u8 },
|
||||
|
||||
#[error("unknown noise message type (encoded value: {encoded})")]
|
||||
UnknownMessageType { encoded: u8 },
|
||||
|
||||
#[error("failed to generate psk for requested version {noise_version}")]
|
||||
PskGenerationFailure { noise_version: u8 },
|
||||
|
||||
#[error("noise initiator attempted to use version v{noise_version} of the protocol - we don't know how to handle it")]
|
||||
UnknownVersionHandshake { noise_version: u8 },
|
||||
|
||||
#[error("noise initiator attempted to use an unexpected noise pattern. we're configured for {configured} while it requested {received}")]
|
||||
UnexpectedNoisePattern {
|
||||
configured: &'static str,
|
||||
received: &'static str,
|
||||
},
|
||||
|
||||
#[error("handshake version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
|
||||
UnexpectedHandshakeVersion {
|
||||
initial: NoiseVersion,
|
||||
received: NoiseVersion,
|
||||
},
|
||||
|
||||
#[error("data packet version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
|
||||
UnexpectedDataVersion {
|
||||
initial: NoiseVersion,
|
||||
received: NoiseVersion,
|
||||
},
|
||||
|
||||
#[error("received a non-handshake message during noise handshake")]
|
||||
NonHandshakeMessageReceived,
|
||||
|
||||
#[error("received a non-data message post noise handshake")]
|
||||
NonDataMessageReceived,
|
||||
|
||||
#[error("handshake message exceeded maximum size (got {size} bytes)")]
|
||||
HandshakeTooBig { size: usize },
|
||||
|
||||
#[error("noise message exceeded maximum size (got {size} bytes)")]
|
||||
DataTooBig { size: usize },
|
||||
#[error("Unknown noise version")]
|
||||
UnknownVersion,
|
||||
|
||||
#[error("Handshake timeout")]
|
||||
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
|
||||
}
|
||||
|
||||
impl NoiseError {
|
||||
pub(crate) fn naive_to_io_error(self) -> std::io::Error {
|
||||
match self {
|
||||
NoiseError::IoError(err) => err,
|
||||
other => std::io::Error::other(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for NoiseError {
|
||||
fn from(err: Error) -> Self {
|
||||
match err {
|
||||
|
||||
+79
-67
@@ -1,63 +1,55 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::NoiseConfig;
|
||||
use crate::connection::Connection;
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::NoiseStream;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use snow::error::Prerequisite;
|
||||
use snow::Error;
|
||||
use sha2::{Digest, Sha256};
|
||||
use snow::{error::Prerequisite, Error};
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::{error, warn};
|
||||
use tracing::*;
|
||||
|
||||
pub mod config;
|
||||
pub mod connection;
|
||||
pub mod error;
|
||||
pub mod stream;
|
||||
|
||||
use crate::config::NoiseConfig;
|
||||
use crate::connection::Connection;
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::NoiseStreamBuilder;
|
||||
|
||||
const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ";
|
||||
|
||||
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1;
|
||||
|
||||
// TODO: this should be behind some trait because presumably, depending on the version,
|
||||
// other arguments would be needed
|
||||
mod psk_gen {
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::Psk;
|
||||
use crate::NOISE_PSK_PREFIX;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use sha2::{Digest, Sha256};
|
||||
fn generate_psk_v1(responder_pub_key: &x25519::PublicKey) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(NOISE_PSK_PREFIX);
|
||||
hasher.update(responder_pub_key.to_bytes());
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
pub(crate) fn generate_psk(
|
||||
responder_pub_key: x25519::PublicKey,
|
||||
version: NoiseVersion,
|
||||
) -> Result<Psk, NoiseError> {
|
||||
match version {
|
||||
NoiseVersion::V1 => Ok(generate_psk_v1(responder_pub_key)),
|
||||
NoiseVersion::Unknown(noise_version) => {
|
||||
Err(NoiseError::PskGenerationFailure { noise_version })
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn upgrade_noise_initiator_v1(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
remote_pub_key: &x25519::PublicKey,
|
||||
) -> Result<Connection, NoiseError> {
|
||||
trace!("Perform Noise Handshake, initiator side");
|
||||
|
||||
fn generate_psk_v1(responder_pub_key: x25519::PublicKey) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(NOISE_PSK_PREFIX);
|
||||
hasher.update(responder_pub_key.to_bytes());
|
||||
hasher.finalize().into()
|
||||
}
|
||||
let secret_hash = generate_psk_v1(remote_pub_key);
|
||||
let noise_stream = NoiseStream::new_initiator(conn, config, remote_pub_key, &secret_hash)?;
|
||||
|
||||
Ok(Connection::Noise(
|
||||
tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_initiator(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<Connection<TcpStream>, NoiseError> {
|
||||
) -> Result<Connection, NoiseError> {
|
||||
if config.unsafe_disabled {
|
||||
warn!("Noise is disabled in the config. Not attempting any handshake");
|
||||
return Ok(Connection::Raw(conn));
|
||||
return Ok(Connection::Tcp(conn));
|
||||
}
|
||||
|
||||
//Get init material
|
||||
@@ -66,34 +58,46 @@ pub async fn upgrade_noise_initiator(
|
||||
Error::Prereq(Prerequisite::RemotePublicKey)
|
||||
})?;
|
||||
|
||||
let Some(key) = config.get_noise_key(&responder_addr) else {
|
||||
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
|
||||
let handshake_version = match key.supported_version {
|
||||
NoiseVersion::V1 => NoiseVersion::V1,
|
||||
|
||||
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
|
||||
// If that node sees we're older, it will try to adapt too.
|
||||
NoiseVersion::Unknown(version) => {
|
||||
warn!("{responder_addr} is announcing an v{version} version of Noise that we don't know how to parse, we will attempt to downgrade to our current highest supported version");
|
||||
LATEST_NOISE_VERSION
|
||||
match config.get_noise_key(&responder_addr) {
|
||||
Some(key) => match key.version {
|
||||
NoiseVersion::V1 => upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey).await,
|
||||
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
|
||||
// If that node sees we're older, it will try to adapt too.
|
||||
NoiseVersion::Unknown => {
|
||||
warn!("{responder_addr} is announcing an unknown version of Noise, we will still attempt our latest known version");
|
||||
upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey)
|
||||
.await
|
||||
.or(Err(NoiseError::UnknownVersion))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
|
||||
Ok(Connection::Tcp(conn))
|
||||
}
|
||||
};
|
||||
|
||||
NoiseStreamBuilder::new(conn)
|
||||
.perform_initiator_handshake(config, handshake_version, key.x25519_pubkey)
|
||||
.await
|
||||
.map(|stream| Connection::Noise(Box::new(stream)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn upgrade_noise_responder_v1(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<Connection, NoiseError> {
|
||||
trace!("Perform Noise Handshake, responder side");
|
||||
|
||||
let secret_hash = generate_psk_v1(config.local_key.public_key());
|
||||
let noise_stream = NoiseStream::new_responder(conn, config, &secret_hash)?;
|
||||
|
||||
Ok(Connection::Noise(
|
||||
tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_responder(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<Connection<TcpStream>, NoiseError> {
|
||||
) -> Result<Connection, NoiseError> {
|
||||
if config.unsafe_disabled {
|
||||
warn!("Noise is disabled in the config. Not attempting any handshake");
|
||||
return Ok(Connection::Raw(conn));
|
||||
return Ok(Connection::Tcp(conn));
|
||||
}
|
||||
|
||||
//Get init material
|
||||
@@ -105,14 +109,22 @@ pub async fn upgrade_noise_responder(
|
||||
}
|
||||
};
|
||||
|
||||
// if responder doesn't announce noise support, we fallback to tcp
|
||||
if config.get_noise_support(initiator_addr.ip()).is_none() {
|
||||
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
|
||||
NoiseStreamBuilder::new(conn)
|
||||
.perform_responder_handshake(config)
|
||||
.await
|
||||
.map(|stream| Connection::Noise(Box::new(stream)))
|
||||
// Port is random and we just need the support info
|
||||
match config.get_noise_support(initiator_addr.ip()) {
|
||||
None => {
|
||||
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
|
||||
Ok(Connection::Tcp(conn))
|
||||
}
|
||||
// responder's info on version is shaky, so ideally, initiator has to adapt.
|
||||
// if we are newer, it won't ba able to, so let's try to meet him on his ground.
|
||||
Some(LATEST_NOISE_VERSION) | Some(NoiseVersion::Unknown) => {
|
||||
// Node is announcing the same version as us, great or
|
||||
// Node is announcing a newer version than us, it should adapt to us though
|
||||
upgrade_noise_responder_v1(conn, config).await
|
||||
} //SW sample of code to allow backwards compatibility when we introduce new versions
|
||||
// Some(IntermediateNoiseVersion) => {
|
||||
// Node is announcing an older version, let's try to adapt
|
||||
// upgrade_noise_responder_Vwhatever
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{config::NoiseConfig, error::NoiseError};
|
||||
use bytes::BytesMut;
|
||||
use futures::{Sink, SinkExt, Stream, StreamExt};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use pin_project::pin_project;
|
||||
use snow::{Builder, HandshakeState, TransportState};
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
use std::{cmp::min, task::ready};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||
net::TcpStream,
|
||||
};
|
||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||
|
||||
const TAGLEN: usize = 16;
|
||||
const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size
|
||||
|
||||
pub(crate) type Psk = [u8; 32];
|
||||
|
||||
/// Wrapper around a TcpStream
|
||||
#[pin_project]
|
||||
pub struct NoiseStream {
|
||||
#[pin]
|
||||
inner_stream: Framed<TcpStream, LengthDelimitedCodec>,
|
||||
handshake: Option<HandshakeState>,
|
||||
noise: Option<TransportState>,
|
||||
dec_buffer: BytesMut,
|
||||
}
|
||||
|
||||
impl NoiseStream {
|
||||
pub(crate) fn new_initiator(
|
||||
inner_stream: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
remote_pub_key: &x25519::PublicKey,
|
||||
psk: &Psk,
|
||||
) -> Result<NoiseStream, NoiseError> {
|
||||
let handshake = Builder::new(config.pattern.as_noise_params())
|
||||
.local_private_key(config.local_key.private_key().as_bytes())
|
||||
.remote_public_key(&remote_pub_key.to_bytes())
|
||||
.psk(config.pattern.psk_position(), psk)
|
||||
.build_initiator()?;
|
||||
Ok(NoiseStream::new_inner(inner_stream, handshake))
|
||||
}
|
||||
|
||||
pub(crate) fn new_responder(
|
||||
inner_stream: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
psk: &Psk,
|
||||
) -> Result<NoiseStream, NoiseError> {
|
||||
let handshake = Builder::new(config.pattern.as_noise_params())
|
||||
.local_private_key(config.local_key.private_key().as_bytes())
|
||||
.psk(config.pattern.psk_position(), psk)
|
||||
.build_responder()?;
|
||||
Ok(NoiseStream::new_inner(inner_stream, handshake))
|
||||
}
|
||||
|
||||
fn new_inner(inner_stream: TcpStream, handshake: HandshakeState) -> NoiseStream {
|
||||
NoiseStream {
|
||||
inner_stream: LengthDelimitedCodec::builder()
|
||||
.length_field_type::<u16>()
|
||||
.new_framed(inner_stream),
|
||||
handshake: Some(handshake),
|
||||
noise: None,
|
||||
dec_buffer: BytesMut::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_handshake(mut self) -> Result<Self, NoiseError> {
|
||||
//Check if we are in the correct state
|
||||
let Some(mut handshake) = self.handshake.take() else {
|
||||
return Err(NoiseError::IncorrectStateError);
|
||||
};
|
||||
|
||||
while !handshake.is_handshake_finished() {
|
||||
if handshake.is_my_turn() {
|
||||
self.send_handshake_msg(&mut handshake).await?;
|
||||
} else {
|
||||
self.recv_handshake_msg(&mut handshake).await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.noise = Some(handshake.into_transport_mode()?);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
async fn send_handshake_msg(
|
||||
&mut self,
|
||||
handshake: &mut HandshakeState,
|
||||
) -> Result<(), NoiseError> {
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
|
||||
let len = handshake.write_message(&[], &mut buf)?;
|
||||
buf.truncate(len);
|
||||
self.inner_stream.send(buf.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_handshake_msg(
|
||||
&mut self,
|
||||
handshake: &mut HandshakeState,
|
||||
) -> Result<(), NoiseError> {
|
||||
match self.inner_stream.next().await {
|
||||
Some(Ok(msg)) => {
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
|
||||
handshake.read_message(&msg, &mut buf)?;
|
||||
Ok(())
|
||||
}
|
||||
Some(Err(err)) => Err(NoiseError::IoError(err)),
|
||||
None => Err(NoiseError::HandshakeError),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peer_addr(&self) -> Result<std::net::SocketAddr, io::Error> {
|
||||
self.inner_stream.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for NoiseStream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let projected_self = self.project();
|
||||
|
||||
let pending = match projected_self.inner_stream.poll_next(cx) {
|
||||
Poll::Pending => {
|
||||
//no new data, a return value of Poll::Pending means the waking is already scheduled
|
||||
//Nothing new to decrypt, only check if we can return something from dec_storage, happens after
|
||||
true
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Ok(noise_msg))) => {
|
||||
// We have a new noise msg
|
||||
let mut dec_msg = BytesMut::zeroed(noise_msg.len() - TAGLEN);
|
||||
let len = match projected_self.noise {
|
||||
Some(transport_state) => {
|
||||
match transport_state.read_message(&noise_msg, &mut dec_msg) {
|
||||
Ok(len) => len,
|
||||
Err(_) => return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())),
|
||||
}
|
||||
}
|
||||
None => return Poll::Ready(Err(io::ErrorKind::Other.into())),
|
||||
};
|
||||
projected_self.dec_buffer.extend(&dec_msg[..len]);
|
||||
false
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)),
|
||||
|
||||
Poll::Ready(None) => {
|
||||
//Stream is done, we might still have data in the buffer though, happens afterwards
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// Checking if there is something to return from the buffer
|
||||
let read_len = min(buf.remaining(), projected_self.dec_buffer.len());
|
||||
if read_len > 0 {
|
||||
buf.put_slice(&projected_self.dec_buffer.split_to(read_len));
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// buf.remaining == 0 or nothing in the buffer, we must return the value we had from the inner_stream
|
||||
if pending {
|
||||
//If we end up here, it means the previous poll_next was pending as well, hence waking is already scheduled
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for NoiseStream {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
let mut projected_self = self.project();
|
||||
|
||||
// returns on Poll::Pending and Poll:Ready(Err)
|
||||
ready!(projected_self.inner_stream.as_mut().poll_ready(cx))?;
|
||||
|
||||
// Ready to send, encrypting message
|
||||
let mut noise_buf = BytesMut::zeroed(buf.len() + TAGLEN);
|
||||
|
||||
let Ok(len) = (match projected_self.noise {
|
||||
Some(transport_state) => transport_state.write_message(buf, &mut noise_buf),
|
||||
None => return Poll::Ready(Err(io::ErrorKind::Other.into())),
|
||||
}) else {
|
||||
return Poll::Ready(Err(io::ErrorKind::InvalidInput.into()));
|
||||
};
|
||||
noise_buf.truncate(len);
|
||||
|
||||
// Tokio uses the same `start_send ` in their SinkWriter implementation. https://docs.rs/tokio-util/latest/src/tokio_util/io/sink_writer.rs.html#104
|
||||
match projected_self
|
||||
.inner_stream
|
||||
.as_mut()
|
||||
.start_send(noise_buf.into())
|
||||
{
|
||||
Ok(()) => Poll::Ready(Ok(buf.len())),
|
||||
Err(e) => Poll::Ready(Err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
self.project().inner_stream.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
self.project().inner_stream.poll_close(cx)
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,10 @@ use nym_gateway_storage::models::WireguardPeer;
|
||||
use nym_task::TaskClient;
|
||||
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio_stream::{wrappers::IntervalStream, StreamExt};
|
||||
|
||||
pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManager>>;
|
||||
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60); // 1 hour
|
||||
|
||||
pub struct PeerHandle {
|
||||
public_key: Key,
|
||||
@@ -28,7 +26,6 @@ pub struct PeerHandle {
|
||||
request_tx: mpsc::Sender<PeerControlRequest>,
|
||||
timeout_check_interval: IntervalStream,
|
||||
task_client: TaskClient,
|
||||
startup_timestamp: SystemTime,
|
||||
}
|
||||
|
||||
impl PeerHandle {
|
||||
@@ -53,7 +50,6 @@ impl PeerHandle {
|
||||
request_tx,
|
||||
timeout_check_interval,
|
||||
task_client,
|
||||
startup_timestamp: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,26 +69,49 @@ impl PeerHandle {
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
async fn active_peer(
|
||||
&mut self,
|
||||
storage_peer: &WireguardPeer,
|
||||
kernel_peer: &Peer,
|
||||
) -> Result<bool, Error> {
|
||||
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
|
||||
if kernel_peer.last_handshake.is_none()
|
||||
&& SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER
|
||||
{
|
||||
let success = self.remove_peer().await?;
|
||||
self.peer_storage_manager.remove_peer();
|
||||
tracing::debug!(
|
||||
"Peer {} has not been active for more then {} seconds, removing it",
|
||||
kernel_peer.public_key.to_string(),
|
||||
AUTO_REMOVE_AFTER.as_secs()
|
||||
fn compute_spent_bandwidth(kernel_peer: &Peer, storage_peer: &WireguardPeer) -> Option<u64> {
|
||||
let storage_peer_rx_bytes = u64::try_from(storage_peer.rx_bytes)
|
||||
.inspect_err(|e| tracing::error!("Storage rx bytes could not be converted: {e}"))
|
||||
.ok()?;
|
||||
let storage_peer_tx_bytes = u64::try_from(storage_peer.tx_bytes)
|
||||
.inspect_err(|e| tracing::error!("Storage tx bytes could not be converted: {e}"))
|
||||
.ok()?;
|
||||
|
||||
let kernel_total = kernel_peer
|
||||
.rx_bytes
|
||||
.checked_add(kernel_peer.tx_bytes)
|
||||
.or_else(|| {
|
||||
tracing::error!(
|
||||
"Overflow on kernel adding bytes: {} + {}",
|
||||
kernel_peer.rx_bytes,
|
||||
kernel_peer.tx_bytes
|
||||
);
|
||||
return Ok(!success);
|
||||
}
|
||||
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
|
||||
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
|
||||
None
|
||||
})?;
|
||||
let storage_total = storage_peer_rx_bytes
|
||||
.checked_add(storage_peer_tx_bytes)
|
||||
.or_else(|| {
|
||||
tracing::error!("Overflow on storage adding bytes: {storage_peer_rx_bytes} + {storage_peer_tx_bytes}");
|
||||
None
|
||||
})?;
|
||||
|
||||
kernel_total.checked_sub(storage_total).or_else(|| {
|
||||
tracing::error!("Overflow on spent bandwidth subtraction: kernel - storage = {kernel_total} - {storage_total}");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
async fn active_peer(&mut self, kernel_peer: &Peer) -> Result<bool, Error> {
|
||||
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
|
||||
log::debug!(
|
||||
"Peer {:?} not in storage anymore, shutting down handle",
|
||||
self.public_key
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
|
||||
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, &storage_peer)
|
||||
.unwrap_or_else(|| {
|
||||
// if gateway restarted, the kernel values restart from 0
|
||||
// and we should restart from 0 in storage as well
|
||||
@@ -100,8 +119,10 @@ impl PeerHandle {
|
||||
self.peer_storage_manager.peer_information.as_mut()
|
||||
{
|
||||
peer_information.force_sync = true;
|
||||
peer_information.peer.rx_bytes = kernel_peer.rx_bytes;
|
||||
peer_information.peer.tx_bytes = kernel_peer.tx_bytes;
|
||||
}
|
||||
kernel_peer.rx_bytes + kernel_peer.tx_bytes
|
||||
0
|
||||
})
|
||||
.try_into()
|
||||
.map_err(|_| Error::InconsistentConsumedBytes)?;
|
||||
@@ -124,14 +145,6 @@ impl PeerHandle {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
|
||||
log::debug!(
|
||||
"Peer {} has been present for 30 days, removing it",
|
||||
self.public_key
|
||||
);
|
||||
let success = self.remove_peer().await?;
|
||||
return Ok(!success);
|
||||
}
|
||||
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
|
||||
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
|
||||
log::debug!(
|
||||
@@ -158,14 +171,7 @@ impl PeerHandle {
|
||||
// the host information hasn't beed updated yet
|
||||
return Ok(true);
|
||||
};
|
||||
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
|
||||
log::debug!(
|
||||
"Peer {:?} not in storage anymore, shutting down handle",
|
||||
self.public_key
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
if !self.active_peer(&storage_peer, &kernel_peer).await? {
|
||||
if !self.active_peer(&kernel_peer).await? {
|
||||
log::debug!(
|
||||
"Peer {:?} is not active anymore, shutting down handle",
|
||||
self.public_key
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
version: "3.7"
|
||||
|
||||
x-network: &NETWORK
|
||||
BECH32_PREFIX: nymt
|
||||
DENOM: nymt
|
||||
STAKE_DENOM: nyxt
|
||||
WASMD_VERSION: v0.27.0
|
||||
|
||||
services:
|
||||
genesis_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args: *NETWORK
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "26657:26657"
|
||||
- "1317:1317"
|
||||
container_name: genesis_validator
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
- "genesis_nyxd:/root/.nyxd"
|
||||
environment: *NETWORK
|
||||
networks:
|
||||
localnet:
|
||||
ipv4_address: 172.168.10.2
|
||||
command: ["genesis"]
|
||||
secondary_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args: *NETWORK
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "36657:26657"
|
||||
- "2317:1317"
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
- "secondary_nyxd:/root/.nyxd"
|
||||
environment: *NETWORK
|
||||
networks:
|
||||
localnet:
|
||||
ipv4_address: 172.168.10.3
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
command: ["secondary"]
|
||||
# mixnet_contract:
|
||||
# build: docker/mixnet_contract
|
||||
# image: contract:latest
|
||||
# volumes:
|
||||
# - ".:/nym"
|
||||
# vesting_contract:
|
||||
# build: docker/vesting_contract
|
||||
# image: vesting_contract:latest
|
||||
# volumes:
|
||||
# - ".:/nym"
|
||||
# contract_uploader:
|
||||
# build: docker/typescript_client
|
||||
# image: contract_uploader:typescript
|
||||
# volumes:
|
||||
# - "genesis_volume:/genesis_volume:ro"
|
||||
# - "contract_volume:/contract_volume"
|
||||
# - ".:/nym"
|
||||
# depends_on:
|
||||
# - "genesis_validator"
|
||||
# - "secondary_validator"
|
||||
# - "mixnet_contract"
|
||||
# environment:
|
||||
# BECH32_PREFIX: *BECH32_PREFIX
|
||||
mnemonic_echo:
|
||||
build: docker/mnemonic_echo
|
||||
image: mnemonic_echo:latest
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
- "secondary_validator"
|
||||
|
||||
# mongo:
|
||||
# image: mongo:latest
|
||||
# command:
|
||||
# - --storageEngine=wiredTiger
|
||||
# volumes:
|
||||
# - mongo_data:/data/db
|
||||
# block_explorer:
|
||||
# build:
|
||||
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
|
||||
# image: block_explorer:v0.41.x-7
|
||||
# ports:
|
||||
# - "3080:3000"
|
||||
# depends_on:
|
||||
# - "mongo"
|
||||
# environment:
|
||||
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
|
||||
# MONGO_URL: mongodb://mongo:27017/meteor
|
||||
# PORT: 3000
|
||||
# METEOR_SETTINGS: ${METEOR_SETTINGS}
|
||||
# explorer:
|
||||
# build:
|
||||
# context: docker/explorer
|
||||
# image: explorer:latest
|
||||
# ports:
|
||||
# - "3040:3000"
|
||||
# depends_on:
|
||||
# - "genesis_validator"
|
||||
# - "block_explorer"
|
||||
|
||||
# service to update geoip binary database, for explorer-api
|
||||
geoipupdate:
|
||||
container_name: geoipupdate
|
||||
image: maxmindinc/geoipupdate
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
|
||||
GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
|
||||
GEOIPUPDATE_EDITION_IDS: ${GEOIPUPDATE_EDITION_IDS}
|
||||
GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY}
|
||||
networks:
|
||||
- geoipupdate
|
||||
volumes:
|
||||
- ${GEOIP_DB_DIRECTORY}:/usr/share/GeoIP
|
||||
|
||||
volumes:
|
||||
genesis_volume:
|
||||
genesis_nyxd:
|
||||
secondary_nyxd:
|
||||
|
||||
# contract_volume:
|
||||
# mongo_data:
|
||||
|
||||
networks:
|
||||
geoipupdate:
|
||||
localnet:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.168.10.0/25
|
||||
@@ -1 +1 @@
|
||||
Friday, June 6th 2025, 09:33:10 UTC
|
||||
Thursday, June 12th 2025, 11:03:30 UTC
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"chain-registry": "^1.19.0",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"lucide-react": "^0.438.0",
|
||||
"next": "^14.2.15",
|
||||
"next": "^15.2.4",
|
||||
"nextra": "2",
|
||||
"nextra-theme-docs": "2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -47,6 +47,53 @@ This page displays a full list of all the changes during our release cycle from
|
||||
|
||||
<VarInfo />
|
||||
|
||||
## `v2025.11-cheddar`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.11-cheddar)
|
||||
- [`nym-node`](nodes/nym-node.mdx) version `1.13.0`
|
||||
|
||||
```sh
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-06-10T09:41:31.291089877Z
|
||||
Build Version: 1.13.0
|
||||
Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51
|
||||
Commit Date: 2025-06-10T11:39:20.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
### Operators Updates & Tools
|
||||
|
||||
- **Nym Improvement Proposals**: NIPs are being introduced, allowing us to propose changes to the Nym network and ecosystem, and decentralise governance
|
||||
- **NIP1** will outline the governance process: Its draft will be published soon for discussion
|
||||
- **NIP2** will be about reducing the stake saturation point variable, which would make reward distribution across the network more equitable
|
||||
- **Join the Operator AMA next Tuesday** for an in-depth overview of what this all means (feat Nym Chief Scientist Claudia Diaz)
|
||||
- Keep an eye on our channels for information about how to participate in the upcoming voting (no need for forum registration, we are building something new!)
|
||||
|
||||
### Features
|
||||
|
||||
- [Track wireguard credential retries](https://github.com/nymtech/nym/pull/5783)
|
||||
|
||||
- [Swap a decode into a `fromrow` to please future `postgres` feature](https://github.com/nymtech/nym/pull/5785): This PR change a sqlx derive from `Decode` to `FromRow` because a future PR will bring the `postgres` feature and it doesn't like that `Decode` there
|
||||
|
||||
- [Fix contains ticketbook function that always returned true](https://github.com/nymtech/nym/pull/5787)
|
||||
|
||||
- [QoL: `RequestPath` trait for `http-api-client`](https://github.com/nymtech/nym/pull/5788): Those `PathSegments` in `ApiClient` weren't very user-friendly. Instead we've defined a `RequestPath` trait that allows you to also pass a good old `&str` or `String` and internally it does exactly the same sanitization as before. `PathSegments` are also fully supported to not break any existing compatibility.
|
||||
|
||||
- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800):
|
||||
- Types for `nym-vpn-client`: It introduces `VpnClientStatsReport` which will be used by nym vpn clients to report statistics. Those types are in the monorepo because they're used by the `num-statistics-API`
|
||||
- Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code).
|
||||
|
||||
- [Resolve 1.87 clippy warnings](https://github.com/nymtech/nym/pull/5802)
|
||||
|
||||
- [Adjusted wallet storybook mocks to fix the build](https://github.com/nymtech/nym/pull/5804)
|
||||
|
||||
- [Set cached storage counters to 0](https://github.com/nymtech/nym/pull/5812)
|
||||
|
||||
- [No autoremoval of peers](https://github.com/nymtech/nym/pull/5831)
|
||||
|
||||
## `v2025.10-brie`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.10-brie)
|
||||
|
||||
@@ -18,12 +18,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
|
||||
## Current version
|
||||
|
||||
```sh
|
||||
nym-node
|
||||
ym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-05-27T10:13:18.511075453Z
|
||||
Build Version: 1.12.0
|
||||
Commit SHA: 1c6db86259d08d80e8bcfbc4fcc71ccb147fcfd0
|
||||
Commit Date: 2025-05-27T12:11:13.000000000+02:00
|
||||
Build Timestamp: 2025-06-10T09:41:31.291089877Z
|
||||
Build Version: 1.13.0
|
||||
Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51
|
||||
Commit Date: 2025-06-10T11:39:20.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
|
||||
Generated
+1631
-500
File diff suppressed because it is too large
Load Diff
-25
@@ -1,25 +0,0 @@
|
||||
CONFIGURED=true
|
||||
|
||||
RUST_LOG=info
|
||||
RUST_BACKTRACE=1
|
||||
NETWORK_NAME=qa
|
||||
BECH32_PREFIX=n
|
||||
MIX_DENOM=unym
|
||||
MIX_DENOM_DISPLAY=nym
|
||||
STAKE_DENOM=unyx
|
||||
STAKE_DENOM_DISPLAY=nyx
|
||||
DENOMS_EXPONENT=6
|
||||
|
||||
MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296
|
||||
ECASH_CONTRACT_ADDRESS=n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7
|
||||
GROUP_CONTRACT_ADDRESS=n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t
|
||||
MULTISIG_CONTRACT_ADDRESS=n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr
|
||||
VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj
|
||||
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
|
||||
|
||||
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/
|
||||
NYXD=https://rpc.qa-validator.qa.nymte.ch
|
||||
NYXD_WS=wss://rpc.qa-validator.qa.nymte.ch/websocket
|
||||
NYM_API=https://qa-nym-api.qa.nymte.ch/api/
|
||||
NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "nym-bity-integration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
k256 = { workspace = true, features = ["ecdsa", "sha256"] }
|
||||
eyre = { workspace = true }
|
||||
|
||||
cosmrs = { workspace = true }
|
||||
|
||||
nym-cli-commands = { path = "../../common/commands" }
|
||||
nym-validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
@@ -1,53 +0,0 @@
|
||||
# Buy NYM with Bity
|
||||
|
||||
This crate allows Bity to verify orders for purchasing NYM tokens. The same crate is used by the wallet to sign orders for purchases.
|
||||
|
||||
## Signing
|
||||
|
||||
The Nym Wallet user will sign an order message provided by Bity to create a signed order with the following fields:
|
||||
|
||||
```
|
||||
account_id: n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf
|
||||
message: "This is the order message from Bity"
|
||||
order signature:
|
||||
{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
```
|
||||
|
||||
The `signature` field of the order contains a JSON representation of:
|
||||
|
||||
- the Cosmos address of the signer (`account_id`)
|
||||
- the Cosmos public key
|
||||
- a hex string digest of the Bity order message signed by the user
|
||||
|
||||
Note: the `signature_as_hex` is not in recoverable form (e.g. allows recovering the public key from the signature in `secp256k1`). This is why the public key is supplied along with the account id, as the prefix cannot be recovered.
|
||||
|
||||
## Verification
|
||||
|
||||
Verification has been wrapped up into taking a single struct that can be parsed from JSON:
|
||||
|
||||
```
|
||||
{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": << ORDER SIGNATURE JSON GOES HERE >>
|
||||
}
|
||||
```
|
||||
|
||||
The following will be checked:
|
||||
|
||||
- the `account_id` supplied matches:
|
||||
- the account id derived from the public key
|
||||
- the account id field in the order signature JSON
|
||||
- the account id is for Nym mainnet
|
||||
- the signature is for the message
|
||||
- all data structures parse correctly
|
||||
- nested structs
|
||||
- account ids
|
||||
- Cosmos public keys
|
||||
@@ -1,220 +0,0 @@
|
||||
pub mod order;
|
||||
pub mod sign;
|
||||
pub mod verify;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::order::{Order, OrderSignature};
|
||||
use crate::{sign, verify};
|
||||
use cosmrs::AccountId;
|
||||
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn get_order(prefix: &str) -> anyhow::Result<(OrderSignature, String)> {
|
||||
let mnemonic = "crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove";
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse()?);
|
||||
|
||||
let accounts = wallet.try_derive_accounts()?;
|
||||
|
||||
let message = "This is the order message from Bity";
|
||||
let signature = sign::sign_order(&wallet, &accounts[0], message.to_string())?;
|
||||
|
||||
Ok((signature, message.to_string()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_happy_path() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let account_id = AccountId::from_str("n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf").unwrap();
|
||||
assert_eq!(account_id.to_string(), signature.account_id.to_string());
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
Ok(verify::verify_order(Order {
|
||||
account_id,
|
||||
signature,
|
||||
message,
|
||||
})?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_fails_on_non_mainnet_address() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("nymt")?;
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: signature.clone().account_id,
|
||||
signature,
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_fails_on_non_mainnet_address_variant2() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("atom")?;
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: signature.clone().account_id,
|
||||
signature,
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_change_account_id() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let OrderSignature {
|
||||
signature_as_hex,
|
||||
public_key,
|
||||
account_id: _,
|
||||
} = signature;
|
||||
|
||||
// use a different account id to the one that signed the order
|
||||
let account_id = AccountId::from_str("n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es").unwrap();
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: account_id.clone(),
|
||||
signature: OrderSignature {
|
||||
account_id,
|
||||
signature_as_hex,
|
||||
public_key,
|
||||
},
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_change_signature() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let OrderSignature {
|
||||
signature_as_hex: _,
|
||||
public_key,
|
||||
account_id,
|
||||
} = signature;
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: account_id.clone(),
|
||||
signature: OrderSignature {
|
||||
account_id,
|
||||
signature_as_hex: "this is not the signature you were looking for".to_string(),
|
||||
public_key,
|
||||
},
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_happy_path() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
|
||||
Ok(verify::verify_order(order)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_signature() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "A different message to the one signed",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_account_id() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_account_id_variation_2() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use cosmrs::crypto::PublicKey;
|
||||
use cosmrs::AccountId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct OrderSignature {
|
||||
pub public_key: PublicKey,
|
||||
pub account_id: AccountId,
|
||||
pub signature_as_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Order {
|
||||
pub account_id: AccountId,
|
||||
pub message: String,
|
||||
pub signature: OrderSignature,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use crate::order::OrderSignature;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
|
||||
use nym_validator_client::signing::signer::OfflineSigner;
|
||||
use nym_validator_client::signing::AccountData;
|
||||
|
||||
/// Signs an order message to purchase Nym with Bity
|
||||
pub fn sign_order(
|
||||
wallet: &DirectSecp256k1HdWallet,
|
||||
signer: &AccountData,
|
||||
message: String,
|
||||
) -> Result<OrderSignature, NyxdError> {
|
||||
Ok(OrderSignature {
|
||||
account_id: signer.address().clone(),
|
||||
public_key: signer.public_key(),
|
||||
signature_as_hex: wallet
|
||||
.sign_raw_with_account(signer, message.into_bytes())?
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use nym_cli_commands::validator::signature::helpers::secp256k1_verify_with_public_key;
|
||||
|
||||
use crate::order::Order;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VerifyOrderError {
|
||||
#[error("{source}")]
|
||||
K256Error {
|
||||
#[from]
|
||||
source: k256::ecdsa::Error,
|
||||
},
|
||||
#[error("{source}")]
|
||||
ErrorReport {
|
||||
#[from]
|
||||
source: eyre::Report,
|
||||
},
|
||||
#[error("Account id does not match public key")]
|
||||
AccountIdDoesNotMatchPubKey,
|
||||
#[error("Unsupported key type. Only secp256k1 is currently supported.")]
|
||||
UnsupportedKeyType,
|
||||
#[error("Signature error - {0}")]
|
||||
SignatureError(k256::ecdsa::signature::Error),
|
||||
#[error("Account id is not a Nyx mainnet account")]
|
||||
AccountIdPrefixIncorrect,
|
||||
}
|
||||
|
||||
/// Verifies an order
|
||||
pub fn verify_order(order: Order) -> Result<(), VerifyOrderError> {
|
||||
let account_id = order.signature.public_key.account_id("n")?;
|
||||
|
||||
if order.signature.account_id.prefix() != "n" || order.account_id.prefix() != "n" {
|
||||
return Err(VerifyOrderError::AccountIdPrefixIncorrect);
|
||||
}
|
||||
|
||||
// the account id in the order must match the account id derived from the public key
|
||||
if account_id != order.signature.account_id {
|
||||
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
|
||||
}
|
||||
|
||||
// the user provided account id in the order must match the derived account id
|
||||
if account_id != order.account_id || account_id != order.signature.account_id {
|
||||
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
|
||||
}
|
||||
|
||||
if order.signature.public_key.type_url() != cosmrs::crypto::PublicKey::SECP256K1_TYPE_URL {
|
||||
return Err(VerifyOrderError::UnsupportedKeyType);
|
||||
}
|
||||
|
||||
match secp256k1_verify_with_public_key(
|
||||
&order.signature.public_key.to_bytes(),
|
||||
order.signature.signature_as_hex,
|
||||
order.message,
|
||||
) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(VerifyOrderError::SignatureError(e)),
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.59"
|
||||
version = "1.1.60"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
@@ -895,39 +895,15 @@ pub struct HostKeys {
|
||||
|
||||
pub current_x25519_sphinx_key: SphinxKey,
|
||||
|
||||
#[serde(default)]
|
||||
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
|
||||
|
||||
#[serde(default)]
|
||||
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
pub struct SphinxKey {
|
||||
pub rotation_id: u32,
|
||||
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub public_key: x25519::PublicKey,
|
||||
}
|
||||
|
||||
impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKey {
|
||||
fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self {
|
||||
SphinxKey {
|
||||
rotation_id: value.rotation_id,
|
||||
public_key: value.public_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
|
||||
fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
|
||||
HostKeys {
|
||||
ed25519: value.ed25519_identity,
|
||||
x25519: value.x25519_sphinx,
|
||||
current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
|
||||
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
|
||||
x25519_versioned_noise: value.x25519_versioned_noise,
|
||||
}
|
||||
}
|
||||
@@ -1108,11 +1084,10 @@ impl NymNodeDescription {
|
||||
|
||||
pub fn to_semi_skimmed_node(
|
||||
&self,
|
||||
current_rotation_id: u32,
|
||||
role: NodeRole,
|
||||
performance: Performance,
|
||||
) -> SemiSkimmedNode {
|
||||
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
|
||||
let skimmed_node = self.to_skimmed_node(role, performance);
|
||||
|
||||
SemiSkimmedNode {
|
||||
basic: skimmed_node,
|
||||
|
||||
@@ -8,7 +8,7 @@ use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_mixnet_contract_common::nym_node::Role;
|
||||
use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use nym_mixnet_contract_common::{EpochId, Interval, NodeId};
|
||||
use nym_mixnet_contract_common::{Interval, NodeId};
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -1,13 +1,97 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation};
|
||||
use crate::nym_nodes::handlers::unstable::NodesParamsWithRole;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::unstable_routes::v1::nym_nodes::helpers::NodesParamsWithRole;
|
||||
use axum::extract::{Query, State};
|
||||
use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode};
|
||||
use nym_api_requests::models::{
|
||||
NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponse, SemiSkimmedNode};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_common::FormattedResponse;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_topology::CachedEpochRewardedSet;
|
||||
use std::collections::HashMap;
|
||||
use tracing::trace;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type PaginatedSemiSkimmedNodes =
|
||||
AxumResult<FormattedResponse<PaginatedCachedNodesResponse<SemiSkimmedNode>>>;
|
||||
|
||||
//SW TODO : this is copied from skimmed nodes, surely we can do better than that
|
||||
fn build_nym_nodes_response<'a, NI>(
|
||||
rewarded_set: &CachedEpochRewardedSet,
|
||||
nym_nodes_subset: NI,
|
||||
annotations: &HashMap<NodeId, NodeAnnotation>,
|
||||
active_only: bool,
|
||||
) -> Vec<SemiSkimmedNode>
|
||||
where
|
||||
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
|
||||
{
|
||||
let mut nodes = Vec::new();
|
||||
for nym_node in nym_nodes_subset {
|
||||
let node_id = nym_node.node_id;
|
||||
|
||||
let role: NodeRole = rewarded_set.role(node_id).into();
|
||||
|
||||
// if the role is inactive, see if our filter allows it
|
||||
if active_only && role.is_inactive() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// honestly, not sure under what exact circumstances this value could be missing,
|
||||
// but in that case just use 0 performance
|
||||
let annotation = annotations.get(&node_id).copied().unwrap_or_default();
|
||||
|
||||
nodes.push(nym_node.to_semi_skimmed_node(role, annotation.last_24h_performance));
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
//SW TODO : this is copied from skimmed nodes, surely we can do better than that
|
||||
/// Given all relevant caches, add appropriate legacy nodes to the part of the response
|
||||
fn add_legacy<LN>(
|
||||
nodes: &mut Vec<SemiSkimmedNode>,
|
||||
rewarded_set: &CachedEpochRewardedSet,
|
||||
describe_cache: &DescribedNodes,
|
||||
annotated_legacy_nodes: &HashMap<NodeId, LN>,
|
||||
) where
|
||||
LN: LegacyAnnotation,
|
||||
{
|
||||
for (node_id, legacy) in annotated_legacy_nodes.iter() {
|
||||
let role: NodeRole = rewarded_set.role(*node_id).into();
|
||||
|
||||
// if we have self-described info, prefer it over contract data
|
||||
if let Some(described) = describe_cache.get_node(node_id) {
|
||||
nodes.push(described.to_semi_skimmed_node(role, legacy.performance()))
|
||||
} else {
|
||||
match legacy.try_to_semi_skimmed_node(role) {
|
||||
Ok(node) => nodes.push(node),
|
||||
Err(err) => {
|
||||
let id = legacy.identity();
|
||||
trace!("node {id} is malformed: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // not dead, used in OpenAPI docs
|
||||
#[derive(ToSchema)]
|
||||
#[schema(title = "PaginatedCachedNodesExpandedResponseSchema")]
|
||||
pub struct PaginatedCachedNodesExpandedResponseSchema {
|
||||
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
|
||||
#[schema(value_type = SemiSkimmedNode)]
|
||||
pub nodes: PaginatedResponse<SemiSkimmedNode>,
|
||||
}
|
||||
|
||||
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
@@ -15,13 +99,43 @@ use nym_http_api_common::FormattedResponse;
|
||||
path = "",
|
||||
context_path = "/v1/unstable/nym-nodes/semi-skimmed",
|
||||
responses(
|
||||
// (status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
|
||||
(status = 501)
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesExpandedResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesExpandedResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesExpandedResponseSchema = "application/bincode")
|
||||
))
|
||||
)
|
||||
)]
|
||||
pub(crate) async fn nodes_expanded(
|
||||
_state: State<AppState>,
|
||||
_query_params: Query<NodesParamsWithRole>,
|
||||
) -> AxumResult<FormattedResponse<CachedNodesResponse<SemiSkimmedNode>>> {
|
||||
Err(AxumErrorResponse::not_implemented())
|
||||
pub(super) async fn nodes_expanded(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParamsWithRole>,
|
||||
) -> PaginatedSemiSkimmedNodes {
|
||||
// 1. grab all relevant described nym-nodes
|
||||
let rewarded_set = state.rewarded_set().await?;
|
||||
|
||||
let describe_cache = state.describe_nodes_cache_data().await?;
|
||||
let all_nym_nodes = describe_cache.all_nym_nodes();
|
||||
let annotations = state.node_annotations().await?;
|
||||
let legacy_mixnodes = state.legacy_mixnode_annotations().await?;
|
||||
let legacy_gateways = state.legacy_gateways_annotations().await?;
|
||||
|
||||
let mut nodes = build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations, false);
|
||||
|
||||
// add legacy gateways to the response
|
||||
add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_gateways);
|
||||
|
||||
// add legacy mixnodes to the response
|
||||
add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_mixnodes);
|
||||
|
||||
// min of all caches
|
||||
let refreshed_at = refreshed_at([
|
||||
rewarded_set.timestamp(),
|
||||
annotations.timestamp(),
|
||||
describe_cache.timestamp(),
|
||||
legacy_mixnodes.timestamp(),
|
||||
legacy_gateways.timestamp(),
|
||||
]);
|
||||
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
Ok(output.to_response(PaginatedCachedNodesResponse::new_full(refreshed_at, nodes)))
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"@nymproject/eslint-config-react-typescript"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.eslint.json"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
# environment
|
||||
.env.dev
|
||||
|
||||
# error logs
|
||||
yarn-error.log
|
||||
dist/
|
||||
build/
|
||||
**/*.wasm
|
||||
**/*.js.map
|
||||
.env*
|
||||
!.env.example
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
16
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
RPC_URL=
|
||||
VALIDATOR_URL=
|
||||
PREFIX=
|
||||
MIXNET_CONTRACT_ADDRESS=
|
||||
VESTING_CONTRACT_ADDRESS=
|
||||
DENOM=
|
||||
BLOCK_EXPLORER_URL=
|
||||
+108
-23
@@ -1,40 +1,125 @@
|
||||
# Nym Browser Extension
|
||||
# Nym Browser Extension Storage
|
||||
|
||||
The Nym browser extension lets you access your Nym wallet via the browser.
|
||||
A WebAssembly-based storage component for securely managing mnemonics in extensions. This component provides encrypted storage functionality using IndexedDB with password-based encryption.
|
||||
|
||||
## Getting started
|
||||
## Overview
|
||||
|
||||
You will need:
|
||||
This storage component is built in Rust and compiled to WebAssembly, providing:
|
||||
|
||||
- NodeJS (use `nvm install` to automatically install the correct version)
|
||||
- `npm`
|
||||
- `yarn`
|
||||
- **Secure mnemonic storage**: Password-encrypted storage of BIP39 mnemonics
|
||||
- **IndexedDB integration**: Browser-native persistent storage
|
||||
- **Multiple account support**: Store and manage multiple mnemonic phrases with custom names
|
||||
- **Type-safe API**: Promise-based JavaScript API with proper error handling
|
||||
|
||||
> **Note**: This project is part of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them.
|
||||
## Getting Started
|
||||
|
||||
From the [root of the repository](../README.md) run the following to build shared packages:
|
||||
### Prerequisites
|
||||
|
||||
```
|
||||
yarn
|
||||
yarn build
|
||||
- Rust (latest stable)
|
||||
- `wasm-pack` tool for building WebAssembly
|
||||
- Node.js (for the demo server)
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
cd storage
|
||||
make wasm-pack
|
||||
```
|
||||
|
||||
From the `nym-browser-extension` directory of the `nym` monorepo, run:
|
||||
This will compile the Rust code to WebAssembly and generate the necessary JavaScript bindings.
|
||||
|
||||
`yarn dev` to run the extension in dev mode.
|
||||
### Example Usage
|
||||
|
||||
You can then open a browser to http://localhost:9000 and start development.
|
||||
See the [internal-dev example](./storage/internal-dev/index.js) for complete usage examples.
|
||||
|
||||
OR
|
||||
Basic usage:
|
||||
|
||||
`yarn build` to build the extension.
|
||||
```javascript
|
||||
import init, { ExtensionStorage, set_panic_hook } from "@nymproject/extension-storage"
|
||||
|
||||
The extension will build to the `nym-browser-extension/dist` directory.
|
||||
// Initialize the WASM module first
|
||||
await init();
|
||||
|
||||
## Load extension
|
||||
// Set up better error handling
|
||||
set_panic_hook();
|
||||
|
||||
To load the extension into a Chrome browser
|
||||
// Create storage instance with password
|
||||
const storage = await new ExtensionStorage("your-secure-password");
|
||||
|
||||
- Go to `settings > extensions > manage extensions`
|
||||
- Select `Load unpacked`
|
||||
- Select the `nym-browser-extension/dist`
|
||||
// Store a mnemonic
|
||||
const mnemonic = "your twenty four word mnemonic phrase goes here...";
|
||||
await storage.store_mnemonic("my-wallet", mnemonic);
|
||||
|
||||
// Read a mnemonic
|
||||
const retrievedMnemonic = await storage.read_mnemonic("my-wallet");
|
||||
|
||||
// Check if a mnemonic exists
|
||||
const exists = await storage.has_mnemonic("my-wallet");
|
||||
|
||||
// Get all stored mnemonic keys
|
||||
const allKeys = await storage.get_all_mnemonic_keys();
|
||||
|
||||
// Remove a mnemonic
|
||||
await storage.remove_mnemonic("my-wallet");
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To run the internal development example:
|
||||
|
||||
```bash
|
||||
cd storage
|
||||
|
||||
make demo
|
||||
|
||||
# Option 2: Manual server setup
|
||||
cd internal-dev && node serve.js
|
||||
|
||||
# Then open http://localhost:8000 in your browser
|
||||
```
|
||||
|
||||
**Note**: The demo requires a server that properly serves WASM files with `application/wasm` MIME type. The Node.js server is recommended as it handles MIME types more reliably.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Initialization
|
||||
- `init()` - **Required**: Initialize the WASM module before using other functions
|
||||
|
||||
### Constructor
|
||||
- `new ExtensionStorage(password: string)` - Creates a new storage instance with the given password
|
||||
|
||||
### Static Methods
|
||||
- `ExtensionStorage.exists()` - Check if storage database exists
|
||||
|
||||
### Instance Methods
|
||||
- `store_mnemonic(name: string, mnemonic: string)` - Store a mnemonic with the given name
|
||||
- `read_mnemonic(name: string)` - Retrieve a mnemonic by name (returns null if not found)
|
||||
- `has_mnemonic(name: string)` - Check if a mnemonic with the given name exists
|
||||
- `get_all_mnemonic_keys()` - Get all stored mnemonic names
|
||||
- `remove_mnemonic(name: string)` - Remove a mnemonic by name
|
||||
|
||||
### Error Handling
|
||||
- `set_panic_hook()` - Set up better stack traces for Rust panics in development
|
||||
|
||||
## Security Features
|
||||
|
||||
- **Password-based encryption**: All data is encrypted using the provided password
|
||||
- **BIP39 validation**: Mnemonics are validated before storage
|
||||
- **Secure memory handling**: Sensitive data is zeroed from memory when no longer needed
|
||||
- **Browser sandbox**: Runs within the browser's security model
|
||||
|
||||
## Architecture
|
||||
|
||||
The storage component consists of:
|
||||
|
||||
- **Rust core** (`src/storage.rs`): Main storage implementation with encryption
|
||||
- **WASM bindings** (`src/lib.rs`): WebAssembly interface layer
|
||||
- **Error handling** (`src/error.rs`): Comprehensive error types
|
||||
- **Build configuration** (`Cargo.toml`, `Makefile`): Build and dependency management
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **WASM Initialization**: Always call `await init()` before using any other functions
|
||||
2. **MIME Types**: The demo requires a server that properly serves WASM files
|
||||
3. **Browser Compatibility**: Requires modern browsers with WebAssembly support
|
||||
4. **Module Loading**: Uses ES modules - ensure your build system supports them
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
{
|
||||
"name": "@nym/browser-extension",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "yarn webpack serve --config webpack.dev.js",
|
||||
"build": "yarn preinstall && webpack build --progress --config webpack.prod.js",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint:ts": "tsc --noEmit",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook:build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.7.0",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.15",
|
||||
"@mui/system": "^5.11.15",
|
||||
"@nymproject/mui-theme": "^1.0.0",
|
||||
"@nymproject/nym-validator-client": ">=1.2.0-rc.0 || 1",
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@nymproject/extension-storage": ">=1.2.0-rc.0 || 1",
|
||||
"@storybook/react": "^6.5.16",
|
||||
"big.js": "^6.2.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/big.js": "^6.1.6",
|
||||
"@types/crypto-js": "4.1.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^18.16.1",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-root-import": "^1.0.4",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-storybook": "^0.5.12",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "^27.1.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"prettier": "^2.8.7",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-refresh-typescript": "^2.0.8",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.5.0",
|
||||
"webpack-favicons": "^1.3.8",
|
||||
"webpack-merge": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NymBrowserExtThemeWithMode } from './theme/NymBrowserExtensionTheme';
|
||||
import { AppRoutes } from './routes';
|
||||
import { AppLayout } from './layouts/AppLayout';
|
||||
import { AppProvider } from './context';
|
||||
|
||||
export const App = () => (
|
||||
<NymBrowserExtThemeWithMode mode="light">
|
||||
<AppProvider>
|
||||
<AppLayout>
|
||||
<AppRoutes />
|
||||
</AppLayout>
|
||||
</AppProvider>
|
||||
</NymBrowserExtThemeWithMode>
|
||||
);
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { AccountActions } from './Actions';
|
||||
|
||||
const AccountItem = ({
|
||||
accountName,
|
||||
disabled,
|
||||
onSelect,
|
||||
}: {
|
||||
accountName: string;
|
||||
disabled: boolean;
|
||||
onSelect: () => void;
|
||||
}) => (
|
||||
<ListItem disableGutters disablePadding secondaryAction={<AccountActions accountName={accountName} />} divider>
|
||||
<ListItemButton onClick={onSelect} disabled={disabled}>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{accountName[0]}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={accountName} secondary={disabled && '(Selected)'} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
export const AccountList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { accounts, selectAccount, selectedAccount } = useAppContext();
|
||||
|
||||
const handleSelectAccount = async (accountName: string) => {
|
||||
await selectAccount(accountName);
|
||||
navigate('/user/balance');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{accounts.map((accountName) => (
|
||||
<AccountItem
|
||||
disabled={selectedAccount === accountName}
|
||||
accountName={accountName}
|
||||
key={accountName}
|
||||
onSelect={() => handleSelectAccount(accountName)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
|
||||
import { MoreVert, VisibilityOutlined } from '@mui/icons-material';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
type ActionType = {
|
||||
title: string;
|
||||
Icon: React.ReactNode;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
const ActionItem = ({ action }: { action: ActionType }) => (
|
||||
<MenuItem dense onClick={action.onSelect}>
|
||||
<ListItemIcon>{action.Icon}</ListItemIcon>
|
||||
<ListItemText>{action.title}</ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
export const AccountActions = ({ accountName }: { accountName: string }) => {
|
||||
const { setShowSeedForAccount } = useAppContext();
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const actions: Array<ActionType> = [
|
||||
{
|
||||
title: 'View seed phrase',
|
||||
Icon: <VisibilityOutlined />,
|
||||
onSelect: () => {
|
||||
setShowSeedForAccount(accountName);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleClick}>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
<Menu anchorEl={anchorEl} id="account-menu" open={open} onClose={handleClose} onClick={handleClose}>
|
||||
{actions.map((action) => (
|
||||
<ActionItem action={action} key={action.title} />
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Card, CardContent, Typography } from '@mui/material';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { Button, ConfirmationModal } from 'src/components/ui';
|
||||
|
||||
const ShowSeedButton = ({ handleShowSeedPhrase }: { handleShowSeedPhrase: () => void }) => (
|
||||
<Button fullWidth variant="contained" onClick={handleShowSeedPhrase}>
|
||||
Show seed phrase
|
||||
</Button>
|
||||
);
|
||||
|
||||
const DoneButton = ({ onDone }: { onDone: () => void }) => (
|
||||
<Button fullWidth variant="contained" onClick={onDone}>
|
||||
Done
|
||||
</Button>
|
||||
);
|
||||
|
||||
const Seed = ({ seed }: { seed: string }) => (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography>{seed}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export const ViewSeedPhrase = ({ accountName, onDone }: { accountName: string; onDone: () => void }) => {
|
||||
const [seed, setSeed] = useState<string>();
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const handleShowSeedPhrase = async () => {
|
||||
try {
|
||||
const storage = await new ExtensionStorage(password);
|
||||
const accountSeed = await storage.read_mnemonic(accountName);
|
||||
setSeed(accountSeed);
|
||||
} catch (e) {
|
||||
setError('Could not retrieve seed phrase. Please check your password');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open
|
||||
onClose={onDone}
|
||||
title={seed ? 'Account seed phrase' : 'Password'}
|
||||
subtitle={seed ? '' : 'Enter your account password'}
|
||||
ConfirmButton={
|
||||
seed ? <DoneButton onDone={onDone} /> : <ShowSeedButton handleShowSeedPhrase={handleShowSeedPhrase} />
|
||||
}
|
||||
>
|
||||
{seed ? (
|
||||
<Seed seed={seed} />
|
||||
) : (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
error={error}
|
||||
password={password}
|
||||
onUpdatePassword={(pw: string) => {
|
||||
setPassword(pw);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Accounts';
|
||||
export * from './Actions';
|
||||
export * from './ViewSeedPhrase';
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Stack } from '@mui/system';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const Address = () => {
|
||||
const { client } = useAppContext();
|
||||
|
||||
return (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography fontWeight={700}>Address</Typography>
|
||||
<ClientAddress withCopy address={client?.address || ''} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const Balance = () => {
|
||||
const { balance, fiatBalance, currency, getBalance } = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
getBalance();
|
||||
}, []);
|
||||
|
||||
const fiat = fiatBalance ? `~ ${Intl.NumberFormat().format(fiatBalance)} ${currency.toUpperCase()}` : '-';
|
||||
|
||||
return (
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Typography sx={{ color: 'grey.600' }}>Available</Typography>
|
||||
<Typography variant="h4" textAlign="center">
|
||||
{balance} NYM
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'grey.600' }}>{fiat}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './accounts';
|
||||
export * from './address';
|
||||
export * from './balance';
|
||||
export * from './receive';
|
||||
export * from './send';
|
||||
export * from './ui';
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { Close } from '@mui/icons-material';
|
||||
|
||||
export const ReceiveModal = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
|
||||
const { client } = useAppContext();
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||
<DialogTitle>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontWeight={700}>Receive</Typography>
|
||||
<IconButton size="small" onClick={onClose} sx={{ padding: 0 }}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack gap={1} alignItems="center">
|
||||
<Card elevation={3} sx={{ my: 2, width: 200 }}>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<QRCodeSVG value={client?.address || ''} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Typography variant="body2" fontWeight={700}>
|
||||
Your Nym address
|
||||
</Typography>
|
||||
<ClientAddress address={client?.address || ''} withCopy smallIcons />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './ReceiveModal';
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { ConfirmationModal, Button } from 'src/components/ui';
|
||||
|
||||
export const SendConfirmationModal = ({
|
||||
amount,
|
||||
txUrl,
|
||||
onConfirm,
|
||||
}: {
|
||||
amount: string;
|
||||
txUrl: string;
|
||||
onConfirm: () => void;
|
||||
}) => (
|
||||
<ConfirmationModal
|
||||
open
|
||||
fullWidth
|
||||
title="You sent"
|
||||
ConfirmButton={
|
||||
<Button fullWidth variant="contained" size="large" onClick={onConfirm}>
|
||||
Done
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h6">{amount}</Typography>
|
||||
<Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />
|
||||
</Box>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export * from './SendConfirmationModal';
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { AppBar as MUIAppBar } from '@mui/material/';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
||||
export const AppBar = ({ Action }: { Action: React.ReactNode }) => (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<MUIAppBar position="static" elevation={0} sx={{ bgcolor: 'rgba(103, 80, 164, 0.14)' }}>
|
||||
<Toolbar variant="dense">{Action}</Toolbar>
|
||||
</MUIAppBar>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default AppBar;
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowBackIosRounded } from '@mui/icons-material';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
export const BackButton = ({ onBack }: { onBack?: () => void }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton size="small" onClick={handleClick}>
|
||||
<ArrowBackIosRounded fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button as MUIButton, ButtonProps } from '@mui/material';
|
||||
|
||||
export const Button = (props: ButtonProps) => (
|
||||
<MUIButton {...props} disableElevation sx={{ textTransform: 'initial' }} />
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NymLogoBW } from '@nymproject/react/logo/NymLogoBW';
|
||||
|
||||
export const Logo = ({ small }: { small?: boolean }) => (
|
||||
<NymLogoBW width={small ? '37.5px' : '75px'} height={small ? '37.5px' : '75px'} />
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Logo } from '../Logo';
|
||||
import { Title } from '../Title';
|
||||
|
||||
export const LogoWithText = ({
|
||||
logoSmall,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
logoSmall?: boolean;
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => (
|
||||
<Stack alignItems="center" justifyContent="center" gap={3}>
|
||||
<Logo small={logoSmall} />
|
||||
<Title>{title}</Title>
|
||||
<Typography sx={{ color: 'grey.700', textAlign: 'center' }}>{description}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { AccountBalanceWalletRounded, AccountCircleRounded, ArrowDownwardRounded } from '@mui/icons-material';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const menuSchema = [
|
||||
{
|
||||
title: 'Accounts',
|
||||
Icon: <AccountCircleRounded />,
|
||||
path: '/user/accounts',
|
||||
},
|
||||
{
|
||||
title: 'Balance',
|
||||
Icon: <AccountBalanceWalletRounded />,
|
||||
path: '/user/balance',
|
||||
},
|
||||
{
|
||||
title: 'Send',
|
||||
Icon: <ArrowDownwardRounded />,
|
||||
path: '/user/send',
|
||||
},
|
||||
];
|
||||
|
||||
export const MenuDrawer = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
|
||||
const list = () => (
|
||||
<Box sx={{ width: 250 }} role="presentation" onClick={() => {}}>
|
||||
<List>
|
||||
{menuSchema.map(({ title, Icon, path }) => (
|
||||
<Link to={path} style={{ textDecoration: 'none', color: 'unset' }} key={title}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemIcon>{Icon}</ListItemIcon>
|
||||
<ListItemText primary={title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer anchor="left" open={open} onClose={onClose}>
|
||||
{list()}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface ErrorModalProps {
|
||||
open: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ErrorModal = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ErrorModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>
|
||||
<Button variant="contained" size="large" fullWidth onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
|
||||
|
||||
const modalStyle: SxProps = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 300,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
borderRadius: '16px',
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export const LoadingModal = ({ sx, backdropProps }: { sx?: SxProps; backdropProps?: object }) => (
|
||||
<Modal open BackdropProps={backdropProps}>
|
||||
<Box sx={{ border: (t) => `1px solid ${t.palette.grey[500]}`, ...modalStyle, ...sx }} textAlign="center">
|
||||
<Stack spacing={4} direction="row" alignItems="center">
|
||||
<CircularProgress />
|
||||
<Typography sx={{ color: 'text.primary' }}>Please wait...</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
open: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
ConfirmButton: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
ConfirmButton,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ConfirmationModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Modal';
|
||||
export * from './LoadingModal';
|
||||
export * from './ErrorModal';
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const FONT_WEIGHT = 400;
|
||||
|
||||
export const Title = ({ children }: { children: string }) => (
|
||||
<Typography variant="h5" fontWeight={FONT_WEIGHT}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
export * from './AppBar';
|
||||
export * from './Button';
|
||||
export * from './BackButton';
|
||||
export * from './Logo';
|
||||
export * from './LogoWithText';
|
||||
export * from './MenuDrawer';
|
||||
export * from './Modal';
|
||||
export * from './Title';
|
||||
@@ -1,8 +0,0 @@
|
||||
export const config = {
|
||||
rpcUrl: process.env.RPC_URL || '',
|
||||
validatorUrl: process.env.VALIDATOR_URL || '',
|
||||
prefix: process.env.PREFIX || '',
|
||||
mixnetContractAddress: process.env.MIXNET_CONTRACT_ADDRESS || '',
|
||||
vestingContractAddress: process.env.VESTING_CONTRACT_ADDRESS || '',
|
||||
denom: process.env.DENOM || '',
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import ValidatorClient from '@nymproject/nym-validator-client';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { connectToValidator } from 'src/validator-client';
|
||||
import { unymToNym } from 'src/utils/coin';
|
||||
import { Currency, getTokenPrice } from 'src/utils/price';
|
||||
|
||||
type TAppContext = {
|
||||
client?: ValidatorClient;
|
||||
accounts: string[];
|
||||
balance?: string;
|
||||
fiatBalance?: number;
|
||||
denom: 'NYM';
|
||||
minorDenom: 'unym';
|
||||
currency: Currency;
|
||||
showSeedForAccount?: string;
|
||||
selectedAccount: string;
|
||||
storage?: ExtensionStorage;
|
||||
selectAccount: (accountName: string) => Promise<void>;
|
||||
setAccounts: (accounts: string[]) => void;
|
||||
setShowSeedForAccount: (accountName?: string) => void;
|
||||
handleUnlockWallet: (password: string) => void;
|
||||
getBalance: () => void;
|
||||
};
|
||||
|
||||
type TBalanceInNYMs = string;
|
||||
|
||||
const DEFAULT_ACCOUNT_NAME = 'Default account';
|
||||
|
||||
const AppContext = React.createContext({} as TAppContext);
|
||||
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [client, setClient] = useState<ValidatorClient>();
|
||||
const [selectedAccount, setSelected] = useState<string>(DEFAULT_ACCOUNT_NAME);
|
||||
const [balance, setBalance] = useState<TBalanceInNYMs>();
|
||||
const [fiatBalance, setFiatBalance] = useState<number>();
|
||||
const [accounts, setAccounts] = useState<string[]>([]);
|
||||
const [showSeedForAccount, setShowSeedForAccount] = useState<string>();
|
||||
const [storage, setStorage] = useState<ExtensionStorage>();
|
||||
|
||||
const denom = 'NYM';
|
||||
const minorDenom = 'unym';
|
||||
const currency = 'gbp';
|
||||
|
||||
const handleUnlockWallet = async (password: string) => {
|
||||
const store = await new ExtensionStorage(password);
|
||||
const mnemonic = await store.read_mnemonic(DEFAULT_ACCOUNT_NAME);
|
||||
const userAccounts = await store.get_all_mnemonic_keys();
|
||||
const clientFromMnemonic = await connectToValidator(mnemonic);
|
||||
|
||||
setStorage(store);
|
||||
setAccounts(userAccounts);
|
||||
setClient(clientFromMnemonic);
|
||||
};
|
||||
|
||||
const selectAccount = async (accountName: string) => {
|
||||
const mnemonic = await storage!.read_mnemonic(accountName);
|
||||
const clientFromMnemonic = await connectToValidator(mnemonic);
|
||||
setSelected(accountName);
|
||||
setClient(clientFromMnemonic);
|
||||
};
|
||||
|
||||
const getFiatBalance = async (bal: number) => {
|
||||
const tokenPrice = await getTokenPrice('nym', currency);
|
||||
const fiatBal = tokenPrice.nym.gbp * bal;
|
||||
return fiatBal;
|
||||
};
|
||||
|
||||
const getBalance = async () => {
|
||||
const bal = await client?.getBalance(client.address);
|
||||
if (bal) {
|
||||
const nym = unymToNym(Number(bal.amount));
|
||||
const fiat = await getFiatBalance(nym);
|
||||
setFiatBalance(fiat);
|
||||
setBalance(nym.toString());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (client) {
|
||||
getBalance();
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
const value = useMemo<TAppContext>(
|
||||
() => ({
|
||||
client,
|
||||
accounts,
|
||||
balance,
|
||||
fiatBalance,
|
||||
currency,
|
||||
denom,
|
||||
minorDenom,
|
||||
selectedAccount,
|
||||
storage,
|
||||
handleUnlockWallet,
|
||||
getBalance,
|
||||
setShowSeedForAccount,
|
||||
showSeedForAccount,
|
||||
setAccounts,
|
||||
selectAccount,
|
||||
}),
|
||||
[client, accounts, balance, fiatBalance, denom, minorDenom, selectedAccount, showSeedForAccount, storage],
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAppContext = () => React.useContext(AppContext);
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './app';
|
||||
export * from './send';
|
||||
export * from './register';
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
|
||||
const RegisterContext = React.createContext({} as TRegisterContext);
|
||||
|
||||
type TRegisterContext = {
|
||||
userPassword: string;
|
||||
userMnemonic: string;
|
||||
accountName: string;
|
||||
checkAccountName: () => Promise<boolean>;
|
||||
setUserPassword: (password: string) => void;
|
||||
setUserMnemonic: (mnemonic: string) => void;
|
||||
setAccountName: (name: string) => void;
|
||||
createAccount: (args: { mnemonic: string; password: string; accName: string }) => Promise<void>;
|
||||
importAccount: () => Promise<string[]>;
|
||||
resetState: () => void;
|
||||
};
|
||||
|
||||
export const RegisterContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [userPassword, setUserPassword] = useState('');
|
||||
const [userMnemonic, setUserMnemonic] = useState('');
|
||||
const [accountName, setAccountName] = useState('');
|
||||
|
||||
const resetState = () => {
|
||||
setUserMnemonic('');
|
||||
setUserPassword('');
|
||||
setAccountName('');
|
||||
};
|
||||
|
||||
const createAccount = async ({
|
||||
mnemonic,
|
||||
password,
|
||||
accName,
|
||||
}: {
|
||||
mnemonic: string;
|
||||
password: string;
|
||||
accName: string;
|
||||
}) => {
|
||||
const storage = await new ExtensionStorage(password);
|
||||
await storage.store_mnemonic(accName, mnemonic);
|
||||
};
|
||||
|
||||
const importAccount = async () => {
|
||||
const storage = await new ExtensionStorage(userPassword);
|
||||
await storage.store_mnemonic(accountName, userMnemonic);
|
||||
const accounts = await storage.get_all_mnemonic_keys();
|
||||
return accounts;
|
||||
};
|
||||
|
||||
const checkAccountName = async () => true;
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
userPassword,
|
||||
setUserPassword,
|
||||
userMnemonic,
|
||||
accountName,
|
||||
setAccountName,
|
||||
setUserMnemonic,
|
||||
createAccount,
|
||||
checkAccountName,
|
||||
importAccount,
|
||||
resetState,
|
||||
}),
|
||||
[userPassword, userMnemonic, accountName],
|
||||
);
|
||||
|
||||
return <RegisterContext.Provider value={value}>{children}</RegisterContext.Provider>;
|
||||
};
|
||||
|
||||
export const useRegisterContext = () => React.useContext(RegisterContext);
|
||||
@@ -1,112 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { DecCoin } from '@nymproject/types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { nymToUnym } from 'src/utils/coin';
|
||||
import { TTransaction } from 'src/types';
|
||||
import { Fee, useGetFee } from 'src/hooks/useGetFee';
|
||||
import { createFeeObject } from 'src/utils/fee';
|
||||
import { useAppContext } from './app';
|
||||
|
||||
type TSendContext = {
|
||||
address?: string;
|
||||
amount?: DecCoin;
|
||||
transaction?: TTransaction;
|
||||
fee?: Fee;
|
||||
handleChangeAddress: (address?: string) => void;
|
||||
handleChangeAmount: (amount?: DecCoin) => void;
|
||||
handleSend: () => void;
|
||||
resetTx: () => void;
|
||||
onDone: () => void;
|
||||
handleGetFee: (address: string, amount: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const SendContext = React.createContext({} as TSendContext);
|
||||
|
||||
export const SendProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [address, setAddress] = useState<string>();
|
||||
const [amount, setAmount] = useState<DecCoin>();
|
||||
const [transaction, setTransaction] = useState<TTransaction>();
|
||||
|
||||
const { client, minorDenom } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChangeAddress = (_address?: string) => setAddress(_address);
|
||||
|
||||
const handleChangeAmount = (_amount?: DecCoin) => setAmount(_amount);
|
||||
|
||||
const { getFee, fee } = useGetFee();
|
||||
|
||||
const handleGetFee = async (addressVal: string, amountVal: string) => {
|
||||
const unym = nymToUnym(Number(amountVal));
|
||||
|
||||
if (client) {
|
||||
// client loses its 'this' context when passing the method
|
||||
// TODO find a better way of doing this.
|
||||
getFee(client.simulateSend.bind(client), {
|
||||
signingAddress: client.address,
|
||||
from: client.address,
|
||||
to: addressVal,
|
||||
amount: [{ amount: unym.toString(), denom: minorDenom }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
setTransaction({ status: 'loading', type: 'send' });
|
||||
let unyms;
|
||||
|
||||
if (!Number(amount?.amount)) {
|
||||
setTransaction({ status: 'error', type: 'send', message: 'Amount is not a valid number' });
|
||||
}
|
||||
|
||||
if (amount) {
|
||||
unyms = nymToUnym(Number(amount.amount));
|
||||
}
|
||||
|
||||
if (client && address && unyms) {
|
||||
try {
|
||||
const response = await client.send(
|
||||
address,
|
||||
[{ amount: unyms.toString(), denom: minorDenom }],
|
||||
createFeeObject(fee?.unym),
|
||||
);
|
||||
|
||||
setTransaction({ status: 'success', type: 'send', txHash: response?.transactionHash });
|
||||
} catch (e) {
|
||||
setTransaction({
|
||||
status: 'error',
|
||||
type: 'send',
|
||||
message: e instanceof Error ? e.message : 'Error making send transaction. Please try again',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetTx = () => {
|
||||
setTransaction(undefined);
|
||||
};
|
||||
|
||||
const onDone = () => {
|
||||
navigate('/user/balance');
|
||||
};
|
||||
|
||||
const value = useMemo<TSendContext>(
|
||||
() => ({
|
||||
address,
|
||||
amount,
|
||||
transaction,
|
||||
fee,
|
||||
handleChangeAddress,
|
||||
handleChangeAmount,
|
||||
handleSend,
|
||||
resetTx,
|
||||
onDone,
|
||||
handleGetFee,
|
||||
}),
|
||||
[address, amount, transaction, fee],
|
||||
);
|
||||
|
||||
return <SendContext.Provider value={value}>{children}</SendContext.Provider>;
|
||||
};
|
||||
|
||||
export const useSendContext = () => React.useContext(SendContext);
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useCreatePassword = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isSafePassword, setIsSafePassword] = useState(false);
|
||||
const [hasReadTerms, setHasReadTerms] = useState(false);
|
||||
|
||||
const canProceed = isSafePassword && hasReadTerms && password === confirmPassword;
|
||||
|
||||
return {
|
||||
password,
|
||||
setPassword,
|
||||
confirmPassword,
|
||||
setConfirmPassword,
|
||||
setIsSafePassword,
|
||||
canProceed,
|
||||
setHasReadTerms,
|
||||
hasReadTerms,
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user