getting closer
This commit is contained in:
parent
37df1f6506
commit
4ca292c3e1
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -73,6 +73,9 @@ name = "anyhow"
|
|||||||
version = "1.0.57"
|
version = "1.0.57"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
|
3
TODO.md
3
TODO.md
@ -1,5 +1,8 @@
|
|||||||
# Todo
|
# Todo
|
||||||
|
|
||||||
|
- [ ] it works for a few seconds and then gets stuck on something
|
||||||
|
- [ ] rpc errors propagate too far. one subscription failing ends the app. isolate the providers more
|
||||||
|
- [ ] its working with one backend node, but multiple breaks. something to do with pending transactions
|
||||||
- [ ] if web3 proxy gets an http error back, retry another node
|
- [ ] if web3 proxy gets an http error back, retry another node
|
||||||
- [x] refactor Connection::spawn. have it return a handle to the spawned future of it running with block and transaction subscriptions
|
- [x] refactor Connection::spawn. have it return a handle to the spawned future of it running with block and transaction subscriptions
|
||||||
- [x] refactor Connections::spawn. have it return a handle that is selecting on those handles?
|
- [x] refactor Connections::spawn. have it return a handle that is selecting on those handles?
|
||||||
|
@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.57"
|
anyhow = { version = "1.0.57", features = ["backtrace"] }
|
||||||
arc-swap = "1.5.0"
|
arc-swap = "1.5.0"
|
||||||
argh = "0.1.7"
|
argh = "0.1.7"
|
||||||
axum = { version = "0.5.7", features = ["serde_json", "tokio-tungstenite", "ws"] }
|
axum = { version = "0.5.7", features = ["serde_json", "tokio-tungstenite", "ws"] }
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
use crate::config::Web3ConnectionConfig;
|
|
||||||
use crate::connections::Web3Connections;
|
|
||||||
use crate::jsonrpc::JsonRpcForwardedResponse;
|
|
||||||
use crate::jsonrpc::JsonRpcForwardedResponseEnum;
|
|
||||||
use crate::jsonrpc::JsonRpcRequest;
|
|
||||||
use crate::jsonrpc::JsonRpcRequestEnum;
|
|
||||||
use axum::extract::ws::Message;
|
use axum::extract::ws::Message;
|
||||||
use dashmap::mapref::entry::Entry as DashMapEntry;
|
use dashmap::mapref::entry::Entry as DashMapEntry;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
@ -20,12 +14,19 @@ use std::fmt;
|
|||||||
use std::sync::atomic::{self, AtomicUsize};
|
use std::sync::atomic::{self, AtomicUsize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::{broadcast, watch};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tokio_stream::wrappers::WatchStream;
|
use tokio_stream::wrappers::WatchStream;
|
||||||
use tracing::{debug, info, info_span, instrument, trace, warn, Instrument};
|
use tracing::{debug, info, info_span, instrument, trace, warn, Instrument};
|
||||||
|
|
||||||
|
use crate::config::Web3ConnectionConfig;
|
||||||
|
use crate::connections::Web3Connections;
|
||||||
|
use crate::jsonrpc::JsonRpcForwardedResponse;
|
||||||
|
use crate::jsonrpc::JsonRpcForwardedResponseEnum;
|
||||||
|
use crate::jsonrpc::JsonRpcRequest;
|
||||||
|
use crate::jsonrpc::JsonRpcRequestEnum;
|
||||||
|
|
||||||
static APP_USER_AGENT: &str = concat!(
|
static APP_USER_AGENT: &str = concat!(
|
||||||
"satoshiandkin/",
|
"satoshiandkin/",
|
||||||
env!("CARGO_PKG_NAME"),
|
env!("CARGO_PKG_NAME"),
|
||||||
@ -53,9 +54,11 @@ pub async fn flatten_handle<T>(handle: AnyhowJoinHandle<T>) -> anyhow::Result<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum TxState {
|
pub enum TxState {
|
||||||
Confirmed(Transaction),
|
Known(TxHash),
|
||||||
Pending(Transaction),
|
Pending(Transaction),
|
||||||
|
Confirmed(Transaction),
|
||||||
Orphaned(Transaction),
|
Orphaned(Transaction),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,9 +73,9 @@ pub struct Web3ProxyApp {
|
|||||||
incoming_requests: ActiveRequestsMap,
|
incoming_requests: ActiveRequestsMap,
|
||||||
response_cache: ResponseLrcCache,
|
response_cache: ResponseLrcCache,
|
||||||
// don't drop this or the sender will stop working
|
// don't drop this or the sender will stop working
|
||||||
|
// TODO: broadcast channel instead?
|
||||||
head_block_receiver: watch::Receiver<Block<TxHash>>,
|
head_block_receiver: watch::Receiver<Block<TxHash>>,
|
||||||
// TODO: i think we want a TxState enum for Confirmed(TxHash, BlockHash) or Pending(TxHash) or Orphan(TxHash, BlockHash)
|
pending_tx_sender: broadcast::Sender<TxState>,
|
||||||
pending_tx_receiver: flume::Receiver<TxState>,
|
|
||||||
next_subscription_id: AtomicUsize,
|
next_subscription_id: AtomicUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +125,9 @@ impl Web3ProxyApp {
|
|||||||
|
|
||||||
// TODO: subscribe to pending transactions on the private rpcs, too?
|
// TODO: subscribe to pending transactions on the private rpcs, too?
|
||||||
let (head_block_sender, head_block_receiver) = watch::channel(Block::default());
|
let (head_block_sender, head_block_receiver) = watch::channel(Block::default());
|
||||||
let (pending_tx_sender, pending_tx_receiver) = flume::unbounded();
|
// TODO: will one receiver lagging be okay?
|
||||||
|
let (pending_tx_sender, pending_tx_receiver) = broadcast::channel(16);
|
||||||
|
drop(pending_tx_receiver);
|
||||||
|
|
||||||
// TODO: attach context to this error
|
// TODO: attach context to this error
|
||||||
let (balanced_rpcs, balanced_handle) = Web3Connections::spawn(
|
let (balanced_rpcs, balanced_handle) = Web3Connections::spawn(
|
||||||
@ -131,7 +136,7 @@ impl Web3ProxyApp {
|
|||||||
http_client.as_ref(),
|
http_client.as_ref(),
|
||||||
rate_limiter.as_ref(),
|
rate_limiter.as_ref(),
|
||||||
Some(head_block_sender),
|
Some(head_block_sender),
|
||||||
Some(pending_tx_sender),
|
Some(pending_tx_sender.clone()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -165,7 +170,7 @@ impl Web3ProxyApp {
|
|||||||
incoming_requests: Default::default(),
|
incoming_requests: Default::default(),
|
||||||
response_cache: Default::default(),
|
response_cache: Default::default(),
|
||||||
head_block_receiver,
|
head_block_receiver,
|
||||||
pending_tx_receiver,
|
pending_tx_sender,
|
||||||
next_subscription_id: 1.into(),
|
next_subscription_id: 1.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -241,12 +246,48 @@ impl Web3ProxyApp {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
r#"["newPendingTransactions"]"# => {
|
r#"["newPendingTransactions"]"# => {
|
||||||
let pending_tx_receiver = self.pending_tx_receiver.clone();
|
let mut pending_tx_receiver = self.pending_tx_sender.subscribe();
|
||||||
|
|
||||||
trace!(?subscription_id, "pending transactions subscription");
|
trace!(?subscription_id, "pending transactions subscription");
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Ok(new_tx_state) = pending_tx_receiver.recv_async().await {
|
while let Ok(new_tx_state) = pending_tx_receiver.recv().await {
|
||||||
let new_tx = match new_tx_state {
|
let new_tx = match new_tx_state {
|
||||||
|
TxState::Known(..) => continue,
|
||||||
|
TxState::Confirmed(..) => continue,
|
||||||
|
TxState::Orphaned(tx) => tx,
|
||||||
|
TxState::Pending(tx) => tx,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: make a struct for this? using our JsonRpcForwardedResponse won't work because it needs an id
|
||||||
|
let msg = json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "eth_subscription",
|
||||||
|
"params": {
|
||||||
|
"subscription": subscription_id,
|
||||||
|
"result": new_tx.hash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let msg = Message::Text(serde_json::to_string(&msg).unwrap());
|
||||||
|
|
||||||
|
if subscription_tx.send_async(msg).await.is_err() {
|
||||||
|
// TODO: cancel this subscription earlier? select on head_block_receiver.next() and an abort handle?
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(?subscription_id, "closed new heads subscription");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
r#"["newPendingFullTransactions"]"# => {
|
||||||
|
// TODO: too much copy/pasta with newPendingTransactions
|
||||||
|
let mut pending_tx_receiver = self.pending_tx_sender.subscribe();
|
||||||
|
|
||||||
|
trace!(?subscription_id, "pending transactions subscription");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(new_tx_state) = pending_tx_receiver.recv().await {
|
||||||
|
let new_tx = match new_tx_state {
|
||||||
|
TxState::Known(..) => continue,
|
||||||
TxState::Confirmed(..) => continue,
|
TxState::Confirmed(..) => continue,
|
||||||
TxState::Orphaned(tx) => tx,
|
TxState::Orphaned(tx) => tx,
|
||||||
TxState::Pending(tx) => tx,
|
TxState::Pending(tx) => tx,
|
||||||
@ -282,6 +323,8 @@ impl Web3ProxyApp {
|
|||||||
|
|
||||||
let response = JsonRpcForwardedResponse::from_string(subscription_id, id);
|
let response = JsonRpcForwardedResponse::from_string(subscription_id, id);
|
||||||
|
|
||||||
|
// TODO: make a `SubscriptonHandle(AbortHandle, JoinHandle)` struct?
|
||||||
|
|
||||||
Ok((subscription_abort_handle, response))
|
Ok((subscription_abort_handle, response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use crate::app::AnyhowJoinHandle;
|
|
||||||
use crate::connection::Web3Connection;
|
|
||||||
use crate::Web3ProxyApp;
|
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
use ethers::prelude::{Block, TxHash};
|
use ethers::prelude::{Block, TxHash};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::app::AnyhowJoinHandle;
|
||||||
|
use crate::connection::Web3Connection;
|
||||||
|
use crate::Web3ProxyApp;
|
||||||
|
|
||||||
#[derive(Debug, FromArgs)]
|
#[derive(Debug, FromArgs)]
|
||||||
/// Web3-proxy is a fast caching and load balancing proxy for web3 (Ethereum or similar) JsonRPC servers.
|
/// Web3-proxy is a fast caching and load balancing proxy for web3 (Ethereum or similar) JsonRPC servers.
|
||||||
pub struct CliConfig {
|
pub struct CliConfig {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
///! Rate-limited communication with a web3 provider
|
///! Rate-limited communication with a web3 provider
|
||||||
|
use anyhow::Context;
|
||||||
use derive_more::From;
|
use derive_more::From;
|
||||||
use ethers::prelude::{Block, Middleware, ProviderError, TxHash};
|
use ethers::prelude::{Block, Middleware, ProviderError, TxHash};
|
||||||
|
use futures::future::try_join_all;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use redis_cell_client::RedisCellClient;
|
use redis_cell_client::RedisCellClient;
|
||||||
use serde::ser::{SerializeStruct, Serializer};
|
use serde::ser::{SerializeStruct, Serializer};
|
||||||
@ -9,13 +11,12 @@ use std::fmt;
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::sync::atomic::{self, AtomicU32};
|
use std::sync::atomic::{self, AtomicU32};
|
||||||
use std::{cmp::Ordering, sync::Arc};
|
use std::{cmp::Ordering, sync::Arc};
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tokio::time::{interval, sleep, Duration, MissedTickBehavior};
|
use tokio::time::{interval, sleep, Duration, MissedTickBehavior};
|
||||||
use tracing::{error, info, instrument, trace, warn};
|
use tracing::{error, info, instrument, trace, warn};
|
||||||
|
|
||||||
use crate::app::AnyhowJoinHandle;
|
use crate::app::{flatten_handle, AnyhowJoinHandle};
|
||||||
|
|
||||||
/// TODO: instead of an enum, I tried to use Box<dyn Provider>, but hit https://github.com/gakonst/ethers-rs/issues/592
|
/// TODO: instead of an enum, I tried to use Box<dyn Provider>, but hit https://github.com/gakonst/ethers-rs/issues/592
|
||||||
#[derive(From)]
|
#[derive(From)]
|
||||||
@ -203,7 +204,7 @@ impl Web3Connection {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn reconnect(
|
pub async fn reconnect(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
block_sender: &flume::Sender<(Block<TxHash>, Arc<Self>)>,
|
block_sender: Option<flume::Sender<(Block<TxHash>, Arc<Self>)>>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// websocket doesn't need the http client
|
// websocket doesn't need the http client
|
||||||
let http_client = None;
|
let http_client = None;
|
||||||
@ -214,10 +215,14 @@ impl Web3Connection {
|
|||||||
*provider = None;
|
*provider = None;
|
||||||
|
|
||||||
// tell the block subscriber that we are at 0
|
// tell the block subscriber that we are at 0
|
||||||
|
if let Some(block_sender) = block_sender {
|
||||||
block_sender
|
block_sender
|
||||||
.send_async((Block::default(), self.clone()))
|
.send_async((Block::default(), self.clone()))
|
||||||
.await?;
|
.await
|
||||||
|
.context("block_sender at 0")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: if this fails, keep retrying
|
||||||
let new_provider = Web3Provider::from_str(&self.url, http_client).await?;
|
let new_provider = Web3Provider::from_str(&self.url, http_client).await?;
|
||||||
|
|
||||||
*provider = Some(Arc::new(new_provider));
|
*provider = Some(Arc::new(new_provider));
|
||||||
@ -249,7 +254,10 @@ impl Web3Connection {
|
|||||||
match block {
|
match block {
|
||||||
Ok(block) => {
|
Ok(block) => {
|
||||||
// TODO: i'm pretty sure we don't need send_async, but double check
|
// TODO: i'm pretty sure we don't need send_async, but double check
|
||||||
block_sender.send_async((block, self.clone())).await?;
|
block_sender
|
||||||
|
.send_async((block, self.clone()))
|
||||||
|
.await
|
||||||
|
.context("block_sender")?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("unable to get block from {}: {}", self, e);
|
warn!("unable to get block from {}: {}", self, e);
|
||||||
@ -265,54 +273,53 @@ impl Web3Connection {
|
|||||||
tx_id_sender: Option<flume::Sender<(TxHash, Arc<Self>)>>,
|
tx_id_sender: Option<flume::Sender<(TxHash, Arc<Self>)>>,
|
||||||
reconnect: bool,
|
reconnect: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
match (block_sender, tx_id_sender) {
|
|
||||||
(None, None) => {
|
|
||||||
// TODO: is there a better way to make a channel that is never ready?
|
|
||||||
let (tx, rx) = oneshot::channel::<()>();
|
|
||||||
rx.await?;
|
|
||||||
drop(tx);
|
|
||||||
}
|
|
||||||
(Some(block_sender), Some(tx_id_sender)) => {
|
|
||||||
// TODO: make these abortable so that if one fails the other can be cancelled?
|
|
||||||
loop {
|
loop {
|
||||||
let new_heads = {
|
let mut futures = vec![];
|
||||||
let clone = self.clone();
|
|
||||||
let block_sender = block_sender.clone();
|
|
||||||
|
|
||||||
clone.subscribe_new_heads(block_sender)
|
if let Some(block_sender) = &block_sender {
|
||||||
};
|
let f = self.clone().subscribe_new_heads(block_sender.clone());
|
||||||
|
|
||||||
let pending_txs = {
|
futures.push(flatten_handle(tokio::spawn(f)));
|
||||||
let clone = self.clone();
|
}
|
||||||
let tx_id_sender = tx_id_sender.clone();
|
|
||||||
|
|
||||||
clone.subscribe_pending_transactions(tx_id_sender)
|
if let Some(tx_id_sender) = &tx_id_sender {
|
||||||
};
|
let f = self
|
||||||
|
.clone()
|
||||||
|
.subscribe_pending_transactions(tx_id_sender.clone());
|
||||||
|
|
||||||
match tokio::try_join!(new_heads, pending_txs) {
|
futures.push(flatten_handle(tokio::spawn(f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if futures.is_empty() {
|
||||||
|
// TODO: is there a better way to make a channel that is never ready?
|
||||||
|
info!(?self, "no-op subscription");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match try_join_all(futures).await {
|
||||||
Ok(_) => break,
|
Ok(_) => break,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if reconnect {
|
if reconnect {
|
||||||
// TODO: exponential backoff
|
// TODO: exponential backoff
|
||||||
// TODO: share code with new heads subscription
|
let retry_in = Duration::from_secs(1);
|
||||||
warn!(
|
warn!(
|
||||||
"subscription exited. Attempting to reconnect in 1 second. {:?}", err
|
?self,
|
||||||
|
"subscription exited. Attempting to reconnect in {:?}. {:?}",
|
||||||
|
retry_in,
|
||||||
|
err
|
||||||
);
|
);
|
||||||
sleep(Duration::from_secs(1)).await;
|
sleep(retry_in).await;
|
||||||
|
|
||||||
// TODO: loop on reconnecting! do not return with a "?" here
|
// TODO: loop on reconnecting! do not return with a "?" here
|
||||||
// TODO: this isn't going to work. it will get in a loop with newHeads
|
// TODO: this isn't going to work. it will get in a loop with newHeads
|
||||||
self.reconnect(&block_sender).await?;
|
self.reconnect(block_sender.clone()).await?;
|
||||||
} else {
|
} else {
|
||||||
error!("subscription exited. {:?}", err);
|
error!(?self, ?err, "subscription exited");
|
||||||
break;
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -463,13 +470,14 @@ impl Web3Connection {
|
|||||||
// TODO: query existing pending txs?
|
// TODO: query existing pending txs?
|
||||||
|
|
||||||
// TODO: should the stream have a timeout on it here?
|
// TODO: should the stream have a timeout on it here?
|
||||||
// TODO: although reconnects will make this less of an issue
|
// TODO: i don't think loop match is what we want. i think while let would be better
|
||||||
loop {
|
loop {
|
||||||
match stream.next().await {
|
match stream.next().await {
|
||||||
Some(pending_tx_id) => {
|
Some(pending_tx_id) => {
|
||||||
tx_id_sender
|
tx_id_sender
|
||||||
.send_async((pending_tx_id, self.clone()))
|
.send_async((pending_tx_id, self.clone()))
|
||||||
.await?;
|
.await
|
||||||
|
.context("tx_id_sender")?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
warn!("subscription ended");
|
warn!("subscription ended");
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
///! Load balanced communication with a group of web3 providers
|
///! Load balanced communication with a group of web3 providers
|
||||||
|
use anyhow::Context;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use counter::Counter;
|
use counter::Counter;
|
||||||
use dashmap::mapref::entry::Entry as DashMapEntry;
|
use dashmap::mapref::entry::Entry as DashMapEntry;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use derive_more::From;
|
use derive_more::From;
|
||||||
use ethers::prelude::{Block, ProviderError, Transaction, TxHash, H256};
|
use ethers::prelude::{Block, ProviderError, Transaction, TxHash, H256};
|
||||||
|
use futures::future::try_join_all;
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
@ -16,12 +18,12 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::{broadcast, watch};
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{debug, info, info_span, instrument, trace, warn};
|
use tracing::{debug, info, info_span, instrument, trace, warn};
|
||||||
|
|
||||||
use crate::app::{AnyhowJoinHandle, TxState};
|
use crate::app::{flatten_handle, AnyhowJoinHandle, TxState};
|
||||||
use crate::config::Web3ConnectionConfig;
|
use crate::config::Web3ConnectionConfig;
|
||||||
use crate::connection::{ActiveRequestHandle, Web3Connection};
|
use crate::connection::{ActiveRequestHandle, Web3Connection};
|
||||||
use crate::jsonrpc::{JsonRpcForwardedResponse, JsonRpcRequest};
|
use crate::jsonrpc::{JsonRpcForwardedResponse, JsonRpcRequest};
|
||||||
@ -34,6 +36,7 @@ struct SyncedConnections {
|
|||||||
// TODO: this should be able to serialize, but it isn't
|
// TODO: this should be able to serialize, but it isn't
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
inner: BTreeSet<Arc<Web3Connection>>,
|
inner: BTreeSet<Arc<Web3Connection>>,
|
||||||
|
// TODO: use petgraph for keeping track of the chain so we can do better fork handling
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for SyncedConnections {
|
impl fmt::Debug for SyncedConnections {
|
||||||
@ -54,7 +57,7 @@ impl SyncedConnections {
|
|||||||
pub struct Web3Connections {
|
pub struct Web3Connections {
|
||||||
inner: Vec<Arc<Web3Connection>>,
|
inner: Vec<Arc<Web3Connection>>,
|
||||||
synced_connections: ArcSwap<SyncedConnections>,
|
synced_connections: ArcSwap<SyncedConnections>,
|
||||||
pending_transactions: DashMap<TxHash, Transaction>,
|
pending_transactions: DashMap<TxHash, TxState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for Web3Connections {
|
impl Serialize for Web3Connections {
|
||||||
@ -89,7 +92,7 @@ impl Web3Connections {
|
|||||||
http_client: Option<&reqwest::Client>,
|
http_client: Option<&reqwest::Client>,
|
||||||
rate_limiter: Option<&redis_cell_client::MultiplexedConnection>,
|
rate_limiter: Option<&redis_cell_client::MultiplexedConnection>,
|
||||||
head_block_sender: Option<watch::Sender<Block<TxHash>>>,
|
head_block_sender: Option<watch::Sender<Block<TxHash>>>,
|
||||||
pending_tx_sender: Option<flume::Sender<TxState>>,
|
pending_tx_sender: Option<broadcast::Sender<TxState>>,
|
||||||
) -> anyhow::Result<(Arc<Self>, AnyhowJoinHandle<()>)> {
|
) -> anyhow::Result<(Arc<Self>, AnyhowJoinHandle<()>)> {
|
||||||
let num_connections = server_configs.len();
|
let num_connections = server_configs.len();
|
||||||
|
|
||||||
@ -153,15 +156,86 @@ impl Web3Connections {
|
|||||||
Ok((connections, handle))
|
Ok((connections, handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_transaction(
|
||||||
|
self: Arc<Self>,
|
||||||
|
rpc: Arc<Web3Connection>,
|
||||||
|
pending_tx_id: TxHash,
|
||||||
|
pending_tx_sender: broadcast::Sender<TxState>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for i in 0..30 {
|
||||||
|
// TODO: also check the "confirmed transactions" mapping? maybe one shared mapping with TxState in it?
|
||||||
|
match self.pending_transactions.entry(pending_tx_id) {
|
||||||
|
DashMapEntry::Occupied(_entry) => {
|
||||||
|
// TODO: if its occupied, but still only "Known", multiple nodes have this transaction. ask both
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
DashMapEntry::Vacant(entry) => {
|
||||||
|
let request_handle = rpc.wait_for_request_handle().await;
|
||||||
|
|
||||||
|
// TODO: how many retries?
|
||||||
|
// TODO: use a generic retry provider instead?
|
||||||
|
let tx_result = request_handle
|
||||||
|
.request("eth_getTransactionByHash", (pending_tx_id,))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// TODO: yearn devs have had better luck with batching these, but i think that's likely just adding a delay itself
|
||||||
|
// TODO: there is a race here sometimes the rpc isn't yet ready to serve the transaction (even though they told us about it!)
|
||||||
|
let pending_transaction = match tx_result {
|
||||||
|
Ok(tx) => Some(tx),
|
||||||
|
Err(err) => {
|
||||||
|
trace!(
|
||||||
|
?i,
|
||||||
|
?err,
|
||||||
|
?pending_tx_id,
|
||||||
|
"error getting transaction by hash"
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: how long? exponential backoff?
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(?pending_transaction, "pending");
|
||||||
|
|
||||||
|
let pending_transaction: Transaction = pending_transaction.unwrap();
|
||||||
|
|
||||||
|
// TODO: do not unwrap. orphans might make this unsafe
|
||||||
|
let tx_state = match &pending_transaction.block_hash {
|
||||||
|
Some(_block_hash) => {
|
||||||
|
// the transaction is already confirmed. no need to save in the pending_transactions map
|
||||||
|
TxState::Confirmed(pending_transaction)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let state = TxState::Pending(pending_transaction);
|
||||||
|
entry.insert(state.clone());
|
||||||
|
state
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: maybe we should just send the txid and they can get it from the dashmap?
|
||||||
|
let _ = pending_tx_sender.send(tx_state);
|
||||||
|
|
||||||
|
info!(?pending_tx_id, "sent");
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(?pending_tx_id, "not found");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// subscribe to all the backend rpcs
|
/// subscribe to all the backend rpcs
|
||||||
async fn subscribe(
|
async fn subscribe(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
pending_tx_id_receiver: flume::Receiver<(TxHash, Arc<Web3Connection>)>,
|
pending_tx_id_receiver: flume::Receiver<(TxHash, Arc<Web3Connection>)>,
|
||||||
block_receiver: flume::Receiver<(Block<TxHash>, Arc<Web3Connection>)>,
|
block_receiver: flume::Receiver<(Block<TxHash>, Arc<Web3Connection>)>,
|
||||||
head_block_sender: Option<watch::Sender<Block<TxHash>>>,
|
head_block_sender: Option<watch::Sender<Block<TxHash>>>,
|
||||||
pending_tx_sender: Option<flume::Sender<TxState>>,
|
pending_tx_sender: Option<broadcast::Sender<TxState>>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut futures = FuturesUnordered::new();
|
let mut futures = vec![];
|
||||||
|
|
||||||
// setup the transaction funnel
|
// setup the transaction funnel
|
||||||
// it skips any duplicates (unless they are being orphaned)
|
// it skips any duplicates (unless they are being orphaned)
|
||||||
@ -171,39 +245,23 @@ impl Web3Connections {
|
|||||||
// TODO: do something with the handle so we can catch any errors
|
// TODO: do something with the handle so we can catch any errors
|
||||||
let clone = self.clone();
|
let clone = self.clone();
|
||||||
let handle = task::spawn(async move {
|
let handle = task::spawn(async move {
|
||||||
while let Ok((pending_transaction_id, rpc)) =
|
while let Ok((pending_tx_id, rpc)) = pending_tx_id_receiver.recv_async().await {
|
||||||
pending_tx_id_receiver.recv_async().await
|
// TODO: spawn this
|
||||||
{
|
let f = clone.clone().send_transaction(
|
||||||
match clone.pending_transactions.entry(pending_transaction_id) {
|
rpc,
|
||||||
DashMapEntry::Occupied(_entry) => continue,
|
pending_tx_id,
|
||||||
DashMapEntry::Vacant(entry) => {
|
pending_tx_sender.clone(),
|
||||||
let request_handle = rpc.wait_for_request_handle().await;
|
);
|
||||||
|
|
||||||
let pending_transaction: Transaction = request_handle
|
tokio::spawn(f);
|
||||||
.request("eth_getTransactionByHash", (pending_transaction_id,))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
trace!(?pending_transaction, "pending");
|
|
||||||
|
|
||||||
// TODO: do not unwrap. orphans might make this unsafe
|
|
||||||
let tx_state = match &pending_transaction.block_hash {
|
|
||||||
Some(_block_hash) => TxState::Confirmed(pending_transaction),
|
|
||||||
None => {
|
|
||||||
entry.insert(pending_transaction.clone());
|
|
||||||
TxState::Pending(pending_transaction)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: maybe we should just send the txid and they can get it from the dashmap?
|
|
||||||
pending_tx_sender.send_async(tx_state).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
futures.push(handle);
|
futures.push(flatten_handle(handle));
|
||||||
|
} else {
|
||||||
|
unimplemented!();
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup the block funnel
|
// setup the block funnel
|
||||||
@ -218,7 +276,7 @@ impl Web3Connections {
|
|||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
futures.push(handle);
|
futures.push(flatten_handle(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
if futures.is_empty() {
|
if futures.is_empty() {
|
||||||
@ -226,8 +284,8 @@ impl Web3Connections {
|
|||||||
unimplemented!("every second, check that the provider is still connected");
|
unimplemented!("every second, check that the provider is still connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Err(e)) = futures.next().await {
|
if let Err(e) = try_join_all(futures).await {
|
||||||
return Err(e.into());
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("subscriptions over: {:?}", self);
|
info!("subscriptions over: {:?}", self);
|
||||||
@ -310,7 +368,7 @@ impl Web3Connections {
|
|||||||
block_receiver: flume::Receiver<(Block<TxHash>, Arc<Web3Connection>)>,
|
block_receiver: flume::Receiver<(Block<TxHash>, Arc<Web3Connection>)>,
|
||||||
head_block_sender: watch::Sender<Block<TxHash>>,
|
head_block_sender: watch::Sender<Block<TxHash>>,
|
||||||
// TODO: use pending_tx_sender
|
// TODO: use pending_tx_sender
|
||||||
pending_tx_sender: Option<flume::Sender<TxState>>,
|
pending_tx_sender: Option<broadcast::Sender<TxState>>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let total_rpcs = self.inner.len();
|
let total_rpcs = self.inner.len();
|
||||||
|
|
||||||
@ -323,8 +381,11 @@ impl Web3Connections {
|
|||||||
let new_block_num = match new_block.number {
|
let new_block_num = match new_block.number {
|
||||||
Some(x) => x.as_u64(),
|
Some(x) => x.as_u64(),
|
||||||
None => {
|
None => {
|
||||||
// TODO: wth. how is this happening? need more logs
|
// block without a number is expected a node is syncing or
|
||||||
|
if new_block.hash.is_some() {
|
||||||
|
// this seems unlikely, but i'm pretty sure we see it
|
||||||
warn!(?new_block, "Block without number!");
|
warn!(?new_block, "Block without number!");
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -360,7 +421,9 @@ impl Web3Connections {
|
|||||||
// TODO: if the parent hash isn't our previous best block, ignore it
|
// TODO: if the parent hash isn't our previous best block, ignore it
|
||||||
pending_synced_connections.head_block_hash = new_block_hash;
|
pending_synced_connections.head_block_hash = new_block_hash;
|
||||||
|
|
||||||
head_block_sender.send(new_block.clone())?;
|
head_block_sender
|
||||||
|
.send(new_block.clone())
|
||||||
|
.context("head_block_sender")?;
|
||||||
|
|
||||||
// TODO: mark all transactions as confirmed
|
// TODO: mark all transactions as confirmed
|
||||||
// TODO: mark any orphaned transactions as unconfirmed
|
// TODO: mark any orphaned transactions as unconfirmed
|
||||||
@ -415,7 +478,9 @@ impl Web3Connections {
|
|||||||
|
|
||||||
// TODO: do this more efficiently?
|
// TODO: do this more efficiently?
|
||||||
if pending_synced_connections.head_block_hash != most_common_head_hash {
|
if pending_synced_connections.head_block_hash != most_common_head_hash {
|
||||||
head_block_sender.send(new_block.clone())?;
|
head_block_sender
|
||||||
|
.send(new_block.clone())
|
||||||
|
.context("head_block_sender")?;
|
||||||
pending_synced_connections.head_block_hash = most_common_head_hash;
|
pending_synced_connections.head_block_hash = most_common_head_hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -449,15 +514,10 @@ impl Web3Connections {
|
|||||||
// TODO: what if the hashes don't match?
|
// TODO: what if the hashes don't match?
|
||||||
if pending_synced_connections.head_block_hash == new_block_hash {
|
if pending_synced_connections.head_block_hash == new_block_hash {
|
||||||
// mark all transactions in the block as confirmed
|
// mark all transactions in the block as confirmed
|
||||||
if let Some(pending_tx_sender) = &pending_tx_sender {
|
if pending_tx_sender.is_some() {
|
||||||
// TODO: we need new_block to be the new_head_block
|
|
||||||
for tx_hash in &new_block.transactions {
|
for tx_hash in &new_block.transactions {
|
||||||
match self.pending_transactions.remove(tx_hash) {
|
// TODO: should we mark as confirmed via pending_tx_sender so that orphans are easier?
|
||||||
Some((_tx_id, tx)) => {
|
let _ = self.pending_transactions.remove(tx_hash);
|
||||||
pending_tx_sender.send_async(TxState::Confirmed(tx)).await?;
|
|
||||||
}
|
|
||||||
None => continue,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use crate::jsonrpc::JsonRpcForwardedResponse;
|
|
||||||
use axum::{http::StatusCode, response::IntoResponse, Json};
|
use axum::{http::StatusCode, response::IntoResponse, Json};
|
||||||
use serde_json::value::RawValue;
|
use serde_json::value::RawValue;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::jsonrpc::JsonRpcForwardedResponse;
|
||||||
|
|
||||||
/// TODO: pretty 404 page? or us a json error fine?
|
/// TODO: pretty 404 page? or us a json error fine?
|
||||||
pub async fn handler_404() -> impl IntoResponse {
|
pub async fn handler_404() -> impl IntoResponse {
|
||||||
let err = anyhow::anyhow!("nothing to see here");
|
let err = anyhow::anyhow!("nothing to see here");
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use crate::app::Web3ProxyApp;
|
|
||||||
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
|
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::app::Web3ProxyApp;
|
||||||
|
|
||||||
/// a page for configuring your wallet with all the rpcs
|
/// a page for configuring your wallet with all the rpcs
|
||||||
/// TODO: check auth (from authp?) here
|
/// TODO: check auth (from authp?) here
|
||||||
/// TODO: return actual html
|
/// TODO: return actual html
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use super::errors::handle_anyhow_error;
|
|
||||||
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
|
|
||||||
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
|
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::errors::handle_anyhow_error;
|
||||||
|
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
|
||||||
|
|
||||||
pub async fn proxy_web3_rpc(
|
pub async fn proxy_web3_rpc(
|
||||||
payload: Json<JsonRpcRequestEnum>,
|
payload: Json<JsonRpcRequestEnum>,
|
||||||
app: Extension<Arc<Web3ProxyApp>>,
|
app: Extension<Arc<Web3ProxyApp>>,
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
use crate::{
|
|
||||||
app::Web3ProxyApp,
|
|
||||||
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
|
|
||||||
};
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::ws::{Message, WebSocket, WebSocketUpgrade},
|
extract::ws::{Message, WebSocket, WebSocketUpgrade},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
@ -16,7 +12,12 @@ use hashbrown::HashMap;
|
|||||||
use serde_json::value::RawValue;
|
use serde_json::value::RawValue;
|
||||||
use std::str::from_utf8_mut;
|
use std::str::from_utf8_mut;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::Web3ProxyApp,
|
||||||
|
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn websocket_handler(
|
pub async fn websocket_handler(
|
||||||
app: Extension<Arc<Web3ProxyApp>>,
|
app: Extension<Arc<Web3ProxyApp>>,
|
||||||
@ -154,8 +155,8 @@ async fn write_web3_socket(
|
|||||||
) {
|
) {
|
||||||
while let Ok(msg) = response_rx.recv_async().await {
|
while let Ok(msg) = response_rx.recv_async().await {
|
||||||
// a response is ready. write it to ws_tx
|
// a response is ready. write it to ws_tx
|
||||||
if ws_tx.send(msg).await.is_err() {
|
if let Err(err) = ws_tx.send(msg).await {
|
||||||
// TODO: log the error
|
warn!(?err, "unable to write to websocket");
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,18 @@ mod connections;
|
|||||||
mod frontend;
|
mod frontend;
|
||||||
mod jsonrpc;
|
mod jsonrpc;
|
||||||
|
|
||||||
use crate::app::{flatten_handle, Web3ProxyApp};
|
|
||||||
use crate::config::{CliConfig, RpcConfig};
|
|
||||||
use parking_lot::deadlock;
|
use parking_lot::deadlock;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::sync::atomic::{self, AtomicUsize};
|
use std::sync::atomic::{self, AtomicUsize};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::runtime;
|
use tokio::runtime;
|
||||||
use tracing::{error, info, trace};
|
use tracing::{info, trace};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use crate::app::{flatten_handle, Web3ProxyApp};
|
||||||
|
use crate::config::{CliConfig, RpcConfig};
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
// if RUST_LOG isn't set, configure a default
|
// if RUST_LOG isn't set, configure a default
|
||||||
// TODO: is there a better way to do this?
|
// TODO: is there a better way to do this?
|
||||||
@ -86,15 +87,17 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let frontend_handle = tokio::spawn(frontend::run(cli_config.port, app));
|
let frontend_handle = tokio::spawn(frontend::run(cli_config.port, app));
|
||||||
|
|
||||||
match tokio::try_join!(flatten_handle(app_handle), flatten_handle(frontend_handle)) {
|
// if everything is working, these should both run forever
|
||||||
Ok(_) => {
|
tokio::select! {
|
||||||
// do something with the values
|
x = flatten_handle(app_handle) => {
|
||||||
info!("app completed")
|
// TODO: error log if error
|
||||||
}
|
info!(?x, "app_handle exited");
|
||||||
Err(err) => {
|
|
||||||
error!(?err, "app failed");
|
|
||||||
}
|
}
|
||||||
|
x = flatten_handle(frontend_handle) => {
|
||||||
|
// TODO: error log if error
|
||||||
|
info!(?x, "frontend exited");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user