Skip to content

Commit

Permalink
Merge branch 'feat/multi-region' of https://github.com/WalletConnect/…
Browse files Browse the repository at this point in the history
…rpc-proxy into feat/latency-dns-routing
  • Loading branch information
chris13524 committed Aug 26, 2024
2 parents 5c1c152 + 030e689 commit fe36832
Show file tree
Hide file tree
Showing 16 changed files with 279 additions and 126 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export RPC_PROXY_PROVIDER_ONE_INCH_API_KEY=""
export RPC_PROXY_PROVIDER_GETBLOCK_ACCESS_TOKENS='{}'
export RPC_PROXY_PROVIDER_PIMLICO_API_KEY=""
export RPC_PROXY_PROVIDER_SOLSCAN_API_V1_TOKEN=""
export RPC_PROXY_PROVIDER_SOLSCAN_API_V2_TOKEN=""

# PostgreSQL URI connection string
export RPC_PROXY_POSTGRES_URI="postgres://postgres@localhost/postgres"
Expand Down
1 change: 1 addition & 0 deletions .env.terraform.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export TF_VAR_one_inch_api_key=""
export TF_VAR_getblock_access_tokens='{}'
export TF_VAR_pimlico_api_key=""
export TF_VAR_solscan_api_v1_token=""
export TF_VAR_solscan_api_v2_token=""
export TF_VAR_grafana_endpoint=$(aws grafana list-workspaces | jq -r '.workspaces[] | select( .tags.Env == "prod") | select( .tags.Name == "grafana-9") | .endpoint')
export TF_VAR_registry_api_auth_token=""
export TF_VAR_debug_secret=""
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.

- - -
## 0.93.0 - 2024-08-26
#### Features
- **(transactions)** implementing Solana transactions history support (#742) - (1c65dc0) - Max Kalashnikoff | maksy.eth

- - -

## 0.92.0 - 2024-08-23
#### Features
- **(grafana)** adding provider call retries panel (#736) - (d2ca278) - Max Kalashnikoff | maksy.eth
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rpc-proxy"
version = "0.92.0"
version = "0.93.0"
edition = "2021"
authors = [
"Derek <[email protected]>",
Expand Down
60 changes: 50 additions & 10 deletions integration/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { getTestSetup } from './init';
describe('Transactions history', () => {
const { baseUrl, projectId, httpClient } = getTestSetup();

const fulfilled_address = '0x63755B7B300228254FB7d16321eCD3B87f98ca2a'
const empty_history_address = '0x5b6262592954B925B510651462b63ddEbcc22eaD'
const fulfilled_eth_address = '0x63755B7B300228254FB7d16321eCD3B87f98ca2a'
const fulfilled_solana_address = 'D8cjxcb8pC2SBhWesQ7oxtCRPjw4856CcvXdWzPHNCqU'

it('fulfilled history', async () => {
const empty_eth_address = '0x5b6262592954B925B510651462b63ddEbcc22eaD'
const empty_solana_address = '7ar3r6Mau1Bk7pGLWHCMj1C1bk2eCDwGWTP77j9MXTtd'

it('fulfilled history Ethereum address', async () => {
let resp: any = await httpClient.get(
`${baseUrl}/v1/account/${fulfilled_address}/history?projectId=${projectId}`,
`${baseUrl}/v1/account/${fulfilled_eth_address}/history?projectId=${projectId}`,
)
expect(resp.status).toBe(200)
expect(typeof resp.data.data).toBe('object')
Expand All @@ -29,24 +32,60 @@ describe('Transactions history', () => {
expect(typeof item.transfers).toBe('object')
}
})
it('empty history', async () => {

it('fulfilled history Solana address', async () => {
let chainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'
let resp: any = await httpClient.get(
`${baseUrl}/v1/account/${fulfilled_solana_address}/history?projectId=${projectId}&chainId=${chainId}`,
)
expect(resp.status).toBe(200)
expect(typeof resp.data.data).toBe('object')
expect(resp.data.data.length).toBeGreaterThanOrEqual(2)

for (const item of resp.data.data) {
expect(item.id).toBeDefined()
expect(typeof item.metadata).toBe('object')
// expect chain to be null or caip-2 format
if (item.metadata.chain !== null) {
expect(item.metadata.chain).toEqual(expect.stringMatching(/^(solana:)?\d+$/));
} else {
expect(item.metadata.chain).toBeNull();
}
expect(typeof item.transfers).toBe('object')
}
})

it('empty history Ethereum address', async () => {
let resp: any = await httpClient.get(
`${baseUrl}/v1/account/${empty_history_address}/history?projectId=${projectId}`,
`${baseUrl}/v1/account/${empty_eth_address}/history?projectId=${projectId}`,
)
expect(resp.status).toBe(200)
expect(typeof resp.data.data).toBe('object')
expect(resp.data.data).toHaveLength(0)
expect(resp.data.next).toBeNull()
})
it('wrong address', async () => {

it('empty history Solana address', async () => {
let chainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'
let resp: any = await httpClient.get(
`${baseUrl}/v1/account/0X${fulfilled_address}/history?projectId=${projectId}`,
`${baseUrl}/v1/account/${empty_solana_address}/history?projectId=${projectId}&chainId=${chainId}`,
)
expect(resp.status).toBe(200)
expect(typeof resp.data.data).toBe('object')
expect(resp.data.data).toHaveLength(0)
expect(resp.data.next).toBeNull()
})

it('wrong address format', async () => {
let resp: any = await httpClient.get(
`${baseUrl}/v1/account/X${fulfilled_eth_address}/history?projectId=${projectId}`,
)
expect(resp.status).toBe(400)
})

it('onramp Coinbase provider', async () => {
let resp: any = await httpClient.get(
`${baseUrl}/v1/account/${fulfilled_address}/history?onramp=coinbase&projectId=${projectId}`,
`${baseUrl}/v1/account/${fulfilled_eth_address}/history?onramp=coinbase&projectId=${projectId}`,
)
expect(resp.status).toBe(200)
expect(typeof resp.data.next).toBe('string')
Expand All @@ -58,9 +97,10 @@ describe('Transactions history', () => {
expect(first.metadata.operationType).toBe('buy')
expect(first.metadata.status).toEqual(expect.stringMatching(/^ONRAMP_TRANSACTION_STATUS_/));
})

it('onramp wrong provider', async () => {
let resp: any = await httpClient.get(
`${baseUrl}/v1/account/${fulfilled_address}/history?onramp=some&projectId=${projectId}`,
`${baseUrl}/v1/account/${fulfilled_eth_address}/history?onramp=some&projectId=${projectId}`,
)
expect(resp.status).toBe(400)
})
Expand Down
5 changes: 5 additions & 0 deletions src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ mod test {
"RPC_PROXY_PROVIDER_SOLSCAN_API_V1_TOKEN",
"SOLSCAN_API_V1_TOKEN",
),
(
"RPC_PROXY_PROVIDER_SOLSCAN_API_V2_TOKEN",
"SOLSCAN_API_V2_TOKEN",
),
(
"RPC_PROXY_PROVIDER_PROMETHEUS_QUERY_URL",
"PROMETHEUS_QUERY_URL",
Expand Down Expand Up @@ -252,6 +256,7 @@ mod test {
getblock_access_tokens: Some("{}".to_owned()),
pimlico_api_key: "PIMLICO_API_KEY".to_string(),
solscan_api_v1_token: "SOLSCAN_API_V1_TOKEN".to_string(),
solscan_api_v2_token: "SOLSCAN_API_V2_TOKEN".to_string(),
},
rate_limiting: RateLimitingConfig {
max_tokens: Some(100),
Expand Down
47 changes: 32 additions & 15 deletions src/handlers/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@ use {
error::RpcError,
providers::ProviderKind,
state::AppState,
utils::network,
utils::{crypto, network},
},
axum::{
extract::{ConnectInfo, MatchedPath, Path, Query, State},
response::{IntoResponse, Response},
Json,
},
ethers::types::H160,
hyper::HeaderMap,
serde::{Deserialize, Serialize},
std::{net::SocketAddr, str::FromStr, sync::Arc},
std::{net::SocketAddr, sync::Arc},
tap::TapFallible,
tracing::log::error,
wc::future::FutureExt,
Expand All @@ -26,6 +25,7 @@ use {
pub struct HistoryQueryParams {
pub currency: Option<String>,
pub project_id: String,
pub chain_id: Option<String>,
pub cursor: Option<String>,
pub onramp: Option<String>,
}
Expand Down Expand Up @@ -141,16 +141,28 @@ async fn handler_internal(
) -> Result<Response, RpcError> {
let project_id = query.project_id.clone();

// Checking for the H160 address correctness
H160::from_str(&address).map_err(|_| RpcError::InvalidAddress)?;
// If the chainId is not provided, then default to the Ethereum namespace
let namespace = query
.chain_id
.as_ref()
.map(|chain_id| {
crypto::disassemble_caip2(chain_id)
.map(|(namespace, _)| namespace)
.unwrap_or(crypto::CaipNamespaces::Eip155)
})
.unwrap_or(crypto::CaipNamespaces::Eip155);

if !crypto::is_address_valid(&address, &namespace) {
return Err(RpcError::InvalidAddress);
}

let latency_tracker_start = std::time::SystemTime::now();
let history_provider: ProviderKind;
let history_provider_kind: ProviderKind;
let response: HistoryResponseBody = if let Some(onramp) = query.onramp.clone() {
if onramp == "coinbase" {
if onramp == "coinbase" && namespace == crypto::CaipNamespaces::Eip155 {
// We don't want to validate the quota for the onramp
state.validate_project_access(&project_id).await?;
history_provider = ProviderKind::Coinbase;
history_provider_kind = ProviderKind::Coinbase;
state
.providers
.coinbase_pay_provider
Expand All @@ -164,10 +176,13 @@ async fn handler_internal(
}
} else {
state.validate_project_access_and_quota(&project_id).await?;
history_provider = ProviderKind::Zerion;
state
history_provider_kind = ProviderKind::Zerion;
let provider = state
.providers
.history_provider
.history_providers
.get(&namespace)
.ok_or_else(|| RpcError::UnsupportedNamespace(namespace))?;
provider
.get_transactions(address.clone(), query.0.clone(), state.http_client.clone())
.await
.tap_err(|e| {
Expand All @@ -178,7 +193,7 @@ async fn handler_internal(
let latency_tracker = latency_tracker_start
.elapsed()
.unwrap_or(std::time::Duration::from_secs(0));
state.metrics.add_history_lookup(&history_provider);
state.metrics.add_history_lookup(&history_provider_kind);

let origin = headers
.get("origin")
Expand All @@ -191,7 +206,7 @@ async fn handler_internal(
.unwrap_or((None, None, None));

// Different analytics for different history providers
match history_provider {
match history_provider_kind {
ProviderKind::Zerion => {
state.analytics.history_lookup(HistoryLookupInfo::new(
address,
Expand Down Expand Up @@ -280,10 +295,12 @@ async fn handler_internal(
let latency_tracker = latency_tracker_start
.elapsed()
.unwrap_or(std::time::Duration::from_secs(0));
state.metrics.add_history_lookup_success(&history_provider);
state
.metrics
.add_history_lookup_latency(&history_provider, latency_tracker);
.add_history_lookup_success(&history_provider_kind);
state
.metrics
.add_history_lookup_latency(&history_provider_kind, latency_tracker);

Ok(Json(response).into_response())
}
18 changes: 13 additions & 5 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ pub struct ProvidersConfig {
pub getblock_access_tokens: Option<String>,
/// Pimlico API token key
pub pimlico_api_key: String,
/// SolScan API v1 token key
/// SolScan API v1 and v2 token keys
pub solscan_api_v1_token: String,
pub solscan_api_v2_token: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand All @@ -121,7 +122,7 @@ pub struct ProviderRepository {
prometheus_client: prometheus_http_query::Client,
prometheus_workspace_header: String,

pub history_provider: Arc<dyn HistoryProvider>,
pub history_providers: HashMap<CaipNamespaces, Arc<dyn HistoryProvider>>,
pub portfolio_provider: Arc<dyn PortfolioProvider>,
pub coinbase_pay_provider: Arc<dyn HistoryProvider>,
pub onramp_provider: Arc<dyn OnRampProvider>,
Expand Down Expand Up @@ -183,15 +184,22 @@ impl ProviderRepository {

let zerion_provider = Arc::new(ZerionProvider::new(zerion_api_key));
let one_inch_provider = Arc::new(OneInchProvider::new(one_inch_api_key, one_inch_referrer));
let history_provider = zerion_provider.clone();
let portfolio_provider = zerion_provider.clone();
let solscan_provider = Arc::new(SolScanProvider::new(config.solscan_api_v1_token.clone()));
let solscan_provider = Arc::new(SolScanProvider::new(
config.solscan_api_v1_token.clone(),
config.solscan_api_v2_token.clone(),
));

let mut balance_providers: HashMap<CaipNamespaces, Arc<dyn BalanceProvider>> =
HashMap::new();
balance_providers.insert(CaipNamespaces::Eip155, zerion_provider.clone());
balance_providers.insert(CaipNamespaces::Solana, solscan_provider.clone());

let mut history_providers: HashMap<CaipNamespaces, Arc<dyn HistoryProvider>> =
HashMap::new();
history_providers.insert(CaipNamespaces::Eip155, zerion_provider.clone());
history_providers.insert(CaipNamespaces::Solana, solscan_provider.clone());

let coinbase_pay_provider = Arc::new(CoinbaseProvider::new(
coinbase_api_key,
coinbase_app_id,
Expand All @@ -211,7 +219,7 @@ impl ProviderRepository {
ws_weight_resolver: HashMap::new(),
prometheus_client,
prometheus_workspace_header,
history_provider,
history_providers,
portfolio_provider,
coinbase_pay_provider: coinbase_pay_provider.clone(),
onramp_provider: coinbase_pay_provider,
Expand Down
Loading

0 comments on commit fe36832

Please sign in to comment.