Compare commits

...

1 Commits

Author SHA1 Message Date
Tommy Verrall 4d609a071f 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
2025-02-25 13:56:53 +01:00
12 changed files with 114 additions and 9 deletions
Generated
+2
View File
@@ -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",
+2
View File
@@ -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]
+6 -6
View File
@@ -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
```
@@ -22,6 +22,15 @@ pub(crate) struct Args {
)]
pub watch_for_transfer_recipient_accounts: Option<Vec<AccountId>>,
/// (Override) Watch for transfers to these recipient accounts
#[clap(
long,
value_delimiter = ',',
env = NYX_RECORD_BEARER_VALUE
)]
pub record_bearer_token: Option<String>,
/// (Override) Watch for chain messages of these types
#[clap(
long,
@@ -14,6 +14,7 @@ pub(crate) fn get_run_config(args: Args) -> Result<Config, NyxChainWatcherError>
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<Config, NyxChainWatcherError>
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 });
+12
View File
@@ -49,6 +49,8 @@ pub struct ConfigBuilder {
pub payment_watcher_config: Option<PaymentWatcherConfig>,
pub logging: Option<LoggingSettings>,
pub bearer_token: Option<String>,
}
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<PaymentWatcherConfig>,
pub bearer_token: Option<String>,
#[serde(default)]
pub logging: LoggingSettings,
}
impl NymConfigTemplate for Config {
+3 -1
View File
@@ -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,
}
+32 -1
View File
@@ -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<i64> {
let result = sqlx::query_scalar!("SELECT MAX(height) FROM payments")
@@ -8,6 +10,35 @@ pub async fn get_last_checked_height(pool: &DbPool) -> Result<i64> {
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<Option<PaymentRecord>> {
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,
+1
View File
@@ -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";
+3 -1
View File
@@ -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<AppState>,
@@ -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,
+30
View File
@@ -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<AppState> {
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<AppState>,
Path(record_txs): Path<String>,
) -> HttpResult<Json<PaymentRecord>> {
get_transaction_record(state.db_pool(), &record_txs)
.await
.map_err(|_| Error::internal())?
.map(Json::from)
.ok_or_else(|| Error::not_found(&record_txs))
}
+8
View File
@@ -1,3 +1,4 @@
use tracing::error;
pub(crate) type HttpResult<T> = Result<T, Error>;
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 {