Skip to content

Commit

Permalink
Payment data (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
naps62 authored Aug 6, 2024
1 parent fe33995 commit 6c12d34
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:

- run: cargo test
env:
TEST_DATABASE_URL: "postgres://postgres:postgres@localhost/db"
TEST_DATABASE_URL: "postgres://postgres:postgres@localhost/test-db"

cargo-deny:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions ethui-indexer.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ jwt_secret_env = "ETHUI_JWT_SECRET"
[db]
url = "postgres://ethui_indexer:ethui-indexer&12345@localhost/ethui_indexer"

[payment]
address = "0x0063A660Fb166E9deF01C7B4fd0303B054Ed1B9e"
min_amount = "10000000000000000" # 0.01 ether

[whitelist]
file = "/home/naps62/ethui/whitelists/lists/all.txt"
114 changes: 94 additions & 20 deletions src/api/app.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::str::FromStr as _;

use axum::{
extract::State,
middleware::from_extractor,
response::IntoResponse,
routing::{get, post},
Extension, Json, Router,
};
use ethers_core::types::Signature;
use ethers_core::types::{Address, Signature};
use jsonwebtoken::{encode, DecodingKey, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use serde_json::json;
Expand All @@ -15,6 +17,7 @@ use super::{
app_state::AppState,
auth::{Claims, IndexerAuth},
error::{ApiError, ApiResult},
registration::RegistrationProof,
};

pub fn app(jwt_secret: String, state: AppState) -> Router {
Expand All @@ -27,7 +30,8 @@ pub fn app(jwt_secret: String, state: AppState) -> Router {

let public_routes = Router::new()
.route("/health", get(health))
.route("/auth", post(auth));
.route("/auth", post(auth))
.route("/register", post(register));

Router::new()
.nest("/api", protected_routes)
Expand All @@ -40,6 +44,30 @@ pub fn app(jwt_secret: String, state: AppState) -> Router {

async fn health() -> impl IntoResponse {}

pub async fn test() -> impl IntoResponse {
Json(json!({"foo": "bar"}))
}

#[derive(Debug, Deserialize, Serialize)]
pub struct RegisterRequest {
address: Address,
proof: RegistrationProof,
}

// POST /api/register
pub async fn register(
State(state): State<AppState>,
Json(register): Json<RegisterRequest>,
) -> ApiResult<impl IntoResponse> {
let addr = reth_primitives::Address::from_str(&format!("0x{:x}", register.address)).unwrap();

register.proof.validate(addr, &state).await?;

state.db.register(register.address.into()).await?;

Ok(Json(json!({"result": "success"})))
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AuthRequest {
signature: Signature,
Expand All @@ -51,34 +79,31 @@ pub struct AuthResponse {
access_token: String,
}

pub async fn test() -> impl IntoResponse {
Json(json!({"foo": "bar"}))
}

// POST /api/auth
pub async fn auth(
Extension(encoding_key): Extension<EncodingKey>,
State(AppState { db, config }): State<AppState>,
State(AppState { db, .. }): State<AppState>,
Json(auth): Json<AuthRequest>,
) -> ApiResult<impl IntoResponse> {
auth.data
.check(&auth.signature)
.map_err(|_| ApiError::InvalidCredentials)?;

if config.whitelist.is_whitelisted(&auth.data.get_address()) {
// TODO: this registration needs to be verified (is the user whitelisted? did the user pay?)
db.register(auth.data.address.into()).await?;
let access_token = encode(&Header::default(), &Claims::from(auth.data), &encoding_key)?;

// Send the authorized token
Ok(Json(AuthResponse { access_token }))
} else {
Err(ApiError::InvalidCredentials)
if !db.is_registered(auth.data.address.into()).await? {
return Err(ApiError::NotRegistered);
}

let access_token = encode(&Header::default(), &Claims::from(auth.data), &encoding_key)?;

// Send the authorized token
Ok(Json(AuthResponse { access_token }))
}

#[cfg(test)]
mod test {

use std::sync::Arc;

use axum::{
body::Body,
http::{Request, StatusCode},
Expand All @@ -94,13 +119,15 @@ mod test {
use super::AuthRequest;
use crate::{
api::{
app::AuthResponse,
app::{AuthResponse, RegisterRequest},
app_state::AppState,
auth::IndexerAuth,
registration::RegistrationProof,
test_utils::{address, now, sign_typed_data, to_json_resp},
},
config::Config,
db::Db,
sync::RethProviderFactory,
};

fn get(uri: &str) -> Request<Body> {
Expand Down Expand Up @@ -134,15 +161,35 @@ mod test {
async fn build_app() -> Router {
let jwt_secret = "secret".to_owned();
let db = Db::connect_test().await.unwrap();
let config = Config::for_test();

let state = AppState {
db,
config: Config::for_test(),
config,
provider_factory: None,
};

super::app(jwt_secret, state)
}

#[rstest]
#[tokio::test]
#[serial]
async fn test_register(address: Address) -> Result<()> {
let app = build_app().await;
let req = post(
"/api/register",
RegisterRequest {
address,
proof: RegistrationProof::Test,
},
);
let resp = app.clone().oneshot(req).await?;

assert_eq!(resp.status(), StatusCode::OK);
Ok(())
}

#[rstest]
#[tokio::test]
#[serial]
Expand All @@ -151,15 +198,24 @@ mod test {
let valid_until = now + 20 * 60;
let data = IndexerAuth::new(address, valid_until);

let req = post(
let registration = post(
"/api/register",
RegisterRequest {
address,
proof: RegistrationProof::Test,
},
);
app.clone().oneshot(registration).await?;

let auth = post(
"/api/auth",
AuthRequest {
signature: sign_typed_data(&data).await?,
data,
},
);

let resp = app.oneshot(req).await?;
let resp = app.oneshot(auth).await?;
assert_eq!(resp.status(), StatusCode::OK);
Ok(())
}
Expand All @@ -172,6 +228,15 @@ mod test {
let valid_until = now + 20 * 60;
let data = IndexerAuth::new(address, valid_until);

let registration = post(
"/api/register",
RegisterRequest {
address,
proof: RegistrationProof::Test,
},
);
app.clone().oneshot(registration).await?;

let req = post(
"/api/auth",
AuthRequest {
Expand Down Expand Up @@ -257,6 +322,15 @@ mod test {
let valid_until = now + 20;
let data = IndexerAuth::new(address, valid_until);

let registration = post(
"/api/register",
RegisterRequest {
address,
proof: RegistrationProof::Test,
},
);
app.clone().oneshot(registration).await?;

let req = post(
"/api/auth",
AuthRequest {
Expand Down
5 changes: 4 additions & 1 deletion src/api/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use crate::{config::Config, db::Db};
use std::sync::Arc;

use crate::{config::Config, db::Db, sync::RethProviderFactory};

#[derive(Clone)]
pub struct AppState {
pub db: Db,
pub config: Config,
pub provider_factory: Option<Arc<RethProviderFactory>>,
}
2 changes: 2 additions & 0 deletions src/api/auth/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ impl From<Claims> for IndexerAuth {
}

impl IndexerAuth {
#[allow(dead_code)]
pub fn new(address: Address, valid_until: u64) -> Self {
Self {
address,
Expand Down Expand Up @@ -69,6 +70,7 @@ impl IndexerAuth {
Ok(())
}

#[allow(dead_code)]
pub fn get_address(&self) -> reth_primitives::Address {
reth_primitives::Address::from_str(&format!("0x{:x}", self.address)).unwrap()
}
Expand Down
7 changes: 6 additions & 1 deletion src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ pub enum ApiError {
#[error("Invalid Credentials")]
InvalidCredentials,

#[error("Not Registered")]
NotRegistered,

#[error(transparent)]
Jsonwebtoken(#[from] jsonwebtoken::errors::Error),

Expand All @@ -20,7 +23,9 @@ pub type ApiResult<T> = Result<T, ApiError>;
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let status_code = match self {
ApiError::InvalidCredentials | ApiError::Jsonwebtoken(_) => StatusCode::UNAUTHORIZED,
ApiError::NotRegistered | ApiError::InvalidCredentials | ApiError::Jsonwebtoken(_) => {
StatusCode::UNAUTHORIZED
}
ApiError::Unknown(_) => StatusCode::INTERNAL_SERVER_ERROR,
};

Expand Down
17 changes: 13 additions & 4 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,34 @@ mod app;
mod app_state;
mod auth;
mod error;
mod registration;
mod test_utils;

use std::net::SocketAddr;
use std::{net::SocketAddr, sync::Arc};

use tokio::task::JoinHandle;
use tracing::instrument;

use self::{app::app, app_state::AppState};
use crate::{config::Config, db::Db};
use crate::{config::Config, db::Db, sync::RethProviderFactory};

#[allow(clippy::async_yields_async)]
#[instrument(name = "api", skip(db, config), fields(port = config.http.clone().unwrap().port))]
pub async fn start(db: Db, config: Config) -> JoinHandle<Result<(), std::io::Error>> {
pub async fn start(
db: Db,
config: Config,
provider_factory: Arc<RethProviderFactory>,
) -> JoinHandle<Result<(), std::io::Error>> {
let http_config = config.http.clone().unwrap();

let addr = SocketAddr::from(([0, 0, 0, 0], http_config.port));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();

let state = AppState { db, config };
let state = AppState {
db,
config,
provider_factory: Some(provider_factory),
};
let app = app(http_config.jwt_secret(), state);

tokio::spawn(async move { axum::serve(listener, app).await })
Expand Down
Loading

0 comments on commit 6c12d34

Please sign in to comment.