From 55f4a01986626099c5c6c50b4b0fb32d71aa7b23 Mon Sep 17 00:00:00 2001 From: PoulavBhowmick03 Date: Fri, 18 Jul 2025 23:46:32 +0530 Subject: [PATCH 1/2] added validator section --- migrations/20250718172039_validators.sql | 41 ++++++++ src/http/mod.rs | 2 +- src/http/types.rs | 21 ++++ src/http/validators.rs | 121 +++++++++++++++++++++++ 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 migrations/20250718172039_validators.sql create mode 100644 src/http/validators.rs diff --git a/migrations/20250718172039_validators.sql b/migrations/20250718172039_validators.sql new file mode 100644 index 00000000..d4e96579 --- /dev/null +++ b/migrations/20250718172039_validators.sql @@ -0,0 +1,41 @@ +-- Add migration script here + +-- validators table +CREATE TABLE IF NOT EXISTS validators ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + wallet_address TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + bio TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- programming languages master list +CREATE TABLE IF NOT EXISTS programming_languages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE +); + +-- expertise areas master list +CREATE TABLE IF NOT EXISTS expertise_areas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE +); + +-- many‑to‑many from validators → languages +CREATE TABLE IF NOT EXISTS validator_programming_languages ( + validator_id UUID NOT NULL REFERENCES validators(id) ON DELETE CASCADE, + language_id UUID NOT NULL REFERENCES programming_languages(id) ON DELETE CASCADE, + PRIMARY KEY (validator_id, language_id) +); + +-- many‑to‑many from validators → expertise +CREATE TABLE IF NOT EXISTS validator_expertise_areas ( + validator_id UUID NOT NULL REFERENCES validators(id) ON DELETE CASCADE, + expertise_id UUID NOT NULL REFERENCES expertise_areas(id) ON DELETE CASCADE, + PRIMARY KEY (validator_id, expertise_id) +); + +-- index for fast wallet lookups +CREATE INDEX IF NOT EXISTS idx_validators_wallet + ON validators(wallet_address); diff --git a/src/http/mod.rs b/src/http/mod.rs index 594fabd3..cb7bcb12 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -17,7 +17,7 @@ mod helpers; mod projects; mod support_tickets; mod types; - +mod validators; #[derive(Clone)] pub struct AppState { pub db: Db, diff --git a/src/http/types.rs b/src/http/types.rs index 14030d8b..4741b69f 100644 --- a/src/http/types.rs +++ b/src/http/types.rs @@ -70,3 +70,24 @@ pub struct AllocateBountyRequest { pub currency: String, pub bounty_expiry_date: Option>, // ISO8601 string } + +#[derive(Debug, Serialize, Deserialize)] +pub struct RegisterValidatorParams { + pub wallet_address: String, + pub name: String, + pub bio: String, + pub programming_lang: Vec, + pub expertise_area: Vec, +} + +#[derive(Debug, Serialize)] +pub struct ValidatorProfile { + pub validator_id: Uuid, + pub wallet_address: String, + pub name: String, + pub bio: Option, + pub programming_languages: Vec, + pub expertise_areas: Vec, + pub created_at: DateTime, + pub updated_at: DateTime, +} diff --git a/src/http/validators.rs b/src/http/validators.rs new file mode 100644 index 00000000..4f8e230a --- /dev/null +++ b/src/http/validators.rs @@ -0,0 +1,121 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + routing::post, + Router, +}; +use sqlx::PgPool; +use uuid::Uuid; + +use crate::AppState; + +use super::types::{RegisterValidatorParams, ValidatorProfile}; + +pub(crate) fn router() -> Router { + Router::new().route( + "/validators", + post(register_validator), + ) +} + +async fn register_validator( + State(db): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), StatusCode> { + // wrap in a transaction so we rollback on any failure + let mut tx = db.begin().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // 1) insert validator (enforces unique wallet_address) + let validator = sqlx::query!( + r#" + INSERT INTO validators(wallet_address, name, bio) + VALUES ($1, $2, $3) + ON CONFLICT(wallet_address) DO NOTHING + RETURNING id, wallet_address, name, bio, created_at, updated_at + "#, + payload.wallet_address, + payload.name, + payload.bio, + ) + .fetch_optional(&mut tx) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let row = validator + .ok_or(StatusCode::CONFLICT)?; // already exists → 409 + + let validator_id: Uuid = row.id; + + // Helper: upsert into a master table and return its UUID + async fn upsert_and_get_id( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + table: &str, + value: &str, + ) -> Result { + let q = format!( + "INSERT INTO {table}(name) VALUES ($1) + ON CONFLICT(name) DO UPDATE SET name = EXCLUDED.name + RETURNING id" + ); + let rec = sqlx::query_scalar(&q) + .bind(value) + .fetch_one(&mut *tx) + .await?; + Ok(rec) + } + + // 2) languages + for lang in &payload.programming_lang { + let lang_id = upsert_and_get_id(&mut tx, "programming_languages", lang).await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + sqlx::query!( + r#" + INSERT INTO validator_programming_languages + (validator_id, language_id) + VALUES ($1, $2) + ON CONFLICT DO NOTHING + "#, + validator_id, + lang_id, + ) + .execute(&mut tx) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } + + // 3) expertise areas + for area in &payload.expertise_area { + let exp_id = upsert_and_get_id(&mut tx, "expertise_areas", area).await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + sqlx::query!( + r#" + INSERT INTO validator_expertise_areas + (validator_id, expertise_id) + VALUES ($1, $2) + ON CONFLICT DO NOTHING + "#, + validator_id, + exp_id, + ) + .execute(&mut tx) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } + + // commit everything + tx.commit().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // build response + let profile = ValidatorProfile { + validator_id, + wallet_address: row.wallet_address, + name: row.name, + bio: row.bio, + programming_languages: payload.programming_lang, + expertise_areas: payload.expertise_area, + created_at: row.created_at, + updated_at: row.updated_at, + }; + + Ok((StatusCode::CREATED, Json(profile))) +} From 692ad65f2a08dd9a3dd2ec2c00dbf7f6ff26d3d8 Mon Sep 17 00:00:00 2001 From: PoulavBhowmick03 Date: Sat, 19 Jul 2025 16:29:53 +0530 Subject: [PATCH 2/2] fixes --- src/http/validators.rs | 100 +++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/src/http/validators.rs b/src/http/validators.rs index 4f8e230a..5e5789d0 100644 --- a/src/http/validators.rs +++ b/src/http/validators.rs @@ -1,81 +1,80 @@ use axum::{ + Router, extract::{Json, State}, http::StatusCode, routing::post, - Router, }; -use sqlx::PgPool; -use uuid::Uuid; - -use crate::AppState; +use sqlx::{Executor, Postgres}; use super::types::{RegisterValidatorParams, ValidatorProfile}; +use crate::AppState; pub(crate) fn router() -> Router { - Router::new().route( - "/validators", - post(register_validator), - ) + Router::new().route("/validators", post(register_validator)) } async fn register_validator( - State(db): State, + State(state): State, Json(payload): Json, ) -> Result<(StatusCode, Json), StatusCode> { - // wrap in a transaction so we rollback on any failure - let mut tx = db.begin().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let mut tx = state + .db + .pool + .begin() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - // 1) insert validator (enforces unique wallet_address) - let validator = sqlx::query!( + // 2) insert validator (UUID PK) + let row = sqlx::query!( r#" INSERT INTO validators(wallet_address, name, bio) VALUES ($1, $2, $3) ON CONFLICT(wallet_address) DO NOTHING - RETURNING id, wallet_address, name, bio, created_at, updated_at + RETURNING id AS "validator_id!: Uuid", + wallet_address, + name, + bio, + created_at, + updated_at "#, payload.wallet_address, payload.name, payload.bio, ) - .fetch_optional(&mut tx) + .fetch_optional(&mut *tx) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let row = validator - .ok_or(StatusCode::CONFLICT)?; // already exists → 409 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::CONFLICT)?; // 409 on duplicate - let validator_id: Uuid = row.id; - - // Helper: upsert into a master table and return its UUID + // helper: upsert into a text‑lookup table, returning its numeric ID async fn upsert_and_get_id( - tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + executor: impl Executor<'_, Database = Postgres>, // Generic executor table: &str, value: &str, - ) -> Result { - let q = format!( - "INSERT INTO {table}(name) VALUES ($1) - ON CONFLICT(name) DO UPDATE SET name = EXCLUDED.name - RETURNING id" + ) -> Result { + let sql = format!( + "INSERT INTO {table} (value) VALUES ($1) ON CONFLICT (value) DO UPDATE SET value = EXCLUDED.value RETURNING id", + table = table ); - let rec = sqlx::query_scalar(&q) + sqlx::query_scalar(&sql) .bind(value) - .fetch_one(&mut *tx) - .await?; - Ok(rec) + .fetch_one(executor) + .await } - // 2) languages + // 3) languages (note: payload.programming_languages) for lang in &payload.programming_lang { - let lang_id = upsert_and_get_id(&mut tx, "programming_languages", lang).await + let lang_id: i32 = upsert_and_get_id(&mut *tx, "programming_languages", lang) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; sqlx::query!( r#" - INSERT INTO validator_programming_languages - (validator_id, language_id) + INSERT INTO validator_programming_lang + (validator_id, language_id) VALUES ($1, $2) ON CONFLICT DO NOTHING "#, - validator_id, + row.validator_id, lang_id, ) .execute(&mut tx) @@ -83,18 +82,19 @@ async fn register_validator( .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } - // 3) expertise areas + // 4) expertise areas (note: payload.expertise_areas) for area in &payload.expertise_area { - let exp_id = upsert_and_get_id(&mut tx, "expertise_areas", area).await + let exp_id: i32 = upsert_and_get_id(&mut *tx, "expertise_areas", area) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; sqlx::query!( r#" - INSERT INTO validator_expertise_areas - (validator_id, expertise_id) + INSERT INTO validator_expertise_area + (validator_id, expertise_id) VALUES ($1, $2) ON CONFLICT DO NOTHING "#, - validator_id, + row.validator_id, exp_id, ) .execute(&mut tx) @@ -102,17 +102,19 @@ async fn register_validator( .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } - // commit everything - tx.commit().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + // 5) commit + tx.commit() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - // build response + // 6) build our JSON response let profile = ValidatorProfile { - validator_id, + validator_id: row.validator_id, wallet_address: row.wallet_address, name: row.name, bio: row.bio, - programming_languages: payload.programming_lang, - expertise_areas: payload.expertise_area, + programming_languages: payload.programming_lang.clone(), + expertise_areas: payload.expertise_area.clone(), created_at: row.created_at, updated_at: row.updated_at, };