Skip to content
Closed
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
1 change: 1 addition & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/coins", app.v1Coins)
g.Get("/coins/:mint", app.v1Coin)
g.Get("/coins/ticker/:ticker", app.v1CoinByTicker)
g.Get("/coins/ticker/:ticker/available", app.v1CoinTickerAvailable)
g.Get("/coins/:mint/insights", app.v1CoinInsights)
g.Get("/coins/:mint/members", app.v1CoinsMembers)
g.Post("/coins", app.v1CreateCoin)
Expand Down
48 changes: 48 additions & 0 deletions api/swagger/swagger-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3619,6 +3619,54 @@ paths:
'500':
description: Server error
content: {}
/coins/ticker/{ticker}/available:
get:
tags:
- coins
operationId: Check Coin Ticker Availability
description: 'Checks if a coin ticker is available for use'
parameters:
- name: ticker
in: path
description: The ticker to check for availability
required: true
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add some min/max length props to this?

schema:
type: string
example: $NEWCOIN
responses:
'200':
description: Ticker is taken (not available)
content:
application/json:
schema:
type: object
properties:
available:
type: boolean
example: false
'404':
description: Ticker is available
content:
application/json:
schema:
type: object
properties:
available:
type: boolean
example: true
'400':
description: Bad request - ticker parameter is required
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: true now but maybe too specific generally for a 400, which I usually treat as "something was invalid about your request; more details in the response message"

content:
application/json:
schema:
type: object
properties:
error:
type: string
example: "ticker parameter is required"
'500':
description: Server error
content: {}
/coins/{mint}/insights:
get:
tags:
Expand Down
41 changes: 41 additions & 0 deletions api/v1_coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,44 @@ func (app *ApiServer) v1CoinByTicker(c *fiber.Ctx) error {
"data": coinRow,
})
}

func (app *ApiServer) v1CoinTickerAvailable(c *fiber.Ctx) error {
ticker := c.Params("ticker")
if ticker == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "ticker parameter is required",
})
}

// Check if ticker exists in the database
sql := `
SELECT COUNT(*)
FROM artist_coins
WHERE ticker = @ticker
`

rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{
"ticker": ticker,
})
if err != nil {
return err
}

var count int
err = rows.Scan(&count)
if err != nil {
return err
}

// If count is 0, ticker is available (return 404 like handle validation)
if count == 0 {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"available": true,
Copy link
Contributor

Choose a reason for hiding this comment

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

The pattern of this endpoint feels a little wonky (maybe from trying to match the handle check behavior?)
404 itself indicates the thing you called doesn't exist. If you're looking for a particular record by id, we don't know it, etc. In this case it could mean that the route you called doesn't exist (and we don't really have a way to distinguish between those two cases). Also a little strange to return a response body with a 404 status.

I would suggest either:

  • Return 200 in all cases other than bad params/error and use your boolean to indicate available or not.
  • Don't implement this endpoint at all and just call GET /v1/coins/ticker/:tickerId and treat a 404 from that endpoint as the ticker being available.

Second concern: Curious if client handles this correctly because SDK definitely has special logic for requests that return >= 400 status codes.

})
}

// If count > 0, ticker is taken (return 200 with available: false)
return c.JSON(fiber.Map{
"available": false,
})
}
49 changes: 49 additions & 0 deletions api/v1_coin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,52 @@ func TestV1Coin(t *testing.T) {
assert.Contains(t, string(body), "no rows")
}
}

func TestV1CoinTickerAvailable(t *testing.T) {
app := emptyTestApp(t)

fixtures := database.FixtureMap{
"artist_coins": {
{
"ticker": "$AUDIO",
"decimals": 8,
"user_id": 1,
"mint": "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM",
"name": "Audius",
"created_at": time.Now().Add(-time.Second),
},
},
}

database.Seed(app.pool.Replicas[0], fixtures)

// Test ticker that exists (should return 200 with available: false)
{
status, body := testGet(t, app, "/v1/coins/ticker/$AUDIO/available")
assert.Equal(t, 200, status)

jsonAssert(t, body, map[string]any{
"available": false,
})
}

// Test ticker that doesn't exist (should return 404 with available: true)
{
status, body := testGet(t, app, "/v1/coins/ticker/$NONEXISTENT/available")
assert.Equal(t, 404, status)

jsonAssert(t, body, map[string]any{
"available": true,
})
}

// Test with empty ticker parameter (should return 400)
{
status, body := testGet(t, app, "/v1/coins/ticker//available")
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not certain route matching works like this? Worth double checking that it actually calls the handler for the new endpoint if it can't match a route parameter for the ticker id

assert.Equal(t, 400, status)

jsonAssert(t, body, map[string]any{
"error": "ticker parameter is required",
})
}
}
Loading