diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..1d00971 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.93 + with: + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + api/target + key: ${{ runner.os }}-cargo-${{ hashFiles('api/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install cargo-sort + uses: taiki-e/install-action@v2 + with: + tool: cargo-sort + + - name: Install machete + uses: taiki-e/install-action@v2 + with: + tool: cargo-machete + + - name: Check formatting + working-directory: api + run: cargo fmt --all -- --check + + - name: Run clippy + working-directory: api + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Check dependency sort + working-directory: api + run: cargo sort --check + + - name: Check unused dependencies + working-directory: api + run: cargo machete + + - name: Run tests + working-directory: api + env: + RPC_URL: ${{ secrets.RPC_URL }} + DATABASE_URL: postgres://dummy:dummy@localhost/dummy + REDIS_URL: redis://localhost:6379 + AUTH_SECRET: dummy-secret + PORT: "8080" + run: cargo test diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5b44c..78fb0f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.5.3] - 2026-04-04 +## [Unreleased] + +### Added + +- **CI workflow** (`.github/workflows/ci.yaml`): runs `cargo fmt`, `cargo clippy`, `cargo sort`, and `cargo machete` on every push and PR to `master`. +- **`rust-toolchain.toml`**: pins Rust toolchain to `1.93` (matching the Dockerfile), shared between local dev and CI. ### Fixed - **Re-verification always marked `is_verified=false`**: fixed per-row `is_verified` computation when on-chain hash changes, preventing builds with matching hashes from being incorrectly unverified. - **Duplicate phantom build record on every verification**: removed the spurious `initial_uuid` row that was inserted and immediately marked completed before the real verification build started. +### Removed + +- **`use-external-pdas` feature flag**: dead code — the feature gated imports that were never used anywhere in the codebase. + ## [1.5.2] - 2026-03-25 ### Added diff --git a/api/Cargo.toml b/api/Cargo.toml index 49c61d6..cc5f3d7 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -3,10 +3,6 @@ name = "verified_programs_api" version = "1.5.3" edition = "2021" -[features] -default = [] -use-external-pdas = [] - [dependencies] axum = "0.6.18" borsh = "1.5.1" diff --git a/api/src/db/authority.rs b/api/src/db/authority.rs index 37b545f..7f4c17f 100644 --- a/api/src/db/authority.rs +++ b/api/src/db/authority.rs @@ -249,6 +249,7 @@ mod tests { use super::*; #[tokio::test] + #[ignore = "requires database and Redis"] async fn test_program_authority() { dotenv::dotenv().ok(); let db_url = std::env::var("TEST_DATABASE_URL").unwrap(); @@ -273,6 +274,7 @@ mod tests { } #[tokio::test] + #[ignore = "requires database and Redis"] async fn test_program_frozen_and_closed_status() { dotenv::dotenv().ok(); let db_url = std::env::var("TEST_DATABASE_URL").unwrap(); diff --git a/api/src/db/connection.rs b/api/src/db/connection.rs index 4c239d8..6695f89 100644 --- a/api/src/db/connection.rs +++ b/api/src/db/connection.rs @@ -70,6 +70,7 @@ mod tests { use super::*; #[tokio::test] + #[ignore = "requires database and Redis"] async fn test_db_conn_healthcheck() { dotenv::dotenv().ok(); let db_url = std::env::var("TEST_DATABASE_URL").unwrap(); diff --git a/api/src/db/job.rs b/api/src/db/job.rs index ae85896..7872b12 100644 --- a/api/src/db/job.rs +++ b/api/src/db/job.rs @@ -41,6 +41,7 @@ mod tests { use crate::db::models::JobStatus; #[tokio::test] + #[ignore = "requires database and Redis"] async fn test_job_status_update() { dotenv::dotenv().ok(); let db_url = std::env::var("TEST_DATABASE_URL").unwrap(); diff --git a/api/src/db/logs.rs b/api/src/db/logs.rs index b6031c8..58dd7ac 100644 --- a/api/src/db/logs.rs +++ b/api/src/db/logs.rs @@ -48,6 +48,7 @@ mod tests { use super::*; #[tokio::test] + #[ignore = "requires database and Redis"] async fn test_logs_crud() { dotenv::dotenv().ok(); let db_url = std::env::var("TEST_DATABASE_URL").unwrap(); diff --git a/api/src/db/models/unverify.rs b/api/src/db/models/unverify.rs index 48a4f79..843fbb7 100644 --- a/api/src/db/models/unverify.rs +++ b/api/src/db/models/unverify.rs @@ -76,3 +76,51 @@ pub fn parse_helius_transaction( (StatusCode::BAD_REQUEST, "Invalid payload") }) } + +#[cfg(test)] +mod tests { + use super::*; + + /// If this test fails, the Helius API may have changed the structure of the transaction payload. + /// Review the deserialization of `HeliusParsedTransaction` against the current Helius API response. + #[tokio::test] + async fn test_parse_helius_transaction_from_api() { + dotenv::dotenv().ok(); + let rpc_url = std::env::var("RPC_URL").expect("RPC_URL must be set"); + // Helius RPC URL: https://mainnet.helius-rpc.com/?api-key=KEY + // Helius API URL: https://api.helius.xyz/v0/transactions/?api-key=KEY + let url = rpc_url.replace("mainnet.helius-rpc.com/", "api.helius.xyz/v0/transactions/"); + + let tx_sig = "31AUfFXG6BJQjaqwBsCjjZV5ojEL4zbrJ9gKQfKHDMosPvJKQBy6dKTiZgkkjoKbG1StD11csqgWn1KU5EwQsUgX"; + + let client = reqwest::Client::new(); + let body = serde_json::json!({ "transactions": [tx_sig] }); + + let response = client + .post(&url) + .json(&body) + .send() + .await + .expect("Failed to call Helius API"); + + assert!( + response.status().is_success(), + "Helius API returned non-success status: {}", + response.status() + ); + + let payload: Vec = response + .json() + .await + .expect("Failed to deserialize Helius response"); + + let parsed = + parse_helius_transaction(&payload).expect("Failed to parse transaction payload"); + + assert_eq!(parsed.signature, tx_sig); + assert!( + !parsed.instructions.is_empty(), + "Expected at least one instruction" + ); + } +} diff --git a/api/src/db/params.rs b/api/src/db/params.rs index 76f389d..0fce7fa 100644 --- a/api/src/db/params.rs +++ b/api/src/db/params.rs @@ -106,6 +106,7 @@ mod tests { use chrono::Utc; #[tokio::test] + #[ignore = "requires database and Redis"] async fn test_build_params_operations() { dotenv::dotenv().ok(); let db_url = std::env::var("TEST_DATABASE_URL").unwrap(); diff --git a/api/src/db/programs.rs b/api/src/db/programs.rs index 1380fa3..fe3bbcb 100644 --- a/api/src/db/programs.rs +++ b/api/src/db/programs.rs @@ -465,6 +465,7 @@ mod tests { use super::*; #[tokio::test] + #[ignore = "requires database and Redis"] async fn test_get_verified_programs() { dotenv::dotenv().ok(); let db_url = std::env::var("TEST_DATABASE_URL").unwrap(); diff --git a/api/src/db/redis.rs b/api/src/db/redis.rs index 6d76356..b25d8cf 100644 --- a/api/src/db/redis.rs +++ b/api/src/db/redis.rs @@ -96,6 +96,7 @@ mod tests { use super::*; #[tokio::test] + #[ignore = "requires database and Redis"] async fn test_cache_operations() { dotenv::dotenv().ok(); let db_url = std::env::var("TEST_DATABASE_URL").unwrap(); diff --git a/api/src/services/onchain/program_hash_retriver.rs b/api/src/services/onchain/program_hash_retriver.rs index 8a8a494..ce83223 100644 --- a/api/src/services/onchain/program_hash_retriver.rs +++ b/api/src/services/onchain/program_hash_retriver.rs @@ -96,6 +96,7 @@ mod tests { use super::*; #[tokio::test] + #[ignore = "requires solana-verify binary"] async fn test_get_on_chain_hash() { let program_id = "verifycLy8mB96wd9wqq3WDXQwM4oU6r42Th37Db9fC"; let result = get_on_chain_hash(program_id).await; @@ -109,6 +110,7 @@ mod tests { } #[tokio::test] + #[ignore = "requires solana-verify binary"] async fn test_get_on_chain_hash_closed_program() { // This program has been closed - program data account no longer exists let program_id = "2gFsaXeN9jngaKbQvZsLwxqfUrT2n4WRMraMpeL8NwZM"; diff --git a/api/src/services/onchain/program_metadata_retriever.rs b/api/src/services/onchain/program_metadata_retriever.rs index e2bf7be..6ce5370 100644 --- a/api/src/services/onchain/program_metadata_retriever.rs +++ b/api/src/services/onchain/program_metadata_retriever.rs @@ -6,16 +6,6 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::pubkey::Pubkey; use solana_sdk_ids::bpf_loader_upgradeable; -#[cfg(feature = "use-external-pdas")] -use { - solana_account_decoder::UiAccountEncoding, - solana_client::{ - rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, - rpc_filter::{Memcmp, RpcFilterType}, - }, - solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel}, -}; - /// Program ID for the Otter Verify program pub const OTTER_VERIFY_PROGRAMID: Pubkey = solana_sdk::pubkey!("verifycLy8mB96wd9wqq3WDXQwM4oU6r42Th37Db9fC"); diff --git a/api/src/validation.rs b/api/src/validation.rs index 1b233aa..c56dabe 100644 --- a/api/src/validation.rs +++ b/api/src/validation.rs @@ -69,7 +69,7 @@ mod tests { ); assert_eq!( validate_pubkey("12345678901234567890123456789012345678901"), - Err("Invalid public key: Invalid Base58 string".to_string()) + Err("Invalid public key(12345678901234567890123456789012345678901): Invalid Base58 string".to_string()) ); } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..358641e --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.93" +components = ["rustfmt", "clippy"]