From 00ccea623d65467d48ae8a5e72f762d5ae85c3f8 Mon Sep 17 00:00:00 2001 From: crypdoughdoteth Date: Fri, 13 Mar 2026 16:16:59 -0400 Subject: [PATCH 1/6] Feat: ERC20 batch balance queries, API endpoints, ERC721 batch owner query --- Cargo.lock | 5 +- Cargo.toml | 1 + src/routes/mod.rs | 1 + src/routes/payment.rs | 1 - src/routes/relayer/types.rs | 2 +- src/routes/token_queries.rs | 251 ++++++++++++++++++++++++++++++++++++ 6 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 src/routes/token_queries.rs diff --git a/Cargo.lock b/Cargo.lock index c574bab..3b344ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,6 +1784,7 @@ dependencies = [ "tower-http 0.5.2", "tracing", "tracing-subscriber", + "url", ] [[package]] @@ -5502,9 +5503,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", diff --git a/Cargo.toml b/Cargo.toml index 031f761..18a51e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ tracing-subscriber = "0.3.18" alloy = {version = "1.0", features = ["node-bindings", "network", "rpc-types"]} thiserror = "1.0.63" mimalloc = "0.1.45" +url = "2.5.8" hyper = "1.6.0" hyper-util = { version = "0.1", features = ["full"] } reqwest = { version = "0.12.24", features = ["json", "rustls-tls", "cookies", "stream"] } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 0c57185..1fd5dc5 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,4 +1,5 @@ pub mod activate; +pub mod token_queries; pub mod api_keys; pub mod login; pub mod payment; diff --git a/src/routes/payment.rs b/src/routes/payment.rs index cd97c30..493b96f 100644 --- a/src/routes/payment.rs +++ b/src/routes/payment.rs @@ -381,7 +381,6 @@ pub async fn process_ethereum_payment( let mut fixed = [0u8; 32]; fixed.copy_from_slice(&hash); - #[allow(clippy::needless_borrow)] let eth = reqwest::Url::parse(&_endpoint).unwrap(); let provider = ProviderBuilder::new().connect_http(eth); diff --git a/src/routes/relayer/types.rs b/src/routes/relayer/types.rs index e4e32f0..cf16c12 100644 --- a/src/routes/relayer/types.rs +++ b/src/routes/relayer/types.rs @@ -28,7 +28,7 @@ impl From for HeaderValue { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum PoktChains { #[cfg(any(test, feature = "dev"))] Anvil, diff --git a/src/routes/token_queries.rs b/src/routes/token_queries.rs new file mode 100644 index 0000000..467cf9b --- /dev/null +++ b/src/routes/token_queries.rs @@ -0,0 +1,251 @@ +use alloy::{ + primitives::{Address, U256, address}, + providers::ProviderBuilder, + sol, +}; +use axum::{ + Json, + extract::{Path, Query}, + response::IntoResponse, +}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::routes::{ + payment::D_D_CLOUD_API_KEY, + relayer::types::PoktChains, + token_queries::TokenQueryContract::{TokenBalanceQuery, TokenQueryContractInstance}, +}; + +static TOKEN_QUERY_UTIL_DEPLOYMENTS: std::sync::LazyLock< + std::collections::HashMap, +> = std::sync::LazyLock::new(|| { + let mut map = std::collections::HashMap::new(); + map.insert( + PoktChains::Eth, + address!("0x96a7B30FD0B97BfF5bEdB343049b378011Cc62fd"), + ); + map.insert( + PoktChains::Base, + address!("0x8B52358d9d2651f9264Df0ceA60333263427b86F"), + ); + map.insert( + PoktChains::Poly, + address!("0x87bd0e6aA53B21A9FB8f465cd90801a479321048"), + ); + map.insert( + PoktChains::ArbOne, + address!("2791Bca1f2de4661ED88A30C99A7a9449Aa84174"), + ); + // map.insert( + // PoktChains::Op, + // address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"), + // ); + // map.insert( + // PoktChains::Bsc, + // address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"), + // ); + map +}); + +sol! { + #[sol(rpc)] + contract TokenQueryContract { + #[derive(Debug, Serialize, Deserialize)] + struct TokenBalanceQuery { + address contract_addr; + address user; + } + + #[derive(Debug, Serialize, Deserialize)] + struct TokenBalance { + address contract_addr; + uint256 amount; + address user; + uint8 decimals; + } + + #[derive(Debug, Serialize, Deserialize)] + struct NftInfo { + address owner; + uint256 token_num; + } + + function getBatchNftInfo(address, uint256, uint256) external view returns (NftInfo[] memory); + function aggregateTokenBalsForUser(address[] memory, address) external view returns (TokenBalance[] memory); + function aggregateSingleTokenBals(address[] memory, address) external view returns (TokenBalance[] memory); + function aggregateBalances(TokenBalanceQuery[] memory) external view returns(TokenBalance[] memory); + } +} + +// Many tokens, many users +pub async fn tokens( + Json(payload): Json>, + Query(chain): Query, +) -> Result { + + if payload.len() > 1000 { + Err(QueryError::ERC20QueryLimit)? + } + + let endpoint: String = format!( + "https://api.cloud.developerdao.com/rpc/{}/{}", + chain, *D_D_CLOUD_API_KEY + ); + let eth = reqwest::Url::parse(&endpoint)?; + let provider = ProviderBuilder::new().connect_http(eth); + let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS + .get(&chain) + .ok_or_else(|| QueryError::ChainError)?; + let contract = TokenQueryContractInstance::new( + c_addr, provider, + ); + + let res = contract.aggregateBalances(payload).call().await?; + + Ok((StatusCode::OK, serde_json::to_string(&res)?).into_response()) +} + +// one token, many users +pub async fn aggregate_token_bals_for_user( + Path((chain, address)): Path<(PoktChains, Address)>, + Query(tokens): Query>, +) -> Result { + if tokens.len() > 1000 { + Err(QueryError::ERC20QueryLimit)? + } + + let endpoint: String = format!( + "https://api.cloud.developerdao.com/rpc/{}/{}", + chain, *D_D_CLOUD_API_KEY + ); + let eth = reqwest::Url::parse(&endpoint)?; + let provider = ProviderBuilder::new().connect_http(eth); + let contract = TokenQueryContractInstance::new( + address!("0x96a7B30FD0B97BfF5bEdB343049b378011Cc62fd"), + provider, + ); + let res = contract + .aggregateTokenBalsForUser(tokens, address) + .call() + .await?; + + Ok((StatusCode::OK, serde_json::to_string(&res)?).into_response()) +} + +// many users, one token +pub async fn aggregate_single_token_bals( + Path((chain, token_address)): Path<(PoktChains, Address)>, + Query(users): Query>, +) -> Result { + if users.len() > 1000 { + Err(QueryError::ERC20QueryLimit)? + } + + let endpoint: String = format!( + "https://api.cloud.developerdao.com/rpc/{}/{}", + chain, *D_D_CLOUD_API_KEY + ); + let eth = reqwest::Url::parse(&endpoint)?; + let provider = ProviderBuilder::new().connect_http(eth); + let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS + .get(&chain) + .ok_or_else(|| QueryError::ChainError)?; + let contract = TokenQueryContractInstance::new( + c_addr, provider, + ); + let res = contract + .aggregateSingleTokenBals(users, token_address) + .call() + .await?; + + Ok((StatusCode::OK, serde_json::to_string(&res)?).into_response()) +} + +pub async fn get_batch_nft_info( + Query((chain, collection, offset, limit)): Query<(PoktChains, Address, u16, u16)>, +) -> Result { + + if limit > 10000 { + Err(QueryError::NFTQueryLimit)? + } + + let endpoint: String = format!( + "https://api.cloud.developerdao.com/rpc/{}/{}", + chain, *D_D_CLOUD_API_KEY + ); + let eth = reqwest::Url::parse(&endpoint)?; + let provider = ProviderBuilder::new().connect_http(eth); + let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS + .get(&chain) + .ok_or_else(|| QueryError::ChainError)?; + let contract = TokenQueryContractInstance::new( + c_addr, provider, + ); + + let res = contract + .getBatchNftInfo(collection, U256::from(offset), U256::from(limit)) + .call() + .await?; + + Ok((StatusCode::OK, serde_json::to_string(&res)?).into_response()) +} + +// nft token owners + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("The max number of entries you can request for NFT owners is 10,000")] + NFTQueryLimit, + #[error("The max number of entries you can request for ERC20 balances is 1,000")] + ERC20QueryLimit, + #[error(transparent)] + ParseError(#[from] url::ParseError), + #[error(transparent)] + ContractError(#[from] alloy::contract::Error), + #[error(transparent)] + SerdeError(#[from] serde_json::Error), + #[error("This chain is not yet supported for the token query endpoints.")] + ChainError, +} + +impl IntoResponse for QueryError { + fn into_response(self) -> axum::response::Response { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } +} + +#[cfg(test)] +pub mod test { + + pub use super::*; + #[tokio::test] + async fn basic() { + let endpoint: String = format!( + "https://api.cloud.developerdao.com/rpc/{}/{}", + PoktChains::Eth, + *D_D_CLOUD_API_KEY + ); + let eth = reqwest::Url::parse(&endpoint).unwrap(); + let provider = ProviderBuilder::new().connect_http(eth); + let contract = TokenQueryContractInstance::new( + // mainnet + address!("0x96a7B30FD0B97BfF5bEdB343049b378011Cc62fd"), + provider, + ); + let res = contract + .aggregateBalances(vec![TokenBalanceQuery { + contract_addr: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + .parse() + .unwrap(), + user: "0x940ACd9375b46EC2FA7C0E8aAd9D7241fb01e205" + .parse() + .unwrap(), + }]) + .call() + .await + .unwrap(); + println!("{res:?}"); + } +} From eb3351262923527962f9cae3ed93d9a551629404 Mon Sep 17 00:00:00 2001 From: crypdoughdoteth Date: Fri, 13 Mar 2026 16:59:48 -0400 Subject: [PATCH 2/6] Add token query utils to routes --- src/main.rs | 11 +++++++++++ src/routes/relayer/types.rs | 3 ++- src/routes/token_queries.rs | 29 +++++++++++------------------ 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index f84f3cb..d6a9fe1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use crate::middleware::{ }; use crate::routes::payment::{cancel, downgrade, upgrade}; use crate::routes::relayer::websockets::ws_handler; +use crate::routes::token_queries::{aggregate_balances, aggregate_single_token_bals, aggregate_token_bals_for_user, get_batch_nft_info}; use crate::routes::types::{EmailLogin, JWTKey}; use crate::routes::{ activate::activate_account, @@ -64,10 +65,18 @@ async fn main() { .route("/ws/{chain}/{api_key}", axum::routing::any(ws_handler)) .route_layer(from_fn(validate_subscription_and_update_user_calls)); + let token_queries = Router::new() + .route("/v1/tokens/balances/{chain}/{api_key}", get(aggregate_balances)) + .route("/v1/tokens/single_token_balances/{chain}/{api_key}", get(aggregate_single_token_bals)) + .route("/v1/tokens/many_token_balances/{chain}/{api_key}", get(aggregate_token_bals_for_user)) + .route("/v1/nfts/ownership/{chain}/{api_key}", get(get_batch_nft_info)) + .route_layer(from_fn(validate_subscription_and_update_user_calls)); + let api_keys = Router::new() .route("/api/keys", get(get_all_api_keys).post(generate_api_keys)) .route("/api/keys/{key}", delete(delete_key)) .route_layer(from_fn(verify_jwt)); + let payments = Router::new() .route("/api/pay/eth", post(process_ethereum_payment)) .route("/api/upgrade", post(upgrade)) @@ -76,6 +85,7 @@ async fn main() { .route("/api/balances", get(get_calls_and_balance)) .route("/api/payments", get(get_payments)) .route_layer(from_fn(verify_jwt)); + let siwe = Router::new() .route("/api/refresh", post(refresh)) .route("/api/siwe/add_wallet", post(siwe_add_wallet)) @@ -98,6 +108,7 @@ async fn main() { .merge(siwe) .merge(payments) .layer(cors_api) + .merge(token_queries) .merge(relayer); info!("Initialized D_D RPC on 0.0.0.0:3000"); diff --git a/src/routes/relayer/types.rs b/src/routes/relayer/types.rs index cf16c12..2eb217a 100644 --- a/src/routes/relayer/types.rs +++ b/src/routes/relayer/types.rs @@ -1,6 +1,7 @@ use http::{HeaderValue, header::CONTENT_TYPE}; use axum::body::{Body, Bytes}; use reqwest::Client; +use serde::{Deserialize, Serialize}; use std::{ fmt::{self, Display, Formatter}, future::Future, @@ -28,7 +29,7 @@ impl From for HeaderValue { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum PoktChains { #[cfg(any(test, feature = "dev"))] Anvil, diff --git a/src/routes/token_queries.rs b/src/routes/token_queries.rs index 467cf9b..e0869df 100644 --- a/src/routes/token_queries.rs +++ b/src/routes/token_queries.rs @@ -80,11 +80,10 @@ sol! { } // Many tokens, many users -pub async fn tokens( +pub async fn aggregate_balances( + Path((chain, _)): Path<(PoktChains, String)>, Json(payload): Json>, - Query(chain): Query, ) -> Result { - if payload.len() > 1000 { Err(QueryError::ERC20QueryLimit)? } @@ -98,10 +97,8 @@ pub async fn tokens( let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS .get(&chain) .ok_or_else(|| QueryError::ChainError)?; - let contract = TokenQueryContractInstance::new( - c_addr, provider, - ); + let contract = TokenQueryContractInstance::new(c_addr, provider); let res = contract.aggregateBalances(payload).call().await?; Ok((StatusCode::OK, serde_json::to_string(&res)?).into_response()) @@ -109,8 +106,8 @@ pub async fn tokens( // one token, many users pub async fn aggregate_token_bals_for_user( - Path((chain, address)): Path<(PoktChains, Address)>, - Query(tokens): Query>, + Path((chain, _)): Path<(PoktChains, String)>, + Query((address, tokens)): Query<(Address, Vec
)>, ) -> Result { if tokens.len() > 1000 { Err(QueryError::ERC20QueryLimit)? @@ -136,8 +133,8 @@ pub async fn aggregate_token_bals_for_user( // many users, one token pub async fn aggregate_single_token_bals( - Path((chain, token_address)): Path<(PoktChains, Address)>, - Query(users): Query>, + Path((chain, _)): Path<(PoktChains, String)>, + Query((token_address, users)): Query<(Address, Vec
)>, ) -> Result { if users.len() > 1000 { Err(QueryError::ERC20QueryLimit)? @@ -152,9 +149,7 @@ pub async fn aggregate_single_token_bals( let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS .get(&chain) .ok_or_else(|| QueryError::ChainError)?; - let contract = TokenQueryContractInstance::new( - c_addr, provider, - ); + let contract = TokenQueryContractInstance::new(c_addr, provider); let res = contract .aggregateSingleTokenBals(users, token_address) .call() @@ -164,9 +159,9 @@ pub async fn aggregate_single_token_bals( } pub async fn get_batch_nft_info( - Query((chain, collection, offset, limit)): Query<(PoktChains, Address, u16, u16)>, + Path((chain, _)): Path<(PoktChains, String)>, + Query((collection, offset, limit)): Query<(Address, u16, u16)>, ) -> Result { - if limit > 10000 { Err(QueryError::NFTQueryLimit)? } @@ -180,9 +175,7 @@ pub async fn get_batch_nft_info( let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS .get(&chain) .ok_or_else(|| QueryError::ChainError)?; - let contract = TokenQueryContractInstance::new( - c_addr, provider, - ); + let contract = TokenQueryContractInstance::new(c_addr, provider); let res = contract .getBatchNftInfo(collection, U256::from(offset), U256::from(limit)) From 6cd46373b9a6262db383e7fdb0038e2972b2c3cb Mon Sep 17 00:00:00 2001 From: crypdoughdoteth Date: Tue, 31 Mar 2026 15:04:36 -0400 Subject: [PATCH 3/6] Relax CORS for local development --- src/main.rs | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index d6a9fe1..4e8feab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,10 @@ use crate::middleware::{ }; use crate::routes::payment::{cancel, downgrade, upgrade}; use crate::routes::relayer::websockets::ws_handler; -use crate::routes::token_queries::{aggregate_balances, aggregate_single_token_bals, aggregate_token_bals_for_user, get_batch_nft_info}; +use crate::routes::token_queries::{ + aggregate_balances, aggregate_single_token_bals, aggregate_token_bals_for_user, + get_batch_nft_info, +}; use crate::routes::types::{EmailLogin, JWTKey}; use crate::routes::{ activate::activate_account, @@ -54,22 +57,46 @@ async fn main() { .with_target(false) .init(); + let origin = if cfg!(feature = "dev") { + "http://localhost:5173" + } else { + "https://cloud.developerdao.com" + }; + let cors_api = CorsLayer::new() .allow_credentials(true) - .allow_origin("https://cloud.developerdao.com".parse::().unwrap()) + .allow_origin(origin.parse::().unwrap()) .allow_methods([Method::GET, Method::POST, Method::DELETE]) .allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION, header::COOKIE]); + #[cfg(feature = "dev")] + let relayer = Router::new() + .route("/rpc/{chain}/{api_key}", post(route_call)) + .route("/ws/{chain}/{api_key}", axum::routing::any(ws_handler)); + + #[cfg(not(feature = "dev"))] let relayer = Router::new() .route("/rpc/{chain}/{api_key}", post(route_call)) .route("/ws/{chain}/{api_key}", axum::routing::any(ws_handler)) .route_layer(from_fn(validate_subscription_and_update_user_calls)); let token_queries = Router::new() - .route("/v1/tokens/balances/{chain}/{api_key}", get(aggregate_balances)) - .route("/v1/tokens/single_token_balances/{chain}/{api_key}", get(aggregate_single_token_bals)) - .route("/v1/tokens/many_token_balances/{chain}/{api_key}", get(aggregate_token_bals_for_user)) - .route("/v1/nfts/ownership/{chain}/{api_key}", get(get_batch_nft_info)) + .route( + "/v1/tokens/balances/{chain}/{api_key}", + get(aggregate_balances), + ) + .route( + "/v1/tokens/single_token_balances/{chain}/{api_key}", + get(aggregate_single_token_bals), + ) + .route( + "/v1/tokens/many_token_balances/{chain}/{api_key}", + get(aggregate_token_bals_for_user), + ) + .route( + "/v1/nfts/ownership/{chain}/{api_key}", + get(get_batch_nft_info), + ) .route_layer(from_fn(validate_subscription_and_update_user_calls)); let api_keys = Router::new() From 1a49c721d4b5f2a049d64c1b7897761f305614f2 Mon Sep 17 00:00:00 2001 From: crypdoughdoteth Date: Mon, 6 Apr 2026 21:00:14 -0400 Subject: [PATCH 4/6] Update PATH, add checks to token query endpoints, expand customers table in PG (email preferences) --- .github/workflows/ci.yaml | 10 ---- infra/opentofu/ecs/main.tf | 2 +- migrations/0003_developer_dao_rpc.sql | 2 + src/main.rs | 3 +- src/routes/api_keys.rs | 1 - src/routes/token_queries.rs | 68 +++++++++++++++------------ 6 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 migrations/0003_developer_dao_rpc.sql diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d3ad296..7497bfb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,17 +2,7 @@ name: CI on: push: - branches: - - main - - production - - staging - - ecs-cd pull_request: - branches: - - main - - production - - staging - - ecs-cd env: CARGO_TERM_COLOR: always diff --git a/infra/opentofu/ecs/main.tf b/infra/opentofu/ecs/main.tf index ede2eb5..67db5e6 100644 --- a/infra/opentofu/ecs/main.tf +++ b/infra/opentofu/ecs/main.tf @@ -67,7 +67,7 @@ module "ecs" { cpu = 1024 memory = 2048 essential = false - image = "ghcr.io/pokt-network/path:sha-ca7acdd-rc" + image = "ghcr.io/pokt-network/path:sha-f812b42-rc" memory_reservation = 50 port_mappings = [ { diff --git a/migrations/0003_developer_dao_rpc.sql b/migrations/0003_developer_dao_rpc.sql new file mode 100644 index 0000000..d49d3a8 --- /dev/null +++ b/migrations/0003_developer_dao_rpc.sql @@ -0,0 +1,2 @@ +ALTER TABLE Customers ADD COLUMN suppression_list BOOL NOT NULL DEFAULT FALSE; +ALTER TABLE Customers ADD COLUMN marketing_email_consent BOOL NOT NULL DEFAULT FALSE; diff --git a/src/main.rs b/src/main.rs index 4e8feab..ff84f65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,8 +96,7 @@ async fn main() { .route( "/v1/nfts/ownership/{chain}/{api_key}", get(get_batch_nft_info), - ) - .route_layer(from_fn(validate_subscription_and_update_user_calls)); + ); let api_keys = Router::new() .route("/api/keys", get(get_all_api_keys).post(generate_api_keys)) diff --git a/src/routes/api_keys.rs b/src/routes/api_keys.rs index 41ea8d2..b075c54 100644 --- a/src/routes/api_keys.rs +++ b/src/routes/api_keys.rs @@ -16,7 +16,6 @@ pub struct KeygenLimit { count: Option, } -#[axum::debug_handler] pub async fn generate_api_keys( Extension(jwt): Extension>>, ) -> Result { diff --git a/src/routes/token_queries.rs b/src/routes/token_queries.rs index e0869df..4dd6272 100644 --- a/src/routes/token_queries.rs +++ b/src/routes/token_queries.rs @@ -13,7 +13,6 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::routes::{ - payment::D_D_CLOUD_API_KEY, relayer::types::PoktChains, token_queries::TokenQueryContract::{TokenBalanceQuery, TokenQueryContractInstance}, }; @@ -38,14 +37,6 @@ static TOKEN_QUERY_UTIL_DEPLOYMENTS: std::sync::LazyLock< PoktChains::ArbOne, address!("2791Bca1f2de4661ED88A30C99A7a9449Aa84174"), ); - // map.insert( - // PoktChains::Op, - // address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"), - // ); - // map.insert( - // PoktChains::Bsc, - // address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"), - // ); map }); @@ -81,17 +72,21 @@ sol! { // Many tokens, many users pub async fn aggregate_balances( - Path((chain, _)): Path<(PoktChains, String)>, + Path((chain, api_key)): Path<(PoktChains, String)>, Json(payload): Json>, ) -> Result { if payload.len() > 1000 { Err(QueryError::ERC20QueryLimit)? } - let endpoint: String = format!( - "https://api.cloud.developerdao.com/rpc/{}/{}", - chain, *D_D_CLOUD_API_KEY - ); + if matches!( + chain, + PoktChains::Op | PoktChains::Bsc | PoktChains::Sui | PoktChains::Solana + ) { + return Err(QueryError::ChainError)?; + } + + let endpoint: String = format!("https://api.cloud.developerdao.com/rpc/{chain}/{api_key}"); let eth = reqwest::Url::parse(&endpoint)?; let provider = ProviderBuilder::new().connect_http(eth); let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS @@ -106,18 +101,23 @@ pub async fn aggregate_balances( // one token, many users pub async fn aggregate_token_bals_for_user( - Path((chain, _)): Path<(PoktChains, String)>, + Path((chain, api_key)): Path<(PoktChains, String)>, Query((address, tokens)): Query<(Address, Vec
)>, ) -> Result { if tokens.len() > 1000 { Err(QueryError::ERC20QueryLimit)? } - let endpoint: String = format!( - "https://api.cloud.developerdao.com/rpc/{}/{}", - chain, *D_D_CLOUD_API_KEY - ); + if matches!( + chain, + PoktChains::Op | PoktChains::Bsc | PoktChains::Sui | PoktChains::Solana + ) { + return Err(QueryError::ChainError)?; + } + + let endpoint: String = format!("https://api.cloud.developerdao.com/rpc/{chain}/{api_key}"); let eth = reqwest::Url::parse(&endpoint)?; + let provider = ProviderBuilder::new().connect_http(eth); let contract = TokenQueryContractInstance::new( address!("0x96a7B30FD0B97BfF5bEdB343049b378011Cc62fd"), @@ -133,17 +133,21 @@ pub async fn aggregate_token_bals_for_user( // many users, one token pub async fn aggregate_single_token_bals( - Path((chain, _)): Path<(PoktChains, String)>, + Path((chain, api_key)): Path<(PoktChains, String)>, Query((token_address, users)): Query<(Address, Vec
)>, ) -> Result { if users.len() > 1000 { Err(QueryError::ERC20QueryLimit)? } - let endpoint: String = format!( - "https://api.cloud.developerdao.com/rpc/{}/{}", - chain, *D_D_CLOUD_API_KEY - ); + if matches!( + chain, + PoktChains::Op | PoktChains::Bsc | PoktChains::Sui | PoktChains::Solana + ) { + return Err(QueryError::ChainError)?; + } + + let endpoint: String = format!("https://api.cloud.developerdao.com/rpc/{chain}/{api_key}"); let eth = reqwest::Url::parse(&endpoint)?; let provider = ProviderBuilder::new().connect_http(eth); let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS @@ -159,17 +163,21 @@ pub async fn aggregate_single_token_bals( } pub async fn get_batch_nft_info( - Path((chain, _)): Path<(PoktChains, String)>, + Path((chain, api_key)): Path<(PoktChains, String)>, Query((collection, offset, limit)): Query<(Address, u16, u16)>, ) -> Result { if limit > 10000 { Err(QueryError::NFTQueryLimit)? } - let endpoint: String = format!( - "https://api.cloud.developerdao.com/rpc/{}/{}", - chain, *D_D_CLOUD_API_KEY - ); + if matches!( + chain, + PoktChains::Op | PoktChains::Bsc | PoktChains::Sui | PoktChains::Solana + ) { + return Err(QueryError::ChainError)?; + } + + let endpoint: String = format!("https://api.cloud.developerdao.com/rpc/{chain}/{api_key}"); let eth = reqwest::Url::parse(&endpoint)?; let provider = ProviderBuilder::new().connect_http(eth); let c_addr = *TOKEN_QUERY_UTIL_DEPLOYMENTS @@ -212,6 +220,8 @@ impl IntoResponse for QueryError { #[cfg(test)] pub mod test { + use crate::routes::payment::D_D_CLOUD_API_KEY; + pub use super::*; #[tokio::test] async fn basic() { From 8183094d4a18c1235072d9355d4067cfe1840d79 Mon Sep 17 00:00:00 2001 From: crypdoughdoteth Date: Mon, 6 Apr 2026 21:18:38 -0400 Subject: [PATCH 5/6] x2 --- src/routes/payment.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/payment.rs b/src/routes/payment.rs index 493b96f..b15f323 100644 --- a/src/routes/payment.rs +++ b/src/routes/payment.rs @@ -380,7 +380,8 @@ pub async fn process_ethereum_payment( let hash = hex::decode(&payload.hash)?; let mut fixed = [0u8; 32]; fixed.copy_from_slice(&hash); - + + #[allow(clippy::needless_borrow)] let eth = reqwest::Url::parse(&_endpoint).unwrap(); let provider = ProviderBuilder::new().connect_http(eth); From 2b52711a902cd66f7c37f6dc8de8840461177bd2 Mon Sep 17 00:00:00 2001 From: crypdoughdoteth Date: Mon, 6 Apr 2026 21:24:02 -0400 Subject: [PATCH 6/6] Fix: CI (add missing env var) --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7497bfb..dac4508 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,6 +55,7 @@ jobs: SMTP_USERNAME: ${{ secrets.SMTP_USERNAME }} SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} JWT_KEY: ${{ secrets.JWT_KEY }} + D_D_CLOUD_API_KEY: ${{ secrets.D_D_CLOUD_API_KEY }} run: cargo test --workspace ${{ matrix.flags }} clippy: