more docs
This commit is contained in:
parent
d6662afbe8
commit
848af3d8b3
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -1029,9 +1029,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "counter"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48388d8711a360319610960332b6a6f9fc2b5a63bba9fd10f1b7aa50677d956f"
|
||||
checksum = "2d458e66999348f56fd3ffcfbb7f7951542075ca8359687c703de6500c1ddccd"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
50
TODO.md
50
TODO.md
@ -181,19 +181,22 @@ These are roughly in order of completition
|
||||
- [x] change stats to using the database
|
||||
- [x] emit user stat on retry
|
||||
- [ ] include if archive query or not in the stats
|
||||
- [ ] ability to generate a key from a web endpoint
|
||||
- this is already partially done, but we need to double check it works. preferrably with tests
|
||||
- [ ] ability to domain lock or ip lock said key
|
||||
- [-] ability to domain lock or ip lock said key
|
||||
- the code to check the database and use these entries already exists, but users don't have a way to set them
|
||||
- [ ] view stats about key
|
||||
- [ ] display requests per second per api key (only with authentication!)
|
||||
- [ ] display concurrent requests per api key (only with authentication!)
|
||||
- [ ] display distribution of methods per api key (eth_call, eth_getLogs, etc.) (only with authentication!)
|
||||
- [ ] display logged reverts on an endpoint that requires authentication
|
||||
- [ ] sign in
|
||||
- i think this is done, but double check
|
||||
- [ ] sign out
|
||||
- i think this is done, but double check
|
||||
- [-] new endpoints for users (not totally sure about the exact paths, but these features are all needed):
|
||||
- [ ] ability to generate a key from a web endpoint
|
||||
- [x] sign in
|
||||
- [x] sign out
|
||||
- [-] GET profile endpoint
|
||||
- [-] POST profile endpoint
|
||||
- [-] GET stats endpoint
|
||||
- [ ] display requests per second per api key (only with authentication!)
|
||||
- [ ] display concurrent requests per api key (only with authentication!)
|
||||
- [ ] display distribution of methods per api key (eth_call, eth_getLogs, etc.) (only with authentication!)
|
||||
- [ ] POST key endpoint
|
||||
- allow setting things such as private relay, revert logging, ip/origin/etc checks
|
||||
- [ ] GET logged reverts on an endpoint that requires authentication.
|
||||
- [ ] endpoint for creating/modifying api keys and their advanced security features
|
||||
- [ ] WARN http_request:request: web3_proxy::block_number: could not get block from params err=unexpected params length id=01GF4HTRKM4JV6NX52XSF9AYMW method=POST authorized_request=User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 })
|
||||
- ERROR http_request:request:try_send_all_upstream_servers: web3_proxy::rpcs::request: bad response! err=JsonRpcClientError(JsonRpcError(JsonRpcError { code: -32000, message: "INTERNAL_ERROR: existing tx with same hash", data: None })) method=eth_sendRawTransaction rpc=local_erigon_alpha_archive id=01GF4HV03Y4ZNKQV8DW5NDQ5CG method=POST authorized_request=User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 }) self=Web3Connections { conns: {"local_erigon_alpha_archive_ws": Web3Connection { name: "local_erigon_alpha_archive_ws", blocks: "all", .. }, "local_geth_ws": Web3Connection { name: "local_geth_ws", blocks: 64, .. }, "local_erigon_alpha_archive": Web3Connection { name: "local_erigon_alpha_archive", blocks: "all", .. }}, .. } authorized_request=Some(User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 })) request=JsonRpcRequest { id: RawValue(39), method: "eth_sendRawTransaction", .. } request_metadata=Some(RequestMetadata { datetime: 2022-10-11T22:14:57.406829095Z, period_seconds: 60, request_bytes: 633, backend_requests: 0, no_servers: 0, error_response: false, response_bytes: 0, response_millis: 0 }) block_needed=None
|
||||
@ -231,6 +234,8 @@ These are roughly in order of completition
|
||||
|
||||
These are not yet ordered.
|
||||
|
||||
- [ ] GET balance endpoint
|
||||
- [ ] POST balance endpoint
|
||||
- [ ] eth_1 | 2022-10-11T22:14:57.408114Z ERROR http_request:request:try_send_all_upstream_servers: web3_proxy::rpcs::request: bad response! err=JsonRpcClientError(JsonRpcError(JsonRpcError { code: -32000, message: "INTERNAL_ERROR: existing tx with same hash", data: None })) method=eth_sendRawTransaction rpc=local_erigon_alpha_archive id=01GF4HV03Y4ZNKQV8DW5NDQ5CG method=POST authorized_request=User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 }) self=Web3Connections { conns: {"local_erigon_alpha_archive_ws": Web3Connection { name: "local_erigon_alpha_archive_ws", blocks: "all", .. }, "local_geth_ws": Web3Connection { name: "local_geth_ws", blocks: 64, .. }, "local_erigon_alpha_archive": Web3Connection { name: "local_erigon_alpha_archive", blocks: "all", .. }}, .. } authorized_request=Some(User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 })) request=JsonRpcRequest { id: RawValue(39), method: "eth_sendRawTransaction", .. } request_metadata=Some(RequestMetadata { datetime: 2022-10-11T22:14:57.406829095Z, period_seconds: 60, request_bytes: 633, backend_requests: 0, no_servers: 0, error_response: false, response_bytes: 0, response_millis: 0 }) block_needed=None
|
||||
- eth_sendRawTransaction should accept "INTERNAL_ERROR: existing tx with same hash" as a successful response. we just want to be sure that the server has our tx and in this case, it does.
|
||||
- [ ] EIP1271 for siwe
|
||||
@ -277,29 +282,6 @@ These are not yet ordered.
|
||||
- [ ] if we subscribe to a server that is syncing, it gives us null block_data_limit. when it catches up, we don't ever send queries to it. we need to recheck block_data_limit
|
||||
- [ ] add a "failover" tier that is only used if balanced_rpcs has "no servers synced"
|
||||
- use this tier (and private tier) to check timestamp on latest block. if we are behind that by more than a few seconds, something is wrong
|
||||
|
||||
new endpoints for users (not totally sure about the exact paths, but these features are all needed):
|
||||
- [x] GET /u/:api_key
|
||||
- proxies to web3 websocket
|
||||
- [x] POST /u/:api_key
|
||||
- proxies to web3
|
||||
- [ ] GET /user/login/$address
|
||||
- returns a JSON string for the user to sign
|
||||
- [ ] POST /user/login/$address
|
||||
- returns a JSON string including the api key
|
||||
- sets session cookie
|
||||
- [ ] GET /user/$address
|
||||
- checks for api key in session cookie or header
|
||||
- returns a JSON string including user stats
|
||||
- balance in USD
|
||||
- deposits history (currency, amounts, transaction id)
|
||||
- number of requests used (so we can calculate average spending over a month, burn rate for a user etc, something like "Your balance will be depleted in xx days)
|
||||
- the email address of a user if he opted in to get contacted via email
|
||||
- all the success/retry/fail counts and latencies (but that may better come from somewhere else)
|
||||
- [ ] POST /user/$address
|
||||
- opt-in link email address
|
||||
- checks for api key in session cookie or header
|
||||
- allows modifying user settings
|
||||
- [ ] sometimes when fetching a txid through the proxy it fails, but fetching from the backends works fine
|
||||
- check flashprofits logs for examples
|
||||
- [ ] relevant erigon changelogs: add pendingTransactionWithBody subscription method (#5675)
|
||||
|
@ -27,7 +27,7 @@ axum-client-ip = "0.2.0"
|
||||
axum-macros = "0.2.3"
|
||||
# TODO: import chrono from sea-orm so we always have the same version
|
||||
chrono = "0.4.22"
|
||||
counter = "0.5.6"
|
||||
counter = "0.5.7"
|
||||
derive_more = "0.99.17"
|
||||
dotenv = "0.15.0"
|
||||
ethers = { version = "0.17.0", features = ["rustls", "ws"] }
|
||||
|
@ -178,10 +178,12 @@ impl Web3ProxyApp {
|
||||
Pin<Box<dyn Future<Output = anyhow::Result<()>>>>,
|
||||
)> {
|
||||
// safety checks on the config
|
||||
assert!(
|
||||
top_config.app.redirect_user_url.contains("{{user_id}}"),
|
||||
"redirect user url must contain \"{{user_id}}\""
|
||||
);
|
||||
if let Some(redirect) = &top_config.app.redirect_user_url {
|
||||
assert!(
|
||||
redirect.contains("{{user_id}}"),
|
||||
"redirect_user_url user url must contain \"{{user_id}}\""
|
||||
);
|
||||
}
|
||||
|
||||
// setup metrics
|
||||
let app_metrics = Default::default();
|
||||
|
@ -78,7 +78,7 @@ fn run(
|
||||
let prometheus_handle = tokio::spawn(metrics_frontend::serve(app, app_prometheus_port));
|
||||
|
||||
// if everything is working, these should both run forever
|
||||
// TODO: join these instead and use shutdown handler properly
|
||||
// TODO: join these instead and use shutdown handler properly. probably use tokio's ctrl+c helper
|
||||
tokio::select! {
|
||||
x = app_handle => {
|
||||
match x {
|
||||
@ -220,8 +220,8 @@ mod tests {
|
||||
min_synced_rpcs: 1,
|
||||
frontend_rate_limit_per_minute: 1_000_000,
|
||||
response_cache_max_bytes: 10_usize.pow(7),
|
||||
redirect_public_url: "example.com/".to_string(),
|
||||
redirect_user_url: "example.com/{{user_id}}".to_string(),
|
||||
redirect_public_url: Some("example.com/".to_string()),
|
||||
redirect_user_url: Some("example.com/{{user_id}}".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
balanced_rpcs: HashMap::from([
|
||||
|
@ -91,9 +91,9 @@ pub struct AppConfig {
|
||||
#[serde(default = "default_response_cache_max_bytes")]
|
||||
pub response_cache_max_bytes: usize,
|
||||
/// the stats page url for an anonymous user.
|
||||
pub redirect_public_url: String,
|
||||
/// the stats page url for a logged in user. it must contain "{user_id}"
|
||||
pub redirect_user_url: String,
|
||||
pub redirect_public_url: Option<String>,
|
||||
/// the stats page url for a logged in user. if set, must contain "{user_id}"
|
||||
pub redirect_user_url: Option<String>,
|
||||
}
|
||||
|
||||
/// This might cause a thundering herd!
|
||||
|
@ -1,3 +1,5 @@
|
||||
//! Utilities for authorization of logged in and anonymous users.
|
||||
|
||||
use super::errors::FrontendErrorResponse;
|
||||
use crate::app::{UserKeyData, Web3ProxyApp};
|
||||
use crate::jsonrpc::JsonRpcRequest;
|
||||
@ -21,6 +23,7 @@ use ulid::Ulid;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// This lets us use UUID and ULID while we transition to only ULIDs
|
||||
/// TODO: include the key's description.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
pub enum UserKey {
|
||||
Ulid(Ulid),
|
||||
|
@ -1,3 +1,5 @@
|
||||
//! Utlities for logging errors for admins and displaying errors to users.
|
||||
|
||||
use crate::{app::UserKeyData, jsonrpc::JsonRpcForwardedResponse};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
|
@ -1,9 +1,12 @@
|
||||
//! `frontend` contains HTTP and websocket endpoints for use by users and admins.
|
||||
|
||||
pub mod authorization;
|
||||
mod errors;
|
||||
mod rpc_proxy_http;
|
||||
mod rpc_proxy_ws;
|
||||
mod status;
|
||||
mod users;
|
||||
// TODO: these are only public so docs are generated. What's a better way to do this?
|
||||
pub mod rpc_proxy_http;
|
||||
pub mod rpc_proxy_ws;
|
||||
pub mod status;
|
||||
pub mod users;
|
||||
|
||||
use crate::app::Web3ProxyApp;
|
||||
use axum::{
|
||||
@ -23,7 +26,7 @@ use tower_http::trace::TraceLayer;
|
||||
use tower_request_id::{RequestId, RequestIdLayer};
|
||||
use tracing::{error_span, info};
|
||||
|
||||
/// http and websocket frontend for customers
|
||||
/// Start the frontend server.
|
||||
pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()> {
|
||||
// create a tracing span for each request with a random request id and the method
|
||||
// GET: websocket or static pages
|
||||
@ -48,7 +51,7 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
|
||||
|
||||
// build our axum Router
|
||||
let app = Router::new()
|
||||
// routes should be order most to least common
|
||||
// routes should be ordered most to least common
|
||||
.route("/rpc", post(rpc_proxy_http::proxy_web3_rpc))
|
||||
.route("/rpc", get(rpc_proxy_ws::websocket_handler))
|
||||
.route(
|
||||
@ -60,17 +63,21 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
|
||||
get(rpc_proxy_ws::websocket_handler_with_key),
|
||||
)
|
||||
.route("/health", get(status::health))
|
||||
.route("/user/login/:user_address", get(users::user_login_get))
|
||||
.route(
|
||||
"/user/login/:user_address/:message_eip",
|
||||
get(users::user_login_get),
|
||||
)
|
||||
.route("/user/login", post(users::user_login_post))
|
||||
.route("/user/balance", get(users::user_balance_get))
|
||||
.route("/user/balance/:txid", post(users::user_balance_post))
|
||||
.route("/user/profile", get(users::user_profile_get))
|
||||
.route("/user/profile", post(users::user_profile_post))
|
||||
.route("/user/stats", get(users::user_stats_get))
|
||||
.route("/user/logout", post(users::user_logout_post))
|
||||
.route("/status", get(status::status))
|
||||
// TODO: make this optional or remove it since it is available on another port
|
||||
.route("/prometheus", get(status::prometheus))
|
||||
.route("/user/login/:user_address", get(users::get_login))
|
||||
.route(
|
||||
"/user/login/:user_address/:message_eip",
|
||||
get(users::get_login),
|
||||
)
|
||||
.route("/user/login", post(users::post_login))
|
||||
.route("/user", post(users::post_user))
|
||||
.route("/user/logout", get(users::get_logout))
|
||||
// layers are ordered bottom up
|
||||
// the last layer is first for requests and last for responses
|
||||
// Mark the `Authorization` request header as sensitive so it doesn't show in logs
|
||||
@ -112,8 +119,9 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
|
||||
}
|
||||
|
||||
/// Tokio signal handler that will wait for a user to press CTRL+C.
|
||||
/// We use this in our hyper `Server` method `with_graceful_shutdown`.
|
||||
/// Used in our hyper `Server` method `with_graceful_shutdown`.
|
||||
async fn signal_shutdown() {
|
||||
// TODO: take a shutdown_receiver and select on ctrl_c and it
|
||||
info!("ctrl-c to quit");
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
|
@ -1,3 +1,5 @@
|
||||
//! Take a user's HTTP JSON-RPC requests and either respond from local data or proxy the request to a backend rpc server.
|
||||
|
||||
use super::authorization::{bearer_is_authorized, ip_is_authorized, key_is_authorized};
|
||||
use super::errors::FrontendResult;
|
||||
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
|
||||
@ -10,6 +12,9 @@ use axum_client_ip::ClientIp;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error_span, Instrument};
|
||||
|
||||
/// POST /rpc -- Public entrypoint for HTTP JSON-RPC requests. Web3 wallets use this.
|
||||
/// Defaults to rate limiting by IP address, but can also read the Authorization header for a bearer token.
|
||||
/// If possible, please use a WebSocket instead.
|
||||
pub async fn proxy_web3_rpc(
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
bearer: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
@ -40,6 +45,7 @@ pub async fn proxy_web3_rpc(
|
||||
|
||||
let authorized_request = Arc::new(authorized_request);
|
||||
|
||||
// TODO: spawn earlier?
|
||||
let f = tokio::spawn(async move {
|
||||
app.proxy_web3_rpc(authorized_request, payload)
|
||||
.instrument(request_span)
|
||||
@ -51,6 +57,10 @@ pub async fn proxy_web3_rpc(
|
||||
Ok(Json(&response).into_response())
|
||||
}
|
||||
|
||||
/// Authenticated entrypoint for HTTP JSON-RPC requests. Web3 wallets use this.
|
||||
/// Rate limit and billing based on the api key in the url.
|
||||
/// Can optionally authorized based on origin, referer, or user agent.
|
||||
/// If possible, please use a WebSocket instead.
|
||||
pub async fn proxy_web3_rpc_with_key(
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
ClientIp(ip): ClientIp,
|
||||
@ -64,7 +74,6 @@ pub async fn proxy_web3_rpc_with_key(
|
||||
|
||||
let request_span = error_span!("request", %ip, ?referer, ?user_agent);
|
||||
|
||||
// TODO: this should probably return the user_key_id instead? or maybe both?
|
||||
let (authorized_request, _semaphore) = key_is_authorized(
|
||||
&app,
|
||||
user_key,
|
||||
@ -80,6 +89,8 @@ pub async fn proxy_web3_rpc_with_key(
|
||||
|
||||
let authorized_request = Arc::new(authorized_request);
|
||||
|
||||
// the request can take a while, so we spawn so that we can start serving another request
|
||||
// TODO: spawn even earlier?
|
||||
let f = tokio::spawn(async move {
|
||||
app.proxy_web3_rpc(authorized_request, payload)
|
||||
.instrument(request_span)
|
||||
|
@ -1,3 +1,7 @@
|
||||
//! Take a user's WebSocket JSON-RPC requests and either respond from local data or proxy the request to a backend rpc server.
|
||||
//!
|
||||
//! WebSockets are the preferred method of receiving requests, but not all clients have good support.
|
||||
|
||||
use super::authorization::{
|
||||
bearer_is_authorized, ip_is_authorized, key_is_authorized, AuthorizedRequest,
|
||||
};
|
||||
@ -28,6 +32,8 @@ use crate::{
|
||||
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
|
||||
};
|
||||
|
||||
/// Public entrypoint for WebSocket JSON-RPC requests.
|
||||
/// Defaults to rate limiting by IP address, but can also read the Authorization header for a bearer token.
|
||||
#[debug_handler]
|
||||
pub async fn websocket_handler(
|
||||
bearer: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
@ -66,12 +72,23 @@ pub async fn websocket_handler(
|
||||
})
|
||||
.into_response()),
|
||||
None => {
|
||||
// this is not a websocket. redirect to a friendly page
|
||||
Ok(Redirect::to(&app.config.redirect_public_url).into_response())
|
||||
if let Some(redirect) = &app.config.redirect_public_url {
|
||||
// this is not a websocket. redirect to a friendly page
|
||||
Ok(Redirect::to(redirect).into_response())
|
||||
} else {
|
||||
// TODO: do not use an anyhow error. send the user a 400
|
||||
Err(
|
||||
anyhow::anyhow!("redirect_public_url not set. only websockets work here")
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Authenticated entrypoint for WebSocket JSON-RPC requests. Web3 wallets use this.
|
||||
/// Rate limit and billing based on the api key in the url.
|
||||
/// Can optionally authorized based on origin, referer, or user agent.
|
||||
#[debug_handler]
|
||||
pub async fn websocket_handler_with_key(
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
@ -107,20 +124,25 @@ pub async fn websocket_handler_with_key(
|
||||
proxy_web3_socket(app, authorized_request, socket).instrument(request_span)
|
||||
})),
|
||||
None => {
|
||||
// TODO: store this on the app and use register_template?
|
||||
let reg = Handlebars::new();
|
||||
if let Some(redirect) = &app.config.redirect_user_url {
|
||||
// TODO: store this on the app and use register_template?
|
||||
let reg = Handlebars::new();
|
||||
|
||||
// TODO: show the user's address, not their id (remember to update the checks for {{user_id}}} in app.rs)
|
||||
// TODO: query to get the user's address. expose that instead of user_id
|
||||
let user_url = reg
|
||||
.render_template(
|
||||
&app.config.redirect_user_url,
|
||||
&json!({ "authorized_request": authorized_request }),
|
||||
)
|
||||
.expect("templating should always work");
|
||||
// TODO: show the user's address, not their id (remember to update the checks for {{user_id}}} in app.rs)
|
||||
// TODO: query to get the user's address. expose that instead of user_id
|
||||
let user_url = reg
|
||||
.render_template(
|
||||
redirect,
|
||||
&json!({ "authorized_request": authorized_request }),
|
||||
)
|
||||
.expect("templating should always work");
|
||||
|
||||
// this is not a websocket. redirect to a page for this user
|
||||
Ok(Redirect::to(&user_url).into_response())
|
||||
// this is not a websocket. redirect to a page for this user
|
||||
Ok(Redirect::to(&user_url).into_response())
|
||||
} else {
|
||||
// TODO: do not use an anyhow error. send the user a 400
|
||||
Err(anyhow::anyhow!("redirect_user_url not set. only websockets work here").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
//! Used by admins for health checks and inspecting global statistics.
|
||||
//!
|
||||
//! For ease of development, users can currently access these endponts.
|
||||
//! They will eventually move to another port.
|
||||
|
||||
use crate::app::Web3ProxyApp;
|
||||
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
|
||||
use moka::future::ConcurrentCacheExt;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Health check page for load balancers to use
|
||||
/// Health check page for load balancers to use.
|
||||
pub async fn health(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
|
||||
// TODO: also check that the head block is not too old
|
||||
if app.balanced_rpcs.synced() {
|
||||
@ -14,13 +19,15 @@ pub async fn health(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoRe
|
||||
}
|
||||
}
|
||||
|
||||
/// Prometheus metrics
|
||||
/// Prometheus metrics.
|
||||
///
|
||||
/// TODO: when done debugging, remove this and only allow access on a different port
|
||||
pub async fn prometheus(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
|
||||
app.prometheus_metrics()
|
||||
}
|
||||
|
||||
/// Very basic status page
|
||||
/// Very basic status page.
|
||||
///
|
||||
/// TODO: replace this with proper stats and monitoring
|
||||
pub async fn status(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
|
||||
app.pending_transactions.sync();
|
||||
|
@ -1,11 +1,4 @@
|
||||
// So the API needs to show for any given user:
|
||||
// - show balance in USD
|
||||
// - show deposits history (currency, amounts, transaction id)
|
||||
// - show number of requests used (so we can calculate average spending over a month, burn rate for a user etc, something like "Your balance will be depleted in xx days)
|
||||
// - the email address of a user if he opted in to get contacted via email
|
||||
// - all the monitoring and stats but that will come from someplace else if I understand corectly?
|
||||
// I wonder how we handle payment
|
||||
// probably have to do manual withdrawals
|
||||
//! Handle registration, logins, and managing account data.
|
||||
|
||||
use super::authorization::{login_is_authorized, UserKey};
|
||||
use super::errors::FrontendResult;
|
||||
@ -32,9 +25,20 @@ use std::sync::Arc;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use ulid::Ulid;
|
||||
|
||||
// TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead
|
||||
/// `GET /user/login/:user_address` or `GET /user/login/:user_address/:message_eip` -- Start the "Sign In with Ethereum" (siwe) login flow.
|
||||
///
|
||||
/// `message_eip`s accepted:
|
||||
/// - eip191_bytes
|
||||
/// - eip191_hash
|
||||
/// - eip4361 (default)
|
||||
///
|
||||
/// Coming soon: eip1271
|
||||
///
|
||||
/// This is the initial entrypoint for logging in. Take the response from this endpoint and give it to your user's wallet for singing. POST the response to `/user/login`.
|
||||
///
|
||||
/// Rate limited by IP address.
|
||||
#[debug_handler]
|
||||
pub async fn get_login(
|
||||
pub async fn user_login_get(
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
ClientIp(ip): ClientIp,
|
||||
// TODO: what does axum's error handling look like if the path fails to parse?
|
||||
@ -69,6 +73,7 @@ pub async fn get_login(
|
||||
|
||||
// TODO: get most of these from the app config
|
||||
let message = Message {
|
||||
// TODO: should domain be llamanodes, or llamarpc?
|
||||
domain: "staging.llamanodes.com".parse().unwrap(),
|
||||
address: user_address.to_fixed_bytes(),
|
||||
statement: Some("🦙🦙🦙🦙🦙".to_string()),
|
||||
@ -95,6 +100,7 @@ pub async fn get_login(
|
||||
.await?;
|
||||
|
||||
// there are multiple ways to sign messages and not all wallets support them
|
||||
// TODO: default message eip from config?
|
||||
let message_eip = params
|
||||
.remove("message_eip")
|
||||
.unwrap_or_else(|| "eip4361".to_string());
|
||||
@ -112,35 +118,41 @@ pub async fn get_login(
|
||||
Ok(message.into_response())
|
||||
}
|
||||
|
||||
/// Query params to our `post_login` handler.
|
||||
/// Query params for our `post_login` handler.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PostLoginQuery {
|
||||
invite_code: Option<String>,
|
||||
/// While we are in alpha/beta, we require users to supply an invite code.
|
||||
/// The invite code (if any) is set in the application's config.
|
||||
/// This may eventually provide some sort of referral bonus.
|
||||
pub invite_code: Option<String>,
|
||||
}
|
||||
|
||||
/// JSON body to our `post_login` handler.
|
||||
/// TODO: this should be an enum with the different login methods having different structs
|
||||
/// Currently only siwe logins that send an address, msg, and sig are allowed.
|
||||
#[derive(Deserialize)]
|
||||
pub struct PostLogin {
|
||||
address: Address,
|
||||
msg: String,
|
||||
sig: Bytes,
|
||||
pub address: Address,
|
||||
pub msg: String,
|
||||
pub sig: Bytes,
|
||||
// TODO: do we care about these? we should probably check the version is something we expect
|
||||
// version: String,
|
||||
// signer: String,
|
||||
}
|
||||
|
||||
/// TODO: what information should we return?
|
||||
/// Successful logins receive a bearer_token and all of the user's api keys.
|
||||
#[derive(Serialize)]
|
||||
pub struct PostLoginResponse {
|
||||
/// Used for authenticating additonal requests.
|
||||
bearer_token: Ulid,
|
||||
/// Used for authenticating with the RPC endpoints.
|
||||
api_keys: Vec<UserKey>,
|
||||
}
|
||||
|
||||
/// Post to the user endpoint to register or login.
|
||||
/// It is recommended to save the returned bearer this in a cookie and send bac
|
||||
/// `POST /user/login` - Register or login by posting a signed "siwe" message.
|
||||
/// It is recommended to save the returned bearer token in a cookie.
|
||||
/// The bearer token can be used to authenticate other requests, such as getting the user's stats or modifying the user's profile.
|
||||
#[debug_handler]
|
||||
pub async fn post_login(
|
||||
pub async fn user_login_post(
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
ClientIp(ip): ClientIp,
|
||||
Json(payload): Json<PostLogin>,
|
||||
@ -266,9 +278,9 @@ pub async fn post_login(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Log out the user connected to the given Authentication header.
|
||||
/// `POST /user/logout` - Forget the bearer token in the `Authentication` header.
|
||||
#[debug_handler]
|
||||
pub async fn get_logout(
|
||||
pub async fn user_logout_post(
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
|
||||
) -> FrontendResult {
|
||||
@ -293,9 +305,9 @@ pub struct PostUser {
|
||||
// TODO: make them sign this JSON? cookie in session id is hard because its on a different domain
|
||||
}
|
||||
|
||||
/// `POST /user/profile` -- modify the account connected to the bearer token in the `Authentication` header.
|
||||
#[debug_handler]
|
||||
/// post to the user endpoint to modify your existing account
|
||||
pub async fn post_user(
|
||||
pub async fn user_profile_post(
|
||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||
ClientIp(ip): ClientIp,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
@ -330,17 +342,101 @@ pub async fn post_user(
|
||||
todo!("finish post_user");
|
||||
}
|
||||
|
||||
/// `GET /user/balance` -- Use a bearer token to get the user's balance and spend.
|
||||
///
|
||||
/// - show balance in USD
|
||||
/// - show deposits history (currency, amounts, transaction id)
|
||||
///
|
||||
/// TODO: one key per request? maybe /user/balance/:api_key?
|
||||
/// TODO: this will change as we add better support for secondary users.
|
||||
#[debug_handler]
|
||||
pub async fn user_balance_get(
|
||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
) -> FrontendResult {
|
||||
todo!("user_balance_get");
|
||||
}
|
||||
|
||||
/// `POST /user/balance` -- Manually process a confirmed txid to update a user's balance.
|
||||
///
|
||||
/// We will subscribe to events to watch for any user deposits, but sometimes events can be missed.
|
||||
///
|
||||
/// TODO: rate limit by user
|
||||
/// TODO: one key per request? maybe /user/balance/:api_key?
|
||||
/// TODO: this will change as we add better support for secondary users.
|
||||
#[debug_handler]
|
||||
pub async fn user_balance_post(
|
||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
) -> FrontendResult {
|
||||
todo!("user_balance_post");
|
||||
}
|
||||
|
||||
/// `GET /user/keys` -- Use a bearer token to get the user's api keys and their settings.
|
||||
///
|
||||
/// TODO: one key per request? maybe /user/keys/:api_key?
|
||||
#[debug_handler]
|
||||
pub async fn user_keys_get(
|
||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
) -> FrontendResult {
|
||||
todo!("user_keys_get");
|
||||
}
|
||||
|
||||
/// `POST /user/keys` -- Use a bearer token to create a new key or modify an existing key.
|
||||
///
|
||||
/// TODO: read json from the request body
|
||||
/// TODO: one key per request? maybe /user/keys/:api_key?
|
||||
#[debug_handler]
|
||||
pub async fn user_keys_post(
|
||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
) -> FrontendResult {
|
||||
todo!("user_keys_post");
|
||||
}
|
||||
|
||||
/// `GET /user/profile` -- Use a bearer token to get the user's profile.
|
||||
///
|
||||
/// - the email address of a user if they opted in to get contacted via email
|
||||
///
|
||||
/// TODO: this will change as we add better support for secondary users.
|
||||
#[debug_handler]
|
||||
pub async fn user_profile_get(
|
||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
) -> FrontendResult {
|
||||
todo!("get_user_profile");
|
||||
}
|
||||
|
||||
/// `GET /user/stats` -- Use a bearer token to get the user's key stats such as bandwidth used and methods requested.
|
||||
///
|
||||
/// - show number of requests used (so we can calculate average spending over a month, burn rate for a user etc, something like "Your balance will be depleted in xx days)
|
||||
///
|
||||
/// TODO: one key per request? maybe /user/stats/:api_key?
|
||||
/// TODO: this will change as we add better support for secondary users.
|
||||
#[debug_handler]
|
||||
pub async fn user_stats_get(
|
||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
) -> FrontendResult {
|
||||
todo!("get_user_stats");
|
||||
}
|
||||
|
||||
/// `GET /user/profile` -- Use a bearer token to get the user's profile such as their optional email address.
|
||||
/// Handle authorization for a given address and bearer token.
|
||||
// TODO: what roles should exist?
|
||||
enum ProtectedAction {
|
||||
PostUser,
|
||||
}
|
||||
|
||||
impl ProtectedAction {
|
||||
/// Verify that the given bearer token and address are allowed to take the specified action.
|
||||
async fn verify(
|
||||
self,
|
||||
app: &Web3ProxyApp,
|
||||
// TODO: i don't think we want Bearer here. we want user_key and a helper for bearer -> user_key
|
||||
bearer: Bearer,
|
||||
// TODO: what about secondary addresses? maybe an enum for primary or secondary?
|
||||
primary_address: &Address,
|
||||
) -> anyhow::Result<user::Model> {
|
||||
// get the attached address from redis for the given auth_token.
|
||||
|
Loading…
Reference in New Issue
Block a user