Skip to content

feat(minibf): implement /accounts/{stake_address}/transactions#1127

Open
michalrus wants to merge 2 commits into
mainfrom
feat/minibf-accounts-transactions
Open

feat(minibf): implement /accounts/{stake_address}/transactions#1127
michalrus wants to merge 2 commits into
mainfrom
feat/minibf-accounts-transactions

Conversation

@michalrus

@michalrus michalrus commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Resolves #1074.

Implementation

This adds the account transactions endpoint, listing transactions associated with a stake address. It scans the stake archive index and emits one entry per distinct (transaction, address) pair the account participates in, through either produced outputs or resolved consumed inputs, mirroring the /addresses/{address}/transactions endpoint.

The other six account endpoints now also accept CIP-19 stake credential bech32 forms:(stake_vk, stake_vkh, script) in addition to full stake addresses, resolving them against the node's network. This change was required to make blockfrost-tests pass on the new endpoint, and it only makes sense to include it for the already existing ones.

Testing

The change has been tested by blockfrost-tests, and specifically:

Summary by CodeRabbit

  • New Features

    • Added an account transactions endpoint for stake addresses (/accounts/{stake_address}/transactions).
    • Enhanced account lookup to resolve CIP-19 stake credentials (e.g., stake keys/scripts) into canonical stake addresses for stake-address-based endpoints.
    • Transactions results support stable ordering, pagination, and slot-based filtering.
  • Bug Fixes

    • Invalid or unrecognized stake-address/account inputs now return clear bad-request responses.
    • Encoding/derivation failures are handled gracefully to avoid unexpected crashes.
  • Tests

    • Expanded coverage for CIP-19 credential handling and the new transactions endpoint (ordering, pagination, and error cases).

This adds the account transactions endpoint, listing transactions
associated with a stake address. It scans the `stake` archive index and
emits one entry per distinct `(transaction, address)` pair the account
participates in, through either produced outputs or resolved consumed
inputs, mirroring the `/addresses/{address}/transactions` endpoint.

The other six account endpoints now also accept CIP-19 stake credential
bech32 forms:(`stake_vk`, `stake_vkh`, `script`) in addition to full
stake addresses, resolving them against the node's network. This change
was required to make `blockfrost-tests` pass on the new endpoint, and it
only makes sense to include it for the already existing ones.
@michalrus michalrus self-assigned this Jul 24, 2026
@michalrus michalrus added the enhancement New feature or request label Jul 24, 2026
@michalrus
michalrus requested a review from scarmuega as a code owner July 24, 2026 23:42
@michalrus michalrus added the area:minibf Mini Blockfrost (minibf) API label Jul 24, 2026
@michalrus
michalrus requested a review from Copilot July 24, 2026 23:42
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 432f7a8f-f44c-498a-a815-d93551fd0edb

📥 Commits

Reviewing files that changed from the base of the PR and between 96ce3ac and 7c7fb44.

📒 Files selected for processing (1)
  • crates/minibf/src/routes/accounts.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/minibf/src/routes/accounts.rs

📝 Walkthrough

Walkthrough

The accounts API adds CIP-19 stake credential resolution, propagates network-aware parsing across stake routes, and exposes a paginated transactions endpoint that matches produced and consumed stake-related addresses.

Changes

Stake account API

Layer / File(s) Summary
CIP-19 credential resolution
crates/minibf/src/routes/accounts.rs
Credential forms resolve to canonical stake addresses, invalid inputs return BAD_REQUEST, encoding failures return INTERNAL_SERVER_ERROR, and existing stake routes pass network context.
Stake transaction derivation
crates/minibf/src/routes/accounts.rs
The handler scans stake-indexed blocks, matches produced and consumed addresses, and returns ordered paginated transaction results.
Route wiring and endpoint validation
crates/minibf/src/lib.rs, crates/minibf/src/routes/accounts.rs
Registers GET /accounts/{stake_address}/transactions and tests filtering, pagination, ordering, and error responses.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant by_stake_transactions
  participant IndexStore
  Client->>by_stake_transactions: request account transactions
  by_stake_transactions->>IndexStore: verify account and scan stake-indexed blocks
  IndexStore-->>by_stake_transactions: blocks and transaction data
  by_stake_transactions-->>Client: paginated transaction results
Loading

Suggested reviewers: scarmuega

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the new stake-address transactions endpoint in minibf.
Linked Issues check ✅ Passed The PR implements /accounts/<account>/transactions with a stake-index scan, matching the linked issue's core requirement.
Out of Scope Changes check ✅ Passed The added stake-credential parsing and tests support the endpoint and stay aligned with the PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/minibf-accounts-transactions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Blockfrost-compatible /accounts/{stake_address}/transactions support to the minibf service, and broadens account stake identifier parsing to accept CIP-19 credential bech32 forms resolved against the node network.

Changes:

  • Implement /accounts/{stake_address}/transactions by scanning the stake archive index and extracting distinct (tx, address) participations from both produced outputs and resolved consumed inputs.
  • Extend existing account routes to accept CIP-19 credential forms (stake_vk, stake_vkh, script) in addition to canonical stake addresses.
  • Add unit/integration tests covering CIP-19 parsing and the new transactions endpoint behavior (happy path, pagination, ordering, and error cases).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
crates/minibf/src/routes/accounts.rs Adds CIP-19 stake credential parsing and implements the new account transactions endpoint (plus tests).
crates/minibf/src/lib.rs Registers the new /accounts/{stake_address}/transactions HTTP route.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +910 to +914
async fn account_addresses_in_tx<D>(
domain: &Facade<D>,
account: &[u8],
tx: &MultiEraTx<'_>,
) -> Result<BTreeSet<String>, StatusCode>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, it could look like this (below).

The tests still pass.

@scarmuega, but where to put this shared helper, fn tx_touched_addresses? Maybe crates/minibf/src/routes/mod.rs is a little too shared and disorderly?

diff --git a/crates/minibf/src/routes/accounts.rs b/crates/minibf/src/routes/accounts.rs
index 13831d5a..10626df3 100644
--- a/crates/minibf/src/routes/accounts.rs
+++ b/crates/minibf/src/routes/accounts.rs
@@ -22,7 +22,7 @@ use dolos_cardano::{
     pallas_extras, ChainSummary, FixedNamespace, LeaderRewardLog, MemberRewardLog,
     PoolDepositRefundLog,
 };
-use dolos_core::{ArchiveStore as _, Domain, EntityKey, EraCbor, LogKey, TemporalKey};
+use dolos_core::{ArchiveStore as _, Domain, EntityKey, LogKey, TemporalKey};
 use futures_util::StreamExt;
 use pallas::{
     codec::minicbor,
@@ -915,45 +915,13 @@ async fn account_addresses_in_tx<D>(
 where
     D: Domain + Clone + Send + Sync + 'static,
 {
-    let mut addresses = BTreeSet::new();
+    let touched = crate::routes::tx_touched_addresses(domain, tx).await?;
 
-    for (_, output) in tx.produces() {
-        let candidate = output
-            .address()
-            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
-        if address_belongs_to_account(&candidate, account) {
-            addresses.insert(candidate.to_string());
-        }
-    }
-
-    for input in tx.consumes() {
-        if let Some(EraCbor(era, cbor)) = domain
-            .query()
-            .tx_cbor(input.hash().as_slice().to_vec())
-            .await
-            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
-        {
-            let parsed = MultiEraTx::decode_for_era(
-                era.try_into()
-                    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
-                &cbor,
-            )
-            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
-            if let Some(output) = parsed.produces_at(input.index() as usize) {
-                let candidate = output
-                    .address()
-                    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
-                if address_belongs_to_account(&candidate, account) {
-                    addresses.insert(candidate.to_string());
-                }
-            }
-        }
-    }
-
-    Ok(addresses)
+    Ok(touched
+        .into_iter()
+        .filter(|candidate| address_belongs_to_account(candidate, account))
+        .map(|candidate| candidate.to_string())
+        .collect())
 }
 
 async fn find_account_txs_in_block<D>(
diff --git a/crates/minibf/src/routes/addresses.rs b/crates/minibf/src/routes/addresses.rs
index d8623789..5a72243b 100644
--- a/crates/minibf/src/routes/addresses.rs
+++ b/crates/minibf/src/routes/addresses.rs
@@ -22,7 +22,7 @@ use dolos_cardano::{
     indexes::{AsyncCardanoQueryExt, CardanoIndexExt, SlotOrder},
     pallas_extras, ChainSummary,
 };
-use dolos_core::{BlockBody, BlockSlot, Domain, EraCbor, StateStore as _, TxoRef};
+use dolos_core::{BlockBody, BlockSlot, Domain, StateStore as _, TxoRef};
 
 use crate::{
     error::Error,
@@ -386,42 +386,10 @@ async fn has_address<D>(
 where
     D: Domain + Clone + Send + Sync + 'static,
 {
-    for (_, output) in tx.produces() {
-        let candidate = output
-            .address()
-            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
-        if address_matches(address, &candidate) {
-            return Ok(true);
-        }
-    }
-
-    for input in tx.consumes() {
-        if let Some(EraCbor(era, cbor)) = domain
-            .query()
-            .tx_cbor(input.hash().as_slice().to_vec())
-            .await
-            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
-        {
-            let parsed = MultiEraTx::decode_for_era(
-                era.try_into()
-                    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
-                &cbor,
-            )
-            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-            if let Some(output) = parsed.produces_at(input.index() as usize) {
-                let candidate = output
-                    .address()
-                    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
-                if address_matches(address, &candidate) {
-                    return Ok(true);
-                }
-            }
-        }
-    }
-
-    Ok(false)
+    let touched = crate::routes::tx_touched_addresses(domain, tx).await?;
+    Ok(touched
+        .iter()
+        .any(|candidate| address_matches(address, candidate)))
 }
 
 async fn find_txs<D>(
diff --git a/crates/minibf/src/routes/mod.rs b/crates/minibf/src/routes/mod.rs
index a062670a..380fd259 100644
--- a/crates/minibf/src/routes/mod.rs
+++ b/crates/minibf/src/routes/mod.rs
@@ -18,11 +18,60 @@ pub mod utxos;
 use std::env;
 
 use axum::{extract::State, http::StatusCode, Json};
-use dolos_core::Domain;
+use dolos_core::{Domain, EraCbor};
+use pallas::ledger::{addresses::Address, traverse::MultiEraTx};
 use serde::{Deserialize, Serialize};
 
 use crate::{Facade, MinibfConfig};
 
+/// Every address a transaction touches: the addresses of its produced
+/// outputs plus the resolved outputs of its consumed inputs.
+pub(crate) async fn tx_touched_addresses<D>(
+    domain: &Facade<D>,
+    tx: &MultiEraTx<'_>,
+) -> Result<Vec<Address>, StatusCode>
+where
+    D: Domain + Clone + Send + Sync + 'static,
+{
+    let mut addresses = Vec::new();
+
+    for (_, output) in tx.produces() {
+        addresses.push(
+            output
+                .address()
+                .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
+        );
+    }
+
+    for input in tx.consumes() {
+        let Some(EraCbor(era, cbor)) = domain
+            .query()
+            .tx_cbor(input.hash().as_slice().to_vec())
+            .await
+            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
+        else {
+            continue;
+        };
+
+        let parsed = MultiEraTx::decode_for_era(
+            era.try_into()
+                .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
+            &cbor,
+        )
+        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+        if let Some(output) = parsed.produces_at(input.index() as usize) {
+            addresses.push(
+                output
+                    .address()
+                    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
+            );
+        }
+    }
+
+    Ok(addresses)
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub struct RootResponse {
     url: String,

Comment thread crates/minibf/src/routes/accounts.rs
Comment on lines +931 to +935
if let Some(EraCbor(era, cbor)) = domain
.query()
.tx_cbor(input.hash().as_slice().to_vec())
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good observation, but account_addresses_in_tx deliberately mirrors addresses/transactions, which uses the identical tx_cbor-per-input pattern.

@scarmuega WDYT? I think we'd prefer to fix that as a dedicated follow-up that optimizes both endpoints together rather than diverging them here?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/minibf/src/routes/accounts.rs (2)

900-908: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse address_belongs_to_account in by_stake_addresses.

The same match already exists inline at lines 269-277; switching that call site to this helper keeps one definition of the matching rule. Note to_vec() is also recomputed per candidate there — passing the precomputed account bytes avoids that.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minibf/src/routes/accounts.rs` around lines 900 - 908, The inline
address-matching logic in by_stake_addresses should be replaced with calls to
address_belongs_to_account. Pass the precomputed account bytes to the helper for
each candidate so the matching rule has one definition and account conversion is
not repeated.

930-954: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Group consumed inputs by parent tx hash.

transactions<D>() fetches and decodes the same parent transaction for every spent input, so a transaction spending multiple inputs from one parent can execute repeated identical tx_cbor reads and CBOR decodes sequentially. Group tx.consumes() by input.hash() and resolve each parent once; this also keeps the implementation aligned with the mirrored address-transaction flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minibf/src/routes/accounts.rs` around lines 930 - 954, Update the
consumed-input handling around the tx.consumes() loop to group inputs by
input.hash() before querying parent transactions. Fetch and decode each unique
parent transaction once, then process all grouped input indices with the
existing produces_at and address_belongs_to_account logic, preserving current
error handling and address collection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/minibf/src/routes/accounts.rs`:
- Around line 900-908: The inline address-matching logic in by_stake_addresses
should be replaced with calls to address_belongs_to_account. Pass the
precomputed account bytes to the helper for each candidate so the matching rule
has one definition and account conversion is not repeated.
- Around line 930-954: Update the consumed-input handling around the
tx.consumes() loop to group inputs by input.hash() before querying parent
transactions. Fetch and decode each unique parent transaction once, then process
all grouped input indices with the existing produces_at and
address_belongs_to_account logic, preserving current error handling and address
collection behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b6e783f-d7ee-49c7-b540-112c2a8ef78f

📥 Commits

Reviewing files that changed from the base of the PR and between c797e97 and 96ce3ac.

📒 Files selected for processing (2)
  • crates/minibf/src/lib.rs
  • crates/minibf/src/routes/accounts.rs

Compute `tx_hash`, `block_height`, and `block_time` once per transaction
in the account transactions scan instead of recomputing them for every
address, cloning the precomputed hash string per entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:minibf Mini Blockfrost (minibf) API enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

minibf: add /accounts/<account>/transactions

2 participants