better redirect and jsonrpc handling
This commit is contained in:
parent
7cf82ce156
commit
661a7ad244
4
TODO.md
4
TODO.md
@ -69,6 +69,8 @@
|
|||||||
- [x] Got warning: "WARN subscribe_new_heads:send_block: web3_proxy::connection: unable to get block from https://rpc.ethermine.org: Deserialization Error: expected value at line 1 column 1. Response: error code: 1015". this is cloudflare rate limiting on fetching a block, but this is a private rpc. why is there a block subscription?
|
- [x] Got warning: "WARN subscribe_new_heads:send_block: web3_proxy::connection: unable to get block from https://rpc.ethermine.org: Deserialization Error: expected value at line 1 column 1. Response: error code: 1015". this is cloudflare rate limiting on fetching a block, but this is a private rpc. why is there a block subscription?
|
||||||
- [x] im seeing ethspam occasionally try to query a future block. something must be setting the head block too early
|
- [x] im seeing ethspam occasionally try to query a future block. something must be setting the head block too early
|
||||||
- [x] we were sorting best block the wrong direction. i flipped a.cmp(b) to b.cmp(a) so that the largest would be first, but then i used 'max_by' which looks at the end of the list
|
- [x] we were sorting best block the wrong direction. i flipped a.cmp(b) to b.cmp(a) so that the largest would be first, but then i used 'max_by' which looks at the end of the list
|
||||||
|
- [x] HTTP GET to the websocket endpoints should redirect instead of giving an ugly error
|
||||||
|
- [ ] load the redirected page from config
|
||||||
- [ ] basic request method stats
|
- [ ] basic request method stats
|
||||||
- [ ] use siwe messages and signatures for sign up and login
|
- [ ] use siwe messages and signatures for sign up and login
|
||||||
- [ ] active requests on /status is always 0 even when i'm running requests through
|
- [ ] active requests on /status is always 0 even when i'm running requests through
|
||||||
@ -76,7 +78,6 @@
|
|||||||
- [ ] i think the server isn't following the spec. we need a context attached to this error so we know which one
|
- [ ] i think the server isn't following the spec. we need a context attached to this error so we know which one
|
||||||
- [ ] maybe make jsonrpc an Option
|
- [ ] maybe make jsonrpc an Option
|
||||||
- [ ] "chain is forked" message is wrong. it includes nodes just being on different heights of the same chain. need a smarter check
|
- [ ] "chain is forked" message is wrong. it includes nodes just being on different heights of the same chain. need a smarter check
|
||||||
- [ ] disable redis persistence in dev
|
|
||||||
|
|
||||||
## V1
|
## V1
|
||||||
|
|
||||||
@ -243,3 +244,4 @@ in another repo: event subscriber
|
|||||||
eth_1 | 2022-08-10T23:26:08.917603Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 1/2/5/5 rpcs have 0x0538…bfff
|
eth_1 | 2022-08-10T23:26:08.917603Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 1/2/5/5 rpcs have 0x0538…bfff
|
||||||
eth_1 | 2022-08-10T23:26:10.195014Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 1/2/5/5 rpcs have 0x0538…bfff
|
eth_1 | 2022-08-10T23:26:10.195014Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 1/2/5/5 rpcs have 0x0538…bfff
|
||||||
eth_1 | 2022-08-10T23:26:10.195658Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 2/3/5/5 rpcs have 0x0538…bfff
|
eth_1 | 2022-08-10T23:26:10.195658Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 2/3/5/5 rpcs have 0x0538…bfff
|
||||||
|
- [ ] disable redis persistence in dev
|
||||||
|
@ -10,13 +10,13 @@ use crate::jsonrpc::JsonRpcForwardedResponse;
|
|||||||
pub async fn handler_404() -> Response {
|
pub async fn handler_404() -> Response {
|
||||||
let err = anyhow::anyhow!("nothing to see here");
|
let err = anyhow::anyhow!("nothing to see here");
|
||||||
|
|
||||||
handle_anyhow_error(Some(StatusCode::NOT_FOUND), None, err)
|
anyhow_error_into_response(Some(StatusCode::NOT_FOUND), None, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// handle errors by converting them into something that implements `IntoResponse`
|
/// handle errors by converting them into something that implements `IntoResponse`
|
||||||
/// TODO: use this. i can't get <https://docs.rs/axum/latest/axum/error_handling/index.html> to work
|
/// TODO: use this. i can't get <https://docs.rs/axum/latest/axum/error_handling/index.html> to work
|
||||||
/// TODO: i think we want a custom result type instead. put the anyhow result inside. then `impl IntoResponse for CustomResult`
|
/// TODO: i think we want a custom result type instead. put the anyhow result inside. then `impl IntoResponse for CustomResult`
|
||||||
pub fn handle_anyhow_error(
|
pub fn anyhow_error_into_response(
|
||||||
http_code: Option<StatusCode>,
|
http_code: Option<StatusCode>,
|
||||||
id: Option<Box<RawValue>>,
|
id: Option<Box<RawValue>>,
|
||||||
err: anyhow::Error,
|
err: anyhow::Error,
|
||||||
|
@ -5,8 +5,8 @@ use axum_client_ip::ClientIp;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::errors::handle_anyhow_error;
|
use super::errors::anyhow_error_into_response;
|
||||||
use super::rate_limit::handle_rate_limit_error_response;
|
use super::rate_limit::RateLimitResult;
|
||||||
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
|
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
|
||||||
|
|
||||||
pub async fn public_proxy_web3_rpc(
|
pub async fn public_proxy_web3_rpc(
|
||||||
@ -14,15 +14,18 @@ pub async fn public_proxy_web3_rpc(
|
|||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Some(err_response) =
|
let _ip = match app.rate_limit_by_ip(ip).await {
|
||||||
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
|
Ok(x) => match x.try_into_response().await {
|
||||||
{
|
Ok(RateLimitResult::AllowedIp(x)) => x,
|
||||||
return err_response.into_response();
|
Err(err_response) => return err_response,
|
||||||
}
|
_ => unimplemented!(),
|
||||||
|
},
|
||||||
|
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
match app.proxy_web3_rpc(payload).await {
|
match app.proxy_web3_rpc(payload).await {
|
||||||
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
||||||
Err(err) => handle_anyhow_error(None, None, err).into_response(),
|
Err(err) => anyhow_error_into_response(None, None, err).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,15 +34,17 @@ pub async fn user_proxy_web3_rpc(
|
|||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
Path(user_key): Path<Uuid>,
|
Path(user_key): Path<Uuid>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// TODO: add a helper on this that turns RateLimitResult into error if its not allowed
|
let _user_id = match app.rate_limit_by_key(user_key).await {
|
||||||
if let Some(err_response) =
|
Ok(x) => match x.try_into_response().await {
|
||||||
handle_rate_limit_error_response(app.rate_limit_by_key(user_key).await).await
|
Ok(RateLimitResult::AllowedUser(x)) => x,
|
||||||
{
|
Err(err_response) => return err_response,
|
||||||
return err_response.into_response();
|
_ => unimplemented!(),
|
||||||
}
|
},
|
||||||
|
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
match app.proxy_web3_rpc(payload).await {
|
match app.proxy_web3_rpc(payload).await {
|
||||||
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
||||||
Err(err) => handle_anyhow_error(None, None, err),
|
Err(err) => anyhow_error_into_response(None, None, err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,16 +12,46 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::app::{UserCacheValue, Web3ProxyApp};
|
use crate::app::{UserCacheValue, Web3ProxyApp};
|
||||||
|
|
||||||
use super::errors::handle_anyhow_error;
|
use super::errors::anyhow_error_into_response;
|
||||||
|
|
||||||
pub enum RateLimitResult {
|
pub enum RateLimitResult {
|
||||||
Allowed,
|
AllowedIp(IpAddr),
|
||||||
RateLimitExceeded,
|
AllowedUser(i64),
|
||||||
|
IpRateLimitExceeded(IpAddr),
|
||||||
|
UserRateLimitExceeded(i64),
|
||||||
UnknownKey,
|
UnknownKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RateLimitResult {
|
||||||
|
// TODO: i think this should be a function on RateLimitResult
|
||||||
|
pub async fn try_into_response(self) -> Result<RateLimitResult, Response> {
|
||||||
|
match self {
|
||||||
|
RateLimitResult::AllowedIp(_) => Ok(self),
|
||||||
|
RateLimitResult::AllowedUser(_) => Ok(self),
|
||||||
|
RateLimitResult::IpRateLimitExceeded(ip) => Err(anyhow_error_into_response(
|
||||||
|
Some(StatusCode::TOO_MANY_REQUESTS),
|
||||||
|
None,
|
||||||
|
// TODO: how can we attach context here? maybe add a request id tracing field?
|
||||||
|
anyhow::anyhow!(format!("rate limit exceeded for {}", ip)),
|
||||||
|
)),
|
||||||
|
RateLimitResult::UserRateLimitExceeded(user) => Err(anyhow_error_into_response(
|
||||||
|
Some(StatusCode::TOO_MANY_REQUESTS),
|
||||||
|
None,
|
||||||
|
// TODO: don't expose numeric ids. show the address instead
|
||||||
|
// TODO: how can we attach context here? maybe add a request id tracing field?
|
||||||
|
anyhow::anyhow!(format!("rate limit exceeded for user {}", user)),
|
||||||
|
)),
|
||||||
|
RateLimitResult::UnknownKey => Err(anyhow_error_into_response(
|
||||||
|
Some(StatusCode::FORBIDDEN),
|
||||||
|
None,
|
||||||
|
anyhow::anyhow!("unknown key"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Web3ProxyApp {
|
impl Web3ProxyApp {
|
||||||
pub async fn rate_limit_by_ip(&self, ip: &IpAddr) -> anyhow::Result<RateLimitResult> {
|
pub async fn rate_limit_by_ip(&self, ip: IpAddr) -> anyhow::Result<RateLimitResult> {
|
||||||
let rate_limiter_key = format!("ip-{}", ip);
|
let rate_limiter_key = format!("ip-{}", ip);
|
||||||
|
|
||||||
// TODO: dry this up with rate_limit_by_key
|
// TODO: dry this up with rate_limit_by_key
|
||||||
@ -35,7 +65,7 @@ impl Web3ProxyApp {
|
|||||||
// TODO: set headers so they know when they can retry
|
// TODO: set headers so they know when they can retry
|
||||||
debug!(?rate_limiter_key, "rate limit exceeded"); // this is too verbose, but a stat might be good
|
debug!(?rate_limiter_key, "rate limit exceeded"); // this is too verbose, but a stat might be good
|
||||||
// TODO: use their id if possible
|
// TODO: use their id if possible
|
||||||
return Ok(RateLimitResult::RateLimitExceeded);
|
return Ok(RateLimitResult::IpRateLimitExceeded(ip));
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// internal error, not rate limit being hit
|
// internal error, not rate limit being hit
|
||||||
@ -48,7 +78,7 @@ impl Web3ProxyApp {
|
|||||||
warn!("no rate limiter!");
|
warn!("no rate limiter!");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(RateLimitResult::Allowed)
|
Ok(RateLimitResult::AllowedIp(ip))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rate_limit_by_key(&self, user_key: Uuid) -> anyhow::Result<RateLimitResult> {
|
pub async fn rate_limit_by_key(&self, user_key: Uuid) -> anyhow::Result<RateLimitResult> {
|
||||||
@ -142,26 +172,6 @@ impl Web3ProxyApp {
|
|||||||
unimplemented!("no redis. cannot rate limit")
|
unimplemented!("no redis. cannot rate limit")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(RateLimitResult::Allowed)
|
Ok(RateLimitResult::AllowedUser(user_data.user_id))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_rate_limit_error_response(
|
|
||||||
r: anyhow::Result<RateLimitResult>,
|
|
||||||
) -> Option<Response> {
|
|
||||||
match r {
|
|
||||||
Ok(RateLimitResult::Allowed) => None,
|
|
||||||
Ok(RateLimitResult::RateLimitExceeded) => Some(handle_anyhow_error(
|
|
||||||
Some(StatusCode::TOO_MANY_REQUESTS),
|
|
||||||
None,
|
|
||||||
// TODO: how can we attach context here? maybe add a request id tracing field?
|
|
||||||
anyhow::anyhow!("rate limit exceeded"),
|
|
||||||
)),
|
|
||||||
Ok(RateLimitResult::UnknownKey) => Some(handle_anyhow_error(
|
|
||||||
Some(StatusCode::FORBIDDEN),
|
|
||||||
None,
|
|
||||||
anyhow::anyhow!("unknown key"),
|
|
||||||
)),
|
|
||||||
Err(err) => Some(handle_anyhow_error(None, None, err)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,21 @@
|
|||||||
// I wonder how we handle payment
|
// I wonder how we handle payment
|
||||||
// probably have to do manual withdrawals
|
// probably have to do manual withdrawals
|
||||||
|
|
||||||
use axum::{response::IntoResponse, Extension, Json};
|
use axum::{
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Extension, Json,
|
||||||
|
};
|
||||||
use axum_client_ip::ClientIp;
|
use axum_client_ip::ClientIp;
|
||||||
use entities::user;
|
use entities::user;
|
||||||
use ethers::{prelude::Address, types::Bytes};
|
use ethers::{prelude::Address, types::Bytes};
|
||||||
|
use reqwest::StatusCode;
|
||||||
use sea_orm::ActiveModelTrait;
|
use sea_orm::ActiveModelTrait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::app::Web3ProxyApp;
|
use crate::app::Web3ProxyApp;
|
||||||
|
|
||||||
use super::rate_limit::handle_rate_limit_error_response;
|
use super::{rate_limit::RateLimitResult, errors::anyhow_error_into_response};
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
// this argument tells axum to parse the request body
|
// this argument tells axum to parse the request body
|
||||||
@ -25,12 +29,15 @@ pub async fn create_user(
|
|||||||
Json(payload): Json<CreateUser>,
|
Json(payload): Json<CreateUser>,
|
||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
) -> impl IntoResponse {
|
) -> Response {
|
||||||
if let Some(err_response) =
|
let _ip = match app.rate_limit_by_ip(ip).await {
|
||||||
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
|
Ok(x) => match x.try_into_response().await {
|
||||||
{
|
Ok(RateLimitResult::AllowedIp(x)) => x,
|
||||||
return err_response.into_response();
|
Err(err_response) => return err_response,
|
||||||
}
|
_ => unimplemented!(),
|
||||||
|
},
|
||||||
|
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: check invite_code against the app's config or database
|
// TODO: check invite_code against the app's config or database
|
||||||
if payload.invite_code != "llam4n0des!" {
|
if payload.invite_code != "llam4n0des!" {
|
||||||
@ -58,8 +65,8 @@ pub async fn create_user(
|
|||||||
// TODO: proper error message
|
// TODO: proper error message
|
||||||
let user = user.insert(db).await.unwrap();
|
let user = user.insert(db).await.unwrap();
|
||||||
|
|
||||||
todo!("serialize and return the user: {:?}", user)
|
// TODO: do not expose user ids
|
||||||
// (StatusCode::CREATED, Json(user))
|
(StatusCode::CREATED, Json(user)).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
// the input to our `create_user` handler
|
// the input to our `create_user` handler
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::ws::{Message, WebSocket, WebSocketUpgrade},
|
extract::ws::{Message, WebSocket, WebSocketUpgrade},
|
||||||
extract::Path,
|
extract::Path,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
Extension,
|
Extension,
|
||||||
};
|
};
|
||||||
use axum_client_ip::ClientIp;
|
use axum_client_ip::ClientIp;
|
||||||
|
use fstrings::{format_args_f, format_f};
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
use futures::{
|
use futures::{
|
||||||
future::AbortHandle,
|
future::AbortHandle,
|
||||||
@ -22,18 +23,21 @@ use crate::{
|
|||||||
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
|
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::rate_limit::handle_rate_limit_error_response;
|
use super::{errors::anyhow_error_into_response, rate_limit::RateLimitResult};
|
||||||
|
|
||||||
pub async fn public_websocket_handler(
|
pub async fn public_websocket_handler(
|
||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
ws_upgrade: Option<WebSocketUpgrade>,
|
ws_upgrade: Option<WebSocketUpgrade>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Some(err_response) =
|
let ip = match app.rate_limit_by_ip(ip).await {
|
||||||
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
|
Ok(x) => match x.try_into_response().await {
|
||||||
{
|
Ok(RateLimitResult::AllowedIp(x)) => x,
|
||||||
return err_response.into_response();
|
Err(err_response) => return err_response,
|
||||||
}
|
_ => unimplemented!(),
|
||||||
|
},
|
||||||
|
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
match ws_upgrade {
|
match ws_upgrade {
|
||||||
Some(ws) => ws
|
Some(ws) => ws
|
||||||
@ -41,9 +45,8 @@ pub async fn public_websocket_handler(
|
|||||||
.into_response(),
|
.into_response(),
|
||||||
None => {
|
None => {
|
||||||
// this is not a websocket. give a friendly page. maybe redirect to the llama nodes home
|
// this is not a websocket. give a friendly page. maybe redirect to the llama nodes home
|
||||||
// TODO: make a friendly page
|
// TODO: redirect to a configurable url
|
||||||
// TODO: rate limit this?
|
Redirect::to(&format_f!("https://www.etherscan.io/#userip={ip}")).into_response()
|
||||||
"hello, world".into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,19 +56,23 @@ pub async fn user_websocket_handler(
|
|||||||
Path(user_key): Path<Uuid>,
|
Path(user_key): Path<Uuid>,
|
||||||
ws_upgrade: Option<WebSocketUpgrade>,
|
ws_upgrade: Option<WebSocketUpgrade>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Some(err_response) =
|
// TODO: dry this up. maybe a rate_limit_by_key_response function?
|
||||||
handle_rate_limit_error_response(app.rate_limit_by_key(user_key).await).await
|
let user_id = match app.rate_limit_by_key(user_key).await {
|
||||||
{
|
Ok(x) => match x.try_into_response().await {
|
||||||
return err_response;
|
Ok(RateLimitResult::AllowedUser(x)) => x,
|
||||||
}
|
Err(err_response) => return err_response,
|
||||||
|
_ => unimplemented!(),
|
||||||
|
},
|
||||||
|
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
match ws_upgrade {
|
match ws_upgrade {
|
||||||
Some(ws_upgrade) => ws_upgrade.on_upgrade(|socket| proxy_web3_socket(app, socket)),
|
Some(ws_upgrade) => ws_upgrade.on_upgrade(|socket| proxy_web3_socket(app, socket)),
|
||||||
None => {
|
None => {
|
||||||
// this is not a websocket. give a friendly page with stats for this user
|
// this is not a websocket. give a friendly page with stats for this user
|
||||||
// TODO: make a friendly page
|
// TODO: redirect to a configurable url
|
||||||
// TODO: rate limit this?
|
// TODO: should this use user_key instead? or user's address?
|
||||||
"hello, world".into_response()
|
Redirect::to(&format_f!("https://www.etherscan.io/#userid={user_id}")).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@ use tracing::warn;
|
|||||||
|
|
||||||
#[derive(Clone, serde::Deserialize)]
|
#[derive(Clone, serde::Deserialize)]
|
||||||
pub struct JsonRpcRequest {
|
pub struct JsonRpcRequest {
|
||||||
// TODO: skip jsonrpc entireley?
|
// TODO: skip jsonrpc entirely? its against spec to drop it, but some servers bad
|
||||||
pub jsonrpc: Box<RawValue>,
|
pub jsonrpc: Option<Box<RawValue>>,
|
||||||
/// id could be a stricter type, but many rpcs do things against the spec
|
/// id could be a stricter type, but many rpcs do things against the spec
|
||||||
pub id: Box<RawValue>,
|
pub id: Box<RawValue>,
|
||||||
pub method: String,
|
pub method: String,
|
||||||
|
Loading…
Reference in New Issue
Block a user