diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index 204fcaaf..1a23ac24 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -16492,6 +16492,11 @@ components: type: number description: The balance of the coin in the user's account in USD example: 1.23 + accounts: + type: array + description: Per-token-account breakdown of the user's holdings for this coin. Populated by GET /v1/users/{id}/coins; omitted by GET /v1/wallet/{walletId}/coins. + items: + $ref: "#/components/schemas/user_coin_account" track_feed_item: required: - item diff --git a/api/v1_users_coins.go b/api/v1_users_coins.go index ed0b3ac7..d9a1a8fe 100644 --- a/api/v1_users_coins.go +++ b/api/v1_users_coins.go @@ -73,13 +73,37 @@ func (app *ApiServer) v1UsersCoins(c *fiber.Ctx) error { artist_coins.logo_uri, artist_coins.banner_image_url, COALESCE(balances_by_mint.balance, 0) AS balance, - (COALESCE(balances_by_mint.balance, 0) * COALESCE(coin_prices.price, 0)) / POWER(10, artist_coins.decimals) AS balance_usd + (COALESCE(balances_by_mint.balance, 0) * COALESCE(coin_prices.price, 0)) / POWER(10, artist_coins.decimals) AS balance_usd, + COALESCE( + JSON_AGG( + JSON_BUILD_OBJECT( + 'account', balances.account, + 'owner', balances.owner, + 'balance', balances.balance, + 'balance_usd', (balances.balance * COALESCE(coin_prices.price, 0)) / POWER(10, artist_coins.decimals), + 'is_in_app_wallet', balances.is_in_app_wallet + ) + ORDER BY balances.balance DESC + ) FILTER (WHERE balances.account IS NOT NULL), + '[]'::json + ) AS accounts FROM artist_coins LEFT JOIN balances_by_mint ON balances_by_mint.mint = artist_coins.mint LEFT JOIN artist_coin_prices coin_prices ON coin_prices.mint = artist_coins.mint + LEFT JOIN balances ON balances.mint = artist_coins.mint WHERE artist_coins.user_id = @user_id -- Show owned coins - OR balance > 0 -- Show coins with positive balance - OR ticker = 'AUDIO' -- Always show AUDIO + OR balances_by_mint.balance > 0 -- Show coins with positive balance + OR artist_coins.ticker = 'AUDIO' -- Always show AUDIO + GROUP BY + artist_coins.ticker, + artist_coins.mint, + artist_coins.decimals, + artist_coins.has_discord, + artist_coins.user_id, + artist_coins.logo_uri, + artist_coins.banner_image_url, + balances_by_mint.balance, + coin_prices.price ORDER BY -- Always show user's owned coins first, regardless of balance (artist_coins.user_id = @user_id) DESC, @@ -102,7 +126,7 @@ func (app *ApiServer) v1UsersCoins(c *fiber.Ctx) error { return err } - userCoins, err := pgx.CollectRows(rows, pgx.RowToStructByName[UserCoin]) + userCoins, err := pgx.CollectRows(rows, pgx.RowToStructByName[UserCoinAccounts]) if err != nil { return err } diff --git a/api/v1_users_coins_test.go b/api/v1_users_coins_test.go index 7b691340..e39b73ac 100644 --- a/api/v1_users_coins_test.go +++ b/api/v1_users_coins_test.go @@ -131,17 +131,39 @@ func TestUserCoins(t *testing.T) { assert.Equal(t, 200, status) jsonAssert(t, body, map[string]any{ - "data.#": 2, - "data.0.ticker": "AUDIO", - "data.0.mint": "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM", - "data.0.decimals": 8, - "data.0.owner_id": trashid.MustEncodeHashID(1), - "data.0.balance": 1800000000, // 18 AUDIO - "data.0.balance_usd": 180.0, // Assuming $10 per AUDIO - "data.1.ticker": "USDC", - "data.1.mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - "data.1.balance": 7000000, // 7 USDC - "data.1.balance_usd": 7.0, + "data.#": 2, + "data.0.ticker": "AUDIO", + "data.0.mint": "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM", + "data.0.decimals": 8, + "data.0.owner_id": trashid.MustEncodeHashID(1), + "data.0.balance": 1800000000, // 18 AUDIO + "data.0.balance_usd": 180.0, // Assuming $10 per AUDIO + "data.0.accounts.#": 3, + "data.0.accounts.0.account": "associated", + "data.0.accounts.0.owner": "owner_wallet", + "data.0.accounts.0.balance": 1000000000, // 10 AUDIO + "data.0.accounts.0.balance_usd": 100.0, + "data.0.accounts.0.is_in_app_wallet": false, + "data.0.accounts.1.account": "associated3", + "data.0.accounts.1.owner": "owner_wallet3", + "data.0.accounts.1.balance": 500000000, // 5 AUDIO + "data.0.accounts.1.balance_usd": 50.0, + "data.0.accounts.1.is_in_app_wallet": false, + "data.0.accounts.2.account": "claimable", + "data.0.accounts.2.owner": "claimable_tokens_pda", + "data.0.accounts.2.balance": 300000000, // 3 AUDIO + "data.0.accounts.2.balance_usd": 30.0, + "data.0.accounts.2.is_in_app_wallet": true, + "data.1.ticker": "USDC", + "data.1.mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "data.1.balance": 7000000, // 7 USDC + "data.1.balance_usd": 7.0, + "data.1.accounts.#": 1, + "data.1.accounts.0.account": "associated2", + "data.1.accounts.0.owner": "owner_wallet2", + "data.1.accounts.0.balance": 7000000, + "data.1.accounts.0.balance_usd": 7.0, + "data.1.accounts.0.is_in_app_wallet": false, }) } }