Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion crates/metaboard/src/metaboard_client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use crate::cynic_client::{CynicClient, CynicClientError};
use crate::types::metas::*;
use alloy::primitives::hex::{decode, encode, FromHexError};
use alloy::primitives::{
hex::{decode, encode, FromHexError},
Address,
};
use core::str::FromStr;
use reqwest::Url;
use thiserror::Error;

Expand All @@ -26,6 +30,17 @@ pub enum MetaboardSubgraphClientError {
#[source]
source: FromHexError,
},
#[error("Error parsing metaboard address {address}: {source}")]
AddressParseError {
address: String,
#[source]
source: <Address as FromStr>::Err,
},
#[error("Request error fetching metaboard addresses: {source}")]
RequestErrorMetaBoards {
#[source]
source: CynicClientError,
},
}

pub struct MetaboardSubgraphClient {
Expand Down Expand Up @@ -111,6 +126,35 @@ impl MetaboardSubgraphClient {

Ok(meta_bytes)
}

/// Fetch MetaBoard contract addresses from the subgraph.
pub async fn get_metaboard_addresses(
&self,
first: Option<i32>,
skip: Option<i32>,
) -> Result<Vec<Address>, MetaboardSubgraphClientError> {
let data =
self.query::<MetaBoardAddresses, MetaBoardAddressesVariables>(
MetaBoardAddressesVariables { first, skip },
)
.await
.map_err(|e| MetaboardSubgraphClientError::RequestErrorMetaBoards { source: e })?;

let mut addresses = Vec::with_capacity(data.meta_boards.len());
for board in data.meta_boards {
let address_hex = board.address.0;
let address = Address::from_str(&address_hex).map_err(|e| {
MetaboardSubgraphClientError::AddressParseError {
address: address_hex.clone(),
source: e,
}
})?;

addresses.push(address);
}

Ok(addresses)
}
Comment on lines +130 to +157
Copy link

@coderabbitai coderabbitai bot Oct 2, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Verify empty result handling is intentional.

The implementation correctly parses addresses and handles errors. However, there's an inconsistency with the existing methods:

  • get_metabytes_by_hash and get_metabytes_by_subject return MetaboardSubgraphClientError::Empty when no results are found
  • This method returns an empty Vec<Address> when no results are found

Confirm whether returning an empty vector for zero MetaBoards is the intended behavior, or if it should follow the existing pattern and return an error.


Optional: Consider validating pagination parameters.

The pagination parameters first and skip are passed directly to the subgraph without validation. While the subgraph will likely handle invalid values, client-side validation (e.g., rejecting negative values) could provide clearer error messages.

🤖 Prompt for AI Agents
crates/metaboard/src/metaboard_client.rs lines 130-157: currently
get_metaboard_addresses returns an empty Vec when the subgraph returns zero
MetaBoards, which is inconsistent with other methods that return
MetaboardSubgraphClientError::Empty; change the method to check if
data.meta_boards.is_empty() and return Err(MetaboardSubgraphClientError::Empty)
to match existing behavior (or if empty Vec is intended, add a clear comment and
update other methods for consistency), and optionally add simple validation to
reject negative pagination parameters (first, skip) and return a descriptive
error before making the query.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

an empty array is also fine. this means nothing is found

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I've noted that returning an empty array is the intended behavior for get_metaboard_addresses. The method will remain as-is.

 /\_/\
( o.o )
 > ^ <

/| |_
(
=== _)
`-.-'


✏️ Learnings added
Learnt from: findolor
PR: rainlanguage/rain.metadata#84
File: crates/metaboard/src/metaboard_client.rs:130-157
Timestamp: 2025-10-02T12:33:46.451Z
Learning: In `crates/metaboard/src/metaboard_client.rs`, the `get_metaboard_addresses` method intentionally returns an empty `Vec<Address>` when no MetaBoards are found, rather than returning a `MetaboardSubgraphClientError::Empty` error like other methods in the same file. This design choice is acceptable and means nothing was found.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

#[cfg(test)]
Expand Down Expand Up @@ -289,4 +333,69 @@ mod tests {
_ => panic!("Unexpected result: {:?}", result),
}
}

#[tokio::test]
async fn test_get_metaboard_addresses_success() {
let server = MockServer::start_async().await;
let url = Url::parse(&server.url("/")).unwrap();

server.mock(|when, then| {
when.method(POST).path("/").body_contains("metaBoards");
then.status(200).json_body_obj(&{
serde_json::json!({
"data": {
"metaBoards": [
{
"address": "0x0000000000000000000000000000000000000001",
},
{
"address": "0x0000000000000000000000000000000000000002",
}
]
}
})
});
});

let client = MetaboardSubgraphClient::new(url);

let result = client
.get_metaboard_addresses(Some(10), Some(0))
.await
.unwrap();

assert_eq!(result.len(), 2);

assert_eq!(
result[0],
Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
);
assert_eq!(
result[1],
Address::from_str("0x0000000000000000000000000000000000000002").unwrap()
);
}

#[tokio::test]
async fn test_get_metaboard_addresses_empty() {
let server = MockServer::start_async().await;
let url = Url::parse(&server.url("/")).unwrap();

server.mock(|when, then| {
when.method(POST).path("/").body_contains("metaBoards");
then.status(200).json_body_obj(&{
serde_json::json!({
"data": {
"metaBoards": []
}
})
});
});

let client = MetaboardSubgraphClient::new(url);

let result = client.get_metaboard_addresses(Some(5), None).await.unwrap();

assert!(result.is_empty());
}
Comment on lines +337 to +400
Copy link

@coderabbitai coderabbitai bot Oct 2, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM! Consider adding error path tests.

The success and empty result tests are well-structured and follow the existing test patterns. However, consider adding tests for error paths to improve coverage:

  • Invalid address format (should return AddressParseError)
  • Request failure (should return RequestErrorMetaBoards)

Example test structure:

#[tokio::test]
async fn test_get_metaboard_addresses_invalid_address() {
    let server = MockServer::start_async().await;
    let url = Url::parse(&server.url("/")).unwrap();

    server.mock(|when, then| {
        when.method(POST).path("/").body_contains("metaBoards");
        then.status(200).json_body_obj(&{
            serde_json::json!({
                "data": {
                    "metaBoards": [
                        {
                            "address": "invalid_address",
                        }
                    ]
                }
            })
        });
    });

    let client = MetaboardSubgraphClient::new(url);
    let result = client.get_metaboard_addresses(None, None).await;

    assert!(result.is_err());
    match result {
        Err(MetaboardSubgraphClientError::AddressParseError { .. }) => (),
        _ => panic!("Expected AddressParseError, got: {:?}", result),
    }
}
🤖 Prompt for AI Agents
In crates/metaboard/src/metaboard_client.rs around lines 337-400, add missing
error-path tests for get_metaboard_addresses: create one async test where the
mock returns a 200 JSON with an invalid address string and assert the call
returns Err(MetaboardSubgraphClientError::AddressParseError { .. }), and create
another async test where the mock returns a non-200 status (or triggers a
request failure) and assert it returns
Err(MetaboardSubgraphClientError::RequestErrorMetaBoards { .. }); ensure tests
use MockServer::start_async(), build the client with the mock URL, call
get_metaboard_addresses, and pattern-match the expected error variants.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is fine as contracts are indexed in sg. we are getting proper addresses

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! Since the subgraph ensures valid addresses are indexed, the existing success and empty result tests provide sufficient coverage. Thank you for clarifying! 👍


🧠 Learnings used
Learnt from: findolor
PR: rainlanguage/rain.metadata#84
File: crates/metaboard/src/metaboard_client.rs:130-157
Timestamp: 2025-10-02T12:33:46.451Z
Learning: In `crates/metaboard/src/metaboard_client.rs`, the `get_metaboard_addresses` method intentionally returns an empty `Vec<Address>` when no MetaBoards are found, rather than returning a `MetaboardSubgraphClientError::Empty` error like other methods in the same file. This design choice is acceptable and means nothing was found.

}
13 changes: 13 additions & 0 deletions crates/metaboard/src/types/metas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ pub struct MetasBySubject {
pub meta_v1_s: Vec<MetaV1>,
}

#[derive(cynic::QueryVariables, Debug, Default)]
pub struct MetaBoardAddressesVariables {
pub first: Option<i32>,
pub skip: Option<i32>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "MetaBoardAddressesVariables")]
pub struct MetaBoardAddresses {
#[arguments(first: $first, skip: $skip)]
pub meta_boards: Vec<MetaBoard>,
}

#[derive(cynic::QueryFragment, Debug)]
pub struct MetaV1 {
pub meta_hash: Bytes,
Expand Down