Skip to content
1 change: 1 addition & 0 deletions packages/common/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export * from './tan-query/wallets/useTokenBalance'
export * from './tan-query/jupiter/useSwapTokens'
export * from './tan-query/jupiter/useTokenExchangeRate'
export * from './tan-query/jupiter/utils'
export * from './tan-query/jupiter/constants'

// Saga fetch utils, remove when migration is complete
export * from './tan-query/saga-utils'
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/api/tan-query/jupiter/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PublicKey } from '@solana/web3.js'

import { MintName } from '~/services/audius-backend/solana'
import { Env } from '~/services/env'
import { getOrInitializeRegistry, type TokenConfig } from '~/services/tokens'

Expand All @@ -12,6 +13,13 @@ const isClaimableTokenMint = (symbol: string): symbol is ClaimableTokenMint => {
return CLAIMABLE_TOKEN_MINTS.includes(symbol as ClaimableTokenMint)
}

// Convert token symbol to proper mint name for useTokenBalance
// AUDIO -> wAUDIO, others remain the same
export const getTokenMintName = (symbol: string): MintName => {
const mintName = symbol === 'AUDIO' ? 'wAUDIO' : symbol
return mintName as MintName
}

export const JUPITER_PROGRAM_ID = new PublicKey(
'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4'
)
Expand Down
19 changes: 4 additions & 15 deletions packages/common/src/api/tan-query/wallets/useTokenBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { TokenAccountNotFoundError } from '@solana/spl-token'
import { Commitment } from '@solana/web3.js'
import { useQuery, useQueryClient } from '@tanstack/react-query'

import { useCurrentUserId, useUser } from '~/api'
import { useCurrentAccountUser } from '~/api'
import { useQueryContext } from '~/api/tan-query/utils'
import { ID } from '~/models'
import { Status } from '~/models/Status'
import { MintName } from '~/services/audius-backend/solana'
import { getUserbankAccountInfo, getTokenBySymbol } from '~/services/index'
Expand Down Expand Up @@ -47,22 +46,18 @@ export const getTokenBalanceQueryKey = (
*/
export const useTokenBalance = ({
token,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just checking we dont need this method for arbitrary users?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not sure i understand

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So we dropped the incoming userid so just confirming we don't need it for any user?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

oh yea we will in the future but removing it is chill for now, was causing problems. i'll re-add when SDK stuff is in for the API changes

userId,
isPolling,
pollingInterval = 1000,
commitment = 'processed',
...queryOptions
}: {
token: MintName
userId?: ID
isPolling?: boolean
pollingInterval?: number
commitment?: Commitment
} & QueryOptions) => {
const { audiusSdk, env } = useQueryContext()
const { data: currentUserId } = useCurrentUserId()
const { data: user } = useUser(userId ?? currentUserId)
const isCurrentUser = !userId || userId === currentUserId
const { data: user } = useCurrentAccountUser()
const ethAddress = user?.wallet ?? null
const queryClient = useQueryClient()

Expand Down Expand Up @@ -104,16 +99,10 @@ export const useTokenBalance = ({
)
return null
}
// TODO: temporarily fake balances for testing
if (isCurrentUser) {
return createTokenBalance(100000000, tokenConfig.decimals)
}
return createTokenBalance(
Math.floor(Math.random() * 500000000) + 100000000,
tokenConfig.decimals
)
return createTokenBalance(BigInt(0), tokenConfig.decimals)
}
console.error(`Error fetching ${token} balance:`, e)
// Return null instead of throwing to prevent infinite loading
return null
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/hooks/useBuySellAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Name } from '~/models/Analytics'
import { useAnalytics } from './useAnalytics'

export type SwapDetails = {
activeTab: 'buy' | 'sell'
activeTab: 'buy' | 'sell' | 'convert'
inputToken: string
outputToken: string
inputAmount?: number
Expand Down
11 changes: 10 additions & 1 deletion packages/common/src/messages/buySell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const buySellMessages = {
title: 'BUY / SELL',
buy: 'Buy',
sell: 'Sell',
convert: 'Convert',
youPay: 'You Pay',
youPaid: 'You Paid',
youReceive: 'You Receive',
Expand Down Expand Up @@ -61,5 +62,13 @@ export const buySellMessages = {
stackedBalance: (formattedAvailableBalance: string) =>
`${formattedAvailableBalance} Available`,
tokenTicker: (symbol: string, isStablecoin: boolean) =>
isStablecoin ? symbol : `$${symbol}`
isStablecoin ? symbol : `$${symbol}`,
exchangeRate: (inputSymbol: string, outputSymbol: string, rate: number) =>
`Rate 1 $${inputSymbol} ≈ ${rate} $${outputSymbol}`,
exchangeRateLabel: 'Rate',
exchangeRateValue: (
inputSymbol: string,
outputSymbol: string,
rate: number
) => `1 $${inputSymbol} ≈ ${rate} $${outputSymbol}`
}
2 changes: 1 addition & 1 deletion packages/common/src/models/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2111,7 +2111,7 @@ type BuyUSDCAddFundsManually = {
}

export type BuySellSwapEventFields = {
activeTab: 'buy' | 'sell'
activeTab: 'buy' | 'sell' | 'convert'
inputToken: string
outputToken: string
inputAmount?: number
Expand Down
3 changes: 2 additions & 1 deletion packages/common/src/store/ui/buy-sell/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ComponentType } from 'react'

import { TooltipPlacement } from 'antd/lib/tooltip'

export type BuySellTab = 'buy' | 'sell'
export type BuySellTab = 'buy' | 'sell' | 'convert'

export type Screen = 'input' | 'confirm' | 'success'

Expand Down Expand Up @@ -83,6 +83,7 @@ export type TransactionData = {
* Utility function to get input and output tokens based on active tab and token pair
* For 'buy': user pays with quote token (e.g., USDC) to get base token (e.g., AUDIO)
* For 'sell': user pays with base token (e.g., AUDIO) to get quote token (e.g., USDC)
* For 'convert': user pays with base token (e.g., AUDIO) to get quote token (e.g., BONK)
*/
export const getSwapTokens = (activeTab: BuySellTab, tokenPair: TokenPair) => {
return {
Expand Down
1 change: 0 additions & 1 deletion packages/mobile/src/components/user-badges/UserBadges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export const UserBadges = (props: UserBadgesProps) => {
const { tier } = useTierAndVerifiedForUser(userId)

const { data: coinBalance } = useTokenBalance({
userId,
token: 'BONK'
})

Expand Down
3 changes: 2 additions & 1 deletion packages/mobile/src/screens/buy-sell-screen/BuySellFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export const BuySellFlow = ({
Record<BuySellTab, string>
>({
buy: '',
sell: ''
sell: '',
convert: ''
})

// Update input value for buy tab
Expand Down
127 changes: 89 additions & 38 deletions packages/web/src/components/buy-sell-modal/BuySellFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect, useContext, useMemo } from 'react'

import { useBuySellAnalytics } from '@audius/common/hooks'
import { buySellMessages as messages } from '@audius/common/messages'
import { FeatureFlags } from '@audius/common/services'
import {
useBuySellScreen,
useBuySellSwap,
Expand All @@ -17,9 +18,11 @@ import { Button, Flex, Hint, SegmentedControl, TextLink } from '@audius/harmony'
import { ExternalTextLink } from 'components/link'
import { ModalLoading } from 'components/modal-loading'
import { ToastContext } from 'components/toast/ToastContext'
import { useFlag } from 'hooks/useRemoteConfig'

import { BuyTab } from './BuyTab'
import { ConfirmSwapScreen } from './ConfirmSwapScreen'
import { ConvertTab } from './ConvertTab'
import { SellTab } from './SellTab'
import { TransactionSuccessScreen } from './TransactionSuccessScreen'
import { SUPPORTED_TOKEN_PAIRS, TOKENS } from './constants'
Expand All @@ -37,6 +40,7 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
const { onClose, openAddCashModal, onScreenChange, onLoadingStateChange } =
props
const { toast } = useContext(ToastContext)
const { isEnabled: isArtistCoinsEnabled } = useFlag(FeatureFlags.ARTIST_COINS)
const {
trackSwapRequested,
trackSwapSuccess,
Expand Down Expand Up @@ -65,7 +69,8 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
Record<BuySellTab, string>
>({
buy: '',
sell: ''
sell: '',
convert: ''
})

// Update input value for current tab
Expand All @@ -76,14 +81,57 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
}))
}

// Track if user has attempted to submit the form
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false)

const selectedPair = SUPPORTED_TOKEN_PAIRS[0]

// State for dynamic token selection - separate states for each tab
const [buyTabTokens, setBuyTabTokens] = useState<{
baseToken: string
quoteToken: string
}>({
baseToken: selectedPair.baseToken.symbol, // AUDIO by default
quoteToken: selectedPair.quoteToken.symbol // USDC by default
})

const [sellTabTokens, setSellTabTokens] = useState<{
baseToken: string
quoteToken: string
}>({
baseToken: selectedPair.baseToken.symbol, // AUDIO by default
quoteToken: selectedPair.quoteToken.symbol // USDC by default
})

const [convertTabTokens, setConvertTabTokens] = useState<{
baseToken: string
quoteToken: string
}>({
baseToken: selectedPair.baseToken.symbol, // AUDIO by default
quoteToken: 'BONK' // BONK by default for convert tab (excluding USDC)
})

// Get current tab's token symbols
const currentTabTokens =
activeTab === 'buy'
? buyTabTokens
: activeTab === 'sell'
? sellTabTokens
: convertTabTokens
const baseTokenSymbol = currentTabTokens.baseToken
const quoteTokenSymbol = currentTabTokens.quoteToken

// Handle token changes
const handleInputTokenChange = (symbol: string) => {
if (activeTab === 'sell') {
// On sell tab, input token change means base token change
setBaseTokenSymbol(symbol)
} else {
setSellTabTokens((prev) => ({ ...prev, baseToken: symbol }))
} else if (activeTab === 'buy') {
// On buy tab, input token change means quote token change
setQuoteTokenSymbol(symbol)
setBuyTabTokens((prev) => ({ ...prev, quoteToken: symbol }))
} else {
// On convert tab, input token change means base token change
setConvertTabTokens((prev) => ({ ...prev, baseToken: symbol }))
}
// Reset transaction data when tokens change
resetTransactionData()
Expand All @@ -92,28 +140,18 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
const handleOutputTokenChange = (symbol: string) => {
if (activeTab === 'buy') {
// On buy tab, output token change means base token change
setBaseTokenSymbol(symbol)
} else {
setBuyTabTokens((prev) => ({ ...prev, baseToken: symbol }))
} else if (activeTab === 'sell') {
// On sell tab, output token change means quote token change
setQuoteTokenSymbol(symbol)
setSellTabTokens((prev) => ({ ...prev, quoteToken: symbol }))
} else {
// On convert tab, output token change means quote token change
setConvertTabTokens((prev) => ({ ...prev, quoteToken: symbol }))
}
// Reset transaction data when tokens change
resetTransactionData()
}

// Track if user has attempted to submit the form
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false)

const selectedPair = SUPPORTED_TOKEN_PAIRS[0]

// State for dynamic token selection - using base/quote terminology
const [baseTokenSymbol, setBaseTokenSymbol] = useState<string>(
selectedPair.baseToken.symbol // AUDIO by default
)
const [quoteTokenSymbol, setQuoteTokenSymbol] = useState<string>(
selectedPair.quoteToken.symbol // USDC by default
)

// Get all available tokens
const availableTokens = useMemo(() => {
const tokensSet = new Set<string>()
Expand Down Expand Up @@ -233,10 +271,18 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
trackSwapFailure
])

const tabs = [
{ key: 'buy' as BuySellTab, text: messages.buy },
{ key: 'sell' as BuySellTab, text: messages.sell }
]
const tabs = useMemo(() => {
const baseTabs = [
{ key: 'buy' as BuySellTab, text: messages.buy },
{ key: 'sell' as BuySellTab, text: messages.sell }
]

if (isArtistCoinsEnabled) {
baseTabs.push({ key: 'convert' as BuySellTab, text: messages.convert })
}

return baseTabs
}, [isArtistCoinsEnabled])

const {
successDisplayData,
Expand Down Expand Up @@ -352,16 +398,8 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
errorMessage={displayErrorMessage}
initialInputValue={tabInputValues.buy}
onInputValueChange={handleTabInputValueChange}
availableInputTokens={availableTokens.filter(
(t) => t.symbol !== baseTokenSymbol
)}
availableOutputTokens={availableTokens.filter(
(t) => t.symbol !== quoteTokenSymbol
)}
onInputTokenChange={handleInputTokenChange}
onOutputTokenChange={handleOutputTokenChange}
/>
) : (
) : activeTab === 'sell' ? (
<SellTab
tokenPair={currentTokenPair}
onTransactionDataChange={handleTransactionDataChange}
Expand All @@ -370,15 +408,27 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
initialInputValue={tabInputValues.sell}
onInputValueChange={handleTabInputValueChange}
availableInputTokens={availableTokens.filter(
(t) => t.symbol !== quoteTokenSymbol
(t) => t.symbol !== baseTokenSymbol && t.symbol !== 'USDC'
)}
availableOutputTokens={availableTokens.filter(
(t) => t.symbol !== baseTokenSymbol
(t) => t.symbol !== quoteTokenSymbol && t.symbol !== 'USDC'
)}
onInputTokenChange={handleInputTokenChange}
onOutputTokenChange={handleOutputTokenChange}
/>
)}
) : isArtistCoinsEnabled ? (
<ConvertTab
tokenPair={currentTokenPair}
onTransactionDataChange={handleTransactionDataChange}
error={shouldShowError}
errorMessage={displayErrorMessage}
initialInputValue={tabInputValues.convert}
onInputValueChange={handleTabInputValueChange}
availableTokens={availableTokens}
onInputTokenChange={handleInputTokenChange}
onOutputTokenChange={handleOutputTokenChange}
/>
) : null}

{activeTab === 'buy' && !hasSufficientBalance ? (
<Hint>
Expand All @@ -398,7 +448,8 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
</Hint>
) : null}

{hasSufficientBalance ? (
{hasSufficientBalance &&
(activeTab !== 'convert' || !isArtistCoinsEnabled) ? (
<Hint>
{messages.helpCenter}{' '}
<ExternalTextLink to={WALLET_GUIDE_URL} variant='visible'>
Expand Down Expand Up @@ -447,7 +498,7 @@ export const BuySellFlow = (props: BuySellFlowProps) => {
resetSuccessDisplayData()
setCurrentScreen('input')
// Clear all tab input values on completion
setTabInputValues({ buy: '', sell: '' })
setTabInputValues({ buy: '', sell: '', convert: '' })
}}
/>
) : null}
Expand Down
Loading