diff --git a/api/swagger/swagger-v1-full.yaml b/api/swagger/swagger-v1-full.yaml index 15fb1861..e28080e0 100644 --- a/api/swagger/swagger-v1-full.yaml +++ b/api/swagger/swagger-v1-full.yaml @@ -6394,6 +6394,26 @@ components: type: string description: The description of the coin example: "A majestic bear token for wildlife conservation" + link_1: + type: string + description: Generic link URL for the coin + example: "https://x.com/bear_token" + format: uri + link_2: + type: string + description: Generic link URL for the coin + example: "https://instagram.com/bear_token" + format: uri + link_3: + type: string + description: Generic link URL for the coin + example: "https://tiktok.com/@bear_token" + format: uri + link_4: + type: string + description: Generic link URL for the coin + example: "https://bear-token.com" + format: uri create_coin_response: type: object properties: @@ -6431,6 +6451,26 @@ components: type: string description: The URI for the coin's logo image example: "https://example.com/logo.png" + description: + type: string + description: The description of the coin + example: "A majestic bear token for wildlife conservation" + link_1: + type: string + description: Generic link URL for the coin + example: "https://x.com/bear_token" + link_2: + type: string + description: Generic link URL for the coin + example: "https://instagram.com/bear_token" + link_3: + type: string + description: Generic link URL for the coin + example: "https://tiktok.com/@bear_token" + link_4: + type: string + description: Generic link URL for the coin + example: "https://bear-token.com" created_at: type: string format: date-time diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index 21684795..8ea7cb7f 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -6036,6 +6036,26 @@ components: type: string description: The description of the coin example: "A majestic bear token for wildlife conservation" + link_1: + type: string + description: Generic link URL for the coin + example: "https://x.com/bear_token" + format: uri + link_2: + type: string + description: Generic link URL for the coin + example: "https://instagram.com/bear_token" + format: uri + link_3: + type: string + description: Generic link URL for the coin + example: "https://tiktok.com/@bear_token" + format: uri + link_4: + type: string + description: Generic link URL for the coin + example: "https://bear-token.com" + format: uri create_coin_response: type: object properties: @@ -6073,6 +6093,26 @@ components: type: string description: The URI for the coin's logo image example: "https://example.com/logo.png" + description: + type: string + description: The description of the coin + example: "A majestic bear token for wildlife conservation" + link_1: + type: string + description: Generic link URL for the coin + example: "https://x.com/bear_token" + link_2: + type: string + description: Generic link URL for the coin + example: "https://instagram.com/bear_token" + link_3: + type: string + description: Generic link URL for the coin + example: "https://tiktok.com/@bear_token" + link_4: + type: string + description: Generic link URL for the coin + example: "https://bear-token.com" created_at: type: string format: date-time diff --git a/api/v1_create_coin.go b/api/v1_create_coin.go index e3aa8da7..79aac504 100644 --- a/api/v1_create_coin.go +++ b/api/v1_create_coin.go @@ -2,6 +2,7 @@ package api import ( "errors" + "fmt" "time" "api.audius.co/config" @@ -13,12 +14,16 @@ import ( ) type CreateCoinBody struct { - Mint string `json:"mint" validate:"required,solana_address"` - Ticker string `json:"ticker" validate:"required,min=2,max=10,coin_ticker"` - Decimals int32 `json:"decimals" validate:"required,min=0,max=18"` - Name string `json:"name" validate:"required,min=1,max=32"` - LogoUri string `json:"logo_uri" validate:"omitempty,url"` - Description string `json:"description" validate:"max=2500"` + Mint string `json:"mint" validate:"required,solana_address"` + Ticker string `json:"ticker" validate:"required,min=2,max=10,coin_ticker"` + Decimals int32 `json:"decimals" validate:"required,min=0,max=18"` + Name string `json:"name" validate:"required,min=1,max=32"` + LogoUri string `json:"logo_uri" validate:"omitempty,url"` + Description string `json:"description" validate:"max=2500"` + Link1 *string `json:"link_1,omitempty"` + Link2 *string `json:"link_2,omitempty"` + Link3 *string `json:"link_3,omitempty"` + Link4 *string `json:"link_4,omitempty"` } func (app *ApiServer) v1CreateCoin(c *fiber.Ctx) error { @@ -61,13 +66,44 @@ func (app *ApiServer) v1CreateCoin(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "User has already created a coin") } + linkValues := map[string]interface{}{ + "link_1": nil, + "link_2": nil, + "link_3": nil, + "link_4": nil, + } + + linkInputs := []struct { + field string + value *string + }{ + {"link_1", body.Link1}, + {"link_2", body.Link2}, + {"link_3", body.Link3}, + {"link_4", body.Link4}, + } + + for _, link := range linkInputs { + if link.value == nil { + continue + } + if *link.value != "" { + if err := validateURL(*link.value); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid %s URL format", link.field)) + } + linkValues[link.field] = *link.value + } else { + linkValues[link.field] = nil + } + } + sql := ` - INSERT INTO artist_coins (mint, ticker, user_id, decimals, name, logo_uri, description) - VALUES (@mint, @ticker, @user_id, @decimals, @name, @logo_uri, @description) - RETURNING mint, ticker, user_id, decimals, name, logo_uri, description, created_at + INSERT INTO artist_coins (mint, ticker, user_id, decimals, name, logo_uri, description, link_1, link_2, link_3, link_4) + VALUES (@mint, @ticker, @user_id, @decimals, @name, @logo_uri, @description, @link_1, @link_2, @link_3, @link_4) + RETURNING mint, ticker, user_id, decimals, name, logo_uri, description, link_1, link_2, link_3, link_4, created_at ` - row := app.writePool.QueryRow(c.Context(), sql, pgx.NamedArgs{ + args := pgx.NamedArgs{ "mint": body.Mint, "ticker": body.Ticker, "user_id": userID, @@ -75,7 +111,13 @@ func (app *ApiServer) v1CreateCoin(c *fiber.Ctx) error { "name": body.Name, "logo_uri": body.LogoUri, "description": body.Description, - }) + "link_1": linkValues["link_1"], + "link_2": linkValues["link_2"], + "link_3": linkValues["link_3"], + "link_4": linkValues["link_4"], + } + + row := app.writePool.QueryRow(c.Context(), sql, args) var result struct { Mint string `json:"mint"` @@ -85,10 +127,14 @@ func (app *ApiServer) v1CreateCoin(c *fiber.Ctx) error { Name string `json:"name"` LogoUri string `json:"logo_uri"` Description string `json:"description"` + Link1 *string `json:"link_1,omitempty"` + Link2 *string `json:"link_2,omitempty"` + Link3 *string `json:"link_3,omitempty"` + Link4 *string `json:"link_4,omitempty"` CreatedAt time.Time `json:"created_at"` } - if err := row.Scan(&result.Mint, &result.Ticker, &result.UserID, &result.Decimals, &result.Name, &result.LogoUri, &result.Description, &result.CreatedAt); err != nil { + if err := row.Scan(&result.Mint, &result.Ticker, &result.UserID, &result.Decimals, &result.Name, &result.LogoUri, &result.Description, &result.Link1, &result.Link2, &result.Link3, &result.Link4, &result.CreatedAt); err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation { if pgErr.ConstraintName == "artist_coins_pkey" { diff --git a/api/v1_create_coin_test.go b/api/v1_create_coin_test.go index ca997bd4..ed60b208 100644 --- a/api/v1_create_coin_test.go +++ b/api/v1_create_coin_test.go @@ -1,6 +1,8 @@ package api import ( + "context" + "database/sql" "encoding/json" "testing" "time" @@ -58,6 +60,60 @@ func TestV1CreateCoin(t *testing.T) { } +func TestV1CreateCoin_StoresLinks(t *testing.T) { + app := emptyTestApp(t) + database.Seed(app.pool.Replicas[0], database.FixtureMap{ + "users": { + { + "user_id": 2, + "wallet": "0xc3d1d41e6872ffbd15c473d14fc3a9250be5b5e0", + "is_verified": true, + }, + }, + }) + + link1Val := "https://example.com/alpha" + link2Val := "https://example.com/beta" + link3Val := "" + + requestBody := CreateCoinBody{ + Mint: "lionR26zyyB3fNQm5wWv1ZfN8MPQDUMwaAuoG79b1Yj", + Ticker: "LION", + Decimals: 9, + Name: "LION", + LogoUri: "https://example.com/lion-logo.png", + Description: "Roaring with automatic social links", + Link1: &link1Val, + Link2: &link2Val, + Link3: &link3Val, + } + requestBodyBytes, err := json.Marshal(requestBody) + assert.NoError(t, err) + + status, _ := testPostWithWallet(t, app, "/v1/coins?user_id="+trashid.MustEncodeHashID(2), "0xc3d1d41e6872ffbd15c473d14fc3a9250be5b5e0", requestBodyBytes, map[string]string{ + "Content-Type": "application/json", + }) + + assert.Equal(t, 201, status) + + var dbLink1, dbLink2, dbLink3, dbLink4 sql.NullString + err = app.pool.QueryRow(context.Background(), ` + SELECT link_1, link_2, link_3, link_4 + FROM artist_coins + WHERE mint = $1 + `, requestBody.Mint).Scan(&dbLink1, &dbLink2, &dbLink3, &dbLink4) + assert.NoError(t, err) + + assert.True(t, dbLink1.Valid) + assert.Equal(t, "https://example.com/alpha", dbLink1.String) + + assert.True(t, dbLink2.Valid) + assert.Equal(t, "https://example.com/beta", dbLink2.String) + + assert.False(t, dbLink3.Valid) + assert.False(t, dbLink4.Valid) +} + func TestV1CreateCoin_DuplicateMint(t *testing.T) { app := emptyTestApp(t) database.Seed(app.pool.Replicas[0], database.FixtureMap{ diff --git a/api/v1_update_coin.go b/api/v1_update_coin.go index bbe280ff..2ca62def 100644 --- a/api/v1_update_coin.go +++ b/api/v1_update_coin.go @@ -64,7 +64,6 @@ func (app *ApiServer) v1UpdateCoin(c *fiber.Ctx) error { setParts := []string{"updated_at = NOW()"} args := pgx.NamedArgs{"mint": mint} hasUpdates := false - if body.Description != "" { setParts = append(setParts, "description = @description") args["description"] = body.Description