From ddbcb07dc633ebae09e6141a4071d6b3623024c8 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:52:02 -0700 Subject: [PATCH 1/4] Fix hash ID decoding in playlist contents and grant request bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add trashid.IntId type: decodes hash IDs on input, marshals as plain int — for chain metadata where the indexer expects numeric IDs - PlaylistTrackInfo.TrackId: string → trashid.IntId so playlist_contents track IDs are decoded at the API boundary instead of forwarded raw - addManagerBody.ManagerUserId: string → trashid.HashId (removes manual DecodeHashId call) - approveGrantBody.GrantorUserId: string → trashid.HashId (removes manual DecodeHashId call) Co-Authored-By: Claude Opus 4.6 --- api/v1_grants.go | 22 ++++------------------ api/v1_playlist.go | 6 +++--- trashid/hashid.go | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/api/v1_grants.go b/api/v1_grants.go index 28a0c1e7..bed4d5bd 100644 --- a/api/v1_grants.go +++ b/api/v1_grants.go @@ -20,11 +20,11 @@ type createGrantBody struct { } type addManagerBody struct { - ManagerUserId string `json:"manager_user_id"` + ManagerUserId trashid.HashId `json:"manager_user_id"` } type approveGrantBody struct { - GrantorUserId string `json:"grantor_user_id"` + GrantorUserId trashid.HashId `json:"grantor_user_id"` } // postV1UsersGrant creates a grant from the user to an app (user authorizes app to act on their behalf) @@ -162,17 +162,10 @@ func (app *ApiServer) postV1UsersManager(c *fiber.Ctx) error { "error": "Invalid request body", }) } - managerUserID, err := trashid.DecodeHashId(body.ManagerUserId) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid manager_user_id", - }) - } - // Get manager's wallet (grantee_address) users, err := app.queries.Users(c.Context(), dbv1.GetUsersParams{ MyID: 0, - Ids: []int32{int32(managerUserID)}, + Ids: []int32{int32(body.ManagerUserId)}, }) if err != nil || len(users) == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ @@ -316,20 +309,13 @@ func (app *ApiServer) postV1UsersApproveGrant(c *fiber.Ctx) error { "error": "Invalid request body", }) } - grantorUserID, err := trashid.DecodeHashId(body.GrantorUserId) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid grantor_user_id", - }) - } - signer, err := app.getApiSigner(c) if err != nil { return err } nonce := time.Now().UnixNano() - metadata := map[string]interface{}{"grantor_user_id": int64(grantorUserID)} + metadata := map[string]interface{}{"grantor_user_id": int64(body.GrantorUserId)} metadataBytes, _ := json.Marshal(metadata) manageEntityTx := &corev1.ManageEntityLegacy{ diff --git a/api/v1_playlist.go b/api/v1_playlist.go index f9eab63d..dcb1364e 100644 --- a/api/v1_playlist.go +++ b/api/v1_playlist.go @@ -16,9 +16,9 @@ import ( ) type PlaylistTrackInfo struct { - TrackId string `json:"track_id" validate:"required"` - Timestamp int64 `json:"timestamp" validate:"required,min=0"` - MetadataTimestamp *int64 `json:"metadata_timestamp,omitempty" validate:"omitempty,min=0"` + TrackId trashid.IntId `json:"track_id" validate:"required"` + Timestamp int64 `json:"timestamp" validate:"required,min=0"` + MetadataTimestamp *int64 `json:"metadata_timestamp,omitempty" validate:"omitempty,min=0"` } type CreatePlaylistRequest struct { diff --git a/trashid/hashid.go b/trashid/hashid.go index bd600b24..57a08884 100644 --- a/trashid/hashid.go +++ b/trashid/hashid.go @@ -60,6 +60,30 @@ func MustDecodeHashID(id string) int { return val } +// IntId accepts a hash ID or raw int on input (JSON unmarshal) but +// always marshals back as a plain integer. Use this for fields that are +// part of chain metadata where the indexer expects numeric IDs. +type IntId int + +func (num IntId) MarshalJSON() ([]byte, error) { + return []byte(strconv.Itoa(int(num))), nil +} + +func (num *IntId) UnmarshalJSON(data []byte) error { + if data[0] == '"' { + idStr := strings.Trim(string(data), `"`) + id, err := DecodeHashId(idStr) + if err != nil { + return err + } + *num = IntId(id) + return nil + } + val, err := strconv.Atoi(string(data)) + *num = IntId(val) + return err +} + // type alias for int that will do hashid on the way out the door type HashId int From e1b3d21c70da981901188c477093e7cc5e631597 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:35:46 -0700 Subject: [PATCH 2/4] Update api/v1_playlist.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/v1_playlist.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1_playlist.go b/api/v1_playlist.go index dcb1364e..a28129ad 100644 --- a/api/v1_playlist.go +++ b/api/v1_playlist.go @@ -16,7 +16,7 @@ import ( ) type PlaylistTrackInfo struct { - TrackId trashid.IntId `json:"track_id" validate:"required"` + TrackId trashid.IntId `json:"track_id" validate:"required,min=1"` Timestamp int64 `json:"timestamp" validate:"required,min=0"` MetadataTimestamp *int64 `json:"metadata_timestamp,omitempty" validate:"omitempty,min=0"` } From c571b5194dc4307f9a664ba58a2f7f5e21eb1300 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:37:43 +0000 Subject: [PATCH 3/4] Add unit tests for trashid.IntId Agent-Logs-Url: https://github.com/AudiusProject/api/sessions/1e868cb7-009d-437d-a60f-b0b1a123b27c Co-authored-by: rickyrombo <3690498+rickyrombo@users.noreply.github.com> --- trashid/hashid_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/trashid/hashid_test.go b/trashid/hashid_test.go index 6a649a63..66d7c94e 100644 --- a/trashid/hashid_test.go +++ b/trashid/hashid_test.go @@ -46,3 +46,38 @@ func TestHashId(t *testing.T) { assert.Equal(t, 0, int(h)) } } + +func TestIntId(t *testing.T) { + + // when we serialize... it emits a plain number (not a hash string) + { + i := IntId(44) + j, err := json.Marshal(i) + assert.NoError(t, err) + assert.Equal(t, `44`, string(j)) + } + + // when we parse a hashid string... it decodes to the numeric value + { + var i IntId + err := json.Unmarshal([]byte(`"eYorL"`), &i) + assert.NoError(t, err) + assert.Equal(t, 44, int(i)) + } + + // when we parse a raw number... it works as-is + { + var i IntId + err := json.Unmarshal([]byte("33"), &i) + assert.NoError(t, err) + assert.Equal(t, 33, int(i)) + } + + // errors on bad hashid string + { + var i IntId + err := json.Unmarshal([]byte(`"asdjkfalksdjfaklsdjf"`), &i) + assert.Error(t, err) + assert.Equal(t, 0, int(i)) + } +} From c7c0637cba1421536418e5f2b7b8f3e494e3b97b Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:36:28 -0700 Subject: [PATCH 4/4] Add validation tags and validator calls for grant request bodies Addresses PR feedback: adds validate:"required,min=1" struct tags to ManagerUserId and GrantorUserId, and wires up requestValidator.Validate calls so zero/missing values are rejected before hitting the DB or chain. Co-Authored-By: Claude Opus 4.6 --- api/v1_grants.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api/v1_grants.go b/api/v1_grants.go index bed4d5bd..59ad4549 100644 --- a/api/v1_grants.go +++ b/api/v1_grants.go @@ -20,11 +20,11 @@ type createGrantBody struct { } type addManagerBody struct { - ManagerUserId trashid.HashId `json:"manager_user_id"` + ManagerUserId trashid.HashId `json:"manager_user_id" validate:"required,min=1"` } type approveGrantBody struct { - GrantorUserId trashid.HashId `json:"grantor_user_id"` + GrantorUserId trashid.HashId `json:"grantor_user_id" validate:"required,min=1"` } // postV1UsersGrant creates a grant from the user to an app (user authorizes app to act on their behalf) @@ -162,6 +162,9 @@ func (app *ApiServer) postV1UsersManager(c *fiber.Ctx) error { "error": "Invalid request body", }) } + if err := app.requestValidator.Validate(&body); err != nil { + return err + } // Get manager's wallet (grantee_address) users, err := app.queries.Users(c.Context(), dbv1.GetUsersParams{ MyID: 0, @@ -309,6 +312,9 @@ func (app *ApiServer) postV1UsersApproveGrant(c *fiber.Ctx) error { "error": "Invalid request body", }) } + if err := app.requestValidator.Validate(&body); err != nil { + return err + } signer, err := app.getApiSigner(c) if err != nil { return err