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..5e5789d0 --- /dev/null +++ b/src/http/validators.rs @@ -0,0 +1,123 @@ +use axum::{ + Router, + extract::{Json, State}, + http::StatusCode, + routing::post, +}; +use sqlx::{Executor, Postgres}; + +use super::types::{RegisterValidatorParams, ValidatorProfile}; +use crate::AppState; + +pub(crate) fn router() -> Router { + Router::new().route("/validators", post(register_validator)) +} + +async fn register_validator( + State(state): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), StatusCode> { + let mut tx = state + .db + .pool + .begin() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // 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 AS "validator_id!: Uuid", + 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)? + .ok_or(StatusCode::CONFLICT)?; // 409 on duplicate + + // helper: upsert into a text‑lookup table, returning its numeric ID + async fn upsert_and_get_id( + executor: impl Executor<'_, Database = Postgres>, // Generic executor + table: &str, + value: &str, + ) -> Result { + let sql = format!( + "INSERT INTO {table} (value) VALUES ($1) ON CONFLICT (value) DO UPDATE SET value = EXCLUDED.value RETURNING id", + table = table + ); + sqlx::query_scalar(&sql) + .bind(value) + .fetch_one(executor) + .await + } + + // 3) languages (note: payload.programming_languages) + for lang in &payload.programming_lang { + 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_lang + (validator_id, language_id) + VALUES ($1, $2) + ON CONFLICT DO NOTHING + "#, + row.validator_id, + lang_id, + ) + .execute(&mut tx) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } + + // 4) expertise areas (note: payload.expertise_areas) + for area in &payload.expertise_area { + 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_area + (validator_id, expertise_id) + VALUES ($1, $2) + ON CONFLICT DO NOTHING + "#, + row.validator_id, + exp_id, + ) + .execute(&mut tx) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } + + // 5) commit + tx.commit() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // 6) build our JSON response + let profile = ValidatorProfile { + validator_id: row.validator_id, + wallet_address: row.wallet_address, + name: row.name, + bio: row.bio, + programming_languages: payload.programming_lang.clone(), + expertise_areas: payload.expertise_area.clone(), + created_at: row.created_at, + updated_at: row.updated_at, + }; + + Ok((StatusCode::CREATED, Json(profile))) +}