Skip to content
Open
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
194 changes: 126 additions & 68 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use pallas::ledger::primitives::{PlutusData, ToCanonicalJson};
use reqwest::Client;
use rocket::State;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::futures::future::join_all;
use rocket::http::{Header, Method, Status};
use rocket::response::content::RawHtml;
use serde_json::Value;
Expand Down Expand Up @@ -154,7 +155,7 @@ fn decode_asset_name(hex_name: &str) -> Result<String, Error> {
})
}

fn distance_from_center(x: i32, y: i32, center: PositionInput) -> i32 {
fn distance_from_center(x: i32, y: i32, center: &PositionInput) -> i32 {
(x - center.x).abs() + (y - center.y).abs()
}

Expand Down Expand Up @@ -351,6 +352,103 @@ async fn fetch_utxos_by_address(
.map_err(|e| Error::new(e.to_string()))
}

async fn ships_in_radius(
api: &BlockfrostAPI,
spacetime_address: &str,
spacetime_policy_id: &str,
pellet_policy_id: &str,
radius: i32,
center: &PositionInput,
) -> Result<Vec<PositionalInterface>, Error> {
Ok(
fetch_utxos_by_policy(api, spacetime_address, spacetime_policy_id)
.await?
.into_iter()
.map(|x| {
Ship::try_from((
spacetime_policy_id.to_string(),
pellet_policy_id.to_string(),
x,
))
})
.collect::<Result<Vec<Ship>, Error>>()?
.into_iter()
.filter(|ship| {
let distance = distance_from_center(ship.position.x, ship.position.y, center);
distance < radius
})
.map(|ship| PositionalInterface::Ship(ship.clone()))
.collect::<Vec<PositionalInterface>>(),
)
}

async fn pellets_in_radius(
api: &BlockfrostAPI,
pellet_address: &str,
pellet_policy_id: &str,
radius: i32,
center: &PositionInput,
) -> Result<Vec<PositionalInterface>, Error> {
Ok(
fetch_utxos_by_policy(api, pellet_address, pellet_policy_id)
.await?
.into_iter()
.map(|x| Pellet::try_from((pellet_policy_id.to_string(), x)))
.collect::<Result<Vec<Pellet>, Error>>()?
.into_iter()
.filter(|pellet| {
let distance = distance_from_center(pellet.position.x, pellet.position.y, center);
distance < radius
})
.map(|pellet| PositionalInterface::Pellet(pellet.clone()))
.collect::<Vec<PositionalInterface>>(),
)
}

async fn asteria_in_radius(
api: &BlockfrostAPI,
asteria_address: &str,
spacetime_policy_id: &str,
radius: i32,
center: &PositionInput,
) -> Result<Option<PositionalInterface>, Error> {
for utxo in fetch_utxos_by_address(api, asteria_address).await? {
let datum_json = inline_datum_json(&utxo.inline_datum)?;
let spacetime_policy = datum_bytes(&datum_json, 1)?;
if spacetime_policy != spacetime_policy_id {
continue;
}
let asteria = Asteria::try_from(utxo)?;

let distance = distance_from_center(asteria.position.x, asteria.position.y, center);
if distance >= radius {
continue;
}

return Ok(Some(PositionalInterface::Asteria(asteria)));
}
Ok(None)
}

async fn tokens_in_radius(
api: &BlockfrostAPI,
pellet_address: &str,
token: &TokenInput,
radius: i32,
center: &PositionInput,
) -> Result<Vec<PositionalInterface>, Error> {
Ok(fetch_utxos_by_policy(api, pellet_address, &token.policy_id)
.await?
.into_iter()
.map(|utxo| Token::try_from((token.clone(), utxo)))
.collect::<Result<Vec<Token>, Error>>()?
.into_iter()
.filter(|token| distance_from_center(token.position.x, token.position.y, center) <= radius)
.filter(|token| token.amount > 0)
.map(PositionalInterface::Token)
.collect())
Comment on lines +433 to +449
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align the radius boundary check across object types.

Line 446 uses <= radius, while Lines 378, 401, and 424 use a strict < radius. Tokens on the boundary are therefore returned while ships, pellets, and Asteria at the same distance are not.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main.rs` around lines 433 - 449, tokens_in_radius currently uses
a non-strict boundary check (distance_from_center(...) <= radius) which is
inconsistent with other object queries that use a strict check (< radius);
update the filter in tokens_in_radius to use a strict comparison (< radius) so
Token objects are excluded when exactly on the radius boundary and behavior
matches the other functions (match the check used elsewhere like in the
ship/pellet/asteria filters), leaving the rest of the pipeline (Token::try_from,
Token.amount > 0, mapping to PositionalInterface::Token) unchanged.

}

struct QueryRoot;

#[derive(Clone)]
Expand Down Expand Up @@ -560,75 +658,35 @@ impl QueryRoot {

let mut map_objects = Vec::new();

for utxo in fetch_utxos_by_policy(api, &spacetime_address, &spacetime_policy_id).await? {
let ship =
Ship::try_from((spacetime_policy_id.clone(), pellet_policy_id.clone(), utxo))?;
let distance = distance_from_center(ship.position.x, ship.position.y, center);
if distance >= radius {
continue;
}

map_objects.push(PositionalInterface::Ship(ship));
}

for utxo in fetch_utxos_by_policy(api, &pellet_address, &pellet_policy_id).await? {
let pellet = Pellet::try_from((pellet_policy_id.clone(), utxo))?;
let distance = distance_from_center(pellet.position.x, pellet.position.y, center);
if distance >= radius {
continue;
}

if pellet.spacetime_policy.id.to_string() != spacetime_policy_id {
continue;
}

if pellet.fuel <= 0 {
continue;
}

map_objects.push(PositionalInterface::Pellet(pellet));
}

for utxo in fetch_utxos_by_address(api, &asteria_address).await? {
let datum_json = inline_datum_json(&utxo.inline_datum)?;
let spacetime_policy = datum_bytes(&datum_json, 1)?;
if spacetime_policy != spacetime_policy_id {
continue;
}
let asteria = Asteria::try_from(utxo)?;

let distance = distance_from_center(asteria.position.x, asteria.position.y, center);
if distance >= radius {
continue;
}

map_objects.push(PositionalInterface::Asteria(asteria));
}

if let Some(tokens) = tokens {
for token in tokens {
let token_utxos =
fetch_utxos_by_policy(api, &pellet_address, &token.policy_id).await?;

for utxo in token_utxos {
let map_token = Token::try_from((token.clone(), utxo))?;
let distance =
distance_from_center(map_token.position.x, map_token.position.y, center);
if distance >= radius {
continue;
}

if map_token.spacetime_policy.id.to_string() != spacetime_policy_id {
continue;
}

if map_token.amount <= 0 {
continue;
}

map_objects.push(PositionalInterface::Token(map_token));
let (ships, pellets, asteria, tokens) = tokio::join!(
ships_in_radius(
api,
&spacetime_address,
&spacetime_policy_id,
&pellet_policy_id,
radius,
&center,
),
pellets_in_radius(api, &pellet_address, &pellet_policy_id, radius, &center),
asteria_in_radius(api, &asteria_address, &spacetime_policy_id, radius, &center),
async {
match tokens.as_ref().map(|tokens| {
tokens
.iter()
.map(|token| tokens_in_radius(api, &pellet_address, token, radius, &center))
.collect::<Vec<_>>()
}) {
Some(futs) => join_all(futs).await,
None => Vec::new(),
Comment on lines +672 to +680
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bound the token fan-out before calling Blockfrost.

This branch turns a user-supplied tokens array into one Pagination::all() upstream scan per entry, all in flight at once. A large request can burn rate limits and amplify latency for the whole resolver. Please cap the list size or switch to bounded concurrency instead of join_all.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main.rs` around lines 672 - 680, The current code maps each
user-supplied tokens entry into an unbounded set of futures and calls join_all
(see the tokens variable, tokens_in_radius function, and join_all usage), which
can trigger Pagination::all() for every token concurrently; instead either
truncate the input (e.g., limit tokens.iter().take(MAX_TOKENS)) or replace
join_all with a bounded concurrency pattern such as converting the futures
vector into a stream and using buffer_unordered(CONCURRENCY_LIMIT) (or
FuturesUnordered with a semaphore) to limit how many tokens_in_radius calls run
at once; add a configurable MAX_TOKENS or CONCURRENCY_LIMIT constant and apply
it where the tokens are mapped before awaiting results.

}
}
);

map_objects.extend(ships?);
map_objects.extend(pellets?);
map_objects.extend(asteria?.into_iter());
for token_objects in tokens {
map_objects.extend(token_objects?);
}

Ok(map_objects)
Expand Down
Loading