Compare commits

..

17 Commits

Author SHA1 Message Date
mfahampshire f75cd90092 cargo lock after rebase on develop 2025-03-06 17:32:57 +01:00
mfahampshire 1164f66d7a change hardcoded file to tempdir 2025-03-06 17:32:22 +01:00
Jon Häggblad 8ddadf2aa5 Set DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE to 50 2025-03-06 17:32:22 +01:00
benedettadavico b0234d0140 bump api version 2025-03-06 17:32:22 +01:00
benedettadavico 6c2b917706 update changelog for patched-dorina 2025-03-06 17:32:22 +01:00
benedettadavico 0b8a5d5d86 bumping versions dorina patched 2025-03-06 17:32:22 +01:00
Jędrzej Stuczyński 74623fa2b8 bugfix: make sure to correctly decode response content when putting it into error message (#5571) 2025-03-06 17:32:22 +01:00
Jędrzej Stuczyński 454712b520 chore: additional logs when attempting to load ecash keys (#5567) 2025-03-06 17:32:22 +01:00
Jędrzej Stuczyński 5fe5541d02 fix: gateway protocol negotation for v3/v4 2025-03-06 17:32:22 +01:00
Jędrzej Stuczyński c25c4548b2 feature: v2 authentication request (#5537) (#5563)
* introduced v2 authentication request between clients and gateways

* client to send v2 auth when possible

* added persistence to last used authentication timestamp

* added clients identity to signed plaintext
2025-03-06 17:32:22 +01:00
Jon Häggblad d6e996e8e9 Tweak surb management to be more conservative (#5570)
To reduce the risk of the IPR DoS the client:

- Lower the timeout until the IPR will disconnect a client
- Reduce fewer surbs at a time. Large surb requests increases the
  latency until all fragments in the response have been delivered. The
  efficiency gains of having large surb requests dimishes quickly for
  large sizes as well
2025-03-06 17:32:22 +01:00
Jon Häggblad cf88364dda Deserialize v5 authenticator requests (#5568) 2025-03-06 17:32:22 +01:00
Jędrzej Stuczyński 24d32a64e0 hotfix: ensure we bail on merkle leaves insertion upon missing data (#5565)
* hotfix: ensure we bail on merkle leaves insertion upon missing data

* Update Cargo.toml

---------

Co-authored-by: benedetta davico <46782255+benedettadavico@users.noreply.github.com>
2025-03-06 17:32:22 +01:00
Jędrzej Stuczyński 28d1d9d989 add full response body to error message upon decoding failure (#5566) 2025-03-06 17:32:22 +01:00
Jon Häggblad 74a4197b77 Create authenticator v5 request/response types (#5561)
* Create authenticator v5 request/response types

* Support v5 in the authenticator

* Fix tests

* Bump nym-node version
2025-03-06 17:32:22 +01:00
Jon Häggblad 3bc7301d94 Handle disconnect in IPR (#5547)
* Implement disconnect in the IPR

* Remove unused async
2025-03-06 17:32:22 +01:00
Jon Häggblad cc342458f9 Allow IPR reconnect to session (#5562) 2025-03-06 17:32:22 +01:00
74 changed files with 479 additions and 1713 deletions
-2
View File
@@ -6,7 +6,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [2025.4-dorina-patched] (2025-03-06)
- use legacy crypto for constructing SURB headers ([#5579])
- bugfix: make sure to correctly decode response content when putting it into error message ([#5571])
- Tweak surb management to be more conservative ([#5570])
- Deserialize v5 authenticator requests ([#5568])
@@ -16,7 +15,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- feature: v2 authentication request (#5537) ([#5563])
- Create authenticator v5 request/response types ([#5561])
[#5579]: https://github.com/nymtech/nym/pull/5579
[#5571]: https://github.com/nymtech/nym/pull/5571
[#5570]: https://github.com/nymtech/nym/pull/5570
[#5568]: https://github.com/nymtech/nym/pull/5568
Generated
+47 -67
View File
@@ -902,9 +902,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
dependencies = [
"serde",
]
@@ -941,7 +941,7 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a"
dependencies = [
"camino",
"cargo-platform",
"semver 1.0.26",
"semver 1.0.25",
"serde",
"serde_json",
"thiserror 1.0.69",
@@ -955,7 +955,7 @@ checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
dependencies = [
"camino",
"cargo-platform",
"semver 1.0.26",
"semver 1.0.25",
"serde",
"serde_json",
"thiserror 1.0.69",
@@ -1587,7 +1587,7 @@ dependencies = [
"bitflags 2.8.0",
"crossterm_winapi",
"parking_lot",
"rustix 0.38.44",
"rustix",
"winapi",
]
@@ -1799,7 +1799,7 @@ dependencies = [
"cosmwasm-std",
"cw2",
"schemars",
"semver 1.0.26",
"semver 1.0.25",
"serde",
"thiserror 1.0.69",
]
@@ -1814,7 +1814,7 @@ dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"schemars",
"semver 1.0.26",
"semver 1.0.25",
"serde",
"thiserror 1.0.69",
]
@@ -2527,9 +2527,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "ff"
version = "0.13.1"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"rand_core 0.6.4",
"subtle 2.6.1",
@@ -4144,12 +4144,6 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
[[package]]
name = "lioness"
version = "0.1.2"
@@ -4847,7 +4841,7 @@ dependencies = [
"rand_chacha 0.3.1",
"reqwest 0.12.4",
"schemars",
"semver 1.0.26",
"semver 1.0.25",
"serde",
"serde_json",
"sha2 0.9.9",
@@ -5960,7 +5954,6 @@ dependencies = [
"bytes",
"nym-bin-common",
"nym-crypto",
"nym-sdk",
"nym-service-provider-requests-common",
"nym-sphinx",
"rand 0.8.5",
@@ -5969,7 +5962,6 @@ dependencies = [
"time",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
@@ -6063,7 +6055,7 @@ dependencies = [
"nym-contracts-common",
"rand_chacha 0.3.1",
"schemars",
"semver 1.0.26",
"semver 1.0.25",
"serde",
"serde-json-wasm",
"serde_repr",
@@ -6215,7 +6207,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.6.2"
version = "1.6.1"
dependencies = [
"anyhow",
"arc-swap",
@@ -6264,7 +6256,7 @@ dependencies = [
"nym-wireguard",
"nym-wireguard-types",
"rand 0.8.5",
"semver 1.0.26",
"semver 1.0.25",
"serde",
"serde_json",
"si-scale",
@@ -7235,7 +7227,7 @@ dependencies = [
[[package]]
name = "nyx-chain-watcher"
version = "0.1.13"
version = "0.1.11"
dependencies = [
"anyhow",
"async-trait",
@@ -7250,6 +7242,7 @@ dependencies = [
"nym-validator-client",
"nyxd-scraper",
"reqwest 0.12.4",
"rocket",
"schemars",
"serde",
"serde_json",
@@ -8417,9 +8410,9 @@ dependencies = [
[[package]]
name = "ring"
version = "0.17.13"
version = "0.17.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24"
dependencies = [
"cc",
"cfg-if",
@@ -8606,9 +8599,9 @@ dependencies = [
[[package]]
name = "rs_merkle"
version = "1.5.0"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb09b49230ba22e8c676e7b75dfe2887dea8121f18b530ae0ba519ce442d2b21"
checksum = "3b241d2e59b74ef9e98d94c78c47623d04c8392abaf82014dfd372a16041128f"
dependencies = [
"sha2 0.10.8",
]
@@ -8710,7 +8703,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver 1.0.26",
"semver 1.0.25",
]
[[package]]
@@ -8722,20 +8715,7 @@ dependencies = [
"bitflags 2.8.0",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657"
dependencies = [
"bitflags 2.8.0",
"errno",
"libc",
"linux-raw-sys 0.9.2",
"linux-raw-sys",
"windows-sys 0.59.0",
]
@@ -9011,9 +8991,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.26"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
dependencies = [
"serde",
]
@@ -9026,9 +9006,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [
"serde_derive",
]
@@ -9066,18 +9046,18 @@ dependencies = [
[[package]]
name = "serde_bytes"
version = "0.11.17"
version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96"
checksum = "364fec0df39c49a083c9a8a18a23a6bcfd9af130fe9fe321d18520a0d113e09e"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [
"proc-macro2",
"quote",
@@ -9180,9 +9160,9 @@ dependencies = [
[[package]]
name = "serde_repr"
version = "0.1.20"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
@@ -9453,9 +9433,9 @@ dependencies = [
[[package]]
name = "sphinx-packet"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c23047e0cf36ff6904603f499fd13153425cdf5ba47bfbaedbc999da0bd92f4e"
checksum = "535f2c430778bf59c22249fcc1ed6d384129eb2f0f694706015d636c688f9ac6"
dependencies = [
"aes",
"arrayref",
@@ -9974,15 +9954,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.18.0"
version = "3.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
dependencies = [
"cfg-if",
"fastrand 2.3.0",
"getrandom 0.3.1",
"once_cell",
"rustix 1.0.1",
"rustix",
"windows-sys 0.59.0",
]
@@ -10110,7 +10090,7 @@ dependencies = [
"pin-project",
"rand 0.8.5",
"reqwest 0.11.27",
"semver 1.0.26",
"semver 1.0.25",
"serde",
"serde_bytes",
"serde_json",
@@ -10270,9 +10250,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.39"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
@@ -10288,15 +10268,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.20"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
@@ -10339,9 +10319,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.44.0"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
@@ -12066,8 +12046,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
dependencies = [
"libc",
"linux-raw-sys 0.4.15",
"rustix 0.38.44",
"linux-raw-sys",
"rustix",
]
[[package]]
+10 -10
View File
@@ -209,7 +209,7 @@ blake3 = "1.6.1"
bloomfilter = "1.0.14"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.10.1"
bytes = "1.7.2"
cargo_metadata = "0.18.1"
celes = "2.5.0"
cfg-if = "1.0.0"
@@ -308,12 +308,12 @@ reqwest = { version = "0.12.4", default-features = false }
rocket = "0.5.0"
rocket_cors = "0.6.0"
rocket_okapi = "0.8.0"
rs_merkle = "1.5.0"
rs_merkle = "1.4.2"
safer-ffi = "0.1.13"
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
serde_bytes = "0.11.17"
semver = "1.0.25"
serde = "1.0.217"
serde_bytes = "0.11.16"
serde_derive = "1.0"
serde_json = "1.0.140"
serde_json_path = "0.7.2"
@@ -322,7 +322,7 @@ serde_with = "3.9.0"
serde_yaml = "0.9.25"
sha2 = "0.10.8"
si-scale = "0.2.3"
sphinx-packet = "=0.3.2"
sphinx-packet = "0.3.1"
sqlx = "0.7.4"
strum = "0.26"
strum_macros = "0.26"
@@ -331,10 +331,10 @@ syn = "1"
sysinfo = "0.33.0"
tap = "1.0.1"
tar = "0.4.44"
tempfile = "3.18"
tempfile = "3.15"
thiserror = "2.0"
time = "0.3.39"
tokio = "1.44"
time = "0.3.37"
tokio = "1.43"
tokio-postgres = "0.7"
tokio-stream = "0.1.17"
tokio-test = "0.4.4"
@@ -372,7 +372,7 @@ prometheus = { version = "0.13.0" }
# plus to make our live easier we need serde support from https://github.com/zkcrypto/bls12_381/pull/125
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" }
group = { version = "0.13.0", default-features = false }
ff = { version = "0.13.1", default-features = false }
ff = { version = "0.13.0", default-features = false }
subtle = "2.5.0"
# cosmwasm-related
+2 -2
View File
@@ -117,7 +117,7 @@ impl Drop for Handler {
}
impl Handler {
async fn get_lane_queue_length(&mut self, connection_id: ConnectionId) -> Option<ServerResponse> {
async fn get_lane_queue_length(&self, connection_id: ConnectionId) -> Option<ServerResponse> {
let req_start = Instant::now();
// get the base queue length
@@ -282,7 +282,7 @@ impl Handler {
None
}
async fn handle_get_lane_queue_length(&mut self, connection_id: u64) -> Option<ServerResponse> {
async fn handle_get_lane_queue_length(&self, connection_id: u64) -> Option<ServerResponse> {
self.get_lane_queue_length(connection_id).await
}
@@ -658,9 +658,6 @@ pub struct ReplySurbs {
/// Specifies the number of mixnet hops the packet should go through. If not specified, then
/// the default value is used.
pub surb_mix_hops: Option<u8>,
/// Specifies if we should reset all the sender tags on startup
pub fresh_sender_tags: bool,
}
impl Default for ReplySurbs {
@@ -678,7 +675,6 @@ impl Default for ReplySurbs {
maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE,
maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE,
surb_mix_hops: None,
fresh_sender_tags: false,
}
}
}
@@ -88,7 +88,7 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
let db_path = db_path.as_ref();
if db_path.exists() {
info!("loading existing surb database");
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
match fs_backend::Backend::try_load(db_path).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");
@@ -18,7 +18,7 @@ pub type ClientRequestSender = tokio::sync::mpsc::Sender<ClientRequest>;
pub mod transceiver;
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 5;
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
const MAX_FAILURE_COUNT: usize = 100;
// that's also disgusting.
@@ -96,7 +96,6 @@ impl MixTrafficController {
mut mix_packets: Vec<MixPacket>,
) -> Result<(), ErasedGatewayError> {
debug_assert!(!mix_packets.is_empty());
info!("on_messages: received {} mix_packets", mix_packets.len());
let result = if mix_packets.len() == 1 {
let mix_packet = mix_packets.pop().unwrap();
@@ -69,7 +69,7 @@ where
// offload reply handling to the dedicated task
if let Err(err) = self
.reply_controller_sender
.send_reply(recipient_tag, data, lane).await
.send_reply(recipient_tag, data, lane)
{
if !self.task_client.is_shutdown_poll() {
error!("failed to send a reply - {err}");
@@ -111,17 +111,12 @@ where
}
async fn on_input_message(&mut self, msg: InputMessage) {
log::info!(
"input_message_listener: received a message: {:?}",
msg.lane()
);
match msg {
InputMessage::Regular {
recipient,
data,
lane,
} => {
log::info!("input_message_listener: regular");
self.handle_plain_message(recipient, data, lane, PacketType::Mix)
.await
}
@@ -131,7 +126,6 @@ where
reply_surbs,
lane,
} => {
log::info!("input_message_listener: anonymous");
self.handle_repliable_message(recipient, data, reply_surbs, lane, PacketType::Mix)
.await
}
@@ -140,13 +134,9 @@ where
data,
lane,
} => {
log::info!("input_message_listener: reply");
self.handle_reply(recipient_tag, data, lane).await;
}
InputMessage::Premade { msgs, lane } => {
log::info!("input_message_listener: premade");
self.handle_premade_packets(msgs, lane).await
}
InputMessage::Premade { msgs, lane } => self.handle_premade_packets(msgs, lane).await,
InputMessage::MessageWrapper {
message,
packet_type,
@@ -156,8 +146,6 @@ where
data,
lane,
} => {
log::info!("input_message_listener: wrapper regular");
self.handle_plain_message(recipient, data, lane, packet_type)
.await
}
@@ -167,7 +155,6 @@ where
reply_surbs,
lane,
} => {
log::info!("input_message_listener: wrapper anonymous");
self.handle_repliable_message(recipient, data, reply_surbs, lane, packet_type)
.await
}
@@ -176,11 +163,9 @@ where
data,
lane,
} => {
log::info!("input_message_listener: wrapper reply");
self.handle_reply(recipient_tag, data, lane).await;
}
InputMessage::Premade { msgs, lane } => {
log::info!("input_message_listener: wrapper premade");
self.handle_premade_packets(msgs, lane).await
}
// MessageWrappers can't be nested
@@ -99,15 +99,11 @@ where
} => {
// if this is retransmission for reply, offload it to the dedicated task
// that deals with all the surbs
if let Err(err) = self
.reply_controller_sender
.send_retransmission_data(
*recipient_tag,
weak_timed_out_ack,
*extra_surb_request,
)
.await
{
if let Err(err) = self.reply_controller_sender.send_retransmission_data(
*recipient_tag,
weak_timed_out_ack,
*extra_surb_request,
) {
if !self.task_client.is_shutdown_poll() {
error!("Failed to send retransmission data to the reply controller: {err}");
}
@@ -230,7 +230,7 @@ where
let (next_message, fragment_id, packet_size) = match next_message {
StreamMessage::Cover => {
let cover_traffic_packet_size = self.loop_cover_message_size();
info!("the next loop cover message will be put in a {cover_traffic_packet_size} packet");
trace!("the next loop cover message will be put in a {cover_traffic_packet_size} packet");
// TODO for way down the line: in very rare cases (during topology update) we might have
// to wait a really tiny bit before actually obtaining the permit hence messing with our
@@ -277,8 +277,6 @@ where
}
};
let mix_tx_capacity = self.mix_tx.capacity();
log::info!("mix_tx_capacity: {mix_tx_capacity}");
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
if !self.task_client.is_shutdown_poll() {
log::error!("Failed to send: {err}");
@@ -538,13 +536,13 @@ where
format!("Packet backlog: {lane_status}, avg delay: {delay}ms ({mult})")
};
// if packets > 1000 {
// log::warn!("{status_str}");
// } else if packets > 0 {
if packets > 1000 {
log::warn!("{status_str}");
} else if packets > 0 {
log::info!("{status_str}");
// } else {
// log::debug!("{status_str}");
// }
} else {
log::debug!("{status_str}");
}
// Send status message to whoever is listening (possibly UI)
if mult == self.sending_delay_controller.max_multiplier() {
@@ -561,7 +559,7 @@ where
#[cfg(not(target_arch = "wasm32"))]
{
let mut status_timer = tokio::time::interval(Duration::from_secs(1));
let mut status_timer = tokio::time::interval(Duration::from_secs(5));
while !shutdown.is_shutdown() {
tokio::select! {
@@ -574,7 +572,6 @@ where
self.log_status(&mut shutdown);
}
next_message = self.next() => if let Some(next_message) = next_message {
self.log_status(&mut shutdown);
self.on_message(next_message).await;
} else {
log::trace!("OutQueueControl: Stopping since channel closed");
@@ -224,7 +224,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
msgs.into_iter().map(Into::into).collect()
}
async fn handle_reconstructed_repliable_messages(
fn handle_reconstructed_repliable_messages(
&mut self,
msgs: Vec<RepliableMessage>,
) -> Vec<ReconstructedMessage> {
@@ -261,11 +261,11 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
};
if let Err(err) = self
.reply_controller_sender
.send_additional_surbs(msg.sender_tag, reply_surbs, from_surb_request)
.await
{
if let Err(err) = self.reply_controller_sender.send_additional_surbs(
msg.sender_tag,
reply_surbs,
from_surb_request,
) {
if !self.task_client.is_shutdown_poll() {
error!("{err}");
}
@@ -274,7 +274,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
reconstructed
}
async fn handle_reconstructed_reply_messages(
fn handle_reconstructed_reply_messages(
&mut self,
msgs: Vec<ReplyMessage>,
) -> Vec<ReconstructedMessage> {
@@ -287,7 +287,6 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
if let Err(err) = self
.reply_controller_sender
.send_additional_surbs_request(*recipient, amount)
.await
{
if !self.task_client.is_shutdown_poll() {
error!("{err}");
@@ -317,16 +316,10 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
let mut reconstructed_messages = self.handle_reconstructed_plain_messages(plain_messages);
reconstructed_messages.append(
&mut self
.handle_reconstructed_repliable_messages(repliable_messages)
.await,
);
reconstructed_messages.append(
&mut self
.handle_reconstructed_reply_messages(reply_messages)
.await,
);
reconstructed_messages
.append(&mut self.handle_reconstructed_repliable_messages(repliable_messages));
reconstructed_messages
.append(&mut self.handle_reconstructed_reply_messages(reply_messages));
let mut inner_guard = self.inner.lock().await;
debug!(
@@ -2,10 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement;
use futures::{
channel::{mpsc, oneshot},
SinkExt,
};
use futures::channel::{mpsc, oneshot};
use log::error;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
@@ -14,128 +11,117 @@ use nym_task::connections::{ConnectionId, TransmissionLane};
use std::sync::Weak;
pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerReceiver) {
let (tx, rx) = mpsc::channel(8);
let (tx, rx) = mpsc::unbounded();
(tx.into(), rx)
}
#[derive(Debug, thiserror::Error)]
pub enum ReplyControllerSenderError {
#[error("failed to send retransmission data to reply controller")]
// SendRetransmissionData(#[source] mpsc::TrySendError<ReplyControllerMessage>),
SendRetransmissionData,
SendRetransmissionData(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("failed to send reply to reply controller")]
// SendReply(#[source] mpsc::TrySendError<ReplyControllerMessage>),
SendReply,
SendReply(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("failed to send additional surbs to reply controller")]
// AdditionalSurbs(#[source] mpsc::TrySendError<ReplyControllerMessage>),
AdditionalSurbs,
AdditionalSurbs(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("failed to send additional surbs request to reply controller")]
// AdditionalSurbsRequest(#[source] mpsc::TrySendError<ReplyControllerMessage>),
AdditionalSurbsRequest,
AdditionalSurbsRequest(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("failed to request lane queue length from reply controller")]
// LaneQueueLength(#[source] mpsc::TrySendError<ReplyControllerMessage>),
LaneQueueLength,
LaneQueueLength(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("response channel was dropped before we could receive the response")]
// ResponseChannelDropped(#[source] oneshot::Canceled),
ResponseChannelDropped,
ResponseChannelDropped(#[source] oneshot::Canceled),
}
#[derive(Debug, Clone)]
pub struct ReplyControllerSender(mpsc::Sender<ReplyControllerMessage>);
pub struct ReplyControllerSender(mpsc::UnboundedSender<ReplyControllerMessage>);
impl From<mpsc::Sender<ReplyControllerMessage>> for ReplyControllerSender {
fn from(inner: mpsc::Sender<ReplyControllerMessage>) -> Self {
impl From<mpsc::UnboundedSender<ReplyControllerMessage>> for ReplyControllerSender {
fn from(inner: mpsc::UnboundedSender<ReplyControllerMessage>) -> Self {
ReplyControllerSender(inner)
}
}
impl ReplyControllerSender {
pub(crate) async fn send_retransmission_data(
&mut self,
pub(crate) fn send_retransmission_data(
&self,
recipient: AnonymousSenderTag,
timed_out_ack: Weak<PendingAcknowledgement>,
extra_surb_request: bool,
) -> Result<(), ReplyControllerSenderError> {
self.0
.send(ReplyControllerMessage::RetransmitReply {
.unbounded_send(ReplyControllerMessage::RetransmitReply {
recipient,
timed_out_ack,
extra_surb_request,
})
.await
.map_err(|_| ReplyControllerSenderError::SendRetransmissionData)
.map_err(ReplyControllerSenderError::SendRetransmissionData)
}
pub(crate) async fn send_reply(
&mut self,
pub(crate) fn send_reply(
&self,
recipient: AnonymousSenderTag,
message: Vec<u8>,
lane: TransmissionLane,
) -> Result<(), ReplyControllerSenderError> {
self.0
.send(ReplyControllerMessage::SendReply {
.unbounded_send(ReplyControllerMessage::SendReply {
recipient,
message,
lane,
})
.await
.map_err(|_| ReplyControllerSenderError::SendReply)
.map_err(ReplyControllerSenderError::SendReply)
}
pub(crate) async fn send_additional_surbs(
&mut self,
pub(crate) fn send_additional_surbs(
&self,
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
from_surb_request: bool,
) -> Result<(), ReplyControllerSenderError> {
self.0
.send(ReplyControllerMessage::AdditionalSurbs {
.unbounded_send(ReplyControllerMessage::AdditionalSurbs {
sender_tag,
reply_surbs,
from_surb_request,
})
.await
.map_err(|_| ReplyControllerSenderError::AdditionalSurbs)
.map_err(ReplyControllerSenderError::AdditionalSurbs)
}
pub(crate) async fn send_additional_surbs_request(
&mut self,
pub(crate) fn send_additional_surbs_request(
&self,
recipient: Recipient,
amount: u32,
) -> Result<(), ReplyControllerSenderError> {
self.0
.send(ReplyControllerMessage::AdditionalSurbsRequest {
.unbounded_send(ReplyControllerMessage::AdditionalSurbsRequest {
recipient: Box::new(recipient),
amount,
})
.await
.map_err(|_| ReplyControllerSenderError::AdditionalSurbsRequest)
.map_err(ReplyControllerSenderError::AdditionalSurbsRequest)
}
pub async fn get_lane_queue_length(
&mut self,
&self,
connection_id: ConnectionId,
) -> Result<usize, ReplyControllerSenderError> {
let (response_tx, response_rx) = oneshot::channel();
if let Err(_err) = self
if let Err(err) = self
.0
.send(ReplyControllerMessage::LaneQueueLength {
.unbounded_send(ReplyControllerMessage::LaneQueueLength {
connection_id,
response_channel: response_tx,
})
.await
{
return Err(ReplyControllerSenderError::LaneQueueLength);
return Err(ReplyControllerSenderError::LaneQueueLength(err));
}
response_rx
.await
.map_err(|_| ReplyControllerSenderError::ResponseChannelDropped)
.map_err(ReplyControllerSenderError::ResponseChannelDropped)
}
}
@@ -151,7 +137,7 @@ impl ReplyQueueLengths {
}
pub async fn get_lane_queue_length(
&mut self,
&self,
connection_id: ConnectionId,
) -> Result<usize, ReplyControllerSenderError> {
self.reply_controller_sender
@@ -160,7 +146,7 @@ impl ReplyQueueLengths {
}
}
pub(crate) type ReplyControllerReceiver = mpsc::Receiver<ReplyControllerMessage>;
pub(crate) type ReplyControllerReceiver = mpsc::UnboundedReceiver<ReplyControllerMessage>;
#[derive(Debug)]
pub enum ReplyControllerMessage {
@@ -10,7 +10,7 @@ use crate::{
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
};
use async_trait::async_trait;
use log::{debug, error, info, warn};
use log::{error, info, warn};
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf};
@@ -52,10 +52,7 @@ impl Backend {
Ok(backend)
}
pub async fn try_load<P: AsRef<Path>>(
database_path: P,
fresh_sender_tags: bool,
) -> Result<Self, StorageError> {
pub async fn try_load<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
let owned_path: PathBuf = database_path.as_ref().into();
if owned_path.file_name().is_none() {
return Err(StorageError::DatabasePathWithoutFilename {
@@ -121,9 +118,6 @@ impl Backend {
if days > 2 {
info!("it's been over {days} days and {hours} hours since we last used our data store. our used sender tags are already outdated - we're going to purge them now.");
manager.delete_all_tags().await?;
} else if fresh_sender_tags {
debug!("starting with fresh sender tags");
manager.delete_all_tags().await?;
}
Ok(Backend {
-2
View File
@@ -21,5 +21,3 @@ thiserror = { workspace = true }
time = { workspace = true }
tokio = { workspace = true, features = ["time"] }
tokio-util = { workspace = true, features = ["codec"] }
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
tracing.workspace = true
-33
View File
@@ -74,39 +74,6 @@ impl MultiIpPacketCodec {
Some(packets)
}
}
pub async fn buffer_timeout2(
&mut self,
lane_queue_lengths: nym_sdk::mixnet::LaneQueueLengths,
) -> Option<Bytes> {
// Wait for buffer_timeout to tick
let _ = self.buffer_timeout.tick().await;
// wait for lane_queue_lenghts to go to zero
{
let now = std::time::Instant::now();
loop {
let lane_queue_lengths = lane_queue_lengths.lock().unwrap().total();
if lane_queue_lengths < 10 {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let elapsed = now.elapsed();
tracing::error!(
"Waited for lane_queue_lengths to go to zero for {:?}",
elapsed
);
}
// Flush the buffer and return it
let packets = self.flush_current_buffer();
if packets.is_empty() {
None
} else {
Some(packets)
}
}
}
impl Encoder<Bytes> for MultiIpPacketCodec {
@@ -7,7 +7,7 @@ use nym_sphinx_addressing::clients::Recipient;
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN};
use nym_sphinx_params::packet_sizes::PacketSize;
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm};
use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB, UPDATED_LEGACY_VERSION};
use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB};
use nym_topology::{NymRouteProvider, NymTopologyError};
use rand::{CryptoRng, RngCore};
use serde::de::{Error as SerdeError, Visitor};
@@ -101,8 +101,7 @@ impl ReplySurb {
let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len());
let destination = recipient.as_sphinx_destination();
let surb_material =
SURBMaterial::new(route, delays, destination).with_version(UPDATED_LEGACY_VERSION);
let surb_material = SURBMaterial::new(route, delays, destination);
// this can't fail as we know we have a valid route to gateway and have correct number of delays
Ok(ReplySurb {
-4
View File
@@ -103,8 +103,4 @@ impl LaneQueueLengthsInner {
{
self.map.entry(*lane).and_modify(f);
}
pub fn total(&self) -> usize {
self.map.values().sum()
}
}
@@ -494,9 +494,6 @@ pub struct ReplySurbsWasm {
/// Defines how many mix nodes the reply surb should go through.
/// If not set, the default value is going to be used.
pub surb_mix_hops: Option<u8>,
/// Specifies if we should reset all the sender tags on startup
pub fresh_sender_tags: bool,
}
impl Default for ReplySurbsWasm {
@@ -528,7 +525,6 @@ impl From<ReplySurbsWasm> for ConfigReplySurbs {
reply_surbs.maximum_reply_key_age_ms as u64,
),
surb_mix_hops: reply_surbs.surb_mix_hops,
fresh_sender_tags: reply_surbs.fresh_sender_tags,
}
}
}
@@ -552,7 +548,6 @@ impl From<ConfigReplySurbs> for ReplySurbsWasm {
maximum_reply_surb_age_ms: reply_surbs.maximum_reply_surb_age.as_millis() as u32,
maximum_reply_key_age_ms: reply_surbs.maximum_reply_key_age.as_millis() as u32,
surb_mix_hops: reply_surbs.surb_mix_hops,
fresh_sender_tags: reply_surbs.fresh_sender_tags,
}
}
}
@@ -378,9 +378,6 @@ pub struct ReplySurbsWasmOverride {
#[tsify(optional)]
pub surb_mix_hops: Option<u8>,
/// Specifies if we should reset all the sender tags on startup
pub fresh_sender_tags: bool,
}
impl From<ReplySurbsWasmOverride> for ReplySurbsWasm {
@@ -419,7 +416,6 @@ impl From<ReplySurbsWasmOverride> for ReplySurbsWasm {
.maximum_reply_key_age_ms
.unwrap_or(def.maximum_reply_key_age_ms),
surb_mix_hops: value.surb_mix_hops,
fresh_sender_tags: value.fresh_sender_tags,
}
}
}
+12 -12
View File
@@ -1522,18 +1522,18 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.26"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
@@ -1558,9 +1558,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@@ -1777,9 +1777,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.39"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
@@ -1794,15 +1794,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.20"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
+1 -3
View File
@@ -7,6 +7,4 @@ package-lock.json
# local env files
.env*.local
.env
scratch.md
.env
@@ -1,10 +0,0 @@
- Run synchronization against [pool.ntp.org](https://www.ntppool.org/en/):
```bash
ntpdate -q pool.ntp.org
```
- Enable `ntp` service, start and review the status:
```bash
systemctl enable --now ntp
service ntp start
service ntp status
```
@@ -1 +1 @@
808_623_916
807_251_217
@@ -1 +1 @@
1_028_488
1_025_628
@@ -1 +1 @@
404_311_958
403_625_608
@@ -1,7 +1,7 @@
| **Item** | **Description** | **Amount in NYM** |
|:-------------------|:------------------------------------------------------|--------------------:|
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
| Mixmining Reserve | Tokens releasing for operators rewards | 191_376_083 |
| Mixmining Reserve | Tokens releasing for operators rewards | 192_748_782 |
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
| Circulating Supply | Amount of unlocked tokens | 808_623_916 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_028_488 |
| Circulating Supply | Amount of unlocked tokens | 807_251_217 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_025_628 |
@@ -1 +1 @@
Tuesday, March 11th 2025, 11:04:18 UTC
Wednesday, February 26th 2025, 16:02:47 UTC
@@ -1,5 +0,0 @@
{
"mainnet":"Mainnet Endpoints",
"sandbox":"Sandbox Endpoints"
}
@@ -1,5 +0,0 @@
{
"mainnet":"Mainnet Endpoints",
"sandbox":"Sandbox Endpoints"
}
+5 -2
View File
@@ -1,8 +1,11 @@
import { Callout } from 'nextra/components'
# Node Status API
The Node Status API serves information about individual `nym-nodes` in the Mixnet, such as which role they are operating in, statistics about them, services such as Network Requesters, as well as summaries of the state of the Mixnet.
The Node Status API contains information about the network, its topology, and the routing scores of all nodes within it. It offers broadly similar information to the experimental [Habourmaster frontend](https://harbourmaster.nymtech.net/) but is stable where the Harbourmaster is subject to sudden changes as we modify and experiment with how we scrape and present data about the Mixnet infrastructure.
<Callout type="info">
We recommend that developers building applications such as explorers or analytics interfaces about the Mixnet run their own instance of the API, in order to promote a robust network of downstream services, and spread the load of API calls amongst as many endpoints as possible.
People building applications or dashboards which requires information about nodes, their uptime, and their delegations should use this instead of Habourmaster.
</Callout>
The code for this service can be found [in our monorepo](https://github.com/nymtech/nym/tree/develop/nym-node-status-api). In the future we will encourage developers to run their own instance of this API in order to distribute endpoints and query load.
@@ -1,5 +0,0 @@
{
"ns-api-run-deploy":"Run Instance",
"mainnet":"Mainnet Endpoints",
"sandbox":"Sandbox Endpoints"
}
@@ -1,11 +1,9 @@
import { RedocStandalone } from 'redoc';
import { Callout } from 'nextra/components'
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json](https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json) which is also used to generate the Swagger docs deployed at [https://mainnet-node-status-api.nymtech.cc/swagger/](https://mainnet-node-status-api.nymtech.cc/swagger/).
<br /><br />
<RedocStandalone
specUrl="https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json"
options={{
@@ -1,336 +0,0 @@
import { Callout } from 'nextra/components'
import { AccordionTemplate } from 'components/accordion-template.tsx'
# NS API: Deployment Guide
## Components
The Node Status API is made up of 3 components:
- `nym-node-status-api`
- `nym-node-status-client`
- `nym-node-status-agent`
The API stores its data in a local SQLite database. It periodically gets most of its data from a [NymAPI](./nym-api) instance, and for instances also running with the Gateway Probe, uses the stored topology to conduct performance probe tests.
## Gateway Probe
You can run the `nym-node-status-api` alone, but if you want to run probes for `nym-node`s running as Gateways, you have to also run the `nym-node-status-agent` (which consumes the `nym-node-status-client` lib). The probe will run a set of tests per Gateway node, and return the sort of results seen [here](https://harbourmaster.nymtech.net/gateway/23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb).
## UI
The API exposes a [Swagger](https://swagger.io/docs/) documentation UI by default on port `8000` (see an example [here](https://mainnet-node-status-api.nymtech.cc/swagger/)).
Currently we are not shipping a custom UI component for the Node Status API. The [Harbourmaster](https://harbourmaster.nymtech.net/) frontend consumes this API, amongst other things, but this is an internal repo we use to experiment with new APIs and data on, so it is not public yet.
<Callout type="info">
We invite developers to roll their own UI for their Node Status API instance.
</Callout>
## Docker Images
We will ship Docker images for both the `agent` and `api` in the future. There are Docker images for both in root of each corresponding crate ([`agent`](https://github.com/nymtech/nym/blob/09ea406c02e9a3beebc062f525e4ea1b4222dcbb/nym-node-status-api/nym-node-status-agent/Dockerfile), [`api`](https://github.com/nymtech/nym/blob/develop/nym-node-status-api/nym-node-status-api/Dockerfile)) which are used internally, which could be a starting point for developers to dockerize their instance for the moment.
## Build
### Prerequisites
- Rust
- SQLite
- Get an `ipinfo` key following instructions [here](https://github.com/ipinfo/rust?tab=readme-ov-file#getting-started).
### Compilation
```shell
cargo build --release --package nym-node-status-api --package nym-node-status-agent --package nym-node-status-client
```
## Run
Since the Node Status API depends on both flags and environmental variables, it might be easier to run the binary via a script like the one below - this this script essentially just `source`-s the defined `.env` file after exporting certain binary-specific variables, and then runs the binary. You can find the `.env` files [here](https://github.com/nymtech/nym/tree/master/envs).
<Callout type="info">
All CLI flags are configurable as environmental variables and vice versa, so take the following scripts / setups as guides that you can change however best suits your setup. You can see all definitions [here](https://github.com/nymtech/nym/blob/develop/nym-node-status-api/nym-node-status-api/src/cli/mod.rs#L14).
</Callout>
```bash
#!/bin/bash
set -e
export ENVIRONMENT=${ENVIRONMENT:-"mainnet"} # see nym/envs/ for all possible environments
export NYM_API_CLIENT_TIMEOUT=60
export NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL=120
export IPINFO_API_TOKEN=<YOUR_IPINFO_API_KEY>
monorepo_root=<PATH/TO/NYM/>
set -a
source "${monorepo_root}/envs/${ENVIRONMENT}.env"
echo ${monorepo_root}/envs/${ENVIRONMENT}.env
set +a
export RUST_LOG=${RUST_LOG:-debug} # debug is useful to check everything is working initially, but quite verbose
echo "Verifying environment variables were properly sourced:"
echo "RUST_LOG=${RUST_LOG}"
echo "BECH32_PREFIX=${BECH32_PREFIX}"
echo "NETWORK_NAME=${NETWORK_NAME}"
<PATH/TO/>nym-node-status-api -- --ipinfo-api-token $IPINFO_API_TOKEN
```
### Functionality Without Gateway Probe
Data will be restricted to information that doesn't involve Probe results; `routing` and `config` scores will be `0` and `last_probe` results `null`, as you can see in this snipped output of the `gateways` endpoint:
<AccordionTemplate name="Output">
```shell
{
"gateway_identity_key": "23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb",
"bonded": true,
"performance": 99,
"self_described": {
"authenticator": {
"address": "6Gdtw13Fa46AvkqkHELZZCMKWASDodoJeK9APRNpjjdj.7ji8DDkpjA2AdgwK7wbZm8yi4xZGogGJeypBQt4hAw3P@23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb"
},
"auxiliary_details": {
"accepted_operator_terms_and_conditions": true,
"announce_ports": {
"mix_port": null,
"verloc_port": null
},
"location": null
},
"build_information": {
"binary_name": "nym-node",
"build_timestamp": "2025-02-13T11:49:34.670488195Z",
"build_version": "1.5.0",
"cargo_profile": "release",
"cargo_triple": "x86_64-unknown-linux-gnu",
"commit_branch": "HEAD",
"commit_sha": "a3e19b4563843055b305ea9a397eb1ad84b5c378",
"commit_timestamp": "2025-02-10T18:14:47.000000000+01:00",
"rustc_channel": "stable",
"rustc_version": "1.84.1"
},
"declared_role": {
"entry": true,
"exit_ipr": true,
"exit_nr": true,
"mixnode": false
},
"host_information": {
"hostname": "bwng1.bwnym.xyz",
"ip_address": [
"95.164.2.86"
],
"keys": {
"ed25519": "23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb",
"x25519": "H6pFjqtdSVxkxEQ3wFnuSoobDAUqHx1bYMVJzPZdRByn",
"x25519_noise": null
}
},
"ip_packet_router": {
"address": "7ms2D2uYiTuhX6MKeVL5rz5usgehEoxAAovwYm9nJyBF.3siMjk3wTU7ykaXLNi9c7LpX8yonYKPCA4BQoMwhsfTV@23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb"
},
"last_polled": "2025-03-03 09:48:03.635274187 +00:00:00",
"mixnet_websockets": {
"ws_port": 9000,
"wss_port": 9001
},
"network_requester": {
"address": "HuNL1pFprNSKW6jdqppibXP5KNKCNJxDh7ivpYcoULN9.C62NahRTUf6kqpNtDVHXoVriQr6yyaU5LtxdgpbsGrtA@23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb",
"uses_exit_policy": true
},
"wireguard": {
"port": 51822,
"public_key": "6o8x9GitFjcrkjrJnivWaQCPnxXykQPYLneNr2FEB8Vq"
}
},
"explorer_pretty_bond": {
"identity_key": "23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb",
"location": {
"latitude": 52.5083,
"longitude": 5.475,
"two_letter_iso_country_code": "NL"
},
"owner": "n1cp5gq0apat6c7qmenqp5zjprn2vwvc7jl29j8r",
"pledge_amount": {
"amount": "100000000",
"denom": "unym"
}
},
"description": {
"moniker": "bwn_g1",
"website": "https://bwnym.xyz",
"security_contact": "bwnym@proton.me",
"details": "This gateway is part of the NYM project, which is dedicated to create outstanding privacy software that is legally compliant without sacrificing integrity or having any backdoors."
},
"last_probe_result": null,
"last_probe_log": null,
"last_testrun_utc": null,
"last_updated_utc": "2025-03-03T10:45:48+00:00",
"routing_score": 0.0,
"config_score": 0
},
```
</AccordionTemplate>
If you have already run the API before, make sure to add the following to your script `--database-url "sqlite://node-status-api.sqlite?mode=rwc` so it will continue using the same DB.
### Functionality with Gateway Probe
If you want to enable Gateway node probes and have the NS API store that data, you need to periodically run the `nym-node-status-agent`, authenticated with your `nym-node-status-api` instance. Authentication is to make sure that only the `-agent` you are operating (or others you trust) are submitting data to your API instance.
#### Compile Gateway Probe
The `nym-node-status-agent` is a thin wrapper around the Gateway Probe binary which currently is in the NymVPN repo. `git checkout` to the most recent [release](https://github.com/nymtech/nym-vpn-client/releases), and compile the probe by following the [readme instructions](https://github.com/nymtech/nym-vpn-client/tree/develop/nym-vpn-core/crates/nym-gateway-probe). You will point the `-agent` at this binary when doing Probe testruns.
#### Generate Keypair
```shell
<PATH/TO/>nym-node-status-agent generate-keypair --path <PATH/TO/KEY/FILE/TO/GENERATE>/<KEY_NAME>
# e.g.
# nym-node-status-agent generate-keypair --path ~/.ssh/ns-agent-key
```
You will then want to export the generated `public-key` so its accessible to the `nym-node-status-api` however you are setting your environmental variables, as `NODE_STATUS_API_AGENT_KEY_LIST`:
```bash
export NODE_STATUS_API_AGENT_KEY_LIST=<YOUR_KEY> # e.g. "H4z8kx5Kkf5JNQHfxaE1MwRndjDCD1C7HsVhHTFfBZ4J"
```
In this situation, you are probably only using one key. However, it is possible to set multiple keys as a comma seperated list, in case you wish to whitelist multiple `-agent`s to be able to submit to a single `-api` instance.
#### Run the Node Status Agent
```shell
<PATH/TO/>nym-node-status-agent run-probe --server-address http://127.0.0.1 --server-port 8000 --ns-api-auth-key "<NS_AGENT_PRIVATE_KEY>" --probe-path <PATH/TO/> nym-gateway-probe
```
You will see a lot of output like so:
<AccordionTemplate name="Output">
```shell
listen_port=48586
public_key=3f95caf771b8a63b9bdae6c186f7aba2eae93483f1c82c0243b5e00c85b4ec26
preshared_key=0000000000000000000000000000000000000000000000000000000000000000
protocol_version=1
endpoint=185.186.78.251:51822
last_handshake_time_sec=0
last_handshake_time_nsec=0
tx_bytes=0
rx_bytes=0
persistent_keepalive_interval=0
allowed_ip=0.0.0.0/0
2025/03/03 18:58:28 Pinging nymtech.net seq=0
2025/03/03 18:58:29 Ping latency: 44.503483ms
2025/03/03 18:58:29 Pinging nymtech.net seq=1
2025/03/03 18:58:29 Ping latency: 42.852414ms
2025/03/03 18:58:29 Pinging nymtech.net seq=2
2025/03/03 18:58:29 Ping latency: 43.627256ms
2025/03/03 18:58:29 Pinging nymtech.net seq=3
2025/03/03 18:58:29 Ping latency: 43.638839ms
2025/03/03 18:58:29 Pinging nymtech.net seq=4
2025/03/03 18:58:29 Ping latency: 43.345357ms
2025/03/03 18:58:29 Pinging 1.1.1.1 seq=0
2025/03/03 18:58:29 Ping latency: 46.327233ms
2025/03/03 18:58:34 Pinging 1.1.1.1 seq=1
2025/03/03 18:58:34 Ping latency: 46.273726ms
2025/03/03 18:58:39 Pinging 1.1.1.1 seq=2
2025/03/03 18:58:39 Ping latency: 46.542774ms
2025/03/03 18:58:44 Pinging 1.1.1.1 seq=3
2025/03/03 18:58:44 Ping latency: 45.663545ms
2025/03/03 18:58:49 Pinging 1.1.1.1 seq=4
2025/03/03 18:58:49 Ping latency: 43.803063ms
2025/03/03 18:58:56 Downloaded file content length: 1.00 MB
2025/03/03 18:58:56 Download duration: 1.308072386s
2025/03/03 18:58:56 private_key=1083749e43f4f8fb008f3f7deef9107ef86a68969670ddbb9f07bf85f94fb564
listen_port=39129
public_key=3f95caf771b8a63b9bdae6c186f7aba2eae93483f1c82c0243b5e00c85b4ec26
preshared_key=0000000000000000000000000000000000000000000000000000000000000000
protocol_version=1
endpoint=185.186.78.251:51822
last_handshake_time_sec=0
last_handshake_time_nsec=0
tx_bytes=0
rx_bytes=0
persistent_keepalive_interval=0
allowed_ip=::/0
2025/03/03 18:58:56 Pinging ipv6.google.com seq=0
2025/03/03 18:58:56 Ping latency: 42.839528ms
2025/03/03 18:58:56 Pinging ipv6.google.com seq=1
2025/03/03 18:58:56 Ping latency: 54.844651ms
2025/03/03 18:58:56 Pinging ipv6.google.com seq=2
2025/03/03 18:58:56 Ping latency: 51.23104ms
2025/03/03 18:58:56 Pinging ipv6.google.com seq=3
2025/03/03 18:58:56 Ping latency: 43.320409ms
2025/03/03 18:58:56 Pinging ipv6.google.com seq=4
2025/03/03 18:58:56 Ping latency: 63.517358ms
2025/03/03 18:58:56 Pinging 2001:4860:4860::8888 seq=0
2025/03/03 18:58:56 Ping latency: 54.682534ms
2025/03/03 18:59:01 Pinging 2001:4860:4860::8888 seq=1
2025/03/03 18:59:01 Ping latency: 55.56235ms
2025/03/03 18:59:06 Pinging 2001:4860:4860::8888 seq=2
2025/03/03 18:59:06 Ping latency: 55.970418ms
2025/03/03 18:59:11 Pinging 2001:4860:4860::8888 seq=3
2025/03/03 18:59:14 Failed to send ping: i/o timeout
2025/03/03 18:59:19 Pinging 2001:4860:4860::8888 seq=4
2025/03/03 18:59:22 Failed to send ping: i/o timeout
2025/03/03 18:59:27 Pinging 2606:4700:4700::1111 seq=0
2025/03/03 18:59:27 Ping latency: 45.072616ms
2025/03/03 18:59:32 Pinging 2606:4700:4700::1111 seq=1
2025/03/03 18:59:32 Ping latency: 44.357306ms
2025/03/03 18:59:37 Pinging 2606:4700:4700::1111 seq=2
2025/03/03 18:59:37 Ping latency: 44.013562ms
2025/03/03 18:59:42 Pinging 2606:4700:4700::1111 seq=3
2025/03/03 18:59:42 Ping latency: 46.94342ms
2025/03/03 18:59:47 Pinging 2606:4700:4700::1111 seq=4
2025/03/03 18:59:48 Ping latency: 43.372288ms
2025/03/03 18:59:53 Pinging 2620:fe::fe seq=0
2025/03/03 18:59:53 Ping latency: 42.164952ms
2025/03/03 18:59:58 Pinging 2620:fe::fe seq=1
2025/03/03 18:59:58 Ping latency: 42.295812ms
2025/03/03 19:00:03 Pinging 2620:fe::fe seq=2
2025/03/03 19:00:03 Ping latency: 43.117534ms
2025/03/03 19:00:08 Pinging 2620:fe::fe seq=3
2025/03/03 19:00:08 Ping latency: 44.26068ms
2025/03/03 19:00:13 Pinging 2620:fe::fe seq=4
2025/03/03 19:00:13 Ping latency: 45.29956ms
2025/03/03 19:00:21 Downloaded file content length: 10.00 MB
2025/03/03 19:00:21 Download duration: 3.39529252s
```
</AccordionTemplate>
Whilst you can run the `-agent` directly, it might be easier to run multiple instances in parallel with a script like so:
```bash
#!/bin/bash
set -eu
export ENVIRONMENT=${ENVIRONMENT:-"mainnet"}
probe_git_ref="nym-vpn-core-v1.4.0" # check for the most recent release
monorepo_root=<PATH/TO/NYM/>
set -a
source "${monorepo_root}/envs/${ENVIRONMENT}.env"
set +a
export RUST_LOG="info"
export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1"
export NODE_STATUS_AGENT_SERVER_PORT="8000"
export NODE_STATUS_AGENT_AUTH_KEY=<NS_AGENT_PRIVATE_KEY>
export NODE_STATUS_AGENT_PROBE_EXTRA_ARGS="netstack-download-timeout-sec=30,netstack-num-ping=2,netstack-send-timeout-sec=1,netstack-recv-timeout-sec=1"
workers=${1:-1}
echo "Running $workers workers in parallel"
function swarm() {
local workers=$1
for ((i = 1; i <= workers; i++)); do
${monorepo_root}/target/release/nym-node-status-agent run-probe --probe-path ~/<PATH/TO>/nym-vpn-client/nym-vpn-core/target/debug/nym-gateway-probe &
done
wait
echo "All agents completed"
}
swarm $workers
```
And run specifying the number of workers with `./<SCRIPT_NAME>.sh <NUMBER_OF_WORKERS>`.
<Callout type="info">
When running the probe, use logging level `RUST_LOG=info`. The Node Status API relies on that granularity for parsing probe results, and the logs grow incessantly if a lower level (e.g. `DEBUG`) is used.
</Callout>
### Ports
By default the API listens on `8000`, so you will need to configure this post to be reachable on your remote server. You can modify this with the `--http_port` flag.
@@ -1,3 +0,0 @@
{
"mainnet":"Mainnet Endpoints"
}
@@ -52,22 +52,21 @@ This page displays a full list of all the changes during our release cycle from
Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch.
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.4-dorina-patched)
- [`nym-node`](nodes/nym-node.mdx) version `1.6.2`
- [`nym-node`](nodes/nym-node.mdx) version `1.6.1`
```shell
nym-node
Binary Name: nym-node
Build Timestamp: 2025-03-06T20:32:36.922212778Z
Build Version: 1.6.2
Commit SHA: 247ebb7c4339de0a298a7fcb2574122a8306c3b8
Commit Date: 2025-03-06T21:26:16.000000000+01:00
Build Timestamp: 2025-03-06T14:28:21.408539599Z
Build Version: 1.6.1
Commit SHA: 1fb2ebad7ad1bad455d5b896ea14204211727417
Commit Date: 2025-03-06T15:26:18.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.85.0
rustc Channel: stable
cargo Profile: release
```
- Use legacy crypto for constructing SURB headers ([#5579])
- Bugfix: make sure to correctly decode response content when putting it into error message ([#5571])
- Tweak surb management to be more conservative ([#5570])
- Deserialize v5 authenticator requests ([#5568])
@@ -77,7 +76,6 @@ cargo Profile: release
- Feature: v2 authentication request (#5537) ([#5563])
- Create authenticator v5 request/response types ([#5561])
[#5579]: https://github.com/nymtech/nym/pull/5579
[#5571]: https://github.com/nymtech/nym/pull/5571
[#5570]: https://github.com/nymtech/nym/pull/5570
[#5568]: https://github.com/nymtech/nym/pull/5568
@@ -20,10 +20,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-03-06T20:32:36.922212778Z
Build Version: 1.6.2
Commit SHA: 247ebb7c4339de0a298a7fcb2574122a8306c3b8
Commit Date: 2025-03-06T21:26:16.000000000+01:00
Build Timestamp: 2025-03-06T14:28:21.408539599Z
Build Version: 1.6.1
Commit SHA: 1fb2ebad7ad1bad455d5b896ea14204211727417
Commit Date: 2025-03-06T15:26:18.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.85.0
rustc Channel: stable
@@ -3,9 +3,8 @@ import { VarInfo } from 'components/variable-info.tsx';
import { Steps } from 'nextra/components';import { Tabs } from 'nextra/components';
import { MyTab } from 'components/generic-tabs.tsx';
import PortsNymNode from 'components/operators/snippets/ports-nym-node.mdx';
import PortsValidator from 'components/operators/snippets/ports-validator.mdx';
import NymNodeSpecs from 'components/operators/snippets/nym-node-specs.mdx';
import NTPSync from 'components/operators/snippets/ntp-time-sync.mdx'
import PortsValidator from 'components/operators/snippets/ports-validator.mdx'
import NymNodeSpecs from 'components/operators/snippets/nym-node-specs.mdx'
# VPS Setup & Configuration
@@ -43,18 +42,14 @@ apt update -y && apt --fix-broken install
```
- Install dependencies
```sh
apt -y install ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git ntp ntpdate
apt -y install ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git
```
- Double check ufw is installed correctly
```sh
apt install ufw --fix-missing
```
###### 2. Synchronize time of your server
<NTPSync />
###### 3. Configure your firewall using Uncomplicated Firewall (UFW)
###### 2. Configure your firewall using Uncomplicated Firewall (UFW)
For a `nym-node` or Nyx validator to recieve traffic, you need to open ports on the server. The following commands will allow you to set up a firewall using `ufw`.
@@ -75,7 +70,7 @@ ufw enable
ufw status
```
###### 4. Open all needed ports to have your firewall for `nym-node` working correctly
###### 3. Open all needed ports to have your firewall for `nym-node` working correctly
<div>
<Tabs items={[
-6
View File
@@ -1,6 +0,0 @@
# Nym Node Status API
The Node Status API serves information about individual `nym-nodes` in the Mixnet, such as which role they are operating in, statistics about them, services such as Network Requesters, as well as summaries of the state of the Mixnet.
We recommend that developers building applications such as explorers or analytics interfaces about the Mixnet run their own instance of the API, in order to promote a robust network of downstream services, and spread the load of API calls amongst as many endpoints as possible.
You can find build and operation instructions in the [docs](https://nym.com/docs/apis/ns-api).
@@ -10,8 +10,7 @@ pub(crate) fn generate_key_pair(path: impl AsRef<Path>) -> anyhow::Result<()> {
let mut private_key_file = File::create(priv_key_path)?;
private_key_file.write_all(keypair.private_key().to_base58_string().as_bytes())?;
let pub_key_path = priv_key_path.with_file_name("public-key");
let pub_key_path = priv_key_path.with_extension("public");
let mut public_key_file = File::create(&pub_key_path)?;
public_key_file.write_all(keypair.public_key().to_base58_string().as_bytes())?;
@@ -1,4 +1,4 @@
use tracing::{debug, error, info};
use tracing::error;
pub(crate) struct GwProbe {
path: String,
@@ -10,62 +10,21 @@ impl GwProbe {
}
pub(crate) async fn version(&self) -> String {
debug!("Attempting to execute binary at: {}", &self.path);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
match tokio::fs::metadata(&self.path).await {
Ok(metadata) => {
let perms = metadata.permissions();
let mode = perms.mode();
if mode & 0o111 == 0 {
error!(
"Binary is not executable: {} (mode: {:o})",
&self.path, mode
);
return "Binary is not executable".to_string();
}
debug!("Binary exists with permissions: {:o}", mode);
}
Err(e) => {
error!("Failed to stat binary at {}: {}", &self.path, e);
return format!("Failed to access binary: {}", e);
}
}
}
let mut command = tokio::process::Command::new(&self.path);
command.stdout(std::process::Stdio::piped());
command.arg("--version");
info!("Executing command: {:?} --version", &self.path);
match command.spawn() {
Ok(child) => match child.wait_with_output().await {
Ok(output) => {
if output.status.success() {
String::from_utf8(output.stdout)
.unwrap_or_else(|_| "Unable to parse version output".to_string())
} else {
let stderr = String::from_utf8(output.stderr)
.unwrap_or_else(|_| "Unable to parse error output".to_string());
error!(
"Command failed with exit code {}: {}",
output.status.code().unwrap_or(-1),
stderr
);
format!("Command failed: {}", stderr)
}
Ok(child) => {
if let Ok(output) = child.wait_with_output().await {
return String::from_utf8(output.stdout)
.unwrap_or("Unable to get log from test run".to_string());
}
Err(e) => {
error!("Failed to get command output: {}", e);
format!("Failed to get command output: {}", e)
}
},
"Unable to get probe version".to_string()
}
Err(e) => {
error!("Failed to spawn process: {}", e);
format!("Failed to spawn process: {}", e)
error!("Failed to get probe version: {}", e);
"Failed to get probe version".to_string()
}
}
}
@@ -296,14 +296,9 @@ impl Monitor {
Some(location) => return location,
None => {
for ip in node.description.host_information.ip_address.iter() {
match self.ipinfo.locate_ip(ip.to_string()).await {
Ok(location) => {
self.geocache.insert(node_id, location.clone()).await;
return location;
}
Err(err) => {
tracing::warn!("Couldn't locate IP {} due to: {}", ip, err)
}
if let Ok(location) = self.ipinfo.locate_ip(ip.to_string()).await {
self.geocache.insert(node_id, location.clone()).await;
return location;
}
}
// if no data could be retrieved
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-node"
version = "1.6.2"
version = "1.6.1"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
+2 -8
View File
@@ -50,10 +50,6 @@ pub struct Debug {
/// The maximum number of client connections the gateway will keep open at once.
pub maximum_open_connections: usize,
/// Specifies the minimum performance of mixnodes in the network that are to be used in internal topologies
/// of the services providers
pub minimum_mix_performance: u8,
/// Defines the maximum age of a signed authentication request before it's deemed too stale to process.
pub maximum_auth_request_age: Duration,
@@ -65,10 +61,9 @@ pub struct Debug {
}
impl Debug {
pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100;
pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50;
pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30);
pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192;
pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100;
pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30);
}
impl Default for Debug {
@@ -77,7 +72,6 @@ impl Default for Debug {
message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT,
maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS,
maximum_auth_request_age: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_AGE,
minimum_mix_performance: Self::DEFAULT_MINIMUM_MIX_PERFORMANCE,
stale_messages: Default::default(),
client_bandwidth: Default::default(),
zk_nym_tickets: Default::default(),
+16 -17
View File
@@ -1583,9 +1583,9 @@ dependencies = [
[[package]]
name = "ff"
version = "0.13.1"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"rand_core 0.6.4",
"subtle",
@@ -3557,7 +3557,6 @@ name = "nym-pemstore"
version = "0.3.0"
dependencies = [
"pem",
"tracing",
]
[[package]]
@@ -5063,18 +5062,18 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.26"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
@@ -5090,18 +5089,18 @@ dependencies = [
[[package]]
name = "serde_bytes"
version = "0.11.17"
version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96"
checksum = "364fec0df39c49a083c9a8a18a23a6bcfd9af130fe9fe321d18520a0d113e09e"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@@ -6056,9 +6055,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.39"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa 1.0.9",
@@ -6073,15 +6072,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.20"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO watcher_execution(start, end, error_message)\n VALUES (?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "1aa7733ad4bbf3e6b8db909b8646bee247bc021b9534f1d4b0fcad32e2e56218"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, tx_hash, height, message_index, sender, recipient, amount, memo, created_at as \"created_at: ::time::OffsetDateTime\"\n FROM transactions\n WHERE height > ?\n ORDER BY height ASC, message_index ASC\n ",
"query": "\n SELECT * FROM transactions\n WHERE height > ? \n ORDER BY height ASC, message_index ASC\n ",
"describe": {
"columns": [
{
@@ -44,7 +44,7 @@
"type_info": "Text"
},
{
"name": "created_at: ::time::OffsetDateTime",
"name": "created_at",
"ordinal": 8,
"type_info": "Datetime"
}
@@ -64,5 +64,5 @@
true
]
},
"hash": "f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08"
"hash": "7b9abf4ff422b8d7a942955dc4fba380e7d5f0127f4745705b8ac9af6c170d19"
}
+2 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nyx-chain-watcher"
version = "0.1.13"
version = "0.1.11"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -29,6 +29,7 @@ nym-node-requests = { path = "../nym-node/nym-node-requests", features = [
nym-validator-client = { path = "../common/client-libs/validator-client" }
nyxd-scraper = { path = "../common/nyxd-scraper" }
reqwest = { workspace = true, features = ["rustls-tls"] }
rocket = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -1,11 +0,0 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
CREATE TABLE watcher_execution
(
start TIMESTAMP WITHOUT TIME ZONE NOT NULL,
end TIMESTAMP WITHOUT TIME ZONE NOT NULL,
error_message TEXT
)
+16 -12
View File
@@ -1,4 +1,4 @@
use crate::config::PaymentWatchersConfig;
use crate::config::PaymentWatcherConfig;
use crate::env::vars::{
NYXD_SCRAPER_START_HEIGHT, NYXD_SCRAPER_UNSAFE_NUKE_DB,
NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT,
@@ -10,7 +10,7 @@ use nyxd_scraper::{
};
use sqlx::SqlitePool;
use std::fs;
use tracing::{error, info, warn};
use tracing::{info, warn};
pub(crate) async fn run_chain_scraper(
config: &crate::config::Config,
@@ -60,7 +60,7 @@ pub(crate) async fn run_chain_scraper(
})
.with_tx_module(EventScraperModule::new(
db_pool,
config.payment_watcher_config.clone(),
config.payment_watcher_config.clone().unwrap_or_default(),
));
let instance = scraper.build_and_start().await?;
@@ -73,11 +73,11 @@ pub(crate) async fn run_chain_scraper(
pub struct EventScraperModule {
db_pool: SqlitePool,
payment_config: PaymentWatchersConfig,
payment_config: PaymentWatcherConfig,
}
impl EventScraperModule {
pub fn new(db_pool: SqlitePool, payment_config: PaymentWatchersConfig) -> Self {
pub fn new(db_pool: SqlitePool, payment_config: PaymentWatcherConfig) -> Self {
Self {
db_pool,
payment_config,
@@ -132,12 +132,6 @@ impl TxModule for EventScraperModule {
return Ok(());
}
if tx.tx.body.messages.len() > 1 {
error!(
"this transaction has more than 1 message in it - payment information will be lost"
);
}
// Process each event
for event in events {
// Only process transfer events
@@ -163,7 +157,17 @@ impl TxModule for EventScraperModule {
// If we have all required fields, check if recipient is watched and store
if let (Some(recipient), Some(sender), Some(amount)) = (recipient, sender, amount) {
// Check if any watcher is watching this recipient
let is_watched = self.payment_config.is_being_watched(&recipient);
let is_watched = self.payment_config.watchers.iter().any(|watcher| {
if let Some(watched_accounts) =
&watcher.watch_for_transfer_recipient_accounts
{
watched_accounts
.iter()
.any(|account| account.to_string() == recipient)
} else {
false
}
});
if is_watched {
if let Err(e) = self
+8 -8
View File
@@ -3,8 +3,8 @@
use crate::cli::DEFAULT_NYX_CHAIN_WATCHER_ID;
use crate::config::payments_watcher::HttpAuthenticationOptions::AuthorizationBearerToken;
use crate::config::payments_watcher::PaymentWatcherConfig;
use crate::config::{default_config_filepath, Config, ConfigBuilder, PaymentWatchersConfig};
use crate::config::payments_watcher::PaymentWatcherEntry;
use crate::config::{default_config_filepath, Config, ConfigBuilder, PaymentWatcherConfig};
use crate::error::NyxChainWatcherError;
use nym_config::save_unformatted_config_to_file;
use nym_validator_client::nyxd::AccountId;
@@ -18,22 +18,22 @@ pub(crate) async fn execute(_args: Args) -> Result<(), NyxChainWatcherError> {
let data_dir = Config::default_data_directory(&config_path)?;
let builder = ConfigBuilder::new(config_path.clone(), data_dir).with_payment_watcher_config(
PaymentWatchersConfig {
watchers: vec![PaymentWatcherConfig {
PaymentWatcherConfig {
watchers: vec![PaymentWatcherEntry {
id: DEFAULT_NYX_CHAIN_WATCHER_ID.to_string(),
webhook_url: "https://webhook.site".to_string(),
watch_for_transfer_recipient_accounts: vec![AccountId::from_str(
watch_for_transfer_recipient_accounts: Some(vec![AccountId::from_str(
"n17g9a2pwwkg8m60wf59pq6mv0c2wusg9ukparkz",
)
.unwrap()],
.unwrap()]),
authentication: Some(AuthorizationBearerToken {
token: "1234".to_string(),
}),
description: None,
watch_for_chain_message_types: vec![
watch_for_chain_message_types: Some(vec![
"/cosmos.bank.v1beta1.MsgSend".to_string(),
"/ibc.applications.transfer.v1.MsgTransfer".to_string(),
],
]),
}],
},
);
@@ -20,7 +20,7 @@ pub(crate) struct Args {
value_delimiter = ',',
env = NYX_CHAIN_WATCHER_WATCH_ACCOUNTS
)]
pub watch_for_transfer_recipient_accounts: Vec<AccountId>,
pub watch_for_transfer_recipient_accounts: Option<Vec<AccountId>>,
/// (Override) Watch for chain messages of these types
#[clap(
@@ -28,7 +28,7 @@ pub(crate) struct Args {
value_delimiter = ',',
env = NYX_CHAIN_WATCHER_WATCH_CHAIN_MESSAGE_TYPES
)]
pub watch_for_chain_message_types: Vec<String>,
pub watch_for_chain_message_types: Option<Vec<String>>,
/// (Override) The webhook to call when we find something
#[clap(
@@ -1,7 +1,7 @@
use crate::cli::commands::run::args::Args;
use crate::cli::DEFAULT_NYX_CHAIN_WATCHER_ID;
use crate::config::payments_watcher::{HttpAuthenticationOptions, PaymentWatcherConfig};
use crate::config::{default_config_filepath, Config, ConfigBuilder, PaymentWatchersConfig};
use crate::config::payments_watcher::{HttpAuthenticationOptions, PaymentWatcherEntry};
use crate::config::{default_config_filepath, Config, ConfigBuilder, PaymentWatcherConfig};
use crate::error::NyxChainWatcherError;
use tracing::{info, warn};
@@ -18,8 +18,8 @@ pub(crate) fn get_run_config(args: Args) -> Result<Config, NyxChainWatcherError>
} = args;
// if there are no args set, then try load the config
if args.watch_for_transfer_recipient_accounts.is_empty()
&& args.watch_for_transfer_recipient_accounts.is_empty()
if args.watch_for_transfer_recipient_accounts.is_none()
&& args.watch_for_transfer_recipient_accounts.is_none()
&& args.chain_watcher_db_path.is_none()
{
info!("Loading default config file...");
@@ -27,12 +27,12 @@ pub(crate) fn get_run_config(args: Args) -> Result<Config, NyxChainWatcherError>
}
// set default messages
if watch_for_chain_message_types.is_empty() {
watch_for_chain_message_types = vec!["/cosmos.bank.v1beta1.MsgSend".to_string()];
if watch_for_chain_message_types.is_none() {
watch_for_chain_message_types = Some(vec!["/cosmos.bank.v1beta1.MsgSend".to_string()]);
}
// warn if no accounts set
if watch_for_transfer_recipient_accounts.is_empty() {
if watch_for_transfer_recipient_accounts.is_none() {
warn!(
"You did not specify any accounts to watch in {}. Only chain data will be stored.",
crate::env::vars::NYX_CHAIN_WATCHER_WATCH_ACCOUNTS
@@ -58,8 +58,8 @@ pub(crate) fn get_run_config(args: Args) -> Result<Config, NyxChainWatcherError>
let authentication =
webhook_auth.map(|token| HttpAuthenticationOptions::AuthorizationBearerToken { token });
let watcher_config = PaymentWatchersConfig {
watchers: vec![PaymentWatcherConfig {
let watcher_config = PaymentWatcherConfig {
watchers: vec![PaymentWatcherEntry {
id: DEFAULT_NYX_CHAIN_WATCHER_ID.to_string(),
description: None,
watch_for_transfer_recipient_accounts: watch_for_transfer_recipient_accounts
+39 -191
View File
@@ -2,114 +2,19 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::NyxChainWatcherError;
use anyhow::Context;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::task::{JoinHandle, JoinSet};
use tokio_util::sync::CancellationToken;
use tracing::{error, info};
use tokio::join;
use tracing::{error, info, trace};
mod args;
mod config;
use crate::chain_scraper::run_chain_scraper;
use crate::db::DbPool;
use crate::http::state::PaymentListenerState;
use crate::payment_listener::PaymentListener;
use crate::{db, http, price_scraper};
use crate::{db, http, payment_listener, price_scraper};
pub(crate) use args::Args;
use nym_task::signal::wait_for_signal;
async fn try_insert_watcher_execution_information(
db_pool: DbPool,
start: OffsetDateTime,
end: OffsetDateTime,
error_message: Option<String>,
) {
let _ = sqlx::query!(
r#"
INSERT INTO watcher_execution(start, end, error_message)
VALUES (?, ?, ?)
"#,
start,
end,
error_message
)
.execute(&db_pool)
.await
.inspect_err(|err| error!("failed to insert run information: {err}"));
}
async fn wait_for_shutdown(
db_pool: DbPool,
start: OffsetDateTime,
main_cancellation_token: CancellationToken,
scraper_cancellation_token: CancellationToken,
mut tasks: JoinSet<Option<anyhow::Result<()>>>,
) {
async fn finalize_shutdown(
db_pool: DbPool,
start: OffsetDateTime,
main_cancellation_token: CancellationToken,
scraper_cancellation_token: CancellationToken,
mut tasks: JoinSet<Option<anyhow::Result<()>>>,
error_message: Option<String>,
) {
// cancel all tasks
main_cancellation_token.cancel();
scraper_cancellation_token.cancel();
// stupid nasty and hacky workaround to make sure all relevant tasks have finished before hard aborting them
// nasty stupid and hacky workaround
tokio::time::sleep(Duration::from_secs(1)).await;
tasks.abort_all();
// insert execution result into the db
try_insert_watcher_execution_information(
db_pool,
start,
OffsetDateTime::now_utc(),
error_message,
)
.await
}
tokio::select! {
// graceful shutdown
_ = wait_for_signal() => {
info!("received shutdown signal");
finalize_shutdown(db_pool, start, main_cancellation_token, scraper_cancellation_token, tasks, None).await;
}
_ = scraper_cancellation_token.cancelled() => {
info!("the scraper has issued cancellation");
finalize_shutdown(db_pool, start, main_cancellation_token, scraper_cancellation_token, tasks, Some("unexpected scraper task cancellation".into())).await;
}
_ = main_cancellation_token.cancelled() => {
info!("one of the tasks has cancelled the token");
finalize_shutdown(db_pool, start, main_cancellation_token, scraper_cancellation_token, tasks, Some("unexpected main task cancellation".into())).await;
}
task_result = tasks.join_next() => {
// the first unwrap is fine => join set was not empty
let error_message = match task_result.unwrap() {
Err(_join_err) => Some("unexpected join error".to_string()),
Ok(Some(Ok(_))) => None,
Ok(Some(Err(err))) => Some(err.to_string()),
Ok(None) => {
Some("unexpected task cancellation".to_string())
}
};
error!("unexpected task termination: {error_message:?}");
finalize_shutdown(db_pool, start, main_cancellation_token, scraper_cancellation_token, tasks, error_message).await;
}
}
}
pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWatcherError> {
let start = OffsetDateTime::now_utc();
info!("passed arguments: {args:#?}");
trace!("passed arguments: {args:#?}");
let config = config::get_run_config(args)?;
@@ -124,7 +29,9 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa
);
info!(
"Chain History Database path is {:?}",
std::path::Path::new(&config.chain_scraper_database_path()).canonicalize()
std::path::Path::new(&config.chain_scraper_database_path())
.canonicalize()
.unwrap_or_default()
);
// Ensure parent directory exists
@@ -134,109 +41,50 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa
let connection_url = format!("sqlite://{}?mode=rwc", db_path);
let storage = db::Storage::init(connection_url).await?;
let watcher_pool = storage.pool_owned();
let watcher_pool = storage.pool_owned().await;
let mut tasks = JoinSet::new();
let cancellation_token = CancellationToken::new();
// Spawn the chain scraper and get its storage
let price_scraper_pool = storage.pool_owned();
let scraper_pool = storage.pool_owned();
let shutdown_pool = storage.pool_owned();
// Spawn the payment listener task
let payment_listener_handle = tokio::spawn({
let price_scraper_pool = storage.pool_owned().await;
let scraper_pool = storage.pool_owned().await;
run_chain_scraper(&config, scraper_pool).await?;
let payment_watcher_config = config.payment_watcher_config.unwrap_or_default();
// construct shared state
let payment_listener_shared_state = PaymentListenerState::new();
// spawn all the tasks
// 1. chain scraper (note: this doesn't really spawn the full scraper on this task, but we don't want to be blocking waiting for its startup)
let scraper_token_handle: JoinHandle<anyhow::Result<CancellationToken>> = tokio::spawn({
let config = config.clone();
async move {
// this only blocks until startup sync is done; it then runs on its own set of tasks
let scraper = run_chain_scraper(&config, scraper_pool).await?;
Ok(scraper.cancel_token())
if let Err(e) =
payment_listener::run_payment_listener(payment_watcher_config, price_scraper_pool)
.await
{
error!("Payment listener error: {}", e);
}
Ok::<_, anyhow::Error>(())
}
});
// 2. payment listener
let token = cancellation_token.clone();
let payment_watcher_config = config.payment_watcher_config.clone();
let payment_listener = PaymentListener::new(
price_scraper_pool,
payment_watcher_config,
payment_listener_shared_state.clone(),
)?;
{
tasks.spawn(async move {
token
.run_until_cancelled(async move {
payment_listener.run().await;
Ok(())
})
.await
});
}
// Clone pool for each task that needs it
//let background_pool = db_pool.clone();
// 3. price scraper (note, this task never terminates on its own)
{
let token = cancellation_token.clone();
tasks.spawn(async move {
token
.run_until_cancelled(async move {
price_scraper::run_price_scraper(&watcher_pool).await;
Ok(())
})
.await
});
}
let price_scraper_handle = tokio::spawn(async move {
price_scraper::run_price_scraper(&watcher_pool).await;
});
// 4. http api
let http_server = http::server::build_http_api(
storage.pool_owned(),
&config,
http_port,
payment_listener_shared_state,
)
.await?;
{
let token = cancellation_token.clone();
tasks.spawn(async move {
info!("Starting HTTP server on port {http_port}",);
async move {
Some(
http_server
.run(token.cancelled_owned())
.await
.context("http server failure"),
)
}
.await
});
}
let shutdown_handles = http::server::start_http_api(storage.pool_owned().await, http_port)
.await
.expect("Failed to start server");
// 1. wait for either shutdown or scraper having finished startup
tokio::select! {
_ = wait_for_signal() => {
info!("received shutdown signal while waiting for scraper to finish its startup");
return Ok(())
}
scraper_token = scraper_token_handle => {
let scraper_token = match scraper_token {
Ok(Ok(token)) => token,
Ok(Err(startup_err)) => {
error!("failed to startup the chain scraper: {startup_err}");
return Err(startup_err.into());
}
Err(runtime_err) => {
error!("failed to finish the scraper startup task: {runtime_err}");
return Ok(())
info!("Started HTTP server on port {}", http_port);
}
};
// Wait for the short-lived tasks to complete
let _ = join!(price_scraper_handle, payment_listener_handle);
wait_for_shutdown(shutdown_pool, start, cancellation_token, scraper_token, tasks).await
}
}
// Wait for a signal to terminate the long-running task
wait_for_signal().await;
if let Err(err) = shutdown_handles.shutdown().await {
error!("{err}");
};
Ok(())
}
+5 -6
View File
@@ -14,7 +14,7 @@ use tracing::{debug, error};
pub(crate) mod payments_watcher;
mod template;
pub use crate::config::payments_watcher::PaymentWatchersConfig;
pub use crate::config::payments_watcher::PaymentWatcherConfig;
use crate::error::NyxChainWatcherError;
const DEFAULT_NYM_CHAIN_WATCHER_DIR: &str = "nym-chain-watcher";
@@ -46,7 +46,7 @@ pub struct ConfigBuilder {
pub chain_scraper_db_path: Option<String>,
pub payment_watcher_config: Option<PaymentWatchersConfig>,
pub payment_watcher_config: Option<PaymentWatcherConfig>,
pub logging: Option<LoggingSettings>,
}
@@ -76,7 +76,7 @@ impl ConfigBuilder {
#[allow(dead_code)]
pub fn with_payment_watcher_config(
mut self,
payment_watcher_config: impl Into<PaymentWatchersConfig>,
payment_watcher_config: impl Into<PaymentWatcherConfig>,
) -> Self {
self.payment_watcher_config = Some(payment_watcher_config.into());
self
@@ -92,7 +92,7 @@ impl ConfigBuilder {
Config {
logging: self.logging.unwrap_or_default(),
save_path: Some(self.config_path),
payment_watcher_config: self.payment_watcher_config.unwrap_or_default(),
payment_watcher_config: self.payment_watcher_config,
data_dir: self.data_dir,
db_path: self.db_path,
chain_scraper_db_path: self.chain_scraper_db_path,
@@ -116,8 +116,7 @@ pub struct Config {
#[serde(skip)]
chain_scraper_db_path: Option<String>,
#[serde(default)]
pub payment_watcher_config: PaymentWatchersConfig,
pub payment_watcher_config: Option<PaymentWatcherConfig>,
#[serde(default)]
pub logging: LoggingSettings,
@@ -1,29 +1,19 @@
use nym_validator_client::nyxd::AccountId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct PaymentWatchersConfig {
pub watchers: Vec<PaymentWatcherConfig>,
}
impl PaymentWatchersConfig {
pub fn is_being_watched(&self, account: &str) -> bool {
self.watchers.iter().any(|watcher| {
watcher
.watch_for_transfer_recipient_accounts
.iter()
.any(|acc| acc.as_ref() == account)
})
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PaymentWatcherConfig {
pub watchers: Vec<PaymentWatcherEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentWatcherConfig {
pub struct PaymentWatcherEntry {
pub id: String,
pub description: Option<String>,
pub webhook_url: String,
pub watch_for_transfer_recipient_accounts: Vec<AccountId>,
pub watch_for_chain_message_types: Vec<String>,
pub watch_for_transfer_recipient_accounts: Option<Vec<AccountId>>,
pub watch_for_chain_message_types: Option<Vec<String>>,
pub authentication: Option<HttpAuthenticationOptions>,
}
+1 -1
View File
@@ -34,7 +34,7 @@ impl Storage {
}
/// Cloning pool is cheap, it's the same underlying set of connections
pub fn pool_owned(&self) -> DbPool {
pub async fn pool_owned(&self) -> DbPool {
self.pool.clone()
}
}
-26
View File
@@ -1,8 +1,4 @@
use anyhow::Context;
use nym_validator_client::nyxd::Coin;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use time::OffsetDateTime;
use utoipa::ToSchema;
#[derive(Clone, Deserialize, Debug, ToSchema)]
@@ -45,25 +41,3 @@ pub(crate) struct PaymentRecord {
pub(crate) timestamp: i64,
pub(crate) height: i64,
}
#[derive(Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct Transaction {
pub(crate) id: i64,
pub(crate) tx_hash: String,
pub(crate) height: i64,
pub(crate) message_index: i64,
pub(crate) sender: String,
pub(crate) recipient: String,
pub(crate) amount: String,
pub(crate) memo: Option<String>,
pub(crate) created_at: Option<OffsetDateTime>,
}
impl Transaction {
pub(crate) fn funds(&self) -> anyhow::Result<Coin> {
self.amount
.as_str()
.parse()
.context("failed to parse transaction amount")
}
}
-55
View File
@@ -1,55 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use serde::{Deserialize, Serialize};
use std::collections::vec_deque::{IntoIter, Iter};
use std::collections::VecDeque;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct RingBuffer<T> {
#[serde(flatten)]
inner: VecDeque<T>,
}
impl<T> RingBuffer<T> {
pub fn new(capacity: usize) -> Self {
Self {
inner: VecDeque::with_capacity(capacity),
}
}
pub fn push(&mut self, item: T) {
if self.inner.len() == self.inner.capacity() {
self.inner.pop_front();
self.inner.push_back(item);
debug_assert!(self.inner.len() == self.inner.capacity());
} else {
self.inner.push_back(item);
}
}
pub fn iter(&self) -> Iter<'_, T> {
self.inner.iter()
}
}
impl<T> From<RingBuffer<T>> for VecDeque<T> {
fn from(value: RingBuffer<T>) -> Self {
value.inner
}
}
impl<T> From<RingBuffer<T>> for Vec<T> {
fn from(value: RingBuffer<T>) -> Self {
value.inner.into()
}
}
impl<T> IntoIterator for RingBuffer<T> {
type Item = T;
type IntoIter = IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.inner.into_iter()
}
}
+2 -8
View File
@@ -8,7 +8,6 @@ use utoipa_swagger_ui::SwaggerUi;
use crate::http::{api_docs, server::HttpServer, state::AppState};
pub(crate) mod price;
pub(crate) mod status;
pub(crate) mod watcher;
pub(crate) struct RouterBuilder {
@@ -26,13 +25,8 @@ impl RouterBuilder {
"/",
axum::routing::get(|| async { Redirect::permanent("/swagger") }),
)
.nest(
"/v1",
Router::new()
.nest("/status", status::routes())
.nest("/price", price::routes())
.nest("/watcher", watcher::routes()),
);
.nest("/v1", Router::new().nest("/price", price::routes()))
.nest("/v1", Router::new().nest("/watcher", watcher::routes()));
Self {
unfinished_router: router,
-98
View File
@@ -1,98 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::http::models::status::{
ActivePaymentWatchersResponse, PaymentListenerFailureDetails, PaymentListenerStatusResponse,
ProcessedPayment, WatcherFailureDetails, WatcherState,
};
use crate::http::state::{AppState, PaymentListenerState};
use axum::extract::State;
use axum::routing::get;
use axum::{Json, Router};
use std::ops::Deref;
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/active-payment-watchers", get(active_payment_watchers))
.route("/payment-listener", get(payment_listener_status))
}
#[utoipa::path(
tag = "Status",
get,
path = "/active-payment-watchers",
context_path = "/v1/status",
responses(
(status = 200, body = ActivePaymentWatchersResponse)
)
)]
pub(crate) async fn active_payment_watchers(
State(state): State<AppState>,
) -> Json<ActivePaymentWatchersResponse> {
Json(ActivePaymentWatchersResponse {
watchers: state.registered_payment_watchers.deref().clone(),
})
}
#[utoipa::path(
tag = "Status",
get,
path = "/payment-listener",
context_path = "/v1/status",
responses(
(status = 200, body = PaymentListenerStatusResponse)
)
)]
pub(crate) async fn payment_listener_status(
State(state): State<PaymentListenerState>,
) -> Json<PaymentListenerStatusResponse> {
let guard = state.inner.read().await;
// sorry for the nasty conversion code here, run out of time : (
Json(PaymentListenerStatusResponse {
last_checked: guard.last_checked,
processed_payments_since_startup: guard.processed_payments_since_startup,
watcher_errors_since_startup: guard.watcher_errors_since_startup,
payment_listener_errors_since_startup: guard.payment_listener_errors_since_startup,
last_processed_payment: guard
.last_processed_payment
.as_ref()
.map(|p| ProcessedPayment {
processed_at: p.processed_at,
tx_hash: p.tx_hash.clone(),
message_index: p.message_index,
height: p.height,
sender: p.sender.clone(),
receiver: p.receiver.clone(),
funds: p.funds.clone(),
memo: p.memo.clone(),
}),
latest_failures: guard
.latest_failures
.iter()
.map(|f| PaymentListenerFailureDetails {
timestamp: f.timestamp,
error: f.error.clone(),
})
.collect(),
watchers: guard
.watchers
.iter()
.map(|(w, state)| {
(
w.clone(),
WatcherState {
latest_failures: state
.latest_failures
.iter()
.map(|f| WatcherFailureDetails {
timestamp: f.timestamp,
error: f.error.clone(),
})
.collect(),
},
)
})
.collect(),
})
}
+29 -3
View File
@@ -1,7 +1,9 @@
use crate::config::Config;
use crate::env;
use crate::http::error::HttpResult;
use crate::http::state::AppState;
use axum::extract::State;
use axum::{Json, Router};
use std::env::var;
pub(crate) fn routes() -> Router<AppState> {
Router::new().route("/addresses", axum::routing::get(get_addresses))
@@ -17,6 +19,30 @@ pub(crate) fn routes() -> Router<AppState> {
)]
/// Fetch the addresses being watched by the chain watcher
async fn get_addresses(State(state): State<AppState>) -> HttpResult<Json<Vec<String>>> {
Ok(Json(state.watched_accounts()))
async fn get_addresses() -> HttpResult<Json<Vec<String>>> {
let addresses = match Config::read_from_toml_file_in_default_location() {
Ok(config) => config
.payment_watcher_config
.as_ref()
.and_then(|config| {
config.watchers.iter().find_map(|watcher| {
watcher
.watch_for_transfer_recipient_accounts
.as_ref()
.map(|accounts| {
accounts
.iter()
.map(|account| account.to_string())
.collect::<Vec<_>>()
})
})
})
.unwrap_or_default(),
// If the config file doesn't exist, fall back to env variable
Err(_) => var(env::vars::NYX_CHAIN_WATCHER_WATCH_ACCOUNTS)
.map(|accounts| accounts.split(',').map(String::from).collect())
.unwrap_or_default(),
};
Ok(Json(addresses))
}
-1
View File
@@ -1,6 +1,5 @@
pub(crate) mod api;
pub(crate) mod api_docs;
pub(crate) mod error;
pub(crate) mod models;
pub(crate) mod server;
pub(crate) mod state;
-92
View File
@@ -1,92 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
// if we ever create some sort of chain watcher client, those would need to be extracted
pub mod status {
use crate::config::payments_watcher::PaymentWatcherConfig;
use crate::models::openapi_schema;
use nym_validator_client::nyxd::Coin;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use time::OffsetDateTime;
use utoipa::ToSchema;
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct ActivePaymentWatchersResponse {
pub watchers: Vec<PaymentWatcher>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PaymentWatcher {
pub id: String,
pub description: String,
pub webhook_url: String,
pub watched_accounts: Vec<String>,
pub watched_message_types: Vec<String>,
}
impl From<&PaymentWatcherConfig> for PaymentWatcher {
fn from(value: &PaymentWatcherConfig) -> Self {
PaymentWatcher {
id: value.id.clone(),
description: value.description.clone().unwrap_or_default(),
webhook_url: value.webhook_url.clone(),
watched_accounts: value
.watch_for_transfer_recipient_accounts
.iter()
.map(|a| a.to_string())
.collect(),
watched_message_types: value.watch_for_chain_message_types.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PaymentListenerStatusResponse {
#[serde(with = "time::serde::rfc3339")]
pub last_checked: OffsetDateTime,
pub processed_payments_since_startup: u64,
pub watcher_errors_since_startup: u64,
pub payment_listener_errors_since_startup: u64,
pub last_processed_payment: Option<ProcessedPayment>,
pub latest_failures: Vec<PaymentListenerFailureDetails>,
pub watchers: HashMap<String, WatcherState>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub(crate) struct ProcessedPayment {
pub processed_at: OffsetDateTime,
pub tx_hash: String,
pub message_index: u64,
pub height: u64,
pub sender: String,
pub receiver: String,
#[schema(value_type = openapi_schema::Coin)]
pub funds: Coin,
pub memo: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub(crate) struct PaymentListenerFailureDetails {
pub(crate) timestamp: OffsetDateTime,
pub(crate) error: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub(crate) struct WatcherState {
pub(crate) latest_failures: Vec<WatcherFailureDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub(crate) struct WatcherFailureDetails {
pub(crate) timestamp: OffsetDateTime,
pub(crate) error: String,
}
}
+51 -19
View File
@@ -1,38 +1,70 @@
use axum::Router;
use core::net::SocketAddr;
use tokio::net::TcpListener;
use tokio_util::sync::WaitForCancellationFutureOwned;
use tokio::{net::TcpListener, task::JoinHandle};
use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned};
use crate::config::Config;
use crate::http::state::PaymentListenerState;
use crate::{
db::DbPool,
http::{api::RouterBuilder, state::AppState},
};
pub(crate) async fn build_http_api(
/// Return handles that allow for graceful shutdown of server + awaiting its
/// background tokio task
pub(crate) async fn start_http_api(
db_pool: DbPool,
config: &Config,
http_port: u16,
payment_listener_state: PaymentListenerState,
) -> anyhow::Result<HttpServer> {
) -> anyhow::Result<ShutdownHandles> {
let router_builder = RouterBuilder::with_default_routes();
let state = AppState::new(
db_pool,
config
.payment_watcher_config
.watchers
.iter()
.map(Into::into)
.collect(),
payment_listener_state,
);
let state = AppState::new(db_pool);
let router = router_builder.with_state(state);
let bind_addr = format!("0.0.0.0:{}", http_port);
let server = router.build_server(bind_addr).await?;
Ok(server)
Ok(start_server(server))
}
fn start_server(server: HttpServer) -> ShutdownHandles {
// one copy is stored to trigger a graceful shutdown later
let shutdown_button = CancellationToken::new();
// other copy is given to server to listen for a shutdown
let shutdown_receiver = shutdown_button.clone();
let shutdown_receiver = shutdown_receiver.cancelled_owned();
let server_handle = tokio::spawn(async move { server.run(shutdown_receiver).await });
ShutdownHandles {
server_handle,
shutdown_button,
}
}
pub(crate) struct ShutdownHandles {
server_handle: JoinHandle<std::io::Result<()>>,
shutdown_button: CancellationToken,
}
impl ShutdownHandles {
/// Send graceful shutdown signal to server and wait for server task to complete
pub(crate) async fn shutdown(self) -> anyhow::Result<()> {
self.shutdown_button.cancel();
match self.server_handle.await {
Ok(Ok(_)) => {
tracing::info!("HTTP server shut down without errors");
}
Ok(Err(err)) => {
tracing::error!("HTTP server terminated with: {err}");
anyhow::bail!(err)
}
Err(err) => {
tracing::error!("Server task panicked: {err}");
}
};
Ok(())
}
}
pub(crate) struct HttpServer {
+2 -169
View File
@@ -1,183 +1,16 @@
use crate::db::DbPool;
use crate::helpers::RingBuffer;
use crate::http::models::status::PaymentWatcher;
use crate::models::WebhookPayload;
use axum::extract::FromRef;
use nym_validator_client::nyxd::Coin;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub(crate) struct AppState {
db_pool: DbPool,
pub(crate) registered_payment_watchers: Arc<Vec<PaymentWatcher>>,
pub(crate) payment_listener_state: PaymentListenerState,
}
impl AppState {
pub(crate) fn new(
db_pool: DbPool,
registered_payment_watchers: Vec<PaymentWatcher>,
payment_listener_state: PaymentListenerState,
) -> Self {
Self {
db_pool,
registered_payment_watchers: Arc::new(registered_payment_watchers),
payment_listener_state,
}
pub(crate) fn new(db_pool: DbPool) -> Self {
Self { db_pool }
}
pub(crate) fn db_pool(&self) -> &DbPool {
&self.db_pool
}
pub(crate) fn watched_accounts(&self) -> Vec<String> {
self.registered_payment_watchers
.iter()
.flat_map(|w| w.watched_accounts.iter())
.cloned()
.collect()
}
}
#[derive(Debug, Clone)]
pub(crate) struct PaymentListenerState {
pub(crate) inner: Arc<RwLock<PaymentListenerStateInner>>,
}
impl PaymentListenerState {
// TODO: make those configurable
const MAX_WATCHER_FAILURES: usize = 20;
const MAX_PAYMENT_LISTENER_FAILURES: usize = 50;
pub(crate) fn new() -> Self {
PaymentListenerState {
inner: Arc::new(RwLock::new(PaymentListenerStateInner {
last_checked: OffsetDateTime::UNIX_EPOCH,
processed_payments_since_startup: 0,
watcher_errors_since_startup: 0,
payment_listener_errors_since_startup: 0,
last_processed_payment: None,
latest_failures: RingBuffer::new(Self::MAX_PAYMENT_LISTENER_FAILURES),
watchers: Default::default(),
})),
}
}
pub(crate) async fn insert_listener_failure(&self, failure: PaymentListenerFailureDetails) {
let mut guard = self.inner.write().await;
guard.payment_listener_errors_since_startup += 1;
guard.latest_failures.push(failure);
}
pub(crate) async fn insert_watcher_failure(&self, id: &str, failure: WatcherFailureDetails) {
self.inner
.write()
.await
.watchers
.entry(id.to_string())
.or_insert(WatcherState {
latest_failures: RingBuffer::new(Self::MAX_WATCHER_FAILURES),
})
.latest_failures
.push(failure);
}
pub(crate) async fn processed_payment_transaction(&self, payment: ProcessedPayment) {
let mut guard = self.inner.write().await;
guard.processed_payments_since_startup += 1;
guard.last_processed_payment = Some(payment)
}
pub(crate) async fn update_last_checked(&self) {
self.inner.write().await.last_checked = OffsetDateTime::now_utc();
}
}
impl FromRef<AppState> for PaymentListenerState {
fn from_ref(input: &AppState) -> Self {
input.payment_listener_state.clone()
}
}
#[derive(Debug)]
pub(crate) struct PaymentListenerStateInner {
pub(crate) last_checked: OffsetDateTime,
pub(crate) processed_payments_since_startup: u64,
pub(crate) watcher_errors_since_startup: u64,
pub(crate) payment_listener_errors_since_startup: u64,
pub(crate) last_processed_payment: Option<ProcessedPayment>,
pub(crate) latest_failures: RingBuffer<PaymentListenerFailureDetails>,
pub(crate) watchers: HashMap<String, WatcherState>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ProcessedPayment {
pub processed_at: OffsetDateTime,
pub tx_hash: String,
pub message_index: u64,
pub height: u64,
pub sender: String,
pub receiver: String,
pub funds: Coin,
pub memo: String,
}
impl From<WebhookPayload> for ProcessedPayment {
fn from(payload: WebhookPayload) -> Self {
ProcessedPayment {
processed_at: OffsetDateTime::now_utc(),
tx_hash: payload.transaction_hash,
message_index: payload.message_index,
height: payload.height as u64,
sender: payload.sender_address,
receiver: payload.receiver_address,
funds: payload.funds.into(),
memo: payload.memo.unwrap_or_default(),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct PaymentListenerFailureDetails {
pub(crate) timestamp: OffsetDateTime,
pub(crate) error: String,
}
impl PaymentListenerFailureDetails {
pub(crate) fn new(error: String) -> Self {
PaymentListenerFailureDetails {
timestamp: OffsetDateTime::now_utc(),
error,
}
}
}
#[derive(Debug)]
pub(crate) struct WatcherState {
pub(crate) latest_failures: RingBuffer<WatcherFailureDetails>,
}
#[derive(Debug)]
pub(crate) struct WatcherFailureDetails {
pub(crate) timestamp: OffsetDateTime,
pub(crate) error: String,
}
impl WatcherFailureDetails {
pub(crate) fn new(error: String) -> Self {
WatcherFailureDetails {
timestamp: OffsetDateTime::now_utc(),
error,
}
}
}
-1
View File
@@ -10,7 +10,6 @@ mod config;
mod db;
mod env;
mod error;
pub(crate) mod helpers;
mod http;
mod logging;
pub mod models;
+1 -1
View File
@@ -1,6 +1,6 @@
use nym_validator_client::nyxd::CosmWasmCoin;
use rocket::serde::{Deserialize, Serialize};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, Clone, JsonSchema, ToSchema)]
+91 -135
View File
@@ -1,152 +1,108 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::config::PaymentWatchersConfig;
use crate::db::models::Transaction;
use crate::config::payments_watcher::HttpAuthenticationOptions;
use crate::config::PaymentWatcherConfig;
use crate::db::queries;
use crate::http::state::{
PaymentListenerFailureDetails, PaymentListenerState, ProcessedPayment, WatcherFailureDetails,
};
use crate::models::WebhookPayload;
use crate::payment_listener::watcher::PaymentWatcher;
use anyhow::Context;
use nym_validator_client::nyxd::Coin;
use reqwest::Client;
use sqlx::SqlitePool;
use std::str::FromStr;
use tokio::time::{self, Duration};
use tracing::{debug, error, info};
use tracing::{error, info};
pub(crate) mod watcher;
pub(crate) async fn run_payment_listener(
payment_watcher_config: PaymentWatcherConfig,
watcher_pool: SqlitePool,
) -> anyhow::Result<()> {
let client = Client::new();
pub(crate) struct PaymentListener {
db_pool: SqlitePool,
payment_watchers: Vec<PaymentWatcher>,
shared_state: PaymentListenerState,
}
loop {
// 1. get the last height this watcher ran at
let last_checked_height = queries::payments::get_last_checked_height(&watcher_pool).await?;
info!("Last checked height: {}", last_checked_height);
impl PaymentListener {
pub(crate) fn new(
db_pool: SqlitePool,
config: PaymentWatchersConfig,
shared_state: PaymentListenerState,
) -> anyhow::Result<Self> {
Ok(PaymentListener {
db_pool,
payment_watchers: config
.watchers
.iter()
.map(|watcher_cfg| PaymentWatcher::new(watcher_cfg.clone()))
.collect::<anyhow::Result<Vec<_>>>()?,
shared_state,
})
}
// 2. iterate through watchers
for watcher in &payment_watcher_config.watchers {
if watcher.watch_for_transfer_recipient_accounts.is_some() {
// 3. Query new transactions for this watcher's recipient accounts
let transactions = sqlx::query!(
r#"
SELECT * FROM transactions
WHERE height > ?
ORDER BY height ASC, message_index ASC
"#,
last_checked_height
)
.fetch_all(&watcher_pool)
.await?;
async fn retrieve_unprocessed_transactions(&self) -> anyhow::Result<Vec<Transaction>> {
let last_checked_height = queries::payments::get_last_checked_height(&self.db_pool).await?;
let txs = sqlx::query_as!(
Transaction,
r#"
SELECT id, tx_hash, height, message_index, sender, recipient, amount, memo, created_at as "created_at: ::time::OffsetDateTime"
FROM transactions
WHERE height > ?
ORDER BY height ASC, message_index ASC
"#,
last_checked_height
)
.fetch_all(&self.db_pool)
.await?;
if !transactions.is_empty() {
info!(
"[watcher = {}] Processing {} transactions",
watcher.id,
transactions.len()
);
}
Ok(txs)
}
for tx in transactions {
let funds = Coin::from_str(&tx.amount)?;
let amount: f64 = funds.amount as f64 / 1e6f64; // convert to major value, there will be precision loss
async fn process_transaction(&self, tx: Transaction) -> anyhow::Result<()> {
// 3.1 process any payments
let funds = tx.funds()?;
let amount: f64 = funds.amount as f64 / 1e6f64; // convert to major value, there will be precision loss
// Store transaction hash for later use
let tx_hash = tx.tx_hash.clone();
let message_index = tx.message_index;
// TODO: FIXME: it may happen that we insert a payment but fail to invoke all webhooks
queries::payments::insert_payment(
&self.db_pool,
tx.tx_hash.clone(),
tx.sender.clone(),
tx.recipient.clone(),
amount,
tx.height,
tx.memo.clone(),
)
.await?;
// 3.1. invoke all relevant webhooks for all registered watchers
let webhook_data = WebhookPayload {
transaction_hash: tx.tx_hash,
message_index: tx.message_index as u64,
sender_address: tx.sender,
receiver_address: tx.recipient,
funds: funds.into(),
height: tx.height as u128,
memo: tx.memo,
};
for watcher in &self.payment_watchers {
if let Err(err) = watcher.invoke_webhook(&webhook_data).await {
error!("watcher {} failure: {err:#}", watcher.id());
self.shared_state
.insert_watcher_failure(
watcher.id(),
WatcherFailureDetails::new(err.to_string()),
queries::payments::insert_payment(
&watcher_pool,
tx.tx_hash,
tx.sender.clone(),
tx.recipient.clone(),
amount,
tx.height,
tx.memo.clone(),
)
.await
.await?;
let webhook_data = WebhookPayload {
transaction_hash: tx_hash.clone(),
message_index: message_index as u64,
sender_address: tx.sender,
receiver_address: tx.recipient,
funds: funds.into(),
height: tx.height as u128,
memo: tx.memo,
};
let mut request_builder = client.post(&watcher.webhook_url).json(&webhook_data);
if let Some(auth) = &watcher.authentication {
match auth {
HttpAuthenticationOptions::AuthorizationBearerToken { token } => {
request_builder = request_builder.bearer_auth(token);
}
}
}
match request_builder.send().await {
Ok(res) => info!(
"[watcher = {}] ✅ Webhook {} {} - tx {}, index {}",
watcher.id,
res.status(),
res.url(),
tx_hash,
message_index,
),
Err(e) => error!(
"[watcher = {}] ❌ Webhook {:?} {:?} error = {}",
watcher.id,
e.status(),
e.url(),
e,
),
}
}
}
}
self.shared_state
.processed_payment_transaction(ProcessedPayment::from(webhook_data))
.await;
Ok(())
}
async fn check_for_unprocessed_payments(&self) -> anyhow::Result<()> {
// 1. retrieve any unprocessed transactions
let unprocessed_transactions = self
.retrieve_unprocessed_transactions()
.await
.context("failed to retrieve unprocessed transactions")?;
if unprocessed_transactions.is_empty() {
debug!("no payment transactions to process.");
return Ok(());
} else {
info!(
"processing {} payment transactions",
unprocessed_transactions.len()
);
}
// 2. attempt to process them
for tx in unprocessed_transactions {
let hash = tx.tx_hash.clone();
let height = tx.height;
self.process_transaction(tx).await.with_context(|| {
format!("failed to process transaction {hash} at height {height}")
})?;
}
Ok(())
}
pub(crate) async fn run(&self) {
loop {
time::sleep(Duration::from_secs(10)).await;
if let Err(err) = self.check_for_unprocessed_payments().await {
error!("failed to fully process payments: {err:#}");
self.shared_state
.insert_listener_failure(PaymentListenerFailureDetails::new(err.to_string()))
.await;
continue;
}
self.shared_state.update_last_checked().await;
}
time::sleep(Duration::from_secs(10)).await;
}
}
@@ -1,73 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::config::payments_watcher::{HttpAuthenticationOptions, PaymentWatcherConfig};
use crate::models::WebhookPayload;
use anyhow::Context;
use reqwest::{Client, Url};
use tracing::{error, info};
pub(crate) struct PaymentWatcher {
webhook_url: Url,
config: PaymentWatcherConfig,
}
impl PaymentWatcher {
pub(crate) fn new(config: PaymentWatcherConfig) -> anyhow::Result<Self> {
Ok(PaymentWatcher {
webhook_url: config
.webhook_url
.as_str()
.parse()
.context("couldn't create payment watcher: provided webhook URL is malformed")?,
config,
})
}
pub(super) fn id(&self) -> &str {
&self.config.id
}
pub(crate) async fn invoke_webhook(&self, payload: &WebhookPayload) -> anyhow::Result<()> {
let client = Client::builder()
.user_agent(format!(
"nyx-chain-watcher/{}/watcher-{}",
env!("CARGO_PKG_VERSION"),
self.config.id
))
.build()
.context("failed to build reqwest client")?;
let mut request_builder = client.post(self.webhook_url.clone()).json(payload);
if let Some(auth) = &self.config.authentication {
match auth {
HttpAuthenticationOptions::AuthorizationBearerToken { token } => {
request_builder = request_builder.bearer_auth(token);
}
}
}
match request_builder.send().await {
Ok(res) => info!(
"[watcher = {}] ✅ Webhook {} {} - tx {}, index {}",
self.config.id,
res.status(),
res.url(),
payload.transaction_hash,
payload.message_index,
),
Err(err) => {
error!(
"[watcher = {}] ❌ Webhook {:?} {:?} error = {err}",
self.config.id,
err.status(),
err.url(),
);
return Err(err.into());
}
}
Ok(())
}
}
+2 -1
View File
@@ -3,6 +3,7 @@ use crate::db::{
queries::price::insert_nym_prices,
};
use core::str;
use tokio::task::JoinHandle;
use tokio::time::Duration;
use crate::db::DbPool;
@@ -12,7 +13,7 @@ const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60 * 2);
const COINGECKO_API_URL: &str =
"https://api.coingecko.com/api/v3/simple/price?ids=nym&vs_currencies=chf,usd,eur,gbp,btc";
pub(crate) async fn run_price_scraper(db_pool: &DbPool) {
pub(crate) async fn run_price_scraper(db_pool: &DbPool) -> JoinHandle<()> {
loop {
tracing::info!("Running in a loop 🏃");
if let Err(e) = get_coingecko_prices(db_pool).await {
+10 -2
View File
@@ -1,5 +1,5 @@
use nym_sdk::mixnet::{
AnonymousSenderTag, MixnetClientBuilder, MixnetMessageSender, ReconstructedMessage,
self, AnonymousSenderTag, MixnetClientBuilder, MixnetMessageSender, ReconstructedMessage,
StoragePaths,
};
use std::path::PathBuf;
@@ -29,8 +29,16 @@ async fn main() {
println!("\nOur client nym address is: {our_address}");
// Send a message through the mixnet to ourselves using our nym address
// client
// .send_plain_message(*our_address, "hello there")
// .await
// .unwrap();
client
.send_plain_message(*our_address, "hello there")
.send_message(
*our_address,
"hello there",
mixnet::IncludedSurbs::Amount(40),
)
.await
.unwrap();
+1 -1
View File
@@ -82,7 +82,7 @@ pub use nym_sphinx::{
pub use nym_statistics_common::clients::{
connection::ConnectionStatsEvent, ClientStatsEvents, ClientStatsSender,
};
pub use nym_task::connections::{LaneQueueLengths, TransmissionLane};
pub use nym_task::connections::TransmissionLane;
pub use nym_topology::{provider_trait::TopologyProvider, NymTopology};
pub use paths::StoragePaths;
pub use socks5_client::Socks5MixnetClient;