Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d609a071f |
Generated
+2
@@ -7263,6 +7263,7 @@ dependencies = [
|
|||||||
"axum 0.7.9",
|
"axum 0.7.9",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"lazy_static",
|
||||||
"nym-bin-common",
|
"nym-bin-common",
|
||||||
"nym-config",
|
"nym-config",
|
||||||
"nym-network-defaults",
|
"nym-network-defaults",
|
||||||
@@ -7270,6 +7271,7 @@ dependencies = [
|
|||||||
"nym-task",
|
"nym-task",
|
||||||
"nym-validator-client",
|
"nym-validator-client",
|
||||||
"nyxd-scraper",
|
"nyxd-scraper",
|
||||||
|
"regex",
|
||||||
"reqwest 0.12.4",
|
"reqwest 0.12.4",
|
||||||
"rocket",
|
"rocket",
|
||||||
"schemars",
|
"schemars",
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ tower-http = { workspace = true, features = ["cors", "trace"] }
|
|||||||
utoipa = { workspace = true, features = ["axum_extras", "time"] }
|
utoipa = { workspace = true, features = ["axum_extras", "time"] }
|
||||||
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
|
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
|
||||||
utoipauto = { workspace = true }
|
utoipauto = { workspace = true }
|
||||||
|
regex = "1.11.1"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ Look in [env.rs](./src/env.rs) for the names of environment variables that can b
|
|||||||
## Running locally
|
## Running locally
|
||||||
|
|
||||||
```
|
```
|
||||||
NYX_CHAIN_WATCHER_HISTORY_DATABASE_PATH=chain_history.sqlite \
|
export NYX_CHAIN_WATCHER_HISTORY_DATABASE_PATH=chain_history.sqlite \
|
||||||
NYX_CHAIN_WATCHER_DATABASE_PATH=nyx_chain_watcher.sqlite \
|
export NYX_CHAIN_WATCHER_DATABASE_PATH=nyx_chain_watcher.sqlite \
|
||||||
NYX_CHAIN_WATCHER_WATCH_ACCOUNTS=n1...,n1...,n1... \
|
export NYX_CHAIN_WATCHER_WATCH_ACCOUNTS=n1...,n1...,n1... \
|
||||||
NYX_CHAIN_WATCHER_WATCH_CHAIN_MESSAGE_TYPES="/cosmos.bank.v1beta1.MsgSend,/ibc.applications.transfer.v1.MsgTransfer"
|
export NYX_CHAIN_WATCHER_WATCH_CHAIN_MESSAGE_TYPES="/cosmos.bank.v1beta1.MsgSend,/ibc.applications.transfer.v1.MsgTransfer"
|
||||||
NYX_CHAIN_WATCHER_WEBHOOK_URL="https://webhook.site" \
|
export NYX_CHAIN_WATCHER_WEBHOOK_URL="https://webhook.site" \
|
||||||
NYX_CHAIN_WATCHER_WEBHOOK_AUTH=1234 \
|
export NYX_CHAIN_WATCHER_WEBHOOK_AUTH=1234 \
|
||||||
cargo run -- run
|
cargo run -- run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ pub(crate) struct Args {
|
|||||||
)]
|
)]
|
||||||
pub watch_for_transfer_recipient_accounts: Option<Vec<AccountId>>,
|
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
|
/// (Override) Watch for chain messages of these types
|
||||||
#[clap(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub(crate) fn get_run_config(args: Args) -> Result<Config, NyxChainWatcherError>
|
|||||||
webhook_auth,
|
webhook_auth,
|
||||||
ref chain_watcher_db_path,
|
ref chain_watcher_db_path,
|
||||||
ref chain_history_db_path,
|
ref chain_history_db_path,
|
||||||
|
ref record_bearer_token,
|
||||||
webhook_url,
|
webhook_url,
|
||||||
} = args;
|
} = 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());
|
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 {
|
if let Some(webhook_url) = webhook_url {
|
||||||
let authentication =
|
let authentication =
|
||||||
webhook_auth.map(|token| HttpAuthenticationOptions::AuthorizationBearerToken { token });
|
webhook_auth.map(|token| HttpAuthenticationOptions::AuthorizationBearerToken { token });
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ pub struct ConfigBuilder {
|
|||||||
pub payment_watcher_config: Option<PaymentWatcherConfig>,
|
pub payment_watcher_config: Option<PaymentWatcherConfig>,
|
||||||
|
|
||||||
pub logging: Option<LoggingSettings>,
|
pub logging: Option<LoggingSettings>,
|
||||||
|
|
||||||
|
pub bearer_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigBuilder {
|
impl ConfigBuilder {
|
||||||
@@ -60,9 +62,15 @@ impl ConfigBuilder {
|
|||||||
logging: None,
|
logging: None,
|
||||||
db_path: None,
|
db_path: None,
|
||||||
chain_scraper_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 {
|
pub fn with_db_path(mut self, db_path: String) -> Self {
|
||||||
self.db_path = Some(db_path);
|
self.db_path = Some(db_path);
|
||||||
self
|
self
|
||||||
@@ -96,6 +104,7 @@ impl ConfigBuilder {
|
|||||||
data_dir: self.data_dir,
|
data_dir: self.data_dir,
|
||||||
db_path: self.db_path,
|
db_path: self.db_path,
|
||||||
chain_scraper_db_path: self.chain_scraper_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 payment_watcher_config: Option<PaymentWatcherConfig>,
|
||||||
|
|
||||||
|
pub bearer_token: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub logging: LoggingSettings,
|
pub logging: LoggingSettings,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NymConfigTemplate for Config {
|
impl NymConfigTemplate for Config {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::FromRow;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Debug, ToSchema)]
|
#[derive(Clone, Deserialize, Debug, ToSchema)]
|
||||||
@@ -32,7 +33,7 @@ pub(crate) struct PriceHistory {
|
|||||||
pub(crate) btc: f64,
|
pub(crate) btc: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, ToSchema)]
|
#[derive(Serialize, Deserialize, Debug, ToSchema, FromRow)]
|
||||||
pub(crate) struct PaymentRecord {
|
pub(crate) struct PaymentRecord {
|
||||||
pub(crate) transaction_hash: String,
|
pub(crate) transaction_hash: String,
|
||||||
pub(crate) sender_address: String,
|
pub(crate) sender_address: String,
|
||||||
@@ -41,3 +42,4 @@ pub(crate) struct PaymentRecord {
|
|||||||
pub(crate) timestamp: i64,
|
pub(crate) timestamp: i64,
|
||||||
pub(crate) height: i64,
|
pub(crate) height: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::db::DbPool;
|
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> {
|
pub async fn get_last_checked_height(pool: &DbPool) -> Result<i64> {
|
||||||
let result = sqlx::query_scalar!("SELECT MAX(height) FROM payments")
|
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))
|
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(
|
pub async fn insert_payment(
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
transaction_hash: String,
|
transaction_hash: String,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod vars {
|
|||||||
pub const NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT: &str =
|
pub const NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT: &str =
|
||||||
"NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT";
|
"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 NYXD_SCRAPER_UNSAFE_NUKE_DB: &str = "NYXD_SCRAPER_UNSAFE_NUKE_DB";
|
||||||
|
|
||||||
pub const NYX_CHAIN_WATCHER_ID_ARG: &str = "NYX_CHAIN_WATCHER_ID";
|
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 price;
|
||||||
pub(crate) mod watcher;
|
pub(crate) mod watcher;
|
||||||
|
pub(crate) mod records;
|
||||||
|
|
||||||
pub(crate) struct RouterBuilder {
|
pub(crate) struct RouterBuilder {
|
||||||
unfinished_router: Router<AppState>,
|
unfinished_router: Router<AppState>,
|
||||||
@@ -26,7 +27,8 @@ impl RouterBuilder {
|
|||||||
axum::routing::get(|| async { Redirect::permanent("/swagger") }),
|
axum::routing::get(|| async { Redirect::permanent("/swagger") }),
|
||||||
)
|
)
|
||||||
.nest("/v1", Router::new().nest("/price", price::routes()))
|
.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 {
|
Self {
|
||||||
unfinished_router: router,
|
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) type HttpResult<T> = Result<T, Error>;
|
||||||
|
|
||||||
pub(crate) struct Error {
|
pub(crate) struct Error {
|
||||||
@@ -12,6 +13,13 @@ impl Error {
|
|||||||
status: axum::http::StatusCode::INTERNAL_SERVER_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 {
|
impl axum::response::IntoResponse for Error {
|
||||||
|
|||||||
Reference in New Issue
Block a user