From 4d609a071fc3ec69bccf76447c2093d26dc0bb14 Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Tue, 25 Feb 2025 13:56:53 +0100 Subject: [PATCH] transaction records for payment watcher - bearer token not implemented yet - needs to be ran and tested - maybe overkill passing two params to retrieve the records - maybe try just one --- Cargo.lock | 2 ++ nyx-chain-watcher/Cargo.toml | 2 ++ nyx-chain-watcher/README.md | 12 +++---- .../src/cli/commands/run/args.rs | 9 +++++ .../src/cli/commands/run/config.rs | 6 ++++ nyx-chain-watcher/src/config/mod.rs | 12 +++++++ nyx-chain-watcher/src/db/models.rs | 4 ++- nyx-chain-watcher/src/db/queries/payments.rs | 33 ++++++++++++++++++- nyx-chain-watcher/src/env.rs | 1 + nyx-chain-watcher/src/http/api/mod.rs | 4 ++- nyx-chain-watcher/src/http/api/records.rs | 30 +++++++++++++++++ nyx-chain-watcher/src/http/error.rs | 8 +++++ 12 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 nyx-chain-watcher/src/http/api/records.rs diff --git a/Cargo.lock b/Cargo.lock index 654fb80097..0bea2c6c47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7263,6 +7263,7 @@ dependencies = [ "axum 0.7.9", "chrono", "clap", + "lazy_static", "nym-bin-common", "nym-config", "nym-network-defaults", @@ -7270,6 +7271,7 @@ dependencies = [ "nym-task", "nym-validator-client", "nyxd-scraper", + "regex", "reqwest 0.12.4", "rocket", "schemars", diff --git a/nyx-chain-watcher/Cargo.toml b/nyx-chain-watcher/Cargo.toml index 285ca3c319..f8dff6ad16 100644 --- a/nyx-chain-watcher/Cargo.toml +++ b/nyx-chain-watcher/Cargo.toml @@ -44,6 +44,8 @@ tower-http = { workspace = true, features = ["cors", "trace"] } utoipa = { workspace = true, features = ["axum_extras", "time"] } utoipa-swagger-ui = { workspace = true, features = ["axum"] } utoipauto = { workspace = true } +regex = "1.11.1" +lazy_static = "1.5.0" [build-dependencies] diff --git a/nyx-chain-watcher/README.md b/nyx-chain-watcher/README.md index 956828d4ef..9e325a7118 100644 --- a/nyx-chain-watcher/README.md +++ b/nyx-chain-watcher/README.md @@ -7,12 +7,12 @@ Look in [env.rs](./src/env.rs) for the names of environment variables that can b ## Running locally ``` -NYX_CHAIN_WATCHER_HISTORY_DATABASE_PATH=chain_history.sqlite \ -NYX_CHAIN_WATCHER_DATABASE_PATH=nyx_chain_watcher.sqlite \ -NYX_CHAIN_WATCHER_WATCH_ACCOUNTS=n1...,n1...,n1... \ -NYX_CHAIN_WATCHER_WATCH_CHAIN_MESSAGE_TYPES="/cosmos.bank.v1beta1.MsgSend,/ibc.applications.transfer.v1.MsgTransfer" -NYX_CHAIN_WATCHER_WEBHOOK_URL="https://webhook.site" \ -NYX_CHAIN_WATCHER_WEBHOOK_AUTH=1234 \ +export NYX_CHAIN_WATCHER_HISTORY_DATABASE_PATH=chain_history.sqlite \ +export NYX_CHAIN_WATCHER_DATABASE_PATH=nyx_chain_watcher.sqlite \ +export NYX_CHAIN_WATCHER_WATCH_ACCOUNTS=n1...,n1...,n1... \ +export NYX_CHAIN_WATCHER_WATCH_CHAIN_MESSAGE_TYPES="/cosmos.bank.v1beta1.MsgSend,/ibc.applications.transfer.v1.MsgTransfer" +export NYX_CHAIN_WATCHER_WEBHOOK_URL="https://webhook.site" \ +export NYX_CHAIN_WATCHER_WEBHOOK_AUTH=1234 \ cargo run -- run ``` diff --git a/nyx-chain-watcher/src/cli/commands/run/args.rs b/nyx-chain-watcher/src/cli/commands/run/args.rs index cc20ae2bac..b05a029636 100644 --- a/nyx-chain-watcher/src/cli/commands/run/args.rs +++ b/nyx-chain-watcher/src/cli/commands/run/args.rs @@ -22,6 +22,15 @@ pub(crate) struct Args { )] pub watch_for_transfer_recipient_accounts: Option>, + + /// (Override) Watch for transfers to these recipient accounts + #[clap( + long, + value_delimiter = ',', + env = NYX_RECORD_BEARER_VALUE + )] + pub record_bearer_token: Option, + /// (Override) Watch for chain messages of these types #[clap( long, diff --git a/nyx-chain-watcher/src/cli/commands/run/config.rs b/nyx-chain-watcher/src/cli/commands/run/config.rs index 02e5a2fadc..afd8085de4 100644 --- a/nyx-chain-watcher/src/cli/commands/run/config.rs +++ b/nyx-chain-watcher/src/cli/commands/run/config.rs @@ -14,6 +14,7 @@ pub(crate) fn get_run_config(args: Args) -> Result webhook_auth, ref chain_watcher_db_path, ref chain_history_db_path, + ref record_bearer_token, webhook_url, } = args; @@ -54,6 +55,11 @@ pub(crate) fn get_run_config(args: Args) -> Result builder = builder.with_chain_scraper_db_path(db_path.clone()); } + if let Some(token) = record_bearer_token { + info!("Setting bearer token for authentication"); + builder = builder.with_record_bearer_token(token.clone()); + } + if let Some(webhook_url) = webhook_url { let authentication = webhook_auth.map(|token| HttpAuthenticationOptions::AuthorizationBearerToken { token }); diff --git a/nyx-chain-watcher/src/config/mod.rs b/nyx-chain-watcher/src/config/mod.rs index 73419a2e9e..a66dea80d1 100644 --- a/nyx-chain-watcher/src/config/mod.rs +++ b/nyx-chain-watcher/src/config/mod.rs @@ -49,6 +49,8 @@ pub struct ConfigBuilder { pub payment_watcher_config: Option, pub logging: Option, + + pub bearer_token: Option, } impl ConfigBuilder { @@ -60,9 +62,15 @@ impl ConfigBuilder { logging: None, db_path: None, chain_scraper_db_path: None, + bearer_token: None, } } + pub fn with_record_bearer_token(mut self, token: String) -> Self { + self.bearer_token = Some(token); + self + } + pub fn with_db_path(mut self, db_path: String) -> Self { self.db_path = Some(db_path); self @@ -96,6 +104,7 @@ impl ConfigBuilder { data_dir: self.data_dir, db_path: self.db_path, chain_scraper_db_path: self.chain_scraper_db_path, + bearer_token: self.bearer_token, } } } @@ -118,8 +127,11 @@ pub struct Config { pub payment_watcher_config: Option, + pub bearer_token: Option, + #[serde(default)] pub logging: LoggingSettings, + } impl NymConfigTemplate for Config { diff --git a/nyx-chain-watcher/src/db/models.rs b/nyx-chain-watcher/src/db/models.rs index 8cddac7f30..9e06f832b1 100644 --- a/nyx-chain-watcher/src/db/models.rs +++ b/nyx-chain-watcher/src/db/models.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use utoipa::ToSchema; #[derive(Clone, Deserialize, Debug, ToSchema)] @@ -32,7 +33,7 @@ pub(crate) struct PriceHistory { pub(crate) btc: f64, } -#[derive(Serialize, Deserialize, Debug, ToSchema)] +#[derive(Serialize, Deserialize, Debug, ToSchema, FromRow)] pub(crate) struct PaymentRecord { pub(crate) transaction_hash: String, pub(crate) sender_address: String, @@ -41,3 +42,4 @@ pub(crate) struct PaymentRecord { pub(crate) timestamp: i64, pub(crate) height: i64, } + diff --git a/nyx-chain-watcher/src/db/queries/payments.rs b/nyx-chain-watcher/src/db/queries/payments.rs index c7757d6e67..64be8a3780 100644 --- a/nyx-chain-watcher/src/db/queries/payments.rs +++ b/nyx-chain-watcher/src/db/queries/payments.rs @@ -1,5 +1,7 @@ use crate::db::DbPool; -use anyhow::Result; +use anyhow::{Context, Result}; +use regex::Regex; +use crate::db::models::PaymentRecord; pub async fn get_last_checked_height(pool: &DbPool) -> Result { let result = sqlx::query_scalar!("SELECT MAX(height) FROM payments") @@ -8,6 +10,35 @@ pub async fn get_last_checked_height(pool: &DbPool) -> Result { Ok(result.unwrap_or(0)) } +lazy_static::lazy_static! { + static ref HEX_PATTERN: Regex = Regex::new(r"^[A-Fa-f0-9]{64}$").unwrap(); + static ref BASE64_PATTERN: Regex = Regex::new(r"^[A-Za-z0-9+/=]+$").unwrap(); +} + +pub async fn get_transaction_record(pool: &DbPool, record_txs: &str) -> Result> { + let query = if HEX_PATTERN.is_match(record_txs) { + "SELECT transaction_hash, sender_address, receiver_address, amount, timestamp, height + FROM transactions WHERE tx_hash = $1" + } else if BASE64_PATTERN.is_match(record_txs) { + "SELECT transaction_hash, sender_address, receiver_address, amount, timestamp, height + FROM transactions WHERE memo LIKE $1" + } else { + return Ok(None); + }; + + let param = if BASE64_PATTERN.is_match(record_txs) { + format!("%{}%", record_txs) + } else { + record_txs.to_string() + }; + + sqlx::query_as::<_, PaymentRecord>(query) + .bind(param) + .fetch_optional(pool) + .await + .context("Database query failed") +} + pub async fn insert_payment( pool: &DbPool, transaction_hash: String, diff --git a/nyx-chain-watcher/src/env.rs b/nyx-chain-watcher/src/env.rs index fc8c1a9f61..e9e739226b 100644 --- a/nyx-chain-watcher/src/env.rs +++ b/nyx-chain-watcher/src/env.rs @@ -14,6 +14,7 @@ pub mod vars { pub const NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT: &str = "NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT"; + pub const NYX_RECORD_BEARER_VALUE: &str = "NYX_RECORD_BEARER_VALUE"; pub const NYXD_SCRAPER_UNSAFE_NUKE_DB: &str = "NYXD_SCRAPER_UNSAFE_NUKE_DB"; pub const NYX_CHAIN_WATCHER_ID_ARG: &str = "NYX_CHAIN_WATCHER_ID"; diff --git a/nyx-chain-watcher/src/http/api/mod.rs b/nyx-chain-watcher/src/http/api/mod.rs index 9e405a439d..c393e0acd3 100644 --- a/nyx-chain-watcher/src/http/api/mod.rs +++ b/nyx-chain-watcher/src/http/api/mod.rs @@ -9,6 +9,7 @@ use crate::http::{api_docs, server::HttpServer, state::AppState}; pub(crate) mod price; pub(crate) mod watcher; +pub(crate) mod records; pub(crate) struct RouterBuilder { unfinished_router: Router, @@ -26,7 +27,8 @@ impl RouterBuilder { axum::routing::get(|| async { Redirect::permanent("/swagger") }), ) .nest("/v1", Router::new().nest("/price", price::routes())) - .nest("/v1", Router::new().nest("/watcher", watcher::routes())); + .nest("/v1", Router::new().nest("/watcher", watcher::routes())) + .nest("/v1", Router::new().nest("/records", records::routes())); Self { unfinished_router: router, diff --git a/nyx-chain-watcher/src/http/api/records.rs b/nyx-chain-watcher/src/http/api/records.rs new file mode 100644 index 0000000000..91f42e00a2 --- /dev/null +++ b/nyx-chain-watcher/src/http/api/records.rs @@ -0,0 +1,30 @@ +use crate::db::models::PaymentRecord; +use crate::db::queries::payments::get_transaction_record; +use crate::http::error::{Error, HttpResult}; +use crate::http::state::AppState; +use axum::{extract::{State, Path}, Json, Router}; + +pub(crate) fn routes() -> Router { + Router::new().route("/records/:record_txs", axum::routing::get(transaction_record)) +} + +#[utoipa::path( + tag = "Watcher Records", + get, + path = "/v1/records/{record_txs}", + responses( + (status = 200, body = PaymentRecord), + (status = 404, description = "Transaction record not found") + ) +)] +/// Fetch a transaction record from the database +async fn transaction_record( + State(state): State, + Path(record_txs): Path, +) -> HttpResult> { + get_transaction_record(state.db_pool(), &record_txs) + .await + .map_err(|_| Error::internal())? + .map(Json::from) + .ok_or_else(|| Error::not_found(&record_txs)) +} diff --git a/nyx-chain-watcher/src/http/error.rs b/nyx-chain-watcher/src/http/error.rs index fa6b274b69..6169c788f4 100644 --- a/nyx-chain-watcher/src/http/error.rs +++ b/nyx-chain-watcher/src/http/error.rs @@ -1,3 +1,4 @@ +use tracing::error; pub(crate) type HttpResult = Result; pub(crate) struct Error { @@ -12,6 +13,13 @@ impl Error { status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, } } + pub(crate) fn not_found(resource: &str) -> Self { + error!("Resource not found: {}", resource); + Self { + message: format!("{} not found", resource), + status: axum::http::StatusCode::NOT_FOUND, + } + } } impl axum::response::IntoResponse for Error {