@@ -20,6 +20,7 @@ import (
2020
2121 "github.com/blinklabs-io/cardano-node-api/internal/node"
2222 "github.com/blinklabs-io/gouroboros/ledger"
23+ "github.com/blinklabs-io/gouroboros/protocol/localstatequery"
2324 "github.com/gin-gonic/gin"
2425)
2526
@@ -30,6 +31,7 @@ func configureLocalStateQueryRoutes(apiGroup *gin.RouterGroup) {
3031 group .GET ("/tip" , handleLocalStateQueryTip )
3132 group .GET ("/era-history" , handleLocalStateQueryEraHistory )
3233 group .GET ("/protocol-params" , handleLocalStateQueryProtocolParams )
34+ group .GET ("/utxos/search-by-asset" , handleLocalStateQuerySearchUTxOsByAsset )
3335 // TODO: uncomment after this is fixed:
3436 // - https://github.com/blinklabs-io/gouroboros/issues/584
3537 // group.GET("/genesis-config", handleLocalStateQueryGenesisConfig)
@@ -374,3 +376,139 @@ func handleLocalStateQueryGenesisConfig(c *gin.Context) {
374376 //}
375377 c .JSON (200 , genesisConfig )
376378}
379+
380+ type responseLocalStateQuerySearchUTxOsByAsset struct {
381+ UTxOs []utxoItem `json:"utxos"`
382+ Count int `json:"count"`
383+ }
384+
385+ type utxoItem struct {
386+ TxHash string `json:"tx_hash"`
387+ Index uint32 `json:"index"`
388+ Address string `json:"address"`
389+ Amount uint64 `json:"amount"`
390+ Assets interface {} `json:"assets,omitempty"`
391+ }
392+
393+ // handleLocalStateQuerySearchUTxOsByAsset godoc
394+ //
395+ // @Summary Search UTxOs by Asset
396+ // @Tags localstatequery
397+ // @Produce json
398+ // @Param policy_id query string true "Policy ID (hex)"
399+ // @Param asset_name query string true "Asset name (hex)"
400+ // @Param address query string false "Optional: Filter by address"
401+ // @Success 200 {object} responseLocalStateQuerySearchUTxOsByAsset
402+ // @Failure 400 {object} responseApiError
403+ // @Failure 500 {object} responseApiError
404+ // @Router /localstatequery/utxos/search-by-asset [get]
405+ func handleLocalStateQuerySearchUTxOsByAsset (c * gin.Context ) {
406+ // Get query parameters
407+ policyIdHex := c .Query ("policy_id" )
408+ assetNameHex := c .Query ("asset_name" )
409+ addressStr := c .Query ("address" )
410+
411+ // Validate required parameters
412+ if policyIdHex == "" {
413+ c .JSON (400 , apiError ("policy_id parameter is required" ))
414+ return
415+ }
416+ if assetNameHex == "" {
417+ c .JSON (400 , apiError ("asset_name parameter is required" ))
418+ return
419+ }
420+
421+ // Parse policy ID (28 bytes)
422+ policyIdBytes , err := hex .DecodeString (policyIdHex )
423+ if err != nil {
424+ c .JSON (400 , apiError ("invalid policy_id hex: " + err .Error ()))
425+ return
426+ }
427+ if len (policyIdBytes ) != 28 {
428+ c .JSON (400 , apiError ("policy_id must be 28 bytes" ))
429+ return
430+ }
431+ var policyId ledger.Blake2b224
432+ copy (policyId [:], policyIdBytes )
433+
434+ // Parse asset name
435+ assetName , err := hex .DecodeString (assetNameHex )
436+ if err != nil {
437+ c .JSON (400 , apiError ("invalid asset_name hex: " + err .Error ()))
438+ return
439+ }
440+
441+ // Parse optional address
442+ var addrs []ledger.Address
443+ if addressStr != "" {
444+ addr , err := ledger .NewAddress (addressStr )
445+ if err != nil {
446+ c .JSON (400 , apiError ("invalid address: " + err .Error ()))
447+ return
448+ }
449+ addrs = append (addrs , addr )
450+ }
451+
452+ // Connect to node
453+ oConn , err := node .GetConnection (nil )
454+ if err != nil {
455+ c .JSON (500 , apiError (err .Error ()))
456+ return
457+ }
458+ // Async error handler
459+ go func () {
460+ err , ok := <- oConn .ErrorChan ()
461+ if ! ok {
462+ return
463+ }
464+ c .JSON (500 , apiError (err .Error ()))
465+ }()
466+ defer func () {
467+ // Close Ouroboros connection
468+ oConn .Close ()
469+ }()
470+ // Start client
471+ oConn .LocalStateQuery ().Client .Start ()
472+
473+ // Get UTxOs (either by address or whole set)
474+ var utxos * localstatequery.UTxOsResult
475+ if len (addrs ) > 0 {
476+ utxos , err = oConn .LocalStateQuery ().Client .GetUTxOByAddress (addrs )
477+ } else {
478+ utxos , err = oConn .LocalStateQuery ().Client .GetUTxOWhole ()
479+ }
480+ if err != nil {
481+ c .JSON (500 , apiError (err .Error ()))
482+ return
483+ }
484+
485+ // Filter UTxOs by asset
486+ results := make ([]utxoItem , 0 )
487+ for utxoId , output := range utxos .Results {
488+ // Check if output has assets
489+ assets := output .Assets ()
490+ if assets == nil {
491+ continue
492+ }
493+
494+ // Check if the asset exists in this UTxO
495+ amount := assets .Asset (policyId , assetName )
496+ if amount > 0 {
497+ item := utxoItem {
498+ TxHash : hex .EncodeToString (utxoId .Hash [:]),
499+ Index : uint32 (utxoId .Idx ),
500+ Address : output .Address ().String (),
501+ Amount : output .Amount (),
502+ Assets : assets ,
503+ }
504+ results = append (results , item )
505+ }
506+ }
507+
508+ // Create response
509+ resp := responseLocalStateQuerySearchUTxOsByAsset {
510+ UTxOs : results ,
511+ Count : len (results ),
512+ }
513+ c .JSON (200 , resp )
514+ }
0 commit comments