Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d609a071f |
Generated
+2
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user