Skip to content

Conversation

@jkawan
Copy link

@jkawan jkawan commented Dec 12, 2025

This pull request adds a new API endpoint to the local state query service, enabling users to search for UTxOs containing a specific asset, with optional filtering by address. The implementation includes input validation, integrates with the local state query protocol, and returns the matching UTxOs in a structured response.

New API endpoint for searching UTxOs by asset:

  • Added a new route GET /localstatequery/utxos/search-by-asset to allow searching for UTxOs by policy_id and asset_name, with optional address filtering.
  • Implemented the handler handleLocalStateQuerySearchUTxOsByAsset, which:
    • Validates and parses input parameters (policy_id, asset_name, and optional address)
    • Connects to the node and fetches UTxOs (by address or the whole set)
    • Filters UTxOs to include only those containing the specified asset
    • Returns a JSON response with the list and count of matching UTxOs

Supporting changes:

  • Imported the localstatequery protocol package to support querying UTxOs.

Summary by cubic

Adds a new API endpoint to search UTxOs by asset, with optional address filtering. This helps quickly find where a specific asset is held and returns matching UTxOs with a count.

  • New Features
    • Added GET /localstatequery/utxos/search-by-asset (policy_id, asset_name in hex; optional address).
    • Input validation and decoding for parameters; clear errors for invalid values.
    • Queries UTxOs via localstatequery (by address or full set) and filters by asset.
    • Returns JSON with matching UTxOs and total count.

Written for commit f8069f1. Summary will update automatically on new commits.

Summary by CodeRabbit

  • New Features
    • Added a new endpoint for searching unspent transaction outputs (UTxOs) by asset, supporting asset identification filters and optional address constraints.
    • Search results include transaction hash, index, address, amount, and associated assets with a total count of matching UTxOs.

✏️ Tip: You can customize this high-level summary in your review settings.

@jkawan jkawan requested a review from a team as a code owner December 12, 2025 00:13
@coderabbitai
Copy link

coderabbitai bot commented Dec 12, 2025

📝 Walkthrough

Walkthrough

A new HTTP GET endpoint /localstatequery/utxos/search-by-asset was added to enable searching UTxOs by asset. The implementation validates required query parameters (policy_id and asset_name) and optional address filtering, parses policy_id as 28-byte hex and asset_name from hex, optionally parses a Bech32 address, establishes an Ouroboros local state query connection, retrieves UTxOs either by address or from the full set, filters results by the specified asset, and returns a structured JSON response with matching UTxOs and their count. New unexported response types and supporting structures were also defined.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–25 minutes

Areas requiring extra attention:

  • Hex parsing and validation: Verify that policy_id is correctly validated as exactly 28 bytes and both policy_id and asset_name hex parsing handle edge cases properly
  • Asset filtering logic: Confirm the filtering mechanism correctly matches UTxOs by the combination of policy_id and asset_name
  • Address parsing: Review the optional address parsing logic for Bech32/Address-like strings and ensure it properly falls back when not provided
  • Ouroboros connection handling: Verify proper connection establishment, error streaming, and cleanup/closure semantics
  • Response structure: Validate that the JSON response format (tx_hash, index, address, amount, assets fields) is consistent with existing endpoints and correctly represents UTxO data

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions 'searchUtxosByAsset' which directly corresponds to the main feature added: a new endpoint to search UTxOs by asset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-search-utxo-by-asset

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 and usage tips.

@jkawan jkawan changed the title feat(searchUtxosByAsset): changes to add support for searchUtxosByAsset WIP-feat(searchUtxosByAsset): changes to add support for searchUtxosByAsset Dec 12, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
internal/api/localstatequery.go (1)

23-24: Route + import look fine; please ensure Swagger/route registration stays consistent across docs and clients.

Also applies to: 34-34

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f995e56 and f8069f1.

📒 Files selected for processing (1)
  • internal/api/localstatequery.go (3 hunks)
🧰 Additional context used
🪛 GitHub Actions: golangci-lint
internal/api/localstatequery.go

[error] 499-499: G115: integer overflow conversion int -> uint32 (gosec)

🪛 GitHub Check: lint
internal/api/localstatequery.go

[failure] 499-499:
G115: integer overflow conversion int -> uint32 (gosec)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Analyze (go)

Comment on lines +380 to +392
type responseLocalStateQuerySearchUTxOsByAsset struct {
UTxOs []utxoItem `json:"utxos"`
Count int `json:"count"`
}

type utxoItem struct {
TxHash string `json:"tx_hash"`
Index uint32 `json:"index"`
Address string `json:"address"`
Amount uint64 `json:"amount"`
Assets interface{} `json:"assets,omitempty"`
}

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

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file mentioned in the review
cat -n internal/api/localstatequery.go | head -100

Repository: blinklabs-io/cardano-node-api

Length of output: 3653


🏁 Script executed:

# Find the context around lines 380-392 to understand the structure better
sed -n '370,400p' internal/api/localstatequery.go

Repository: blinklabs-io/cardano-node-api

Length of output: 891


🏁 Script executed:

# Search for where utxoItem is created/populated to see what's assigned to Assets
rg "utxoItem\{|utxoItem(" internal/api/ -A 5 -B 2

Repository: blinklabs-io/cardano-node-api

Length of output: 163


🏁 Script executed:

# Search for json encoding/marshaling of utxoItem
rg "json\.Marshal|json\.Unmarshal" internal/api/localstatequery.go -B 3 -A 3

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Find the handler implementation
sed -n '392,500p' internal/api/localstatequery.go

Repository: blinklabs-io/cardano-node-api

Length of output: 2941


🏁 Script executed:

# Search for where utxoItem is created
rg "utxoItem" internal/api/ -A 3 -B 1

Repository: blinklabs-io/cardano-node-api

Length of output: 1270


🏁 Script executed:

# Get more of the handler to see how Assets is populated
sed -n '470,530p' internal/api/localstatequery.go

Repository: blinklabs-io/cardano-node-api

Length of output: 1137


🏁 Script executed:

# Search for test files that might show how this is used
find . -name "*localstatequery*test*" -o -name "*test*localstatequery*" | head -20

Repository: blinklabs-io/cardano-node-api

Length of output: 115


🏁 Script executed:

# Check the test file to see what the actual JSON looks like
cat -n ./openapi/test/api_localstatequery_test.go

Repository: blinklabs-io/cardano-node-api

Length of output: 3628


🏁 Script executed:

# Search for the Assets type definition - likely in ledger package
rg "type.*Assets" --type go -B 2 -A 5

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check if there's a custom JSON marshaler for the assets type
rg "MarshalJSON|UnmarshalJSON" internal/api/localstatequery.go

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check what types are imported and understand Assets better
head -30 internal/api/localstatequery.go

Repository: blinklabs-io/cardano-node-api

Length of output: 1145


🏁 Script executed:

# Search for how assets are used elsewhere in codebase
rg "Assets\(\)|output\.Assets" internal/ -B 2 -A 3

Repository: blinklabs-io/cardano-node-api

Length of output: 1028


🏁 Script executed:

# Look for any related issue discussions or comments about Assets
rg -i "asset.*interface|interface.*asset|unstable|json.*asset" internal/api/ -B 2 -A 2

Repository: blinklabs-io/cardano-node-api

Length of output: 926


🏁 Script executed:

# Check if there's any example or documentation about JSON format
rg -i "asset.*json|json.*format" . --type md --type txt

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check go.mod to understand the gouroboros dependency
cat go.mod | grep -A 2 -B 2 "gouroboros"

Repository: blinklabs-io/cardano-node-api

Length of output: 273


🏁 Script executed:

# Search for any integration tests or examples that might show actual JSON output
find . -type f -name "*.go" -exec grep -l "search-by-asset\|SearchUTxOsByAsset" {} \;

Repository: blinklabs-io/cardano-node-api

Length of output: 106


🏁 Script executed:

# Look for any documentation about the Assets response format
find . -type f \( -name "*.md" -o -name "*.yaml" -o -name "*.yml" \) -exec grep -l "assets\|Asset" {} \;

Repository: blinklabs-io/cardano-node-api

Length of output: 104


🏁 Script executed:

# Check if there are any examples or docs in openapi spec
find . -name "*openapi*" -o -name "*swagger*" | head -10

Repository: blinklabs-io/cardano-node-api

Length of output: 248


🏁 Script executed:

# Check the OpenAPI spec for the Assets field definition
rg "responseLocalStateQuerySearchUTxOsByAsset|utxoItem|Assets" ./docs/swagger.yaml -B 3 -A 10

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Also check the JSON version
rg "responseLocalStateQuerySearchUTxOsByAsset|utxoItem|Assets" ./docs/swagger.json -B 3 -A 10

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check the openapi-config to understand how the spec is generated
cat -n openapi-config.yml

Repository: blinklabs-io/cardano-node-api

Length of output: 186


🏁 Script executed:

# Read the swagger.yaml file - check size first
wc -l ./docs/swagger.yaml

Repository: blinklabs-io/cardano-node-api

Length of output: 96


🏁 Script executed:

# Search more specifically in swagger for utxo or asset
grep -i "utxo\|asset" ./docs/swagger.yaml | head -30

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check if Assets field exists in swagger
cat ./docs/swagger.yaml | grep -A 10 -B 10 "assets"

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Read the full swagger file
cat ./docs/swagger.yaml

Repository: blinklabs-io/cardano-node-api

Length of output: 7742


🏁 Script executed:

# Let's also check what the gouroboros library provides - search for any local copies or vendored code
find . -path ./vendor -prune -o -type d -name "gouroboros*" -print

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check if there are any other API response structures we can learn from
rg "type response.*struct" internal/api/ -A 5 | head -50

Repository: blinklabs-io/cardano-node-api

Length of output: 2643


🏁 Script executed:

# Let's check what type the Assets() method returns - search for ledger package
rg "func.*Assets\(\)" . --type go

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check if there are integration tests or real examples showing the JSON output
find . -type f -name "*.go" -exec grep -l "Assets.*json\|json.*Assets" {} \;

Repository: blinklabs-io/cardano-node-api

Length of output: 106


🏁 Script executed:

# Let's check if there's documentation or examples of how the API is actually used
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "README*" \) -exec grep -l "search-by-asset\|Assets" {} \;

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check git history to see if this endpoint was recently added
git log --oneline internal/api/localstatequery.go | head -20

Repository: blinklabs-io/cardano-node-api

Length of output: 126


🏁 Script executed:

# Check the recent commit to understand context
git show f8069f1 --stat

Repository: blinklabs-io/cardano-node-api

Length of output: 5613


🏁 Script executed:

# Let's also look at the full diff of that commit for localstatequery.go
git show f8069f1 -- internal/api/localstatequery.go | head -200

Repository: blinklabs-io/cardano-node-api

Length of output: 5592


🏁 Script executed:

# Check if there are any TODOs or FIXMEs related to Assets
rg "TODO|FIXME|XXX" internal/api/localstatequery.go -B 2 -A 2

Repository: blinklabs-io/cardano-node-api

Length of output: 529


🏁 Script executed:

# Search for the Output type and Assets method definition in available code or comments
rg "Output.*interface|GetAssets|Assets\(\)" . --type go -B 2 -A 5 | head -50

Repository: blinklabs-io/cardano-node-api

Length of output: 1266


🏁 Script executed:

# Check the gouroboros package information from imports and usage
rg "localstatequery\.UTxOsResult\|ledger\." internal/api/localstatequery.go | head -20

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Let's understand what the Assets field should contain by looking at Cardano specifications
# Search for any mapping or conversion of assets in the codebase
rg "asset.*map\|AssetName\|PolicyID" internal/ -B 2 -A 2 | head -50

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Get the full context of assets usage in the handler to understand the type better
sed -n '490,520p' internal/api/localstatequery.go

Repository: blinklabs-io/cardano-node-api

Length of output: 597


🏁 Script executed:

# Look for where assets variable is used to understand its type
rg "assets\." internal/api/localstatequery.go -B 1 -A 1

Repository: blinklabs-io/cardano-node-api

Length of output: 180


🏁 Script executed:

# Check if there's documentation or type hints about what Assets() returns
rg "// Assets|assets interface|Assets interface" . --type go

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Final check - let's see what gets assigned to the Assets field in the response
rg "Assets:" internal/api/localstatequery.go -B 3 -A 1

Repository: blinklabs-io/cardano-node-api

Length of output: 201


Convert Assets interface{} to an explicit, concrete type with a defined JSON structure. Since Assets comes from a third-party library type without a guaranteed JSON format, assign it to a concrete struct (e.g., map[string]map[string]uint64 for {policy_id: {asset_name: quantity}}) or define a custom struct that explicitly represents the asset structure. Update the OpenAPI definition to document the response schema. This ensures API stability and allows clients to depend on a predictable contract.

Comment on lines +405 to +450
func handleLocalStateQuerySearchUTxOsByAsset(c *gin.Context) {
// Get query parameters
policyIdHex := c.Query("policy_id")
assetNameHex := c.Query("asset_name")
addressStr := c.Query("address")

// Validate required parameters
if policyIdHex == "" {
c.JSON(400, apiError("policy_id parameter is required"))
return
}
if assetNameHex == "" {
c.JSON(400, apiError("asset_name parameter is required"))
return
}

// Parse policy ID (28 bytes)
policyIdBytes, err := hex.DecodeString(policyIdHex)
if err != nil {
c.JSON(400, apiError("invalid policy_id hex: "+err.Error()))
return
}
if len(policyIdBytes) != 28 {
c.JSON(400, apiError("policy_id must be 28 bytes"))
return
}
var policyId ledger.Blake2b224
copy(policyId[:], policyIdBytes)

// Parse asset name
assetName, err := hex.DecodeString(assetNameHex)
if err != nil {
c.JSON(400, apiError("invalid asset_name hex: "+err.Error()))
return
}

// Parse optional address
var addrs []ledger.Address
if addressStr != "" {
addr, err := ledger.NewAddress(addressStr)
if err != nil {
c.JSON(400, apiError("invalid address: "+err.Error()))
return
}
addrs = append(addrs, addr)
}
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

Validate asset_name length (and consider upper-bounding query sizes).
Cardano asset names are bounded (commonly max 32 bytes). Without a length check, a large hex string can force large allocations during decode and later filtering.
Proposed patch:

  // Parse asset name
  assetName, err := hex.DecodeString(assetNameHex)
  if err != nil {
    c.JSON(400, apiError("invalid asset_name hex: "+err.Error()))
    return
  }
+ if len(assetName) > 32 {
+   c.JSON(400, apiError("asset_name must be <= 32 bytes"))
+   return
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func handleLocalStateQuerySearchUTxOsByAsset(c *gin.Context) {
// Get query parameters
policyIdHex := c.Query("policy_id")
assetNameHex := c.Query("asset_name")
addressStr := c.Query("address")
// Validate required parameters
if policyIdHex == "" {
c.JSON(400, apiError("policy_id parameter is required"))
return
}
if assetNameHex == "" {
c.JSON(400, apiError("asset_name parameter is required"))
return
}
// Parse policy ID (28 bytes)
policyIdBytes, err := hex.DecodeString(policyIdHex)
if err != nil {
c.JSON(400, apiError("invalid policy_id hex: "+err.Error()))
return
}
if len(policyIdBytes) != 28 {
c.JSON(400, apiError("policy_id must be 28 bytes"))
return
}
var policyId ledger.Blake2b224
copy(policyId[:], policyIdBytes)
// Parse asset name
assetName, err := hex.DecodeString(assetNameHex)
if err != nil {
c.JSON(400, apiError("invalid asset_name hex: "+err.Error()))
return
}
// Parse optional address
var addrs []ledger.Address
if addressStr != "" {
addr, err := ledger.NewAddress(addressStr)
if err != nil {
c.JSON(400, apiError("invalid address: "+err.Error()))
return
}
addrs = append(addrs, addr)
}
func handleLocalStateQuerySearchUTxOsByAsset(c *gin.Context) {
// Get query parameters
policyIdHex := c.Query("policy_id")
assetNameHex := c.Query("asset_name")
addressStr := c.Query("address")
// Validate required parameters
if policyIdHex == "" {
c.JSON(400, apiError("policy_id parameter is required"))
return
}
if assetNameHex == "" {
c.JSON(400, apiError("asset_name parameter is required"))
return
}
// Parse policy ID (28 bytes)
policyIdBytes, err := hex.DecodeString(policyIdHex)
if err != nil {
c.JSON(400, apiError("invalid policy_id hex: "+err.Error()))
return
}
if len(policyIdBytes) != 28 {
c.JSON(400, apiError("policy_id must be 28 bytes"))
return
}
var policyId ledger.Blake2b224
copy(policyId[:], policyIdBytes)
// Parse asset name
assetName, err := hex.DecodeString(assetNameHex)
if err != nil {
c.JSON(400, apiError("invalid asset_name hex: "+err.Error()))
return
}
if len(assetName) > 32 {
c.JSON(400, apiError("asset_name must be <= 32 bytes"))
return
}
// Parse optional address
var addrs []ledger.Address
if addressStr != "" {
addr, err := ledger.NewAddress(addressStr)
if err != nil {
c.JSON(400, apiError("invalid address: "+err.Error()))
return
}
addrs = append(addrs, addr)
}
🤖 Prompt for AI Agents
In internal/api/localstatequery.go around lines 405 to 450, the handler
currently decodes asset_name hex without checking length which can lead to large
allocations and unexpected inputs; before decoding, validate the hex string
length and resulting byte length (e.g. ensure the hex string is even-length and
the decoded asset name is <= 32 bytes, or a configured MAX_ASSET_NAME_BYTES) and
return a 400 error if it exceeds the limit; additionally, enforce a reasonable
upper bound on query parameter sizes (reject overly long
policy_id/asset_name/address strings) to prevent abuse and resource exhaustion.

Comment on lines +452 to +483
// Connect to node
oConn, err := node.GetConnection(nil)
if err != nil {
c.JSON(500, apiError(err.Error()))
return
}
// Async error handler
go func() {
err, ok := <-oConn.ErrorChan()
if !ok {
return
}
c.JSON(500, apiError(err.Error()))
}()
defer func() {
// Close Ouroboros connection
oConn.Close()
}()
// Start client
oConn.LocalStateQuery().Client.Start()

// Get UTxOs (either by address or whole set)
var utxos *localstatequery.UTxOsResult
if len(addrs) > 0 {
utxos, err = oConn.LocalStateQuery().Client.GetUTxOByAddress(addrs)
} else {
utxos, err = oConn.LocalStateQuery().Client.GetUTxOWhole()
}
if err != nil {
c.JSON(500, apiError(err.Error()))
return
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Get the full function context
cd internal/api && head -n 500 localstatequery.go | tail -n +420 | cat -n

Repository: blinklabs-io/cardano-node-api

Length of output: 2552


🏁 Script executed:

# Search for other instances of ErrorChan() patterns in the codebase
rg "ErrorChan()" -A 5 -B 2

Repository: blinklabs-io/cardano-node-api

Length of output: 4894


🏁 Script executed:

# Look for the function signature to understand the full context
rg -A 50 "func.*addrs.*\(" internal/api/localstatequery.go | head -100

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Look for gin.Context definition or usage patterns in the codebase
rg "gin\." internal/api/*.go | head -20

Repository: blinklabs-io/cardano-node-api

Length of output: 1596


🏁 Script executed:

# Search for any response status/write guards in the codebase
rg "Written\|HeadersWritten\|Status" internal/api/*.go | head -20

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🏁 Script executed:

# Check if gin.ResponseWriter has synchronization
rg "sync\|mutex\|lock" internal/api/*.go | grep -i "response\|writer" | head -10

Repository: blinklabs-io/cardano-node-api

Length of output: 55


🌐 Web query:

gin-gonic gin.Context thread safety concurrent writes JSON

💡 Result:

Short answer: gin.Context is not safe for concurrent writes. Don’t call c.JSON / c.Writer methods from multiple goroutines simultaneously — use c.Copy() when starting goroutines and avoid writing to the ResponseWriter after the handler returns. Instead marshal/prepare the response (or synchronize with a mutex) and write once from the handler, or extract needed data and pass plain values/context.Context to background goroutines.

Key references:

  • Gin docs: “When starting new Goroutines ... you SHOULD NOT use the original context inside it, you have to use a read-only copy” (use c.Copy()). [1]
  • pkg.go.dev: Context.Copy() documentation: “Copy returns a copy of the current context that can be safely used outside the request's scope.” [2]
  • Guidance & examples + community issues showing races/overwrites and cancellation pitfalls (don’t rely on *gin.Context for long-running background work; clone request context if you need no cancellation). [3]

🏁 Script executed:

# Verify the specific instance in localstatequery.go around lines 452-483
sed -n '445,490p' internal/api/localstatequery.go | cat -n

Repository: blinklabs-io/cardano-node-api

Length of output: 1410


Don't write to gin.Context from the async ErrorChan() goroutine—this causes race conditions and duplicate response writes.

When the handler returns (e.g., after calling c.JSON() at line 481) and the goroutine later attempts c.JSON(), you get "headers already written" errors or data races on the response. This pattern is repeated across multiple handlers (chainsync.go, localtxmonitor.go, etc.).

Use c.Copy() if passing context to goroutines, or better yet, avoid writing responses from background goroutines. Instead, log the error and close the connection, or signal the error through a channel to let the main handler decide the response.

Comment on lines +473 to +514
// Get UTxOs (either by address or whole set)
var utxos *localstatequery.UTxOsResult
if len(addrs) > 0 {
utxos, err = oConn.LocalStateQuery().Client.GetUTxOByAddress(addrs)
} else {
utxos, err = oConn.LocalStateQuery().Client.GetUTxOWhole()
}
if err != nil {
c.JSON(500, apiError(err.Error()))
return
}

// Filter UTxOs by asset
results := make([]utxoItem, 0)
for utxoId, output := range utxos.Results {
// Check if output has assets
assets := output.Assets()
if assets == nil {
continue
}

// Check if the asset exists in this UTxO
amount := assets.Asset(policyId, assetName)
if amount > 0 {
item := utxoItem{
TxHash: hex.EncodeToString(utxoId.Hash[:]),
Index: uint32(utxoId.Idx),
Address: output.Address().String(),
Amount: output.Amount(),
Assets: assets,
}
results = append(results, item)
}
}

// Create response
resp := responseLocalStateQuerySearchUTxOsByAsset{
UTxOs: results,
Count: len(results),
}
c.JSON(200, resp)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Blocker: fix gosec G115 overflow on uint32(utxoId.Idx) (CI failing).
Add a bounds check before converting:

      if amount > 0 {
+       if utxoId.Idx < 0 || utxoId.Idx > math.MaxUint32 {
+         c.JSON(500, apiError("utxo index out of range"))
+         return
+       }
        item := utxoItem{
          TxHash:  hex.EncodeToString(utxoId.Hash[:]),
-         Index:   uint32(utxoId.Idx),
+         Index:   uint32(utxoId.Idx),
          Address: output.Address().String(),
          Amount:  output.Amount(),
          Assets:  assets,
        }

Also: fetching GetUTxOWhole() when address is omitted is a likely DoS footgun (latency/memory). At minimum, consider adding server-side guardrails (require address, or add pagination/limit + timeouts).

🧰 Tools
🪛 GitHub Actions: golangci-lint

[error] 499-499: G115: integer overflow conversion int -> uint32 (gosec)

🪛 GitHub Check: lint

[failure] 499-499:
G115: integer overflow conversion int -> uint32 (gosec)

🤖 Prompt for AI Agents
In internal/api/localstatequery.go around lines 473-514, add a bounds check
before converting utxoId.Idx to uint32 to fix the gosec G115 overflow: if
utxoId.Idx > math.MaxUint32 either skip that UTxO or return a 400/500 error as
appropriate instead of blindly casting. Additionally, remove or guard the
expensive GetUTxOWhole() path when len(addrs)==0: require an address parameter
(return 400 if missing) or implement server-side guardrails such as a max
limit/pagination (enforce a capped limit on results and support offset/page
token) and wrap the LocalStateQuery client calls with a context timeout to avoid
DoS/latency issues. Ensure any new imports (math, context) are added and
behavior is well-documented in the handler comment.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants