diff --git a/Cargo.lock b/Cargo.lock index 280cf9f29b..3985bd4ea6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -24,15 +24,6 @@ dependencies = [ "psl-types", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -133,9 +124,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ammonia" -version = "4.1.2" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" +checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f" dependencies = [ "cssparser", "html5ever", @@ -217,9 +208,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" @@ -408,7 +399,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -490,7 +481,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -501,7 +492,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -655,7 +646,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -688,21 +679,6 @@ dependencies = [ "url", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -1065,7 +1041,7 @@ dependencies = [ "semver 1.0.26", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -1159,7 +1135,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -1250,7 +1226,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1534,7 +1510,7 @@ checksum = "a782b93fae93e57ca8ad3e9e994e784583f5933aeaaa5c80a545c4b437be2047" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1558,7 +1534,7 @@ checksum = "e01c9214319017f6ebd8e299036e1f717fa9bb6724e758f7d6fb2477599d1a29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1802,7 +1778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1906,7 +1882,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1954,7 +1930,7 @@ dependencies = [ "schemars 0.8.22", "serde", "sha2 0.10.9", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -2058,7 +2034,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2069,7 +2045,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2122,7 +2098,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2165,7 +2141,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2186,7 +2162,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2196,7 +2172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2225,7 +2201,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "unicode-xid", ] @@ -2237,7 +2213,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "unicode-xid", ] @@ -2306,7 +2282,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2359,7 +2335,7 @@ version = "0.1.0" dependencies = [ "cosmwasm-std", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2521,7 +2497,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2618,7 +2594,7 @@ dependencies = [ "console_error_panic_hook", "js-sys", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.12", + "thiserror 2.0.17", "wasm-bindgen", "wasm-bindgen-futures", "wasm-storage", @@ -2651,7 +2627,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2881,7 +2857,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2992,12 +2968,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "glob" version = "0.3.2" @@ -3287,7 +3257,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustls 0.23.29", - "thiserror 2.0.12", + "thiserror 2.0.17", "tinyvec", "tokio", "tokio-rustls 0.26.2", @@ -3313,7 +3283,7 @@ dependencies = [ "resolv-conf", "rustls 0.23.29", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-rustls 0.26.2", "tracing", @@ -3591,9 +3561,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64 0.22.1", "bytes", @@ -3607,7 +3577,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -3791,7 +3761,7 @@ checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -3844,7 +3814,7 @@ dependencies = [ "js-sys", "sealed", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "wasm-bindgen", "wasm-bindgen-futures", @@ -3860,7 +3830,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -3979,17 +3949,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] - [[package]] name = "ip_network" version = "0.4.1" @@ -4120,7 +4079,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4145,9 +4104,9 @@ dependencies = [ [[package]] name = "jwt-simple" -version = "0.12.12" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" +checksum = "6ad8761f175784dfbb83709f322fc4daf6b27afd5bf375492f2876f9e925ef5a" dependencies = [ "anyhow", "binstring", @@ -4165,7 +4124,7 @@ dependencies = [ "serde", "serde_json", "superboring", - "thiserror 2.0.12", + "thiserror 2.0.17", "zeroize", ] @@ -4392,7 +4351,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4404,7 +4363,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4417,7 +4376,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4445,7 +4404,7 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4568,7 +4527,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tsify", "url", @@ -4976,7 +4935,7 @@ dependencies = [ "tempfile", "tendermint", "test-with", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -5022,7 +4981,7 @@ dependencies = [ "sha2 0.10.9", "tendermint", "tendermint-rpc", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "ts-rs", @@ -5055,7 +5014,7 @@ dependencies = [ "nym-validator-client", "nym-wireguard-types", "semver 1.0.26", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -5080,7 +5039,7 @@ dependencies = [ "serde", "sha2 0.10.9", "strum_macros", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "x25519-dalek", ] @@ -5099,7 +5058,7 @@ dependencies = [ "nym-task", "nym-validator-client", "rand 0.8.5", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -5206,7 +5165,7 @@ dependencies = [ "serde_json", "tap", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "toml 0.8.23", @@ -5243,7 +5202,7 @@ dependencies = [ "serde", "serde_json", "tap", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-tungstenite", @@ -5296,7 +5255,7 @@ dependencies = [ "sha2 0.10.9", "si-scale", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -5323,7 +5282,7 @@ dependencies = [ "nym-sphinx-params", "nym-statistics-common", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "url", ] @@ -5338,7 +5297,7 @@ dependencies = [ "nym-gateway-requests", "serde", "sqlx", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -5358,7 +5317,7 @@ dependencies = [ "nym-task", "sqlx", "sqlx-pool-guard", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -5381,7 +5340,7 @@ dependencies = [ "serde", "serde-wasm-bindgen 0.6.5", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio_with_wasm", "tsify", "wasm-bindgen", @@ -5442,7 +5401,7 @@ dependencies = [ "serde", "sha2 0.10.9", "subtle 2.6.1", - "thiserror 2.0.12", + "thiserror 2.0.17", "zeroize", ] @@ -5455,7 +5414,7 @@ dependencies = [ "log", "nym-network-defaults", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "toml 0.8.23", "url", ] @@ -5472,7 +5431,7 @@ dependencies = [ "nym-ip-packet-requests", "nym-sdk", "pnet_packet", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -5490,7 +5449,7 @@ dependencies = [ "schemars 0.8.22", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "utoipa", "vergen 8.3.1", ] @@ -5559,7 +5518,7 @@ dependencies = [ "strum", "strum_macros", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-util", @@ -5600,7 +5559,7 @@ dependencies = [ "strum", "strum_macros", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-util", @@ -5651,7 +5610,7 @@ dependencies = [ "serde", "sqlx", "sqlx-pool-guard", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "zeroize", @@ -5670,7 +5629,7 @@ dependencies = [ "nym-credentials-interface", "nym-ecash-time", "nym-validator-client", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", ] @@ -5696,7 +5655,7 @@ dependencies = [ "nym-upgrade-mode-check", "nym-validator-client", "si-scale", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -5721,7 +5680,7 @@ dependencies = [ "nym-validator-client", "rand 0.8.5", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "zeroize", ] @@ -5739,7 +5698,7 @@ dependencies = [ "serde", "strum", "strum_macros", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "utoipa", ] @@ -5773,11 +5732,43 @@ dependencies = [ "serde_json", "sha2 0.10.9", "subtle-encoding", - "thiserror 2.0.12", + "thiserror 2.0.17", "x25519-dalek", "zeroize", ] +[[package]] +name = "nym-data-observatory" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "chrono", + "clap", + "nym-bin-common", + "nym-config", + "nym-network-defaults", + "nym-task", + "nym-validator-client", + "nyxd-scraper-psql", + "nyxd-scraper-shared", + "reqwest 0.12.22", + "schemars 0.8.22", + "serde", + "sqlx", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-util", + "tower-http", + "tracing", + "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", + "utoipauto", +] + [[package]] name = "nym-dkg" version = "0.1.0" @@ -5796,7 +5787,7 @@ dependencies = [ "serde", "serde_derive", "sha2 0.10.9", - "thiserror 2.0.12", + "thiserror 2.0.17", "zeroize", ] @@ -5811,7 +5802,7 @@ dependencies = [ "cw-utils", "cw2", "nym-multisig-contract-common", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -5824,7 +5815,7 @@ dependencies = [ "nym-network-defaults", "nym-validator-client", "semver 1.0.26", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", "url", @@ -5838,7 +5829,7 @@ dependencies = [ "nym-crypto", "semver 1.0.26", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "url", @@ -5860,7 +5851,7 @@ dependencies = [ "reqwest 0.12.22", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "utoipa", ] @@ -5923,7 +5914,7 @@ dependencies = [ "nym-wireguard-types", "rand 0.8.5", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -5956,7 +5947,7 @@ dependencies = [ "rand 0.8.5", "serde", "si-scale", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -6006,7 +5997,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -6039,7 +6030,7 @@ dependencies = [ "serde_json", "strum", "subtle 2.6.1", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -6058,7 +6049,7 @@ dependencies = [ "nym-statistics-common", "sqlx", "strum", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -6077,7 +6068,7 @@ dependencies = [ "nym-gateway-requests", "nym-sphinx", "sqlx", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -6094,7 +6085,7 @@ dependencies = [ "nym-ffi-shared", "nym-sdk", "nym-sphinx-anonymous-replies", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "uniffi", "uniffi_build", @@ -6135,7 +6126,7 @@ dependencies = [ "serde_json", "serde_plain", "serde_yaml", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", "tracing-subscriber", @@ -6152,7 +6143,7 @@ dependencies = [ "proc-macro2", "quote", "reqwest 0.12.22", - "syn 2.0.106", + "syn 2.0.104", "uuid", ] @@ -6184,7 +6175,7 @@ version = "0.1.0" dependencies = [ "nym-credential-storage", "nym-credentials", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "zeroize", @@ -6210,7 +6201,7 @@ version = "0.1.0" dependencies = [ "log", "rand 0.8.5", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -6222,7 +6213,7 @@ dependencies = [ "futures", "nym-ip-packet-requests", "nym-sdk", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -6240,7 +6231,7 @@ dependencies = [ "nym-sphinx", "rand 0.8.5", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-util", @@ -6281,7 +6272,7 @@ dependencies = [ "reqwest 0.12.22", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-tun", @@ -6297,7 +6288,7 @@ dependencies = [ "k256", "ledger-transport", "ledger-transport-hid", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -6344,7 +6335,7 @@ dependencies = [ "semver 1.0.26", "serde", "serde_repr", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "ts-rs", "utoipa", @@ -6370,7 +6361,7 @@ dependencies = [ "nym-task", "rand 0.8.5", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-util", @@ -6389,7 +6380,7 @@ dependencies = [ "cw4", "schemars 0.8.22", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -6483,7 +6474,7 @@ dependencies = [ "sqlx", "tap", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-tungstenite", @@ -6561,7 +6552,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sysinfo", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -6611,7 +6602,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "url", @@ -6680,7 +6671,7 @@ dependencies = [ "sqlx", "strum", "strum_macros", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -6726,7 +6717,7 @@ dependencies = [ "rand_chacha 0.3.1", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "wasm-utils", ] @@ -6741,7 +6732,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tsify", "wasm-bindgen", @@ -6768,7 +6759,7 @@ dependencies = [ "snow", "strum", "strum_macros", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -6815,7 +6806,7 @@ name = "nym-ordered-buffer" version = "0.1.0" dependencies = [ "log", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -6832,7 +6823,7 @@ dependencies = [ "rand 0.8.5", "rayon", "sphinx-packet", - "thiserror 2.0.12", + "thiserror 2.0.17", "x25519-dalek", "zeroize", ] @@ -6856,7 +6847,7 @@ dependencies = [ "nym-contracts-common", "schemars 0.8.22", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -6868,7 +6859,7 @@ dependencies = [ "cw-controllers", "schemars 0.8.22", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", ] @@ -6885,7 +6876,7 @@ dependencies = [ "nym-registration-common", "nym-sdk", "nym-validator-client", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -6950,7 +6941,7 @@ dependencies = [ "serde", "tap", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -6980,7 +6971,7 @@ version = "0.1.0" dependencies = [ "bincode", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -6996,7 +6987,7 @@ dependencies = [ "nym-sphinx-anonymous-replies", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", ] @@ -7046,7 +7037,7 @@ dependencies = [ "serde", "serde_json", "tap", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "url", @@ -7080,7 +7071,7 @@ dependencies = [ "schemars 0.8.22", "serde", "tap", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "url", ] @@ -7112,7 +7103,7 @@ dependencies = [ "serde", "serde_json", "tap", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -7136,7 +7127,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_distr", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -7155,7 +7146,7 @@ dependencies = [ "nym-topology", "rand 0.8.5", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "zeroize", ] @@ -7169,7 +7160,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -7185,7 +7176,7 @@ dependencies = [ "nym-topology", "rand 0.8.5", "rand_chacha 0.3.1", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "wasm-bindgen", ] @@ -7203,7 +7194,7 @@ dependencies = [ "nym-sphinx-types", "rand 0.8.5", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "utoipa", "wasmtimer", ] @@ -7222,7 +7213,7 @@ dependencies = [ "nym-sphinx-types", "nym-topology", "rand 0.8.5", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -7233,7 +7224,7 @@ dependencies = [ "nym-sphinx-anonymous-replies", "nym-sphinx-params", "nym-sphinx-types", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -7246,7 +7237,7 @@ dependencies = [ "nym-sphinx-forwarding", "nym-sphinx-params", "nym-sphinx-types", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -7259,7 +7250,7 @@ dependencies = [ "nym-crypto", "nym-sphinx-types", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -7268,7 +7259,7 @@ version = "0.1.0" dependencies = [ "nym-sphinx-addressing", "nym-sphinx-types", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -7277,7 +7268,7 @@ version = "0.2.0" dependencies = [ "nym-outfox", "sphinx-packet", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -7328,7 +7319,7 @@ dependencies = [ "strum", "strum_macros", "sysinfo", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "utoipa", @@ -7346,7 +7337,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "zeroize", ] @@ -7359,7 +7350,7 @@ dependencies = [ "futures", "log", "nym-test-utils", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -7409,7 +7400,7 @@ dependencies = [ "reqwest 0.12.22", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "tsify", @@ -7424,7 +7415,7 @@ dependencies = [ "etherparse", "log", "nym-wireguard-types", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-tun", ] @@ -7453,7 +7444,7 @@ dependencies = [ "strum", "strum_macros", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "ts-rs", "url", "utoipa", @@ -7472,7 +7463,7 @@ dependencies = [ "reqwest 0.12.22", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "utoipa", @@ -7519,7 +7510,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tendermint-rpc", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -7564,7 +7555,7 @@ dependencies = [ "serde_with", "sha2 0.10.9", "sqlx", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -7585,7 +7576,7 @@ dependencies = [ "nym-task", "nym-validator-client", "rand 0.8.5", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-util", @@ -7603,7 +7594,7 @@ dependencies = [ "nym-contracts-common", "nym-mixnet-contract-common", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "ts-rs", ] @@ -7624,7 +7615,7 @@ dependencies = [ "serde", "serde-wasm-bindgen 0.6.5", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tsify", "wasm-bindgen", @@ -7669,7 +7660,7 @@ dependencies = [ "nym-node-metrics", "nym-task", "nym-wireguard-types", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -7714,7 +7705,7 @@ dependencies = [ "nym-credentials-interface", "schemars 0.8.22", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "utoipa", ] @@ -7750,7 +7741,7 @@ dependencies = [ "nym-crypto", "rand 0.8.5", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "x25519-dalek", ] @@ -7777,7 +7768,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tar", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tracing", @@ -7803,7 +7794,7 @@ dependencies = [ "schemars 0.8.22", "serde", "sqlx", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-util", @@ -7827,7 +7818,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -7851,7 +7842,7 @@ dependencies = [ "sha2 0.10.9", "tendermint", "tendermint-rpc", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -7867,39 +7858,30 @@ dependencies = [ "async-trait", "nyxd-scraper-shared", "sqlx", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", ] [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.1", ] [[package]] name = "objc2-io-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ "libc", "objc2-core-foundation", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -8202,7 +8184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.17", "ucd-trie", ] @@ -8226,7 +8208,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -8289,7 +8271,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -8318,7 +8300,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -8412,7 +8394,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -8570,11 +8552,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.5", ] [[package]] @@ -8596,7 +8578,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -8629,7 +8611,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -8652,7 +8634,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -8729,7 +8711,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.29", "socket2 0.5.10", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -8750,7 +8732,7 @@ dependencies = [ "rustls 0.23.29", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -8897,7 +8879,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -8917,7 +8899,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9038,7 +9020,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -9152,7 +9134,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.106", + "syn 2.0.104", "walkdir", ] @@ -9182,12 +9164,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "rustc-demangle" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -9441,7 +9417,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.29.1", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9473,7 +9449,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9494,7 +9470,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9579,10 +9555,11 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -9627,14 +9604,23 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9645,7 +9631,7 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9656,19 +9642,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -9684,7 +9671,7 @@ dependencies = [ "serde_json", "serde_json_path_core", "serde_json_path_macros", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -9696,7 +9683,7 @@ dependencies = [ "inventory", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -9718,7 +9705,7 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9748,7 +9735,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9801,7 +9788,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -9953,9 +9940,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "sluice" @@ -10128,7 +10115,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -10147,7 +10134,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -10170,7 +10157,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.106", + "syn 2.0.104", "tokio", "url", ] @@ -10213,7 +10200,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "whoami", @@ -10265,7 +10252,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "whoami", @@ -10291,7 +10278,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "url", @@ -10379,7 +10366,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -10435,9 +10422,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -10467,14 +10454,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "sysinfo" -version = "0.37.0" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" dependencies = [ "libc", "memchr", @@ -10664,7 +10651,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -10700,7 +10687,7 @@ dependencies = [ "serde_json", "sqlx", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "toml 0.8.23", @@ -10729,11 +10716,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -10744,18 +10731,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -10860,34 +10847,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio 1.0.4", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.0", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -11036,7 +11020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e04c1865c281139e5ccf633cb9f76ffdaabeebfe53b703984cf82878e2aabb" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -11056,8 +11040,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -11069,6 +11053,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -11078,11 +11071,32 @@ dependencies = [ "indexmap 2.10.0", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9" +dependencies = [ + "indexmap 2.10.0", + "toml_datetime 0.7.3", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -11247,7 +11261,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -11354,7 +11368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -11411,7 +11425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6" dependencies = [ "lazy_static", - "thiserror 2.0.12", + "thiserror 2.0.17", "ts-rs-macros", ] @@ -11438,7 +11452,7 @@ checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "termcolor", ] @@ -11465,7 +11479,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.28.0", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -11527,7 +11541,7 @@ checksum = "016c26257f448222014296978b2c8456e2cad4de308c35bdb1e383acd569ef5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -11675,7 +11689,7 @@ dependencies = [ "indexmap 2.10.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -11690,7 +11704,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.104", "toml 0.5.11", "uniffi_meta", ] @@ -11817,7 +11831,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.104", "uuid", ] @@ -11856,7 +11870,7 @@ checksum = "268d76aaebb80eba79240b805972e52d7d410d4bcc52321b951318b0f440cd60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -11867,7 +11881,7 @@ checksum = "382673bda1d05c85b4550d32fd4192ccd4cffe9a908543a0795d1e7682b36246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "utoipauto-core", ] @@ -12086,7 +12100,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -12121,7 +12135,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -12156,7 +12170,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -12182,7 +12196,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tsify", "url", @@ -12204,7 +12218,7 @@ dependencies = [ "nym-store-cipher", "serde", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.12", + "thiserror 2.0.17", "wasm-bindgen", "wasm-utils", ] @@ -12383,7 +12397,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -12404,7 +12418,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -12416,7 +12430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -12428,7 +12442,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -12439,7 +12453,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -12448,6 +12462,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -12455,7 +12475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -12464,7 +12484,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -12473,7 +12493,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -12521,6 +12541,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -12589,7 +12618,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -12863,7 +12892,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "synstructure", ] @@ -12884,7 +12913,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -12904,15 +12933,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -12925,7 +12954,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -12958,7 +12987,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -12974,7 +13003,7 @@ dependencies = [ "flate2", "indexmap 2.10.0", "memchr", - "thiserror 2.0.12", + "thiserror 2.0.17", "zopfli", ] @@ -12995,7 +13024,7 @@ dependencies = [ "rand 0.8.5", "reqwest 0.12.22", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tsify", "uuid", @@ -13055,7 +13084,7 @@ dependencies = [ "reqwest 0.12.22", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", "url", diff --git a/Cargo.toml b/Cargo.toml index 838d88185a..28f7f6c319 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,7 @@ members = [ "nym-credential-proxy/nym-credential-proxy", "nym-credential-proxy/nym-credential-proxy-requests", "nym-credential-proxy/vpn-api-lib-wasm", + "nym-data-observatory", "nym-ip-packet-client", "nym-network-monitor", "nym-node", diff --git a/common/nyxd-scraper-psql/sql_migrations/01_metadata.sql b/common/nyxd-scraper-psql/sql_migrations/0001_metadata.sql similarity index 82% rename from common/nyxd-scraper-psql/sql_migrations/01_metadata.sql rename to common/nyxd-scraper-psql/sql_migrations/0001_metadata.sql index bce13940e8..43070210c4 100644 --- a/common/nyxd-scraper-psql/sql_migrations/01_metadata.sql +++ b/common/nyxd-scraper-psql/sql_migrations/0001_metadata.sql @@ -6,5 +6,5 @@ CREATE TABLE METADATA ( id INTEGER PRIMARY KEY CHECK (id = 0), - last_processed_height INTEGER NOT NULL + last_processed_height BIGINT NOT NULL ); \ No newline at end of file diff --git a/common/nyxd-scraper-psql/sql_migrations/02_cosmos.sql b/common/nyxd-scraper-psql/sql_migrations/0002_cosmos.sql similarity index 88% rename from common/nyxd-scraper-psql/sql_migrations/02_cosmos.sql rename to common/nyxd-scraper-psql/sql_migrations/0002_cosmos.sql index f48da6e903..d33b75fae9 100644 --- a/common/nyxd-scraper-psql/sql_migrations/02_cosmos.sql +++ b/common/nyxd-scraper-psql/sql_migrations/0002_cosmos.sql @@ -44,7 +44,7 @@ CREATE TABLE transaction success BOOLEAN NOT NULL, /* Body */ - messages JSON NOT NULL DEFAULT '[]'::JSON, + messages JSONB NOT NULL DEFAULT '[]'::JSONB, memo TEXT, signatures TEXT[] NOT NULL, @@ -63,31 +63,34 @@ CREATE TABLE transaction CREATE INDEX transaction_hash_index ON transaction (hash); CREATE INDEX transaction_height_index ON transaction (height); -CREATE TABLE message_type +CREATE TYPE COIN AS ( - type TEXT NOT NULL UNIQUE, - module TEXT NOT NULL, - label TEXT NOT NULL, - height BIGINT NOT NULL + denom TEXT, + amount TEXT ); -CREATE INDEX message_type_module_index ON message_type (module); -CREATE INDEX message_type_type_index ON message_type (type); CREATE TABLE message ( transaction_hash TEXT NOT NULL, index BIGINT NOT NULL, - type TEXT NOT NULL REFERENCES message_type (type), - value JSON NOT NULL, + type TEXT NOT NULL, + value JSONB NOT NULL, involved_accounts_addresses TEXT[] NOT NULL, - height BIGINT NOT NULL, + + funds COIN[] DEFAULT '{}', + + wasm_sender TEXT, + wasm_contract_address TEXT, + wasm_message_type TEXT, + FOREIGN KEY (transaction_hash) REFERENCES transaction (hash), CONSTRAINT unique_message_per_tx UNIQUE (transaction_hash, index) ); CREATE INDEX message_transaction_hash_index ON message (transaction_hash); CREATE INDEX message_type_index ON message (type); CREATE INDEX message_involved_accounts_index ON message USING GIN (involved_accounts_addresses); +CREATE INDEX message_wasm_contract_message_type_index ON message (wasm_message_type); /** * This function is used to find all the utils that involve any of the given addresses and have @@ -124,4 +127,5 @@ $$ LANGUAGE sql STABLE; CREATE TABLE pruning ( last_pruned_height BIGINT NOT NULL -); \ No newline at end of file +); + diff --git a/common/nyxd-scraper-psql/src/storage/manager.rs b/common/nyxd-scraper-psql/src/storage/manager.rs index 165dfd42f4..ef38189241 100644 --- a/common/nyxd-scraper-psql/src/storage/manager.rs +++ b/common/nyxd-scraper-psql/src/storage/manager.rs @@ -1,10 +1,11 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::models::Coin; use crate::storage::models::{CommitSignature, Validator}; use nyxd_scraper_shared::storage::helpers::log_db_operation_time; use sqlx::types::time::PrimitiveDateTime; -use sqlx::types::{Json, JsonValue}; +use sqlx::types::JsonValue; use sqlx::{Executor, Postgres}; use tokio::time::Instant; use tracing::{instrument, trace}; @@ -200,7 +201,8 @@ impl StorageManager { log_db_operation_time("get_last_processed_height", start); if let Some(row) = maybe_record { - Ok(row.last_processed_height as i64) + #[allow(clippy::useless_conversion)] + Ok(row.last_processed_height.into()) } else { Ok(-1) } @@ -392,6 +394,7 @@ where Ok(()) } +#[allow(clippy::too_many_arguments)] #[instrument(skip(executor))] pub(crate) async fn insert_message<'a, E>( transaction_hash: String, @@ -400,6 +403,10 @@ pub(crate) async fn insert_message<'a, E>( value: JsonValue, involved_account_addresses: Vec, height: i64, + wasm_sender: Option, + wasm_contract_address: Option, + wasm_message_type: Option, + funds: Option>, executor: E, ) -> Result<(), sqlx::Error> where @@ -408,25 +415,55 @@ where trace!("insert_message"); let start = Instant::now(); - sqlx::query!( - r#" - INSERT INTO message(transaction_hash, index, type, value, involved_accounts_addresses, height) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (transaction_hash, index) DO UPDATE - SET height = excluded.height, - type = excluded.type, - value = excluded.value, - involved_accounts_addresses = excluded.involved_accounts_addresses - "#, - transaction_hash, - index, - typ, - value, - &involved_account_addresses, - height - ) - .execute(executor) - .await?; + // sqlx doesn't understand option types + if let Some(coins) = funds { + sqlx::query!( + r#" + INSERT INTO message(transaction_hash, index, type, value, involved_accounts_addresses, height, wasm_sender, wasm_contract_address, wasm_message_type, funds) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (transaction_hash, index) DO UPDATE + SET height = excluded.height, + type = excluded.type, + value = excluded.value, + involved_accounts_addresses = excluded.involved_accounts_addresses + "#, + transaction_hash, + index, + typ, + value, + &involved_account_addresses, + height, + wasm_sender, + wasm_contract_address, + wasm_message_type, + &coins as _, + ) + .execute(executor) + .await?; + } else { + sqlx::query!( + r#" + INSERT INTO message(transaction_hash, index, type, value, involved_accounts_addresses, height, wasm_sender, wasm_contract_address, wasm_message_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (transaction_hash, index) DO UPDATE + SET height = excluded.height, + type = excluded.type, + value = excluded.value, + involved_accounts_addresses = excluded.involved_accounts_addresses + "#, + transaction_hash, + index, + typ, + value, + &involved_account_addresses, + height, + wasm_sender, + wasm_contract_address, + wasm_message_type, + ) + .execute(executor) + .await?; + } log_db_operation_time("insert_message", start); Ok(()) @@ -434,7 +471,7 @@ where #[instrument(skip(executor))] pub(crate) async fn update_last_processed<'a, E>( - height: i32, + height: i64, executor: E, ) -> Result<(), sqlx::Error> where diff --git a/common/nyxd-scraper-psql/src/storage/models.rs b/common/nyxd-scraper-psql/src/storage/models.rs index 6c4c16c633..878eff203b 100644 --- a/common/nyxd-scraper-psql/src/storage/models.rs +++ b/common/nyxd-scraper-psql/src/storage/models.rs @@ -1,8 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use sqlx::FromRow; +use serde::{Deserialize, Serialize}; use sqlx::types::time::OffsetDateTime; +use sqlx::FromRow; #[derive(Debug, Clone, Eq, PartialEq, Hash, FromRow)] pub struct Validator { @@ -28,3 +29,10 @@ pub struct CommitSignature { pub proposer_priority: i64, pub timestamp: OffsetDateTime, } + +#[derive(Debug, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "coin")] +pub struct Coin { + pub amount: String, + pub denom: String, +} diff --git a/common/nyxd-scraper-psql/src/storage/transaction.rs b/common/nyxd-scraper-psql/src/storage/transaction.rs index 2baa2c8130..af118f300a 100644 --- a/common/nyxd-scraper-psql/src/storage/transaction.rs +++ b/common/nyxd-scraper-psql/src/storage/transaction.rs @@ -2,14 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::PostgresScraperError; -use crate::storage::helpers::{parse_addresses_from_events, PlaceholderStruct}; +use crate::models::Coin; +use crate::storage::helpers::parse_addresses_from_events; use crate::storage::manager::{ insert_block, insert_message, insert_precommit, insert_transaction, insert_validator, + update_last_processed, }; use async_trait::async_trait; use base64::engine::general_purpose; use base64::Engine as _; use cosmrs::proto; +use cosmrs::proto::cosmwasm::wasm::v1::MsgExecuteContract; +use cosmrs::proto::prost::Message; use nyxd_scraper_shared::helpers::{ validator_consensus_address, validator_info, validator_pubkey_to_bech32, }; @@ -18,7 +22,7 @@ use nyxd_scraper_shared::storage::{ validators, Block, Commit, CommitSig, NyxdScraperStorageError, NyxdScraperTransaction, }; use nyxd_scraper_shared::{Any, MessageRegistry, ParsedTransactionResponse}; -use serde_json::json; +use serde_json::{json, Value}; use sqlx::types::time::{OffsetDateTime, PrimitiveDateTime}; use sqlx::{Postgres, Transaction}; use std::ops::{Deref, DerefMut}; @@ -211,13 +215,44 @@ impl PostgresStorageTransaction { for chain_tx in txs { let involved_addresses = parse_addresses_from_events(chain_tx); for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() { + let mut wasm_sender: Option = None; + let mut wasm_contract_address: Option = None; + let mut wasm_message_type: Option = None; + let mut funds: Option> = None; + + let value = serde_json::to_value(self.decode_or_skip(msg))?; + + if msg.type_url == "/cosmwasm.wasm.v1.MsgExecuteContract" { + if let Ok(wasm_execute) = MsgExecuteContract::decode(msg.value.as_ref()) { + wasm_sender = Some(wasm_execute.sender); + wasm_contract_address = Some(wasm_execute.contract); + if let Some(raw_msg) = value.get("msg") { + wasm_message_type = get_first_field_name(raw_msg); + } + funds = Some( + wasm_execute + .funds + .iter() + .map(|c| Coin { + amount: c.amount.to_string(), + denom: c.denom.clone(), + }) + .collect(), + ); + } + } + insert_message( chain_tx.hash.to_string(), index as i64, msg.type_url.clone(), - serde_json::to_value(self.decode_or_skip(msg))?, + value, involved_addresses.clone(), chain_tx.height.into(), + wasm_sender, + wasm_contract_address, + wasm_message_type, + funds, self.inner.as_mut(), ) .await? @@ -226,6 +261,20 @@ impl PostgresStorageTransaction { Ok(()) } + + async fn update_last_processed(&mut self, height: i64) -> Result<(), PostgresScraperError> { + debug!("update_last_processed"); + update_last_processed(height, self.inner.as_mut()).await?; + Ok(()) + } +} + +fn get_first_field_name(value: &Value) -> Option { + debug!("value:\n{value}"); + match value.as_object() { + Some(map) => map.keys().next().cloned(), + None => None, + } } #[async_trait] @@ -286,6 +335,8 @@ impl NyxdScraperTransaction for PostgresStorageTransaction { } async fn update_last_processed(&mut self, height: i64) -> Result<(), NyxdScraperStorageError> { - self.update_last_processed(height).await + self.update_last_processed(height) + .await + .map_err(NyxdScraperStorageError::from) } } diff --git a/common/nyxd-scraper-sqlite/src/storage/transaction.rs b/common/nyxd-scraper-sqlite/src/storage/transaction.rs index 713cbb3261..76f5ecd67e 100644 --- a/common/nyxd-scraper-sqlite/src/storage/transaction.rs +++ b/common/nyxd-scraper-sqlite/src/storage/transaction.rs @@ -4,6 +4,7 @@ use crate::error::SqliteScraperError; use crate::storage::manager::{ insert_block, insert_message, insert_precommit, insert_transaction, insert_validator, + update_last_processed, }; use async_trait::async_trait; use nyxd_scraper_shared::helpers::{ @@ -169,6 +170,12 @@ impl SqliteStorageTransaction { Ok(()) } + + async fn update_last_processed(&mut self, height: i64) -> Result<(), SqliteScraperError> { + debug!("update_last_processed"); + update_last_processed(height, self.0.as_mut()).await?; + Ok(()) + } } #[async_trait] diff --git a/nym-data-observatory/.gitignore b/nym-data-observatory/.gitignore new file mode 100644 index 0000000000..36150a9651 --- /dev/null +++ b/nym-data-observatory/.gitignore @@ -0,0 +1,2 @@ +0001_metadata.sql +0002_cosmos.sql \ No newline at end of file diff --git a/nym-data-observatory/.sqlx/query-140df23f816ff5d7501128682ce378d582b7da78c45bc0de934f92c1abe14bda.json b/nym-data-observatory/.sqlx/query-140df23f816ff5d7501128682ce378d582b7da78c45bc0de934f92c1abe14bda.json new file mode 100644 index 0000000000..b190fd0fb1 --- /dev/null +++ b/nym-data-observatory/.sqlx/query-140df23f816ff5d7501128682ce378d582b7da78c45bc0de934f92c1abe14bda.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO price_history\n (timestamp, chf, usd, eur, gbp, btc)\n VALUES\n ($1, $2, $3, $4, $5, $6)\n ON CONFLICT(timestamp) DO UPDATE SET\n chf=excluded.chf,\n usd=excluded.usd,\n eur=excluded.eur,\n gbp=excluded.gbp,\n btc=excluded.btc;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Float8", + "Float8", + "Float8", + "Float8", + "Float8" + ] + }, + "nullable": [] + }, + "hash": "140df23f816ff5d7501128682ce378d582b7da78c45bc0de934f92c1abe14bda" +} diff --git a/nym-data-observatory/.sqlx/query-a57b74a049b33aee36b72741056d60df8ad35a747808d5d1d3d525a76bbf0618.json b/nym-data-observatory/.sqlx/query-a57b74a049b33aee36b72741056d60df8ad35a747808d5d1d3d525a76bbf0618.json new file mode 100644 index 0000000000..0a8728ccb8 --- /dev/null +++ b/nym-data-observatory/.sqlx/query-a57b74a049b33aee36b72741056d60df8ad35a747808d5d1d3d525a76bbf0618.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT timestamp, chf, usd, eur, gbp, btc FROM price_history WHERE timestamp >= $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "timestamp", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "chf", + "type_info": "Float8" + }, + { + "ordinal": 2, + "name": "usd", + "type_info": "Float8" + }, + { + "ordinal": 3, + "name": "eur", + "type_info": "Float8" + }, + { + "ordinal": 4, + "name": "gbp", + "type_info": "Float8" + }, + { + "ordinal": 5, + "name": "btc", + "type_info": "Float8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "a57b74a049b33aee36b72741056d60df8ad35a747808d5d1d3d525a76bbf0618" +} diff --git a/nym-data-observatory/.sqlx/query-f81a3275a1c7cbeefb3fdf7904c677d46a284e0446b96a2fc5bd77630c62d4b8.json b/nym-data-observatory/.sqlx/query-f81a3275a1c7cbeefb3fdf7904c677d46a284e0446b96a2fc5bd77630c62d4b8.json new file mode 100644 index 0000000000..e58a1a7451 --- /dev/null +++ b/nym-data-observatory/.sqlx/query-f81a3275a1c7cbeefb3fdf7904c677d46a284e0446b96a2fc5bd77630c62d4b8.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT timestamp, chf, usd, eur, gbp, btc FROM price_history ORDER BY timestamp DESC LIMIT 1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "timestamp", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "chf", + "type_info": "Float8" + }, + { + "ordinal": 2, + "name": "usd", + "type_info": "Float8" + }, + { + "ordinal": 3, + "name": "eur", + "type_info": "Float8" + }, + { + "ordinal": 4, + "name": "gbp", + "type_info": "Float8" + }, + { + "ordinal": 5, + "name": "btc", + "type_info": "Float8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "f81a3275a1c7cbeefb3fdf7904c677d46a284e0446b96a2fc5bd77630c62d4b8" +} diff --git a/nym-data-observatory/.sqlx/query-fbf7dc2d779476fffcefafaa0a1731dfc6affe6c672df121140a5c7141f71c63.json b/nym-data-observatory/.sqlx/query-fbf7dc2d779476fffcefafaa0a1731dfc6affe6c672df121140a5c7141f71c63.json new file mode 100644 index 0000000000..d70649da8b --- /dev/null +++ b/nym-data-observatory/.sqlx/query-fbf7dc2d779476fffcefafaa0a1731dfc6affe6c672df121140a5c7141f71c63.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO watcher_execution(start_ts, end_ts, error_message)\n VALUES ($1, $2, $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Timestamptz", + "Text" + ] + }, + "nullable": [] + }, + "hash": "fbf7dc2d779476fffcefafaa0a1731dfc6affe6c672df121140a5c7141f71c63" +} diff --git a/nym-data-observatory/Cargo.toml b/nym-data-observatory/Cargo.toml new file mode 100644 index 0000000000..e215a48daa --- /dev/null +++ b/nym-data-observatory/Cargo.toml @@ -0,0 +1,48 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: GPL-3.0-only + +[package] +name = "nym-data-observatory" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +async-trait.workspace = true +axum = { workspace = true, features = ["tokio"] } +chrono = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive", "env"] } +nym-config = { path = "../common/config" } +nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } +nym-network-defaults = { path = "../common/network-defaults" } +nym-task = { path = "../common/task" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +nyxd-scraper-psql = { path = "../common/nyxd-scraper-psql" } +nyxd-scraper-shared = { path = "../common/nyxd-scraper-shared" } +reqwest = { workspace = true, features = ["rustls-tls"] } +schemars = { workspace = true } +serde = { workspace = true, features = ["derive"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "time"] } +thiserror = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["process", "rt-multi-thread"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tower-http = { workspace = true, features = ["cors", "trace"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +utoipauto = { workspace = true } + + +[build-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres"] } diff --git a/nym-data-observatory/Dockerfile b/nym-data-observatory/Dockerfile new file mode 100644 index 0000000000..780564bec1 --- /dev/null +++ b/nym-data-observatory/Dockerfile @@ -0,0 +1,32 @@ +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-data-observatory + +RUN cargo build --release + +#------------------------------------------------------------------- +# The following environment variables are required at runtime: +# +# NYM_DATA_OBSERVATORY_DB_URL=postgres://nym_data_observatory:data-data-data@localhost/nym_data_observatory_data +# +# And optionally: +# +# NYM_DATA_OBSERVATORY_WEBHOOK_URL="https://webhook.site" +# NYM_DATA_OBSERVATORY_WEBHOOK_AUTH=1234 +# NYX_CHAIN_WATCHER_CONFIG_ENV_FILE_ARG = /mnt/sandbox.env for sandbox environment +# +# see https://github.com/nymtech/nym/blob/develop/nym-data-observatory/src/cli/commands/run/args.rs for details +# and https://github.com/nymtech/nym/blob/develop/nym-data-observatory/src/env.rs for env vars +#------------------------------------------------------------------- + +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 + +RUN apt update && apt install -yy curl ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-data-observatory ./ +ENTRYPOINT [ "/nym/nym-data-observatory", "run" ] + diff --git a/nym-data-observatory/Makefile b/nym-data-observatory/Makefile new file mode 100644 index 0000000000..30d5936a10 --- /dev/null +++ b/nym-data-observatory/Makefile @@ -0,0 +1,104 @@ +# Makefile for nyx_chain_scraper database management + +# --- Configuration --- +TEST_DATABASE_URL := postgres://testuser:testpass@localhost:5433/nym_data_observatory_test + +# Docker compose service names +DB_SERVICE_NAME := postgres-test +DB_CONTAINER_NAME := nym_data_observatory_test + +# Default target +.PHONY: default +default: help + +# --- Main Targets --- +.PHONY: prepare-pg +prepare-pg: test-db-up test-db-wait test-db-migrate test-db-prepare test-db-down ## Setup PostgreSQL and prepare SQLx offline cache + +.PHONY: test-db +test-db: test-db-up test-db-wait test-db-migrate test-db-run test-db-down ## Run tests with PostgreSQL database + +.PHONY: dev-db +dev-db: test-db-up test-db-wait test-db-migrate ## Start PostgreSQL for development (keeps running) + @echo "PostgreSQL is running on port 5433" + @echo "Connection string: $(TEST_DATABASE_URL)" + +# --- Docker Compose Targets --- +.PHONY: test-db-up +test-db-up: ## Start the PostgreSQL test database in the background + @echo "Starting PostgreSQL test database..." + docker compose up -d $(DB_SERVICE_NAME) + +.PHONY: test-db-wait +test-db-wait: ## Wait for the PostgreSQL database to be healthy + @echo "Waiting for PostgreSQL database..." + @while ! docker inspect --format='{{.State.Health.Status}}' $(DB_CONTAINER_NAME) 2>/dev/null | grep -q 'healthy'; do \ + echo -n "."; \ + sleep 1; \ + done; \ + echo " Database is healthy!" + +.PHONY: test-db-down +test-db-down: ## Stop and remove the test database + @echo "Stopping PostgreSQL test database..." + docker compose down + +# --- SQLx Targets --- +.PHONY: test-db-migrate +test-db-migrate: ## Run database migrations against PostgreSQL + @echo "Copying common PostgreSQL migrations..." + cp ../common/nyxd-scraper-psql/sql_migrations/* migrations + @echo "Running watcher PostgreSQL migrations..." + RUST_LOG=debug DATABASE_URL="$(TEST_DATABASE_URL)" sqlx migrate run --source migrations + +.PHONY: test-db-prepare +test-db-prepare: ## Run sqlx prepare for compile-time query verification + @echo "Running sqlx prepare for PostgreSQL..." + DATABASE_URL="$(TEST_DATABASE_URL)" cargo sqlx prepare -- + +# --- Build and Test Targets --- +.PHONY: test-db-run +test-db-run: ## Run tests with PostgreSQL feature + @echo "Running tests with PostgreSQL..." + DATABASE_URL="$(TEST_DATABASE_URL)" cargo test --no-default-features + +.PHONY: build-pg +build-pg: ## Build with PostgreSQL feature + @echo "Building with PostgreSQL feature..." + cargo build --no-default-features + +.PHONY: check-pg +check-pg: ## Check code with PostgreSQL feature + @echo "Checking code with PostgreSQL feature..." + cargo check --no-default-features + +.PHONY: clippy +clippy: clippy-pg + +.PHONY: clippy-pg +clippy-pg: ## Run clippy with PostgreSQL feature + @echo "Running clippy with PostgreSQL feature..." + DATABASE_URL="$(TEST_DATABASE_URL)" cargo clippy --no-default-features -- -D warnings + +# --- Cleanup Targets --- +.PHONY: clean +clean: ## Clean build artifacts and SQLx cache + cargo clean + rm -rf .sqlx + +.PHONY: clean-db +clean-db: test-db-down ## Stop database and clean volumes + docker volume rm -f nym_data_observatory_test_data 2>/dev/null || true + +# --- Utility Targets --- +.PHONY: sqlx-cli +sqlx-cli: ## Install sqlx-cli if not already installed + @command -v sqlx >/dev/null 2>&1 || cargo install sqlx-cli --features postgres + +.PHONY: psql +psql: ## Connect to the running PostgreSQL database with psql + @docker exec -it $(DB_CONTAINER_NAME) psql -U testuser -d nym_data_observatory_test + +.PHONY: help +help: ## Show help for Makefile targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' \ No newline at end of file diff --git a/nym-data-observatory/README.md b/nym-data-observatory/README.md new file mode 100644 index 0000000000..bc9b4484c7 --- /dev/null +++ b/nym-data-observatory/README.md @@ -0,0 +1,96 @@ +# Nym Data Observatory + +Collects data about the Nym network including: + +- **Chain scraper** - that parses blocks, transactions and messages on the Nyx chain +- **Price scraper** - to get the NYM/USD token price from CoinGecko +- **Webhooks** - trigger on messages or all messages to call with details + +## Running locally + +### 1. Install Prerequisites + +```bash +# Install sqlx-cli if not already installed +make sqlx-cli +``` + +### 2. Prepare PostgreSQL for Development + +```bash +# This will: +# - Start PostgreSQL in Docker +# - Run migrations +# - Generate SQLx offline query cache +# - Stop the database +make prepare-pg +``` + +### 3. Build + +```bash +make build-pg +``` + +### 4. Run with PostgreSQL + +```bash +# Start PostgreSQL for development (keeps running) +make test-db-up + +# In another terminal, run the application +NYM_DATA_OBSERVATORY_DB_URL=postgres://testuser:testpass@localhost:5433/nym_data_observatory_test \ +NYM_DATA_OBSERVATORY_WEBHOOK_URL="https://webhook.site" \ +NYM_DATA_OBSERVATORY_WEBHOOK_AUTH=1234 \ +cargo run -- run +``` + +To start from a block add the env var: `NYXD_SCRAPER_START_HEIGHT=19266184`. + +## Deploying + +Connect with `psql` to your local database: + +```sql +CREATE USER nym_data_observatory WITH PASSWORD 'data-data-data'; + +CREATE DATABASE nym_data_observatory_data; +GRANT ALL ON DATABASE nym_data_observatory_data TO nym_data_observatory; +``` + +Then run: + +``` +cargo run -- init --db_url postgres://nym_data_observatory:data-data-data@localhost/nym_data_observatory_data +``` + +and then: + +``` +NYM_DATA_OBSERVATORY_DB_URL=postgres://nym_data_observatory:data-data-data@localhost/nym_data_observatory_data \ +NYM_DATA_OBSERVATORY_WEBHOOK_URL="https://webhook.site" \ +NYM_DATA_OBSERVATORY_WEBHOOK_AUTH=1234 \ +cargo run -- run +``` + +## Troubleshooting + +### SQLx Offline Mode + +If you see "no cached data for this query" errors: + +1. Ensure PostgreSQL is running: `make dev-db` +2. Run: `make test-db-prepare` + +Also see [README_SQLX.md](../nyx-chain-watcher/README_SQLX.md). + +### Connection Refused + +If you see "Connection refused" errors: + +1. Check Docker is running: `docker ps` +2. Check PostgreSQL container: `docker ps | grep nym_data_observatory +3. Restart database: `make test-db-down && make dev-db` + + + diff --git a/nym-data-observatory/build.rs b/nym-data-observatory/build.rs new file mode 100644 index 0000000000..c1dcd9dcb3 --- /dev/null +++ b/nym-data-observatory/build.rs @@ -0,0 +1,8 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +fn main() { + if let Ok(database_url) = std::env::var("DATABASE_URL") { + println!("cargo:rustc-env=DATABASE_URL={database_url}"); + } +} diff --git a/nym-data-observatory/docker-compose.yml b/nym-data-observatory/docker-compose.yml new file mode 100644 index 0000000000..64152aa1bd --- /dev/null +++ b/nym-data-observatory/docker-compose.yml @@ -0,0 +1,21 @@ +services: + postgres-test: + image: postgres:16-alpine + container_name: nym_data_observatory_test + environment: + POSTGRES_DB: nym_data_observatory_test + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass + ports: + - '5433:5432' # Map to 5433 to avoid conflicts with default PostgreSQL + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U testuser -d nyx_chain_scraper_test'] + interval: 5s + timeout: 5s + retries: 5 + # Optional: Add volume for persistent data during development +# volumes: +# - nym_data_observatory_test_data:/var/lib/postgresql/data + +#volumes: +# nym_data_observatory_test_data: \ No newline at end of file diff --git a/nym-data-observatory/migrations/0101_price_data.sql b/nym-data-observatory/migrations/0101_price_data.sql new file mode 100644 index 0000000000..80f30d736c --- /dev/null +++ b/nym-data-observatory/migrations/0101_price_data.sql @@ -0,0 +1,8 @@ +CREATE TABLE price_history ( + timestamp bigint PRIMARY KEY, + chf double precision NOT NULL, + usd double precision NOT NULL, + eur double precision NOT NULL, + btc double precision NOT NULL, + gbp double precision NOT NULL +); \ No newline at end of file diff --git a/nym-data-observatory/migrations/0102_payment_transactions.sql b/nym-data-observatory/migrations/0102_payment_transactions.sql new file mode 100644 index 0000000000..05c0ce5549 --- /dev/null +++ b/nym-data-observatory/migrations/0102_payment_transactions.sql @@ -0,0 +1,10 @@ +CREATE TABLE payments ( + id INTEGER PRIMARY KEY, + transaction_hash TEXT NOT NULL UNIQUE, + sender_address TEXT NOT NULL, + receiver_address TEXT NOT NULL, + amount double precision NOT NULL, + timestamp bigint NOT NULL, + height bigint NOT NULL, + memo TEXT +); diff --git a/nym-data-observatory/migrations/0103_create_transactions_table.sql b/nym-data-observatory/migrations/0103_create_transactions_table.sql new file mode 100644 index 0000000000..61f30fc040 --- /dev/null +++ b/nym-data-observatory/migrations/0103_create_transactions_table.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY, + tx_hash TEXT NOT NULL, + height BIGINT NOT NULL, + message_index BIGINT NOT NULL, + sender TEXT NOT NULL, + recipient TEXT NOT NULL, + amount TEXT NOT NULL, + memo TEXT, + created_at DATE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(tx_hash, message_index) +); \ No newline at end of file diff --git a/nym-data-observatory/migrations/0104_add_listener_failure_table.sql b/nym-data-observatory/migrations/0104_add_listener_failure_table.sql new file mode 100644 index 0000000000..3c80ebf473 --- /dev/null +++ b/nym-data-observatory/migrations/0104_add_listener_failure_table.sql @@ -0,0 +1,11 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE watcher_execution +( + start_ts TIMESTAMPTZ NOT NULL, + end_ts TIMESTAMPTZ NOT NULL, + error_message TEXT +); \ No newline at end of file diff --git a/nym-data-observatory/src/chain_scraper/mod.rs b/nym-data-observatory/src/chain_scraper/mod.rs new file mode 100644 index 0000000000..3b6b908f8d --- /dev/null +++ b/nym-data-observatory/src/chain_scraper/mod.rs @@ -0,0 +1,66 @@ +use crate::db::DbPool; +use crate::env::vars::{ + NYXD_SCRAPER_START_HEIGHT, NYXD_SCRAPER_UNSAFE_NUKE_DB, + NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT, +}; +use nyxd_scraper_psql::{PostgresNyxdScraper, PruningOptions}; +use std::fs; +use tracing::{info, warn}; + +pub(crate) mod webhook; + +pub(crate) async fn run_chain_scraper( + config: &crate::config::Config, + _connection_pool: DbPool, +) -> anyhow::Result { + let websocket_url = std::env::var("NYXD_WS").expect("NYXD_WS not defined"); + + let rpc_url = std::env::var("NYXD").expect("NYXD not defined"); + let websocket_url = reqwest::Url::parse(&websocket_url)?; + let rpc_url = reqwest::Url::parse(&rpc_url)?; + + // why are those not part of CLI? : ( + let start_block_height = match std::env::var(NYXD_SCRAPER_START_HEIGHT).ok() { + None => None, + // blow up if passed malformed env value + Some(raw) => Some(raw.parse()?), + }; + + let use_best_effort_start_height = + match std::env::var(NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT).ok() { + None => false, + // blow up if passed malformed env value + Some(raw) => raw.parse()?, + }; + + let nuke_db: bool = match std::env::var(NYXD_SCRAPER_UNSAFE_NUKE_DB).ok() { + None => false, + // blow up if passed malformed env value + Some(raw) => raw.parse()?, + }; + + if nuke_db { + warn!("☢️☢️☢️ NUKING THE SCRAPER DATABASE"); + fs::remove_file(config.chain_scraper_connection_string())?; + } + + let scraper = PostgresNyxdScraper::builder(nyxd_scraper_psql::Config { + websocket_url, + rpc_url, + database_storage: config.chain_scraper_connection_string.clone(), + pruning_options: PruningOptions::nothing(), + store_precommits: false, + start_block: nyxd_scraper_psql::StartingBlockOpts { + start_block_height, + use_best_effort_start_height, + }, + }) + .with_msg_module(webhook::WebhookModule::new(config.clone())?); + + let instance = scraper.build_and_start().await?; + + info!("🚧 blocking until the chain has caught up..."); + instance.wait_for_startup_sync().await; + + Ok(instance) +} diff --git a/nym-data-observatory/src/chain_scraper/webhook.rs b/nym-data-observatory/src/chain_scraper/webhook.rs new file mode 100644 index 0000000000..221d99e008 --- /dev/null +++ b/nym-data-observatory/src/chain_scraper/webhook.rs @@ -0,0 +1,147 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::data_observatory::{HttpAuthenticationOptions, WebhookConfig}; +use crate::models::WebhookPayload; +use anyhow::Context; +use async_trait::async_trait; +use nym_validator_client::nyxd::{Any, Msg, MsgSend, Name}; +use nyxd_scraper_psql::{ + MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, ScraperError, +}; +use nyxd_scraper_shared::{default_message_registry, MessageRegistry}; +use reqwest::{Client, Url}; +use tracing::{error, info, warn}; +use utoipa::gen::serde_json; + +pub struct WebhookModule { + webhooks: Vec, + registry: MessageRegistry, +} + +impl WebhookModule { + pub fn new(config: crate::config::Config) -> anyhow::Result { + let webhooks = config + .data_observatory_config + .webhooks + .iter() + .map(|watcher_cfg| Webhook::new(watcher_cfg.clone())) + .collect::>>()?; + Ok(Self { + webhooks, + registry: default_message_registry(), + }) + } + + fn decode_or_skip(&self, msg: &Any) -> Option { + match self.registry.try_decode(msg) { + Ok(decoded) => Some(decoded), + Err(err) => { + warn!("webhook processing failed {err}"); + None + } + } + } +} + +#[async_trait] +impl MsgModule for WebhookModule { + fn type_url(&self) -> String { + ::Proto::type_url() + } + + async fn handle_msg( + &mut self, + index: usize, + msg: &Any, + tx: &ParsedTransactionResponse, + _storage_tx: &mut dyn NyxdScraperTransaction, + ) -> Result<(), ScraperError> { + let message = serde_json::to_value(self.decode_or_skip(msg)).ok(); + + let payload = WebhookPayload { + height: tx.height.value(), + message_index: index as u64, + transaction_hash: tx.hash.to_string(), + message, + }; + + for webhook in self.webhooks.clone() { + let payload = payload.clone(); + tokio::spawn(async move { + webhook + .invoke_webhook(&payload) + .await + .expect("webhook failed to process"); + }); + } + + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct Webhook { + webhook_url: Url, + config: WebhookConfig, +} + +impl Webhook { + pub(crate) fn new(config: WebhookConfig) -> anyhow::Result { + Ok(Webhook { + webhook_url: config + .webhook_url + .as_str() + .parse() + .context("invalid config: provided webhook URL is malformed")?, + config, + }) + } + + pub(crate) 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!( + "nym-data-observatory/{}/webhook-{}", + env!("CARGO_PKG_VERSION"), + self.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!( + "[webhook = {}] ✅ Webhook {} {} - tx {}, index {}", + self.config.id, + res.status(), + res.url(), + payload.transaction_hash, + payload.message_index, + ), + Err(err) => { + error!( + "[webhook = {}] ❌ Webhook {:?} {:?} error = {err}", + self.config.id, + err.status(), + err.url(), + ); + return Err(err.into()); + } + } + + Ok(()) + } +} diff --git a/nym-data-observatory/src/cli/commands/build_info.rs b/nym-data-observatory/src/cli/commands/build_info.rs new file mode 100644 index 0000000000..e3957b66fa --- /dev/null +++ b/nym-data-observatory/src/cli/commands/build_info.rs @@ -0,0 +1,17 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymDataObservatoryError; +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) -> Result<(), NymDataObservatoryError> { + println!("{}", args.output.format(&bin_info_owned!())); + Ok(()) +} diff --git a/nym-data-observatory/src/cli/commands/init.rs b/nym-data-observatory/src/cli/commands/init.rs new file mode 100644 index 0000000000..1c1fbde3e2 --- /dev/null +++ b/nym-data-observatory/src/cli/commands/init.rs @@ -0,0 +1,46 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::DEFAULT_NYM_DATA_OBSERVATORY_ID; +use crate::config::data_observatory::HttpAuthenticationOptions::AuthorizationBearerToken; +use crate::config::data_observatory::WebhookConfig; +use crate::config::{default_config_filepath, Config, ConfigBuilder, DataObservatoryConfig}; +use crate::env::vars::*; +use crate::error::NymDataObservatoryError; +use nym_config::save_unformatted_config_to_file; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + /// (Override) Postgres connection string for data storage + #[arg(long, env = NYM_DATA_OBSERVATORY_DB_URL, alias = "db_url")] + pub(crate) chain_history_db_connection_string: String, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NymDataObservatoryError> { + let config_path = default_config_filepath(); + let data_dir = Config::default_data_directory(&config_path)?; + + let builder = ConfigBuilder::new( + config_path.clone(), + data_dir, + args.chain_history_db_connection_string, + ) + .with_data_observatory_config(DataObservatoryConfig { + webhooks: vec![WebhookConfig { + id: DEFAULT_NYM_DATA_OBSERVATORY_ID.to_string(), + webhook_url: "https://webhook.site".to_string(), + authentication: Some(AuthorizationBearerToken { + token: "1234".to_string(), + }), + description: None, + watch_for_chain_message_types: vec![ + "/cosmos.bank.v1beta1.MsgSend".to_string(), + "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + ], + }], + }); + + let config = builder.build(); + + Ok(save_unformatted_config_to_file(&config, &config_path)?) +} diff --git a/nym-data-observatory/src/cli/commands/mod.rs b/nym-data-observatory/src/cli/commands/mod.rs new file mode 100644 index 0000000000..b1f63f4ae5 --- /dev/null +++ b/nym-data-observatory/src/cli/commands/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod build_info; +pub(crate) mod init; +pub(crate) mod run; diff --git a/nym-data-observatory/src/cli/commands/run/args.rs b/nym-data-observatory/src/cli/commands/run/args.rs new file mode 100644 index 0000000000..dc34af5715 --- /dev/null +++ b/nym-data-observatory/src/cli/commands/run/args.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::env::vars::*; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + /// (Override) Postgres connection string for chain scraper history + #[arg(long, env = NYM_DATA_OBSERVATORY_DB_URL, alias = "db_url")] + pub(crate) db_connection_string: Option, + + /// (Override) Watch for chain messages of these types + #[clap( + long, + value_delimiter = ',', + env = NYM_DATA_OBSERVATORY_WATCH_CHAIN_MESSAGE_TYPES + )] + pub watch_for_chain_message_types: Vec, + + /// (Override) The webhook to call when we find something + #[clap( + long, + env = NYM_DATA_OBSERVATORY_WEBHOOK_URL + )] + pub webhook_url: Option, + + /// (Override) Optionally, authenticate with the webhook + #[clap( + long, + env = NYM_DATA_OBSERVATORY_WEBHOOK_AUTH + )] + pub webhook_auth: Option, +} diff --git a/nym-data-observatory/src/cli/commands/run/config.rs b/nym-data-observatory/src/cli/commands/run/config.rs new file mode 100644 index 0000000000..0519b149ed --- /dev/null +++ b/nym-data-observatory/src/cli/commands/run/config.rs @@ -0,0 +1,68 @@ +use crate::cli::commands::run::args::Args; +use crate::cli::DEFAULT_NYM_DATA_OBSERVATORY_ID; +use crate::config::data_observatory::{HttpAuthenticationOptions, WebhookConfig}; +use crate::config::{default_config_filepath, Config, ConfigBuilder, DataObservatoryConfig}; +use crate::error::NymDataObservatoryError; +use tracing::{info, warn}; + +pub(crate) fn get_run_config(args: Args) -> Result { + info!("{args:#?}"); + + let Args { + mut watch_for_chain_message_types, + webhook_auth, + webhook_url, + .. + } = args; + + // if there are no args set, then try load the config + if args.db_connection_string.is_none() { + info!("Loading default config file..."); + return Config::read_from_toml_file_in_default_location(); + } + + // set default messages + if watch_for_chain_message_types.is_empty() { + watch_for_chain_message_types = vec!["/cosmos.bank.v1beta1.MsgSend".to_string()]; + } + + let config_path = default_config_filepath(); + let data_dir = Config::default_data_directory(&config_path)?; + + if args.db_connection_string.is_none() { + return Err(NymDataObservatoryError::DbConnectionStringMissing); + } + + let mut builder = ConfigBuilder::new( + config_path, + data_dir, + args.db_connection_string + .expect("db connection string is required"), + ); + + if let Some(webhook_url) = webhook_url { + let authentication = + webhook_auth.map(|token| HttpAuthenticationOptions::AuthorizationBearerToken { token }); + + let watcher_config = DataObservatoryConfig { + webhooks: vec![WebhookConfig { + id: DEFAULT_NYM_DATA_OBSERVATORY_ID.to_string(), + description: None, + watch_for_chain_message_types, + webhook_url, + authentication, + }], + }; + + info!("Overriding watcher config with env vars"); + + builder = builder.with_data_observatory_config(watcher_config); + } else { + warn!( + "You did not specify a webhook in {}. Only database items will be stored.", + crate::env::vars::NYM_DATA_OBSERVATORY_WEBHOOK_URL + ); + } + + Ok(builder.build()) +} diff --git a/nym-data-observatory/src/cli/commands/run/mod.rs b/nym-data-observatory/src/cli/commands/run/mod.rs new file mode 100644 index 0000000000..eb7ee796f0 --- /dev/null +++ b/nym-data-observatory/src/cli/commands/run/mod.rs @@ -0,0 +1,211 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymDataObservatoryError; +use anyhow::Context; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::task::{JoinHandle, JoinSet}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; + +mod args; +mod config; + +use crate::chain_scraper::run_chain_scraper; +use crate::db::DbPool; +use crate::http::state::PriceScraperState; +use crate::price_scraper::PriceScraper; +use crate::{db, http}; +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, +) { + let _ = sqlx::query!( + r#" + INSERT INTO watcher_execution(start_ts, end_ts, error_message) + VALUES ($1, $2, $3) + "#, + start.into(), + end.into(), + 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>>, +) { + async fn finalize_shutdown( + db_pool: DbPool, + start: OffsetDateTime, + main_cancellation_token: CancellationToken, + scraper_cancellation_token: CancellationToken, + mut tasks: JoinSet>>, + error_message: Option, + ) { + // 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<(), NymDataObservatoryError> { + let start = OffsetDateTime::now_utc(); + + info!("passed arguments: {args:#?}"); + + let config = config::get_run_config(args)?; + + let db_connection_string = config.chain_scraper_connection_string(); + + info!("Config is {config:#?}"); + info!( + "Chain History Database path is {:?}", + std::path::Path::new(&config.chain_scraper_connection_string()).canonicalize() + ); + + let storage = db::Storage::init(db_connection_string).await?; + let watcher_pool = storage.pool_owned(); + + let mut tasks = JoinSet::new(); + let cancellation_token = CancellationToken::new(); + + let scraper_pool = storage.pool_owned(); + let shutdown_pool = storage.pool_owned(); + + // construct shared state + let price_scraper_shared_state = PriceScraperState::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> = 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()) + } + }); + + // 2. price scraper (note, this task never terminates on its own) + let price_scraper = PriceScraper::new(price_scraper_shared_state.clone(), watcher_pool); + { + let token = cancellation_token.clone(); + tasks.spawn(async move { + token + .run_until_cancelled(async move { + price_scraper.run().await; + Ok(()) + }) + .await + }); + } + + // 3. http api + let http_server = http::server::build_http_api( + storage.pool_owned(), + &config, + http_port, + price_scraper_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 + }); + } + + // 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(()) + + } + }; + + wait_for_shutdown(shutdown_pool, start, cancellation_token, scraper_token, tasks).await + } + } + + Ok(()) +} diff --git a/nym-data-observatory/src/cli/mod.rs b/nym-data-observatory/src/cli/mod.rs new file mode 100644 index 0000000000..698bb9d293 --- /dev/null +++ b/nym-data-observatory/src/cli/mod.rs @@ -0,0 +1,67 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::commands::{build_info, init, run}; +use crate::env::vars::*; +use crate::error::NymDataObservatoryError; +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::sync::OnceLock; + +mod commands; + +pub const DEFAULT_NYM_DATA_OBSERVATORY_ID: &str = "default-nym-data-observatory"; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + /// Path pointing to an env file that configures the nym-data-observatory and overrides any preconfigured values. + #[clap( + short, + long, + env = NYM_DATA_OBSERVATORY_CONFIG_ENV_FILE_ARG + )] + pub(crate) config_env_file: Option, + + /// Flag used for disabling the printed banner in tty. + #[clap( + long, + env = NYM_DATA_OBSERVATORY_NO_BANNER_ARG + )] + pub(crate) no_banner: bool, + + /// Port to listen on + #[arg(long, default_value_t = 8000, env = "NYM_DATA_OBSERVATORY_HTTP_PORT")] + pub(crate) http_port: u16, + + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub(crate) async fn execute(self) -> Result<(), NymDataObservatoryError> { + match self.command { + Commands::BuildInfo(args) => build_info::execute(args), + Commands::Run(args) => run::execute(*args, self.http_port).await, + Commands::Init(args) => init::execute(args).await, + } + } +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Show build information of this binary + BuildInfo(build_info::Args), + + /// Start this nym-chain-watcher + Run(Box), + + /// Initialise config + Init(init::Args), +} diff --git a/nym-data-observatory/src/config/data_observatory.rs b/nym-data-observatory/src/config/data_observatory.rs new file mode 100644 index 0000000000..36ef39e881 --- /dev/null +++ b/nym-data-observatory/src/config/data_observatory.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct DataObservatoryConfig { + pub webhooks: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookConfig { + pub id: String, + pub description: Option, + pub webhook_url: String, + pub watch_for_chain_message_types: Vec, + pub authentication: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HttpAuthenticationOptions { + AuthorizationBearerToken { token: String }, +} diff --git a/nym-data-observatory/src/config/mod.rs b/nym-data-observatory/src/config/mod.rs new file mode 100644 index 0000000000..3e5b46dcbc --- /dev/null +++ b/nym-data-observatory/src/config/mod.rs @@ -0,0 +1,219 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::template::CONFIG_TEMPLATE; +use nym_bin_common::logging::LoggingSettings; +use nym_config::{ + must_get_home, read_config_from_toml_file, save_unformatted_config_to_file, NymConfigTemplate, + DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, +}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use tracing::{debug, error}; + +pub(crate) mod data_observatory; +mod template; + +pub use crate::config::data_observatory::DataObservatoryConfig; +use crate::error::NymDataObservatoryError; + +const DEFAULT_NYM_DATA_OBSERVATORY_DIR: &str = "nym-data-observatory"; + +/// Derive default path to nym-data-observatory's config directory. +/// It should get resolved to `$HOME/.nym/nym-data-observatory/config` +pub fn default_config_directory() -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_NYM_DATA_OBSERVATORY_DIR) + .join(DEFAULT_CONFIG_DIR) +} + +/// Derive default path to nym-data-observatory's config file. +/// It should get resolved to `$HOME/.nym/nym-data-observatory/config/config.toml` +pub fn default_config_filepath() -> PathBuf { + default_config_directory().join(DEFAULT_CONFIG_FILENAME) +} + +pub struct ConfigBuilder { + pub config_path: PathBuf, + + pub data_dir: PathBuf, + + pub chain_scraper_connection_string: String, + pub data_observatory_config: Option, + + pub logging: Option, +} + +impl ConfigBuilder { + pub fn new( + config_path: PathBuf, + data_dir: PathBuf, + chain_scraper_connection_string: String, + ) -> Self { + ConfigBuilder { + config_path, + data_dir, + data_observatory_config: None, + logging: None, + chain_scraper_connection_string, + } + } + + #[allow(dead_code)] + pub fn with_data_observatory_config( + mut self, + data_observatory_config: impl Into, + ) -> Self { + self.data_observatory_config = Some(data_observatory_config.into()); + self + } + + #[allow(dead_code)] + pub fn with_logging(mut self, section: impl Into>) -> Self { + self.logging = section.into(); + self + } + + pub fn build(self) -> Config { + Config { + logging: self.logging.unwrap_or_default(), + save_path: Some(self.config_path), + data_observatory_config: self.data_observatory_config.unwrap_or_default(), + data_dir: self.data_dir, + chain_scraper_connection_string: self.chain_scraper_connection_string, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + #[serde(skip)] + pub(crate) data_dir: PathBuf, + + pub chain_scraper_connection_string: String, + + #[serde(default)] + pub data_observatory_config: DataObservatoryConfig, + + #[serde(default)] + pub logging: LoggingSettings, +} + +impl NymConfigTemplate for Config { + fn template(&self) -> &'static str { + CONFIG_TEMPLATE + } +} + +impl Config { + #[allow(unused)] + pub fn save(&self) -> Result<(), NymDataObservatoryError> { + let save_location = self.save_location(); + debug!( + "attempting to save config file to '{}'", + save_location.display() + ); + save_unformatted_config_to_file(self, &save_location).map_err(|source| { + NymDataObservatoryError::UnformattedConfigSaveFailure { + path: save_location, + source, + } + }) + } + + #[allow(unused)] + pub fn save_location(&self) -> PathBuf { + self.save_path + .clone() + .unwrap_or(self.default_save_location()) + } + + #[allow(unused)] + pub fn default_save_location(&self) -> PathBuf { + default_config_filepath() + } + + pub fn default_data_directory>( + config_path: P, + ) -> Result { + let config_path = config_path.as_ref(); + + // we got a proper path to the .toml file + let Some(config_dir) = config_path.parent() else { + error!( + "'{}' does not have a parent directory. Have you pointed to the fs root?", + config_path.display() + ); + return Err(NymDataObservatoryError::DataDirDerivationFailure); + }; + + let Some(config_dir_name) = config_dir.file_name() else { + error!( + "could not obtain parent directory name of '{}'. Have you used relative paths?", + config_path.display() + ); + return Err(NymDataObservatoryError::DataDirDerivationFailure); + }; + + if config_dir_name != DEFAULT_CONFIG_DIR { + error!( + "the parent directory of '{}' ({}) is not {DEFAULT_CONFIG_DIR}. currently this is not supported", + config_path.display(), config_dir_name.to_str().unwrap_or("UNKNOWN") + ); + return Err(NymDataObservatoryError::DataDirDerivationFailure); + } + + let Some(node_dir) = config_dir.parent() else { + error!( + "'{}' does not have a parent directory. Have you pointed to the fs root?", + config_dir.display() + ); + return Err(NymDataObservatoryError::DataDirDerivationFailure); + }; + + Ok(node_dir.join(DEFAULT_DATA_DIR)) + } + + pub fn chain_scraper_connection_string(&self) -> String { + self.chain_scraper_connection_string.clone() + } + + // simple wrapper that reads config file and assigns path location + fn read_from_path>( + path: P, + data_dir: P, + ) -> Result { + let path = path.as_ref(); + let data_dir = data_dir.as_ref(); + let mut loaded: Config = read_config_from_toml_file(path).map_err(|source| { + NymDataObservatoryError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + } + })?; + loaded.data_dir = data_dir.to_path_buf(); + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } + + #[allow(unused)] + pub fn read_from_toml_file>( + path: P, + data_dir: P, + ) -> Result { + Self::read_from_path(path, data_dir) + } + + pub fn read_from_toml_file_in_default_location() -> Result { + let config_path = default_config_filepath(); + let data_dir = Config::default_data_directory(&config_path)?; + Self::read_from_path(config_path, data_dir) + } +} diff --git a/nym-data-observatory/src/config/template.rs b/nym-data-observatory/src/config/template.rs new file mode 100644 index 0000000000..ddc3475aac --- /dev/null +++ b/nym-data-observatory/src/config/template.rs @@ -0,0 +1,29 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +// While using normal toml marshalling would have been way simpler with less overhead, +// I think it's useful to have comments attached to the saved config file to explain behaviour of +// particular fields. +// Note: any changes to the template must be reflected in the appropriate structs. +pub(crate) const CONFIG_TEMPLATE: &str = r#" +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +[data_observatory_config] +{{#each data_observatory_config.webhooks }} +[[webhooks]] +id={{this.id}} +description='{{this.description}}' +webhook_url='{{this.webhook_url}}' +{{/each}} + + + + +##### logging configuration options ##### + +[logging] + +# TODO + +"#; diff --git a/nym-data-observatory/src/db/mod.rs b/nym-data-observatory/src/db/mod.rs new file mode 100644 index 0000000000..c00fa97613 --- /dev/null +++ b/nym-data-observatory/src/db/mod.rs @@ -0,0 +1,38 @@ +use anyhow::{anyhow, Result}; +use sqlx::{migrate::Migrator, postgres::PgConnectOptions, Postgres}; +use std::str::FromStr; + +pub(crate) mod models; +pub(crate) mod queries { + pub mod price; +} + +static _MIGRATOR: Migrator = sqlx::migrate!("./migrations"); + +pub(crate) type DbPool = sqlx::Pool; + +pub(crate) struct Storage { + pool: DbPool, +} + +impl Storage { + pub async fn init(connection_url: String) -> Result { + let connect_options = PgConnectOptions::from_str(&connection_url)?; + + let pool = DbPool::connect_with(connect_options) + .await + .map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?; + + // MIGRATOR + // .run(&pool) + // .await + // .map_err(|err| anyhow!("Failed to run migrations: {}", err))?; + + Ok(Storage { pool }) + } + + /// Cloning pool is cheap, it's the same underlying set of connections + pub fn pool_owned(&self) -> DbPool { + self.pool.clone() + } +} diff --git a/nym-data-observatory/src/db/models.rs b/nym-data-observatory/src/db/models.rs new file mode 100644 index 0000000000..c2ec1fcbcd --- /dev/null +++ b/nym-data-observatory/src/db/models.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use time::OffsetDateTime; +use utoipa::ToSchema; + +#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)] +pub(crate) struct CurrencyPrices { + pub(crate) chf: f64, + pub(crate) usd: f64, + pub(crate) eur: f64, + pub(crate) gbp: f64, + pub(crate) btc: f64, +} + +// Struct to hold Coingecko response +#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)] +pub(crate) struct CoingeckoPriceResponse { + pub(crate) nym: CurrencyPrices, +} + +#[derive(Clone, Deserialize, Debug, ToSchema)] +pub(crate) struct PriceRecord { + pub(crate) timestamp: i64, + pub(crate) nym: CurrencyPrices, +} + +#[derive(Serialize, Deserialize, Debug, ToSchema)] +pub(crate) struct PriceHistory { + pub(crate) timestamp: i64, + pub(crate) chf: f64, + pub(crate) usd: f64, + pub(crate) eur: f64, + pub(crate) gbp: f64, + pub(crate) btc: f64, +} + +#[derive(Serialize, Deserialize, Debug, ToSchema)] +pub(crate) struct PaymentRecord { + pub(crate) transaction_hash: String, + pub(crate) sender_address: String, + pub(crate) receiver_address: String, + pub(crate) amount: f64, + 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, + pub(crate) created_at: Option, +} diff --git a/nym-data-observatory/src/db/queries/mod.rs b/nym-data-observatory/src/db/queries/mod.rs new file mode 100644 index 0000000000..e75aa1606a --- /dev/null +++ b/nym-data-observatory/src/db/queries/mod.rs @@ -0,0 +1,5 @@ +mod price; + +// re-exporting allows us to access all queries via `queries::bla`` +pub(crate) use payments::{get_last_checked_height, insert_payment}; +pub(crate) use price::{get_latest_price, insert_nym_prices}; diff --git a/nym-data-observatory/src/db/queries/price.rs b/nym-data-observatory/src/db/queries/price.rs new file mode 100644 index 0000000000..4a6ddbeea3 --- /dev/null +++ b/nym-data-observatory/src/db/queries/price.rs @@ -0,0 +1,119 @@ +use crate::db::models::{PriceHistory, PriceRecord}; +use crate::db::DbPool; +use chrono::Local; +use std::ops::Sub; + +pub(crate) async fn insert_nym_prices( + pool: &DbPool, + price_data: PriceRecord, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + let timestamp = price_data.timestamp; + sqlx::query!( + "INSERT INTO price_history + (timestamp, chf, usd, eur, gbp, btc) + VALUES + ($1, $2, $3, $4, $5, $6) + ON CONFLICT(timestamp) DO UPDATE SET + chf=excluded.chf, + usd=excluded.usd, + eur=excluded.eur, + gbp=excluded.gbp, + btc=excluded.btc;", + timestamp, + price_data.nym.chf, + price_data.nym.usd, + price_data.nym.eur, + price_data.nym.gbp, + price_data.nym.btc, + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} + +pub(crate) async fn get_latest_price(pool: &DbPool) -> anyhow::Result { + let result = sqlx::query!( + "SELECT timestamp, chf, usd, eur, gbp, btc FROM price_history ORDER BY timestamp DESC LIMIT 1;" + ) + .fetch_one(pool) + .await?; + + Ok(PriceHistory { + timestamp: result.timestamp, + chf: result.chf, + usd: result.usd, + eur: result.eur, + gbp: result.gbp, + btc: result.btc, + }) +} + +pub(crate) async fn get_average_price(pool: &DbPool) -> anyhow::Result { + // now less 1 day + let earliest_timestamp = Local::now().sub(chrono::Duration::days(1)).timestamp(); + + let result = sqlx::query!( + "SELECT timestamp, chf, usd, eur, gbp, btc FROM price_history WHERE timestamp >= $1;", + earliest_timestamp + ) + .fetch_all(pool) + .await?; + + let mut price = PriceHistory { + timestamp: Local::now().timestamp(), + chf: 0f64, + usd: 0f64, + eur: 0f64, + gbp: 0f64, + btc: 0f64, + }; + + let mut chf_count = 0; + let mut usd_count = 0; + let mut eur_count = 0; + let mut gbp_count = 0; + let mut btc_count = 0; + + for p in &result { + if p.chf != 0f64 { + price.chf += p.chf; + chf_count += 1; + } + if p.usd != 0f64 { + price.usd += p.usd; + usd_count += 1; + } + if p.eur != 0f64 { + price.eur += p.eur; + eur_count += 1; + } + if p.gbp != 0f64 { + price.gbp += p.gbp; + gbp_count += 1; + } + if p.btc != 0f64 { + price.btc += p.btc; + btc_count += 1; + } + } + + if chf_count > 0 { + price.chf /= chf_count as f64; + } + if usd_count > 0 { + price.usd /= usd_count as f64; + } + if eur_count > 0 { + price.eur /= eur_count as f64; + } + if gbp_count > 0 { + price.gbp /= gbp_count as f64; + } + if btc_count > 0 { + price.btc /= btc_count as f64; + } + + Ok(price) +} diff --git a/nym-data-observatory/src/env.rs b/nym-data-observatory/src/env.rs new file mode 100644 index 0000000000..45f054a839 --- /dev/null +++ b/nym-data-observatory/src/env.rs @@ -0,0 +1,27 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#[allow(unused)] +pub mod vars { + pub const NYM_DATA_OBSERVATORY_NO_BANNER_ARG: &str = "NYM_DATA_OBSERVATORY_NO_BANNER"; + pub const NYM_DATA_OBSERVATORY_CONFIG_ENV_FILE_ARG: &str = + "NYM_DATA_OBSERVATORY_CONFIG_ENV_FILE_ARG"; + + pub const NYM_DATA_OBSERVATORY_DB_URL: &str = "NYM_DATA_OBSERVATORY_DB_URL"; + + pub const NYXD_SCRAPER_START_HEIGHT: &str = "NYXD_SCRAPER_START_HEIGHT"; + pub const NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT: &str = + "NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT"; + + pub const NYXD_SCRAPER_UNSAFE_NUKE_DB: &str = "NYXD_SCRAPER_UNSAFE_NUKE_DB"; + + pub const NYM_DATA_OBSERVATORY_ID_ARG: &str = "NYM_DATA_OBSERVATORY_ID"; + pub const NYM_DATA_OBSERVATORY_OUTPUT_ARG: &str = "NYM_DATA_OBSERVATORY_OUTPUT"; + + pub const NYM_DATA_OBSERVATORY_CONFIG_PATH_ARG: &str = "NYM_DATA_OBSERVATORY_CONFIG"; + + pub const NYM_DATA_OBSERVATORY_WATCH_CHAIN_MESSAGE_TYPES: &str = + "NYM_DATA_OBSERVATORY_WATCH_CHAIN_MESSAGE_TYPES"; + pub const NYM_DATA_OBSERVATORY_WEBHOOK_URL: &str = "NYM_DATA_OBSERVATORY_WEBHOOK_URL"; + pub const NYM_DATA_OBSERVATORY_WEBHOOK_AUTH: &str = "NYM_DATA_OBSERVATORY_WEBHOOK_AUTH"; +} diff --git a/nym-data-observatory/src/error.rs b/nym-data-observatory/src/error.rs new file mode 100644 index 0000000000..86beec2f5f --- /dev/null +++ b/nym-data-observatory/src/error.rs @@ -0,0 +1,43 @@ +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NymDataObservatoryError { + // #[error("failed to save config file using path '{}'. detailed message: {source}", path.display())] + // ConfigSaveFailure { + // path: PathBuf, + // #[source] + // source: io::Error, + // }, + #[error("failed to save config file using path '{}'. detailed message: {source}", path.display())] + UnformattedConfigSaveFailure { + path: PathBuf, + #[source] + source: nym_config::error::NymConfigTomlError, + }, + + #[error("could not derive path to data directory of this nyx chain watcher")] + DataDirDerivationFailure, + + #[error("please provide a database connection string as an env var, cli argument or in a config file")] + DbConnectionStringMissing, + + // #[error("could not derive path to config directory of this nyx chain watcher")] + // ConfigDirDerivationFailure, + #[error("failed to load config file using path '{}'. detailed message: {source}", path.display())] + ConfigLoadFailure { + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error(transparent)] + FileIoFailure(#[from] io::Error), + + #[error(transparent)] + AnyhowFailure(#[from] anyhow::Error), + + #[error(transparent)] + NymConfigTomlE(#[from] nym_config::error::NymConfigTomlError), +} diff --git a/nym-data-observatory/src/http/api/mod.rs b/nym-data-observatory/src/http/api/mod.rs new file mode 100644 index 0000000000..4d2b84ca29 --- /dev/null +++ b/nym-data-observatory/src/http/api/mod.rs @@ -0,0 +1,79 @@ +use anyhow::anyhow; +use axum::{response::Redirect, Router}; +use tokio::net::ToSocketAddrs; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::http::{api_docs, server::HttpServer, state::AppState}; + +pub(crate) mod price; +pub(crate) mod status; + +pub(crate) struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + pub(crate) fn with_default_routes() -> Self { + let router = Router::new() + .merge( + SwaggerUi::new("/swagger") + .url("/api-docs/openapi.json", api_docs::ApiDoc::openapi()), + ) + .route( + "/", + axum::routing::get(|| async { Redirect::permanent("/swagger") }), + ) + .nest( + "/v1", + Router::new() + .nest("/status", status::routes()) + .nest("/price", price::routes()), + ); + + Self { + unfinished_router: router, + } + } + + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + fn finalize_routes(self) -> Router { + // layers added later wrap earlier layers + self.unfinished_router + // CORS layer needs to wrap other API layers + .layer(setup_cors()) + // logger should be outermost layer + .layer(TraceLayer::new_for_http()) + } +} + +pub(crate) struct RouterWithState { + router: Router, +} + +impl RouterWithState { + pub(crate) async fn build_server( + self, + bind_address: A, + ) -> anyhow::Result { + tokio::net::TcpListener::bind(bind_address) + .await + .map(|listener| HttpServer::new(self.router, listener)) + .map_err(|err| anyhow!("Couldn't bind to address due to {}", err)) + } +} + +fn setup_cors() -> CorsLayer { + use axum::http::Method; + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([Method::POST, Method::GET, Method::PATCH, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} diff --git a/nym-data-observatory/src/http/api/price.rs b/nym-data-observatory/src/http/api/price.rs new file mode 100644 index 0000000000..1971f35f7d --- /dev/null +++ b/nym-data-observatory/src/http/api/price.rs @@ -0,0 +1,44 @@ +use crate::db::models::PriceHistory; +use crate::db::queries::price::{get_average_price, get_latest_price}; +use crate::http::error::Error; +use crate::http::error::HttpResult; +use crate::http::state::AppState; +use axum::{extract::State, Json, Router}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(price)) + .route("/average", axum::routing::get(average_price)) +} + +#[utoipa::path( + tag = "NYM Price", + get, + path = "/v1/price", + responses( + (status = 200, body = String) + ) +)] +/// Fetch the latest price cached by this API +async fn price(State(state): State) -> HttpResult> { + get_latest_price(state.db_pool()) + .await + .map(Json::from) + .map_err(|_| Error::internal()) +} + +#[utoipa::path( + tag = "NYM Price", + get, + path = "/v1/price/average", + responses( + (status = 200, body = String) + ) +)] +/// Fetch the average price cached by this API +async fn average_price(State(state): State) -> HttpResult> { + get_average_price(state.db_pool()) + .await + .map(Json::from) + .map_err(|_| Error::internal()) +} diff --git a/nym-data-observatory/src/http/api/status.rs b/nym-data-observatory/src/http/api/status.rs new file mode 100644 index 0000000000..35d7168cd9 --- /dev/null +++ b/nym-data-observatory/src/http/api/status.rs @@ -0,0 +1,79 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::models::status::{ + ApiStatus, HealthResponse, PriceScraperLastError, PriceScraperLastSuccess, + PriceScraperStatusResponse, +}; +use crate::http::state::{AppState, PriceScraperState, StatusState}; +use axum::extract::State; +use axum::routing::get; +use axum::{Json, Router}; +use nym_bin_common::build_information::BinaryBuildInformationOwned; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/health", get(health)) + .route("/build-information", get(build_information)) + .route("/price-scraper", get(price_scraper_status)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/build-information", + context_path = "/v1/status", + responses( + (status = 200, body = BinaryBuildInformationOwned) + ) +)] +async fn build_information(State(state): State) -> Json { + Json(state.build_information.to_owned()) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/health", + context_path = "/v1/status", + responses( + (status = 200, body = HealthResponse) + ) +)] +async fn health(State(state): State) -> Json { + let uptime = state.startup_time.elapsed(); + + let health = HealthResponse { + status: ApiStatus::Up, + uptime: uptime.as_secs(), + }; + Json(health) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/price-scraper", + context_path = "/v1/status", + responses( + (status = 200, body = PriceScraperStatusResponse) + ) +)] +pub(crate) async fn price_scraper_status( + State(state): State, +) -> Json { + let guard = state.inner.read().await; + Json(PriceScraperStatusResponse { + last_success: guard + .last_success + .as_ref() + .map(|s| PriceScraperLastSuccess { + timestamp: s.timestamp, + response: s.response.clone(), + }), + last_failure: guard.last_failure.as_ref().map(|f| PriceScraperLastError { + timestamp: f.timestamp, + message: f.message.clone(), + }), + }) +} diff --git a/nym-data-observatory/src/http/api_docs.rs b/nym-data-observatory/src/http/api_docs.rs new file mode 100644 index 0000000000..c7dbe0118d --- /dev/null +++ b/nym-data-observatory/src/http/api_docs.rs @@ -0,0 +1,14 @@ +use utoipa::OpenApi; +use utoipauto::utoipauto; + +// manually import external structs which are behind feature flags because they +// can't be automatically discovered +// https://github.com/ProbablyClem/utoipauto/issues/13#issuecomment-1974911829 +#[utoipauto(paths = "./nym-data-observatory/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Nym Data Observatory API"), + tags(), + components(schemas()) +)] +pub(super) struct ApiDoc; diff --git a/nym-data-observatory/src/http/error.rs b/nym-data-observatory/src/http/error.rs new file mode 100644 index 0000000000..fa6b274b69 --- /dev/null +++ b/nym-data-observatory/src/http/error.rs @@ -0,0 +1,21 @@ +pub(crate) type HttpResult = Result; + +pub(crate) struct Error { + message: String, + status: axum::http::StatusCode, +} + +impl Error { + pub(crate) fn internal() -> Self { + Self { + message: String::from("Internal server error"), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl axum::response::IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + (self.status, self.message).into_response() + } +} diff --git a/nym-data-observatory/src/http/mod.rs b/nym-data-observatory/src/http/mod.rs new file mode 100644 index 0000000000..c57d17c47b --- /dev/null +++ b/nym-data-observatory/src/http/mod.rs @@ -0,0 +1,6 @@ +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; diff --git a/nym-data-observatory/src/http/models.rs b/nym-data-observatory/src/http/models.rs new file mode 100644 index 0000000000..c1798eb59d --- /dev/null +++ b/nym-data-observatory/src/http/models.rs @@ -0,0 +1,68 @@ +// Copyright 2025 - Nym Technologies SA +// 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::data_observatory::WebhookConfig; + use crate::db::models::CoingeckoPriceResponse; + use serde::{Deserialize, Serialize}; + use time::OffsetDateTime; + use utoipa::ToSchema; + + #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + #[serde(rename_all = "lowercase")] + pub enum ApiStatus { + Up, + } + + #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct HealthResponse { + pub status: ApiStatus, + pub uptime: u64, + } + + #[derive(Debug, Serialize, Deserialize, ToSchema)] + pub struct ActiveWebhooksResponse { + pub watchers: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub struct Webhook { + pub id: String, + pub description: String, + pub webhook_url: String, + pub watched_message_types: Vec, + } + + impl From<&WebhookConfig> for Webhook { + fn from(value: &WebhookConfig) -> Self { + Webhook { + id: value.id.clone(), + description: value.description.clone().unwrap_or_default(), + webhook_url: value.webhook_url.clone(), + watched_message_types: value.watch_for_chain_message_types.clone(), + } + } + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct PriceScraperStatusResponse { + pub(crate) last_success: Option, + pub(crate) last_failure: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct PriceScraperLastSuccess { + #[serde(with = "time::serde::rfc3339")] + pub(crate) timestamp: OffsetDateTime, + pub(crate) response: CoingeckoPriceResponse, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct PriceScraperLastError { + #[serde(with = "time::serde::rfc3339")] + pub(crate) timestamp: OffsetDateTime, + pub(crate) message: String, + } +} diff --git a/nym-data-observatory/src/http/server.rs b/nym-data-observatory/src/http/server.rs new file mode 100644 index 0000000000..fd01f01f87 --- /dev/null +++ b/nym-data-observatory/src/http/server.rs @@ -0,0 +1,59 @@ +use axum::Router; +use core::net::SocketAddr; +use tokio::net::TcpListener; +use tokio_util::sync::WaitForCancellationFutureOwned; + +use crate::config::Config; +use crate::http::state::PriceScraperState; +use crate::{ + db::DbPool, + http::{api::RouterBuilder, state::AppState}, +}; + +pub(crate) async fn build_http_api( + db_pool: DbPool, + config: &Config, + http_port: u16, + price_scraper_state: PriceScraperState, +) -> anyhow::Result { + let router_builder = RouterBuilder::with_default_routes(); + + let state = AppState::new( + db_pool, + config + .data_observatory_config + .webhooks + .iter() + .map(Into::into) + .collect(), + price_scraper_state, + ); + 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) +} + +pub(crate) struct HttpServer { + router: Router, + listener: TcpListener, +} + +impl HttpServer { + pub(crate) fn new(router: Router, listener: TcpListener) -> Self { + Self { router, listener } + } + + pub(crate) async fn run(self, receiver: WaitForCancellationFutureOwned) -> std::io::Result<()> { + // into_make_service_with_connect_info allows us to see client ip address + // in middleware, for logging, TLS, routing etc. + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(receiver) + .await + } +} diff --git a/nym-data-observatory/src/http/state.rs b/nym-data-observatory/src/http/state.rs new file mode 100644 index 0000000000..da24373188 --- /dev/null +++ b/nym-data-observatory/src/http/state.rs @@ -0,0 +1,124 @@ +use crate::db::models::CoingeckoPriceResponse; +use crate::db::DbPool; +use crate::http::models::status::Webhook; +use axum::extract::FromRef; +use nym_bin_common::bin_info; +use nym_bin_common::build_information::BinaryBuildInformation; +use std::ops::Deref; +use std::sync::Arc; +use time::OffsetDateTime; +use tokio::sync::RwLock; +use tokio::time::Instant; + +#[derive(Debug, Clone)] +pub(crate) struct AppState { + db_pool: DbPool, + #[allow(dead_code)] + pub(crate) registered_webhooks: Arc>, + pub(crate) status_state: StatusState, + pub(crate) price_scraper_state: PriceScraperState, +} + +impl AppState { + pub(crate) fn new( + db_pool: DbPool, + registered_payment_watchers: Vec, + price_scraper_state: PriceScraperState, + ) -> Self { + Self { + db_pool, + registered_webhooks: Arc::new(registered_payment_watchers), + status_state: Default::default(), + price_scraper_state, + } + } + + pub(crate) fn db_pool(&self) -> &DbPool { + &self.db_pool + } +} + +#[derive(Clone, Debug)] +pub(crate) struct StatusState { + inner: Arc, +} + +impl Default for StatusState { + fn default() -> Self { + StatusState { + inner: Arc::new(StatusStateInner { + startup_time: Instant::now(), + build_information: bin_info!(), + }), + } + } +} + +impl Deref for StatusState { + type Target = StatusStateInner; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Debug)] +pub(crate) struct StatusStateInner { + pub(crate) startup_time: Instant, + pub(crate) build_information: BinaryBuildInformation, +} + +#[derive(Debug, Clone)] +pub(crate) struct PriceScraperState { + pub(crate) inner: Arc>, +} + +impl PriceScraperState { + pub(crate) fn new() -> Self { + PriceScraperState { + inner: Arc::new(Default::default()), + } + } + + pub(crate) async fn new_failure>(&self, error: S) { + self.inner.write().await.last_failure = Some(PriceScraperLastError { + timestamp: OffsetDateTime::now_utc(), + message: error.into(), + }) + } + pub(crate) async fn new_success(&self, response: CoingeckoPriceResponse) { + self.inner.write().await.last_success = Some(PriceScraperLastSuccess { + timestamp: OffsetDateTime::now_utc(), + response, + }) + } +} + +#[derive(Debug, Default)] +pub(crate) struct PriceScraperStateInner { + pub(crate) last_success: Option, + pub(crate) last_failure: Option, +} + +#[derive(Debug)] +pub(crate) struct PriceScraperLastSuccess { + pub(crate) timestamp: OffsetDateTime, + pub(crate) response: CoingeckoPriceResponse, +} + +#[derive(Debug)] +pub(crate) struct PriceScraperLastError { + pub(crate) timestamp: OffsetDateTime, + pub(crate) message: String, +} + +impl FromRef for StatusState { + fn from_ref(input: &AppState) -> Self { + input.status_state.clone() + } +} + +impl FromRef for PriceScraperState { + fn from_ref(input: &AppState) -> Self { + input.price_scraper_state.clone() + } +} diff --git a/nym-data-observatory/src/logging.rs b/nym-data-observatory/src/logging.rs new file mode 100644 index 0000000000..1c8cbeebb1 --- /dev/null +++ b/nym-data-observatory/src/logging.rs @@ -0,0 +1,43 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +pub(crate) fn setup_tracing_logger() { + fn directive_checked(directive: String) -> Directive { + directive.parse().expect("Failed to parse log directive") + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + // these crates are more granularly filtered + let filter_crates = [ + "nym_bin_common", + "nym_explorer_client", + "nym_network_defaults", + "nym_validator_client", + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in filter_crates { + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))); + } + + log_builder.with_env_filter(filter).init(); +} diff --git a/nym-data-observatory/src/main.rs b/nym-data-observatory/src/main.rs new file mode 100644 index 0000000000..3a62b35d37 --- /dev/null +++ b/nym-data-observatory/src/main.rs @@ -0,0 +1,34 @@ +use clap::{crate_name, crate_version, Parser}; +use nym_bin_common::bin_info_owned; +use nym_bin_common::logging::maybe_print_banner; +use nym_network_defaults::setup_env; +use tracing::info; + +mod chain_scraper; +mod cli; +mod config; +mod db; +mod env; +mod error; +mod http; +mod logging; +pub mod models; +mod price_scraper; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = cli::Cli::parse(); + setup_env(cli.config_env_file.as_ref()); + logging::setup_tracing_logger(); + + if !cli.no_banner { + maybe_print_banner(crate_name!(), crate_version!()); + } + + let bin_info = bin_info_owned!(); + info!("using the following version: {bin_info}"); + + cli.execute().await?; + + Ok(()) +} diff --git a/nym-data-observatory/src/models.rs b/nym-data-observatory/src/models.rs new file mode 100644 index 0000000000..44d983fac4 --- /dev/null +++ b/nym-data-observatory/src/models.rs @@ -0,0 +1,22 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::gen::serde_json; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, JsonSchema, ToSchema)] +pub struct WebhookPayload { + pub height: u64, + pub transaction_hash: String, + pub message_index: u64, + pub message: Option, +} + +pub mod openapi_schema { + use super::*; + + #[derive(ToSchema)] + pub struct Coin { + pub denom: String, + pub amount: String, + } +} diff --git a/nym-data-observatory/src/price_scraper/mod.rs b/nym-data-observatory/src/price_scraper/mod.rs new file mode 100644 index 0000000000..6442118579 --- /dev/null +++ b/nym-data-observatory/src/price_scraper/mod.rs @@ -0,0 +1,76 @@ +use crate::db::{ + models::{CoingeckoPriceResponse, PriceRecord}, + queries::price::insert_nym_prices, +}; +use core::str; +use tokio::time::Duration; + +use crate::db::DbPool; +use crate::http::state::PriceScraperState; + +const REFRESH_DELAY: Duration = Duration::from_secs(300); +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) struct PriceScraper { + shared_state: PriceScraperState, + db_pool: DbPool, +} + +impl PriceScraper { + pub(crate) fn new(shared_state: PriceScraperState, db_pool: DbPool) -> Self { + PriceScraper { + shared_state, + db_pool, + } + } + + async fn get_coingecko_prices(&self) -> anyhow::Result { + tracing::info!("💰 Fetching CoinGecko prices from {COINGECKO_API_URL}"); + + let response = reqwest::get(COINGECKO_API_URL) + .await? + .json::() + .await; + + tracing::info!("Got response {:?}", response); + match response { + Ok(resp) => { + let price_record = PriceRecord { + timestamp: time::OffsetDateTime::now_utc().unix_timestamp(), + nym: resp.nym.clone(), + }; + + insert_nym_prices(&self.db_pool, price_record).await?; + Ok(resp) + } + Err(err) => { + //tracing::info!("💰 CoinGecko price response: {:?}", response); + tracing::error!("Error sending request: {err}"); + Err(err.into()) + } + } + } + + pub(crate) async fn run(&self) { + loop { + tracing::info!("Running in a loop 🏃"); + match self.get_coingecko_prices().await { + Ok(coingecko_price_response) => { + self.shared_state + .new_success(coingecko_price_response) + .await; + tracing::info!("✅ Successfully fetched CoinGecko prices"); + tokio::time::sleep(REFRESH_DELAY).await; + } + Err(err) => { + tracing::error!("❌ Failed to get CoinGecko prices: {err}"); + tracing::info!("Retrying in {}s...", FAILURE_RETRY_DELAY.as_secs()); + self.shared_state.new_failure(err.to_string()).await; + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } + } + } + } +} diff --git a/nyx-chain-watcher/src/cli/commands/run/mod.rs b/nyx-chain-watcher/src/cli/commands/run/mod.rs index 150738dfb6..704a292141 100644 --- a/nyx-chain-watcher/src/cli/commands/run/mod.rs +++ b/nyx-chain-watcher/src/cli/commands/run/mod.rs @@ -15,7 +15,7 @@ mod config; use crate::chain_scraper::run_chain_scraper; use crate::db::DbPool; use crate::http::state::{BankScraperModuleState, PaymentListenerState, PriceScraperState}; -use crate::payment_listener::PaymentListener; +use crate::listener::PaymentListener; use crate::price_scraper::PriceScraper; use crate::{db, http}; pub(crate) use args::Args; diff --git a/nyx-chain-watcher/src/payment_listener/mod.rs b/nyx-chain-watcher/src/listener/mod.rs similarity index 98% rename from nyx-chain-watcher/src/payment_listener/mod.rs rename to nyx-chain-watcher/src/listener/mod.rs index f941e6c012..f98db43521 100644 --- a/nyx-chain-watcher/src/payment_listener/mod.rs +++ b/nyx-chain-watcher/src/listener/mod.rs @@ -7,8 +7,8 @@ use crate::db::queries; use crate::http::state::{ PaymentListenerFailureDetails, PaymentListenerState, ProcessedPayment, WatcherFailureDetails, }; +use crate::listener::watcher::PaymentWatcher; use crate::models::WebhookPayload; -use crate::payment_listener::watcher::PaymentWatcher; use anyhow::Context; use sqlx::SqlitePool; use tokio::time::{self, Duration}; diff --git a/nyx-chain-watcher/src/payment_listener/watcher.rs b/nyx-chain-watcher/src/listener/watcher.rs similarity index 100% rename from nyx-chain-watcher/src/payment_listener/watcher.rs rename to nyx-chain-watcher/src/listener/watcher.rs diff --git a/nyx-chain-watcher/src/main.rs b/nyx-chain-watcher/src/main.rs index 725eee7a06..8281be987c 100644 --- a/nyx-chain-watcher/src/main.rs +++ b/nyx-chain-watcher/src/main.rs @@ -12,9 +12,9 @@ mod env; mod error; pub(crate) mod helpers; mod http; +mod listener; mod logging; pub mod models; -mod payment_listener; mod price_scraper; #[tokio::main]