From 9c4a513e9cfece1fa711a63d583a11f70afae87e Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Thu, 10 Jul 2025 10:52:30 -0500 Subject: [PATCH 01/10] feat: Add comprehensive BONK token support with multi-token swap functionality This combined PR includes all frontend, API, and core logic updates needed for BONK token support and dynamic token swapping. Originally planned as separate PRs 4, 5, and 6, but combined due to tight coupling between components. ## Core Logic Updates (PR 4 scope): - Update buy/sell constants and types for dynamic token pairs - Enhance useBuySellSwap hook with selectedPair support - Add token amount formatting for multiple tokens - Update AudiusBackend and Solana services for BONK support - Add decimal utility functions for token calculations ## API & Jupiter Integration (PR 5 scope): - Add Jupiter API constants, types, and utilities - Implement useTokenBalance hook for multi-token support - Update API index with new token balance exports ## Frontend UI Updates (PR 6 scope): - Update mobile BuySellFlow and ConfirmSwapScreen for dynamic tokens - Update web buy-sell modal components (BuyTab, SellTab, SwapTab) - Add TokenAmountSection component updates - Update buy-sell modal constants for new token support ## SDK & Dependencies: - Add BONK support to SDK configurations (dev/prod/stage) - Update ClaimableTokensClient for BONK token handling - Add mintFixedDecimalMap utilities - Update fixed-decimal package to export BONK currency ## Key Features: - Dynamic token pair creation using environment configuration - Support for AUDIO/USDC, AUDIO/BONK, and USDC/BONK pairs - Comprehensive error handling and validation - Mobile and web UI consistency - Type-safe token operations throughout Depends on: PR 1 (token registry infrastructure) being merged first. Related to: PR 2 (backend/SDK support) - some files overlap for completeness. --- packages/common/src/api/index.ts | 1 + .../src/api/tan-query/jupiter/constants.ts | 47 ++++-- .../common/src/api/tan-query/jupiter/types.ts | 4 +- .../common/src/api/tan-query/jupiter/utils.ts | 6 +- .../api/tan-query/wallets/useTokenBalance.ts | 131 +++++++++++++++++ .../services/audius-backend/AudiusBackend.ts | 8 +- .../src/services/audius-backend/solana.ts | 2 +- .../common/src/store/ui/buy-sell/constants.ts | 92 +++++++----- .../common/src/store/ui/buy-sell/types.ts | 2 +- .../src/store/ui/buy-sell/useBuySellSwap.ts | 63 ++++---- .../ui/buy-sell/useTokenAmountFormatting.ts | 9 +- .../src/store/ui/shared/tokenConstants.ts | 49 +++++-- packages/common/src/utils/decimal.ts | 14 +- packages/fixed-decimal/src/currencies.ts | 13 ++ packages/fixed-decimal/src/index.ts | 4 +- .../screens/buy-sell-screen/BuySellFlow.tsx | 11 +- .../buy-sell-screen/ConfirmSwapScreen.tsx | 11 +- packages/sdk/src/sdk/config/development.ts | 1 + packages/sdk/src/sdk/config/production.ts | 1 + packages/sdk/src/sdk/config/staging.ts | 1 + packages/sdk/src/sdk/config/types.ts | 1 + .../src/sdk/scripts/generateServicesConfig.ts | 3 + .../ClaimableTokensClient.ts | 19 ++- .../ClaimableTokensClient/getDefaultConfig.ts | 3 +- packages/sdk/src/sdk/services/Solana/types.ts | 2 +- .../sdk/src/sdk/utils/mintFixedDecimalMap.ts | 5 +- .../components/buy-sell-modal/BuySellFlow.tsx | 138 +++++++++++++++--- .../src/components/buy-sell-modal/BuyTab.tsx | 46 ++++-- .../src/components/buy-sell-modal/SellTab.tsx | 70 +++++++-- .../src/components/buy-sell-modal/SwapTab.tsx | 14 +- .../buy-sell-modal/TokenAmountSection.tsx | 114 ++++++++++++--- .../components/buy-sell-modal/constants.ts | 65 ++++++--- 32 files changed, 747 insertions(+), 203 deletions(-) create mode 100644 packages/common/src/api/tan-query/wallets/useTokenBalance.ts diff --git a/packages/common/src/api/index.ts b/packages/common/src/api/index.ts index dbfa37e91e9..de7f3cd8184 100644 --- a/packages/common/src/api/index.ts +++ b/packages/common/src/api/index.ts @@ -154,6 +154,7 @@ export * from './tan-query/wallets/useTokenPrice' export * from './tan-query/wallets/useWalletCollectibles' export * from './tan-query/wallets/useWalletOwner' export * from './tan-query/wallets/useUSDCBalance' +export * from './tan-query/wallets/useTokenBalance' export * from './tan-query/jupiter/useSwapTokens' export * from './tan-query/jupiter/useTokenExchangeRate' export * from './tan-query/jupiter/utils' diff --git a/packages/common/src/api/tan-query/jupiter/constants.ts b/packages/common/src/api/tan-query/jupiter/constants.ts index bc357d61dc7..10b9d7910ce 100644 --- a/packages/common/src/api/tan-query/jupiter/constants.ts +++ b/packages/common/src/api/tan-query/jupiter/constants.ts @@ -1,13 +1,18 @@ import { PublicKey } from '@solana/web3.js' import { Env } from '~/services/env' -import { - createTokenListingMap, - TOKEN_LISTING_MAP -} from '~/store/ui/shared/tokenConstants' +import { getOrInitializeRegistry, type TokenConfig } from '~/services/tokens' +import { TOKEN_LISTING_MAP } from '~/store/ui/shared/tokenConstants' import { UserBankManagedTokenInfo } from './types' +const CLAIMABLE_TOKEN_MINTS = ['wAUDIO', 'USDC', 'BONK'] as const +type ClaimableTokenMint = (typeof CLAIMABLE_TOKEN_MINTS)[number] + +const isClaimableTokenMint = (symbol: string): symbol is ClaimableTokenMint => { + return CLAIMABLE_TOKEN_MINTS.includes(symbol as ClaimableTokenMint) +} + export const JUPITER_PROGRAM_ID = new PublicKey( 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4' ) @@ -15,19 +20,24 @@ export const JUPITER_PROGRAM_ID = new PublicKey( export const createUserBankManagedTokens = ( env: Env ): Record => { - const tokenListingMap = createTokenListingMap(env) - return { - [tokenListingMap.AUDIO.address.toUpperCase()]: { - mintAddress: tokenListingMap.AUDIO.address, - claimableTokenMint: 'wAUDIO', - decimals: tokenListingMap.AUDIO.decimals - }, - [tokenListingMap.USDC.address.toUpperCase()]: { - mintAddress: tokenListingMap.USDC.address, - claimableTokenMint: 'USDC', - decimals: tokenListingMap.USDC.decimals + const registry = getOrInitializeRegistry(env.ENVIRONMENT) + + // Get all userbank-enabled tokens from registry + const userbankTokens: TokenConfig[] = registry.getUserbankTokens() + const managedTokens: Record = {} + + // Convert registry tokens to UserBankManagedTokenInfo format + userbankTokens.forEach((token: TokenConfig) => { + if (isClaimableTokenMint(token.symbol)) { + managedTokens[token.address.toUpperCase()] = { + mintAddress: token.address, + claimableTokenMint: token.symbol, + decimals: token.decimals + } } - } + }) + + return managedTokens } export const USER_BANK_MANAGED_TOKENS: Record< @@ -43,5 +53,10 @@ export const USER_BANK_MANAGED_TOKENS: Record< mintAddress: TOKEN_LISTING_MAP.USDC.address, claimableTokenMint: 'USDC', decimals: TOKEN_LISTING_MAP.USDC.decimals + }, + [TOKEN_LISTING_MAP.BONK.address.toUpperCase()]: { + mintAddress: TOKEN_LISTING_MAP.BONK.address, + claimableTokenMint: 'BONK', + decimals: TOKEN_LISTING_MAP.BONK.decimals } } diff --git a/packages/common/src/api/tan-query/jupiter/types.ts b/packages/common/src/api/tan-query/jupiter/types.ts index a873cef0b1a..a8024d3b2dc 100644 --- a/packages/common/src/api/tan-query/jupiter/types.ts +++ b/packages/common/src/api/tan-query/jupiter/types.ts @@ -41,8 +41,10 @@ export type SwapTokensResult = { } } +export type ClaimableTokenMint = 'wAUDIO' | 'USDC' | 'BONK' + export interface UserBankManagedTokenInfo { mintAddress: string - claimableTokenMint: 'wAUDIO' | 'USDC' + claimableTokenMint: ClaimableTokenMint decimals: number } diff --git a/packages/common/src/api/tan-query/jupiter/utils.ts b/packages/common/src/api/tan-query/jupiter/utils.ts index 61296fdda78..90e3861188f 100644 --- a/packages/common/src/api/tan-query/jupiter/utils.ts +++ b/packages/common/src/api/tan-query/jupiter/utils.ts @@ -255,8 +255,12 @@ export function formatUSDCValue( * @returns Formatted price string */ export function formatTokenPrice(price: string, decimalPlaces: number): string { + // USDC constructor uses 6 decimal places, so we need to constrain the display + // to not exceed what's available in the FixedDecimal representation + const maxDecimalPlaces = Math.min(decimalPlaces, 6) + return USDC(price.replace(/,/g, '')).toLocaleString('en-US', { minimumFractionDigits: 2, - maximumFractionDigits: decimalPlaces + maximumFractionDigits: maxDecimalPlaces }) } diff --git a/packages/common/src/api/tan-query/wallets/useTokenBalance.ts b/packages/common/src/api/tan-query/wallets/useTokenBalance.ts new file mode 100644 index 00000000000..250a50c2258 --- /dev/null +++ b/packages/common/src/api/tan-query/wallets/useTokenBalance.ts @@ -0,0 +1,131 @@ +import { useCallback } from 'react' + +import { BONK, USDC, wAUDIO } from '@audius/fixed-decimal' +import { TokenAccountNotFoundError } from '@solana/spl-token' +import { Commitment } from '@solana/web3.js' +import { useQuery, useQueryClient } from '@tanstack/react-query' + +import { useCurrentAccountUser } from '~/api' +import { useQueryContext } from '~/api/tan-query/utils' +import { Status } from '~/models/Status' +import { MintName } from '~/services/audius-backend/solana' +import { getUserbankAccountInfo } from '~/services/index' + +import { QueryOptions } from '../types' + +// Map token symbols to their Fixed Decimal constructors +const TOKEN_CONSTRUCTORS = { + wAUDIO, + USDC, + BONK +} as const + +type TokenSymbol = keyof typeof TOKEN_CONSTRUCTORS + +/** + * Hook to get the balance for any supported token for the current user. + * Uses TanStack Query for data fetching and caching. + * + * @param token The token to fetch balance for + * @param options Options for the query and polling + * @returns Object with status, data, refresh, and cancelPolling + */ +export const useTokenBalance = ({ + token, + isPolling, + pollingInterval = 1000, + commitment = 'processed', + ...queryOptions +}: { + token: MintName + isPolling?: boolean + pollingInterval?: number + commitment?: Commitment +} & QueryOptions) => { + const { audiusSdk } = useQueryContext() + const { data: user } = useCurrentAccountUser() + const ethAddress = user?.wallet ?? null + const queryClient = useQueryClient() + + const result = useQuery({ + queryKey: ['tokenBalance', ethAddress, token, commitment], + queryFn: async () => { + const sdk = await audiusSdk() + if (!ethAddress || !token) { + return null + } + + try { + const account = await getUserbankAccountInfo( + sdk, + { + ethAddress, + mint: token + }, + commitment + ) + + const TokenConstructor = TOKEN_CONSTRUCTORS[token as TokenSymbol] + if (!TokenConstructor) { + console.warn(`Unsupported token: ${token}, returning null balance`) + return null + } + + const balance = + account?.amount !== undefined && account?.amount !== null + ? TokenConstructor(BigInt(account.amount.toString())) + : TokenConstructor(BigInt(0)) + + return balance + } catch (e) { + // If user doesn't have a token account yet, return 0 balance + if (e instanceof TokenAccountNotFoundError) { + const TokenConstructor = TOKEN_CONSTRUCTORS[token as TokenSymbol] + if (!TokenConstructor) { + console.warn(`Unsupported token: ${token}, returning null balance`) + return null + } + return TokenConstructor(BigInt(0)) + } + console.error(`Error fetching ${token} balance:`, e) + // Return null instead of throwing to prevent infinite loading + return null + } + }, + enabled: !!ethAddress && !!token, + // TanStack Query's built-in polling - only poll when isPolling is true + refetchInterval: isPolling ? pollingInterval : false, + // Prevent refetching when window regains focus during polling to avoid conflicts + refetchOnWindowFocus: !isPolling, + ...queryOptions + }) + + // Map TanStack Query states to the Status enum for API compatibility + let status = Status.IDLE + if (result.isPending) { + status = Status.LOADING + } else if (result.isError) { + status = Status.ERROR + } else if (result.isSuccess) { + status = Status.SUCCESS + } + + // For compatibility with existing code + const data = result.data ?? null + + // Function to cancel polling by invalidating and refetching the query + // This effectively stops the current polling cycle + const cancelPolling = useCallback(() => { + queryClient.cancelQueries({ + queryKey: ['tokenBalance', ethAddress, token, commitment] + }) + }, [queryClient, ethAddress, token, commitment]) + + return { + status, + data, + error: result.error, + refresh: result.refetch, + cancelPolling + } +} diff --git a/packages/common/src/services/audius-backend/AudiusBackend.ts b/packages/common/src/services/audius-backend/AudiusBackend.ts index 2f2eae9b416..42f05370502 100644 --- a/packages/common/src/services/audius-backend/AudiusBackend.ts +++ b/packages/common/src/services/audius-backend/AudiusBackend.ts @@ -1001,7 +1001,9 @@ export const audiusBackend = ({ const mintKey = mint === 'wAUDIO' ? new PublicKey(env.WAUDIO_MINT_ADDRESS) - : new PublicKey(env.USDC_MINT_ADDRESS) + : mint === 'USDC' + ? new PublicKey(env.USDC_MINT_ADDRESS) + : new PublicKey('DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263') // BONK mint const addresses = PublicKey.findProgramAddressSync( [ solanaWalletKey.toBuffer(), @@ -1075,7 +1077,9 @@ export const audiusBackend = ({ const mintKey = mint === 'wAUDIO' ? new PublicKey(env.WAUDIO_MINT_ADDRESS) - : new PublicKey(env.USDC_MINT_ADDRESS) + : mint === 'USDC' + ? new PublicKey(env.USDC_MINT_ADDRESS) + : new PublicKey('DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263') // BONK mint const accounts = [ // 0. `[sw]` Funding account (must be a system account) { diff --git a/packages/common/src/services/audius-backend/solana.ts b/packages/common/src/services/audius-backend/solana.ts index 3b3c05849c3..5af6594ce55 100644 --- a/packages/common/src/services/audius-backend/solana.ts +++ b/packages/common/src/services/audius-backend/solana.ts @@ -40,7 +40,7 @@ export const MEMO_PROGRAM_ID = new PublicKey( 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo' ) -export type MintName = 'wAUDIO' | 'USDC' +export type MintName = 'wAUDIO' | 'USDC' | 'BONK' export const DEFAULT_MINT: MintName = 'wAUDIO' type UserBankConfig = { diff --git a/packages/common/src/store/ui/buy-sell/constants.ts b/packages/common/src/store/ui/buy-sell/constants.ts index 4c600812025..aadc3d74324 100644 --- a/packages/common/src/store/ui/buy-sell/constants.ts +++ b/packages/common/src/store/ui/buy-sell/constants.ts @@ -1,9 +1,12 @@ import { Env } from '~/services/env' - import { - createTokenListingMap, - TOKEN_LISTING_MAP -} from '../shared/tokenConstants' + createTokenInfoObjects, + generateTokenPairs, + safeGetTokens, + tokenConfigToTokenInfo +} from '~/services/tokens' + +import { TOKEN_LISTING_MAP } from '../shared/tokenConstants' import { TokenInfo, TokenPair } from './types' @@ -13,28 +16,11 @@ export const MAX_SWAP_AMOUNT_USD = 10000 // $10,000 // Create tokens using environment variables export const createTokens = (env: Env): Record => { - const tokenListingMap = createTokenListingMap(env) - return { - AUDIO: { - symbol: 'AUDIO', - name: 'Audius', - decimals: tokenListingMap.AUDIO.decimals, - balance: null, - isStablecoin: false, - address: tokenListingMap.AUDIO.address - }, - USDC: { - symbol: 'USDC', - name: 'USD Coin', - decimals: tokenListingMap.USDC.decimals, - balance: null, - isStablecoin: true, - address: tokenListingMap.USDC.address - } - } + return createTokenInfoObjects(env) } -// Token metadata without icons (to avoid circular dependency with harmony) +// Legacy token metadata without icons (prefer createTokens) +// @deprecated Use createTokens(env) instead for environment-specific tokens export const TOKENS: Record = { AUDIO: { symbol: 'AUDIO', @@ -51,26 +37,66 @@ export const TOKENS: Record = { balance: null, isStablecoin: true, address: TOKEN_LISTING_MAP.USDC.address + }, + BONK: { + symbol: 'BONK', + name: 'Bonk', + decimals: TOKEN_LISTING_MAP.BONK.decimals, + balance: null, + isStablecoin: false, + address: TOKEN_LISTING_MAP.BONK.address } } +// Cache for token pairs to avoid repeated computation +const tokenPairsCache = new Map() + +/** + * Clear token pairs cache (useful for testing or configuration changes) + */ +export const clearTokenPairsCache = () => { + tokenPairsCache.clear() +} + // Create supported token pairs using environment variables export const createSupportedTokenPairs = (env: Env): TokenPair[] => { - const tokens = createTokens(env) - return [ - { - baseToken: tokens.AUDIO, - quoteToken: tokens.USDC, - exchangeRate: null - } - ] + const cacheKey = env.ENVIRONMENT + + // Return cached result if available + if (tokenPairsCache.has(cacheKey)) { + return tokenPairsCache.get(cacheKey)! + } + + // Get all tradeable tokens (purchasable and sellable) + const tradeableTokens = safeGetTokens( + env, + (token) => token.purchasable || token.sellable + ).map(tokenConfigToTokenInfo) + + // Generate all possible pairs efficiently + const pairs = generateTokenPairs(tradeableTokens) as TokenPair[] + + // Cache the result + tokenPairsCache.set(cacheKey, pairs) + return pairs } -// Define supported token pairs without icons +// Legacy hardcoded supported token pairs (prefer createSupportedTokenPairs) +// @deprecated Use createSupportedTokenPairs(env) instead for dynamic token pairs export const SUPPORTED_TOKEN_PAIRS: TokenPair[] = [ { baseToken: TOKENS.AUDIO, quoteToken: TOKENS.USDC, exchangeRate: null + }, + { + baseToken: TOKENS.AUDIO, + quoteToken: TOKENS.BONK, + exchangeRate: null + }, + { + baseToken: TOKENS.USDC, + quoteToken: TOKENS.BONK, + exchangeRate: null } ] diff --git a/packages/common/src/store/ui/buy-sell/types.ts b/packages/common/src/store/ui/buy-sell/types.ts index 96b0052bb7a..28b31f256cd 100644 --- a/packages/common/src/store/ui/buy-sell/types.ts +++ b/packages/common/src/store/ui/buy-sell/types.ts @@ -6,7 +6,7 @@ export type BuySellTab = 'buy' | 'sell' export type Screen = 'input' | 'confirm' | 'success' -export type TokenType = 'AUDIO' | 'USDC' +export type TokenType = 'AUDIO' | 'USDC' | 'BONK' export type TokenInfo = { symbol: string // e.g., 'AUDIO', 'USDC', 'WETH' diff --git a/packages/common/src/store/ui/buy-sell/useBuySellSwap.ts b/packages/common/src/store/ui/buy-sell/useBuySellSwap.ts index 972d8f0e116..86297c4fa4f 100644 --- a/packages/common/src/store/ui/buy-sell/useBuySellSwap.ts +++ b/packages/common/src/store/ui/buy-sell/useBuySellSwap.ts @@ -1,33 +1,38 @@ -import { useCallback, useEffect, useState, useRef } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' -import { - SLIPPAGE_BPS, - useSwapTokens, - useCurrentAccountUser, - useQueryContext -} from '~/api' +import { SLIPPAGE_BPS, useCurrentAccountUser, useSwapTokens } from '~/api' import { SwapStatus } from '~/api/tan-query/jupiter/types' import { QUERY_KEYS } from '~/api/tan-query/queryKeys' -import { createTokenListingMap } from '../shared/tokenConstants' - -import type { BuySellTab, Screen, SwapResult, TransactionData } from './types' +import type { + BuySellTab, + Screen, + SwapResult, + TokenPair, + TransactionData +} from './types' type UseBuySellSwapProps = { transactionData: TransactionData currentScreen: Screen setCurrentScreen: (screen: Screen) => void activeTab: BuySellTab + selectedPair: TokenPair onClose: () => void } export const useBuySellSwap = (props: UseBuySellSwapProps) => { - const { transactionData, currentScreen, setCurrentScreen, activeTab } = props + const { + transactionData, + currentScreen, + setCurrentScreen, + activeTab, + selectedPair + } = props const queryClient = useQueryClient() const { data: user } = useCurrentAccountUser() - const { env } = useQueryContext() const [swapResult, setSwapResult] = useState(null) const [retryCount, setRetryCount] = useState(0) const [isRetrying, setIsRetrying] = useState(false) @@ -48,30 +53,38 @@ export const useBuySellSwap = (props: UseBuySellSwapProps) => { if (!transactionData || !transactionData.isValid) return const { inputAmount } = transactionData - const tokenListingMap = createTokenListingMap(env) + + // Get the correct input and output token addresses based on the selected pair and active tab + let inputMintAddress: string + let outputMintAddress: string if (activeTab === 'buy') { - swapTokens({ - inputMint: tokenListingMap.USDC.address, - outputMint: tokenListingMap.AUDIO.address, - amountUi: inputAmount, - slippageBps: SLIPPAGE_BPS - }) + // Buy: pay with quote token, receive base token + inputMintAddress = selectedPair.quoteToken.address! + outputMintAddress = selectedPair.baseToken.address! } else { - swapTokens({ - inputMint: tokenListingMap.AUDIO.address, - outputMint: tokenListingMap.USDC.address, - amountUi: inputAmount, - slippageBps: SLIPPAGE_BPS - }) + // Sell: pay with base token, receive quote token + inputMintAddress = selectedPair.baseToken.address! + outputMintAddress = selectedPair.quoteToken.address! } + + swapTokens({ + inputMint: inputMintAddress, + outputMint: outputMintAddress, + amountUi: inputAmount, + slippageBps: SLIPPAGE_BPS + }) } const invalidateBalances = () => { if (user?.wallet) { + // Invalidate balances for all token types that could be involved queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.usdcBalance, user.wallet] }) + queryClient.invalidateQueries({ + queryKey: ['tokenBalance', user.wallet] + }) } if (user?.spl_wallet) { queryClient.invalidateQueries({ diff --git a/packages/common/src/store/ui/buy-sell/useTokenAmountFormatting.ts b/packages/common/src/store/ui/buy-sell/useTokenAmountFormatting.ts index 91bb2d78526..ba93a90e655 100644 --- a/packages/common/src/store/ui/buy-sell/useTokenAmountFormatting.ts +++ b/packages/common/src/store/ui/buy-sell/useTokenAmountFormatting.ts @@ -3,10 +3,7 @@ import { useCallback, useMemo } from 'react' import { AUDIO } from '@audius/fixed-decimal' import { formatUSDCValue } from '../../../api' -import { - getAudioBalanceDecimalPlaces, - getCurrencyDecimalPlaces -} from '../../../utils' +import { getTokenDecimalPlaces, getCurrencyDecimalPlaces } from '../../../utils' export type UseTokenAmountFormattingProps = { amount?: string | number @@ -57,7 +54,7 @@ export const useTokenAmountFormatting = ({ // Use AUDIO for non-stablecoins for now, when we expand to other tokens // we will need to use FixedDecimal itself const audioAmount = AUDIO(availableBalance) - const decimals = getAudioBalanceDecimalPlaces(availableBalance) + const decimals = getTokenDecimalPlaces(availableBalance) return audioAmount.toLocaleString('en-US', { maximumFractionDigits: decimals @@ -76,7 +73,7 @@ export const useTokenAmountFormatting = ({ } const audioAmount = AUDIO(safeNumericAmount) - const decimals = getAudioBalanceDecimalPlaces(safeNumericAmount) + const decimals = getTokenDecimalPlaces(safeNumericAmount) return audioAmount.toLocaleString('en-US', { maximumFractionDigits: decimals diff --git a/packages/common/src/store/ui/shared/tokenConstants.ts b/packages/common/src/store/ui/shared/tokenConstants.ts index 594dc8e5206..d5c22857f8a 100644 --- a/packages/common/src/store/ui/shared/tokenConstants.ts +++ b/packages/common/src/store/ui/shared/tokenConstants.ts @@ -1,4 +1,5 @@ import { Env } from '~/services/env' +import { getOrInitializeRegistry } from '~/services/tokens' import { JupiterTokenListing } from '../buy-audio/types' @@ -30,6 +31,14 @@ const BASE_TOKEN_METADATA = { decimals: 6, logoURI: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png' + }, + BONK: { + chainId: 101, + symbol: 'BONK', + name: 'Bonk', + decimals: 5, + logoURI: + 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263/logo.png' } } as const @@ -38,19 +47,33 @@ const BASE_TOKEN_METADATA = { */ export const createTokenListingMap = ( env: Env -): Record => ({ - AUDIO: { - ...BASE_TOKEN_METADATA.AUDIO, - address: env.WAUDIO_MINT_ADDRESS - }, - SOL: { +): Record => { + const registry = getOrInitializeRegistry(env.ENVIRONMENT) + + // Get all tokens from registry + const allTokens = registry.getAllTokens() + const tokenMap: Record = {} + + // Add tokens from registry + allTokens.forEach((token) => { + const baseMetadata = + BASE_TOKEN_METADATA[token.symbol as keyof typeof BASE_TOKEN_METADATA] + if (baseMetadata) { + tokenMap[token.symbol] = { + ...baseMetadata, + address: token.address, + decimals: token.decimals + } + } + }) + + // Add SOL which is not in the token registry + tokenMap.SOL = { ...BASE_TOKEN_METADATA.SOL - }, - USDC: { - ...BASE_TOKEN_METADATA.USDC, - address: env.USDC_MINT_ADDRESS } -}) + + return tokenMap +} /** * Legacy token listing map with hardcoded addresses for backward compatibility @@ -66,5 +89,9 @@ export const TOKEN_LISTING_MAP: Record = { USDC: { ...BASE_TOKEN_METADATA.USDC, address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' + }, + BONK: { + ...BASE_TOKEN_METADATA.BONK, + address: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263' } } diff --git a/packages/common/src/utils/decimal.ts b/packages/common/src/utils/decimal.ts index 6ddaf9c31f0..b17946f1bbd 100644 --- a/packages/common/src/utils/decimal.ts +++ b/packages/common/src/utils/decimal.ts @@ -81,13 +81,13 @@ export const getCurrencyDecimalPlaces = (priceUSD: number) => { * @returns Number of decimal places to show (minimum 2) * * @example - * getAudioBalanceDecimalPlaces(1234.56) // 2 → "1,234.56" - * getAudioBalanceDecimalPlaces(92.0253) // 2 → "92.02" - * getAudioBalanceDecimalPlaces(1.2345) // 2 → "1.23" - * getAudioBalanceDecimalPlaces(0.1234) // 3 → "0.123" - * getAudioBalanceDecimalPlaces(0.00123) // 5 → "0.00123" + * getTokenDecimalPlaces(1234.56) // 2 → "1,234.56" + * getTokenDecimalPlaces(92.0253) // 2 → "92.02" + * getTokenDecimalPlaces(1.2345) // 2 → "1.23" + * getTokenDecimalPlaces(0.1234) // 3 → "0.123" + * getTokenDecimalPlaces(0.00123) // 5 → "0.00123" */ -export const getAudioBalanceDecimalPlaces = (balance: number) => { +export const getTokenDecimalPlaces = (balance: number) => { const absBalance = Math.abs(balance) if (absBalance >= 1000) { @@ -121,7 +121,7 @@ export const formatAudioBalance = ( locale: string = 'en-US' ): string => { const balanceNumber = Number(AUDIO(balance).toString()) - const decimalPlaces = getAudioBalanceDecimalPlaces(balanceNumber) + const decimalPlaces = getTokenDecimalPlaces(balanceNumber) return AUDIO(balance).toLocaleString(locale, { maximumFractionDigits: decimalPlaces, diff --git a/packages/fixed-decimal/src/currencies.ts b/packages/fixed-decimal/src/currencies.ts index e91ae8bf18d..d9d229f9b39 100644 --- a/packages/fixed-decimal/src/currencies.ts +++ b/packages/fixed-decimal/src/currencies.ts @@ -74,3 +74,16 @@ export const USDC = createTokenConstructor(6, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + +/** + * A `bigint` representing an amount of BONK tokens, which have + * 5 decimal places, as a count of the smallest possible denomination of BONK. + */ +export type BonkWei = Brand +/** + * Constructs an amount of {@link BonkWei} from a fixed decimal string, + * decimal number, or a bigint in the smallest denomination of BONK. + * + * BONK is used for trading and swapping in the platform. + */ +export const BONK = createTokenConstructor(5) diff --git a/packages/fixed-decimal/src/index.ts b/packages/fixed-decimal/src/index.ts index 963fcff2acc..6b69f439047 100644 --- a/packages/fixed-decimal/src/index.ts +++ b/packages/fixed-decimal/src/index.ts @@ -4,8 +4,10 @@ export { wAUDIO, AUDIO, USDC, + BONK, SolWei, wAudioWei, AudioWei, - UsdcWei + UsdcWei, + BonkWei } from './currencies.js' diff --git a/packages/mobile/src/screens/buy-sell-screen/BuySellFlow.tsx b/packages/mobile/src/screens/buy-sell-screen/BuySellFlow.tsx index cc637ece843..febee650baf 100644 --- a/packages/mobile/src/screens/buy-sell-screen/BuySellFlow.tsx +++ b/packages/mobile/src/screens/buy-sell-screen/BuySellFlow.tsx @@ -9,7 +9,7 @@ import { useBuySellTabs, useBuySellTransactionData, useSwapDisplayData, - SUPPORTED_TOKEN_PAIRS, + createSupportedTokenPairs, useAddCashModal, getSwapTokens } from '@audius/common/store' @@ -18,6 +18,7 @@ import { useFocusEffect } from '@react-navigation/native' import { Button, Flex, Hint, TextLink } from '@audius/harmony-native' import { SegmentedControl } from 'app/components/core' import { useNavigation } from 'app/hooks/useNavigation' +import { env } from 'app/services/env' import { BuyScreen, SellScreen } from './components' @@ -85,20 +86,22 @@ export const BuySellFlow = ({ })) } + const [selectedPairIndex] = useState(0) + const supportedTokenPairs = useMemo(() => createSupportedTokenPairs(env), []) + const selectedPair = supportedTokenPairs[selectedPairIndex] + const { handleShowConfirmation, isContinueButtonLoading } = useBuySellSwap({ transactionData, currentScreen, setCurrentScreen, activeTab, + selectedPair, onClose }) // Track if user has attempted to submit the form const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false) - const [selectedPairIndex] = useState(0) - const selectedPair = SUPPORTED_TOKEN_PAIRS[selectedPairIndex] - const swapTokens = useMemo( () => getSwapTokens(activeTab, selectedPair), [activeTab, selectedPair] diff --git a/packages/mobile/src/screens/buy-sell-screen/ConfirmSwapScreen.tsx b/packages/mobile/src/screens/buy-sell-screen/ConfirmSwapScreen.tsx index e02fb89c142..79fd5bdc7cf 100644 --- a/packages/mobile/src/screens/buy-sell-screen/ConfirmSwapScreen.tsx +++ b/packages/mobile/src/screens/buy-sell-screen/ConfirmSwapScreen.tsx @@ -5,7 +5,7 @@ import { useBuySellAnalytics } from '@audius/common/hooks' import { buySellMessages as baseMessages } from '@audius/common/messages' import type { TokenInfo } from '@audius/common/store' import { - SUPPORTED_TOKEN_PAIRS, + createSupportedTokenPairs, useBuySellScreen, useBuySellSwap, useSwapDisplayData, @@ -28,6 +28,7 @@ import { FixedFooterContent } from 'app/components/core' import { useNavigation } from 'app/hooks/useNavigation' +import { env } from 'app/services/env' import { SwapBalanceSection } from '../../components/buy-sell' @@ -129,6 +130,10 @@ export const ConfirmSwapScreen = ({ route }: ConfirmSwapScreenProps) => { // Determine if this is a buy or sell based on token types const activeTab = payTokenInfo.symbol === 'USDC' ? 'buy' : 'sell' + const [selectedPairIndex] = useState(0) + const supportedTokenPairs = useMemo(() => createSupportedTokenPairs(env), []) + const selectedPair = supportedTokenPairs[selectedPairIndex] + const { handleConfirmSwap, isConfirmButtonLoading, @@ -140,12 +145,10 @@ export const ConfirmSwapScreen = ({ route }: ConfirmSwapScreenProps) => { currentScreen, setCurrentScreen, activeTab, + selectedPair, onClose: () => navigation.goBack() }) - const [selectedPairIndex] = useState(0) - const selectedPair = SUPPORTED_TOKEN_PAIRS[selectedPairIndex] - const swapTokens = useMemo( () => getSwapTokens(activeTab, selectedPair), [activeTab, selectedPair] diff --git a/packages/sdk/src/sdk/config/development.ts b/packages/sdk/src/sdk/config/development.ts index 8043ed24c35..1771812c81b 100644 --- a/packages/sdk/src/sdk/config/development.ts +++ b/packages/sdk/src/sdk/config/development.ts @@ -44,6 +44,7 @@ export const developmentConfig: SdkServicesConfig = { "rpcEndpoint": "http://audius-protocol-solana-test-validator-1", "usdcTokenMint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", "wAudioTokenMint": "37RCjhgV1qGV2Q54EHFScdxZ22ydRMdKMtVgod47fDP3", + "bonkTokenMint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", "rewardManagerLookupTableAddress": "GNHKVSmHvoRBt1JJCxz7RSMfzDQGDGhGEjmhHyxb3K5J" }, "ethereum": { diff --git a/packages/sdk/src/sdk/config/production.ts b/packages/sdk/src/sdk/config/production.ts index 043d4c6fb38..420d9e0eddb 100644 --- a/packages/sdk/src/sdk/config/production.ts +++ b/packages/sdk/src/sdk/config/production.ts @@ -398,6 +398,7 @@ export const productionConfig: SdkServicesConfig = { "rpcEndpoint": "https://audius-fe.rpcpool.com", "usdcTokenMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "wAudioTokenMint": "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM", + "bonkTokenMint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", "rewardManagerLookupTableAddress": "4UQwpGupH66RgQrWRqmPM9Two6VJEE68VZ7GeqZ3mvVv" }, "ethereum": { diff --git a/packages/sdk/src/sdk/config/staging.ts b/packages/sdk/src/sdk/config/staging.ts index 116e55103fb..f0ff99f06e2 100644 --- a/packages/sdk/src/sdk/config/staging.ts +++ b/packages/sdk/src/sdk/config/staging.ts @@ -81,6 +81,7 @@ export const stagingConfig: SdkServicesConfig = { "rpcEndpoint": "https://audius-fe.rpcpool.com", "usdcTokenMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "wAudioTokenMint": "BELGiMZQ34SDE6x2FUaML2UHDAgBLS64xvhXjX5tBBZo", + "bonkTokenMint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", "rewardManagerLookupTableAddress": "ChFCWjeFxM6SRySTfT46zXn2K7m89TJsft4HWzEtkB4J" }, "ethereum": { diff --git a/packages/sdk/src/sdk/config/types.ts b/packages/sdk/src/sdk/config/types.ts index b2998418684..9268f8227f9 100644 --- a/packages/sdk/src/sdk/config/types.ts +++ b/packages/sdk/src/sdk/config/types.ts @@ -27,6 +27,7 @@ export type SdkServicesConfig = { rpcEndpoint: string usdcTokenMint: string wAudioTokenMint: string + bonkTokenMint: string rewardManagerLookupTableAddress: string } ethereum: { diff --git a/packages/sdk/src/sdk/scripts/generateServicesConfig.ts b/packages/sdk/src/sdk/scripts/generateServicesConfig.ts index b26a6b141cf..042e4077ac2 100644 --- a/packages/sdk/src/sdk/scripts/generateServicesConfig.ts +++ b/packages/sdk/src/sdk/scripts/generateServicesConfig.ts @@ -95,6 +95,7 @@ const productionConfig: SdkServicesConfig = { rpcEndpoint: 'https://audius-fe.rpcpool.com', usdcTokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', wAudioTokenMint: '9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM', + bonkTokenMint: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', rewardManagerLookupTableAddress: '4UQwpGupH66RgQrWRqmPM9Two6VJEE68VZ7GeqZ3mvVv' }, @@ -140,6 +141,7 @@ const stagingConfig: SdkServicesConfig = { rpcEndpoint: 'https://audius-fe.rpcpool.com', usdcTokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', wAudioTokenMint: 'BELGiMZQ34SDE6x2FUaML2UHDAgBLS64xvhXjX5tBBZo', + bonkTokenMint: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', rewardManagerLookupTableAddress: 'ChFCWjeFxM6SRySTfT46zXn2K7m89TJsft4HWzEtkB4J' }, @@ -198,6 +200,7 @@ const developmentConfig: SdkServicesConfig = { rpcEndpoint: 'http://audius-protocol-solana-test-validator-1', usdcTokenMint: '26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y', wAudioTokenMint: '37RCjhgV1qGV2Q54EHFScdxZ22ydRMdKMtVgod47fDP3', + bonkTokenMint: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', rewardManagerLookupTableAddress: 'GNHKVSmHvoRBt1JJCxz7RSMfzDQGDGhGEjmhHyxb3K5J' }, diff --git a/packages/sdk/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts b/packages/sdk/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts index 8fe30a61e07..3cb34eeb344 100644 --- a/packages/sdk/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts +++ b/packages/sdk/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts @@ -101,16 +101,15 @@ export class ClaimableTokensClient { this.client = configWithDefaults.solanaClient this.programId = configWithDefaults.programId this.mints = configWithDefaults.mints - this.authorities = { - wAUDIO: ClaimableTokensProgram.deriveAuthority({ - programId: configWithDefaults.programId, - mint: configWithDefaults.mints.wAUDIO - }), - USDC: ClaimableTokensProgram.deriveAuthority({ - programId: configWithDefaults.programId, - mint: configWithDefaults.mints.USDC - }) - } + this.authorities = Object.fromEntries( + Object.entries(configWithDefaults.mints).map(([token, mint]) => [ + token, + ClaimableTokensProgram.deriveAuthority({ + programId: configWithDefaults.programId, + mint + }) + ]) + ) as Record this.audiusWalletClient = configWithDefaults.audiusWalletClient this.logger = configWithDefaults.logger.createPrefixedLogger( '[claimable-tokens-client]' diff --git a/packages/sdk/src/sdk/services/Solana/programs/ClaimableTokensClient/getDefaultConfig.ts b/packages/sdk/src/sdk/services/Solana/programs/ClaimableTokensClient/getDefaultConfig.ts index 22b993b4159..0187900d1ed 100644 --- a/packages/sdk/src/sdk/services/Solana/programs/ClaimableTokensClient/getDefaultConfig.ts +++ b/packages/sdk/src/sdk/services/Solana/programs/ClaimableTokensClient/getDefaultConfig.ts @@ -11,7 +11,8 @@ export const getDefaultClaimableTokensConfig = ( programId: new PublicKey(config.solana.claimableTokensProgramAddress), mints: { wAUDIO: new PublicKey(config.solana.wAudioTokenMint), - USDC: new PublicKey(config.solana.usdcTokenMint) + USDC: new PublicKey(config.solana.usdcTokenMint), + BONK: new PublicKey(config.solana.bonkTokenMint) }, logger: new Logger() }) diff --git a/packages/sdk/src/sdk/services/Solana/types.ts b/packages/sdk/src/sdk/services/Solana/types.ts index 266de71f212..e13e199a459 100644 --- a/packages/sdk/src/sdk/services/Solana/types.ts +++ b/packages/sdk/src/sdk/services/Solana/types.ts @@ -40,7 +40,7 @@ export const PublicKeySchema = z.union([ }) ]) -export const TokenNameSchema = z.enum(['wAUDIO', 'USDC']) +export const TokenNameSchema = z.enum(['wAUDIO', 'USDC', 'BONK']) export type TokenName = z.input diff --git a/packages/sdk/src/sdk/utils/mintFixedDecimalMap.ts b/packages/sdk/src/sdk/utils/mintFixedDecimalMap.ts index 19537c0c38d..6b88260d89e 100644 --- a/packages/sdk/src/sdk/utils/mintFixedDecimalMap.ts +++ b/packages/sdk/src/sdk/utils/mintFixedDecimalMap.ts @@ -1,6 +1,7 @@ -import { USDC, wAUDIO } from '@audius/fixed-decimal' +import { USDC, wAUDIO, BONK } from '@audius/fixed-decimal' export const mintFixedDecimalMap = { USDC, - wAUDIO + wAUDIO, + BONK } diff --git a/packages/web/src/components/buy-sell-modal/BuySellFlow.tsx b/packages/web/src/components/buy-sell-modal/BuySellFlow.tsx index f6aca512479..5a68969be44 100644 --- a/packages/web/src/components/buy-sell-modal/BuySellFlow.tsx +++ b/packages/web/src/components/buy-sell-modal/BuySellFlow.tsx @@ -10,7 +10,7 @@ import { useSwapDisplayData, BuySellTab, Screen, - getSwapTokens + TokenInfo } from '@audius/common/store' import { Button, Flex, Hint, SegmentedControl, TextLink } from '@audius/harmony' @@ -22,7 +22,7 @@ import { BuyTab } from './BuyTab' import { ConfirmSwapScreen } from './ConfirmSwapScreen' import { SellTab } from './SellTab' import { TransactionSuccessScreen } from './TransactionSuccessScreen' -import { SUPPORTED_TOKEN_PAIRS } from './constants' +import { SUPPORTED_TOKEN_PAIRS, TOKENS } from './constants' const WALLET_GUIDE_URL = 'https://help.audius.co/product/wallet-guide' @@ -76,6 +76,104 @@ export const BuySellFlow = (props: BuySellFlowProps) => { })) } + // Handle token changes + const handleInputTokenChange = (symbol: string) => { + if (activeTab === 'sell') { + // On sell tab, input token change means base token change + setBaseTokenSymbol(symbol) + } else { + // On buy tab, input token change means quote token change + setQuoteTokenSymbol(symbol) + } + // Reset transaction data when tokens change + resetTransactionData() + } + + const handleOutputTokenChange = (symbol: string) => { + if (activeTab === 'buy') { + // On buy tab, output token change means base token change + setBaseTokenSymbol(symbol) + } else { + // On sell tab, output token change means quote token change + setQuoteTokenSymbol(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( + selectedPair.baseToken.symbol // AUDIO by default + ) + const [quoteTokenSymbol, setQuoteTokenSymbol] = useState( + selectedPair.quoteToken.symbol // USDC by default + ) + + // Get all available tokens + const availableTokens = useMemo(() => { + const tokensSet = new Set() + SUPPORTED_TOKEN_PAIRS.forEach((pair) => { + tokensSet.add(pair.baseToken.symbol) + tokensSet.add(pair.quoteToken.symbol) + }) + return Array.from(tokensSet) + .map((symbol) => Object.values(TOKENS).find((t) => t.symbol === symbol)) + .filter(Boolean) as TokenInfo[] + }, []) + + // Create current token pair based on selected base and quote tokens + const currentTokenPair = useMemo(() => { + const baseTokenInfo = availableTokens.find( + (t) => t.symbol === baseTokenSymbol + ) + const quoteTokenInfo = availableTokens.find( + (t) => t.symbol === quoteTokenSymbol + ) + + if (!baseTokenInfo || !quoteTokenInfo) { + return selectedPair + } + + // Find existing pair that matches our tokens + const pair = SUPPORTED_TOKEN_PAIRS.find( + (p) => + p.baseToken.symbol === baseTokenSymbol && + p.quoteToken.symbol === quoteTokenSymbol + ) + + if (pair) { + return pair + } + + // Create a dynamic pair if no exact match found + return { + baseToken: baseTokenInfo, + quoteToken: quoteTokenInfo, + exchangeRate: null + } + }, [baseTokenSymbol, quoteTokenSymbol, availableTokens, selectedPair]) + + const swapTokens = useMemo( + () => ({ + inputToken: activeTab === 'buy' ? quoteTokenSymbol : baseTokenSymbol, + outputToken: activeTab === 'buy' ? baseTokenSymbol : quoteTokenSymbol, + inputTokenInfo: + activeTab === 'buy' + ? currentTokenPair.quoteToken + : currentTokenPair.baseToken, + outputTokenInfo: + activeTab === 'buy' + ? currentTokenPair.baseToken + : currentTokenPair.quoteToken + }), + [activeTab, baseTokenSymbol, quoteTokenSymbol, currentTokenPair] + ) + const { handleShowConfirmation, handleConfirmSwap, @@ -89,20 +187,10 @@ export const BuySellFlow = (props: BuySellFlowProps) => { currentScreen, setCurrentScreen, activeTab, + selectedPair: currentTokenPair, onClose }) - // Track if user has attempted to submit the form - const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false) - - const [selectedPairIndex] = useState(0) - const selectedPair = SUPPORTED_TOKEN_PAIRS[selectedPairIndex] - - const swapTokens = useMemo( - () => getSwapTokens(activeTab, selectedPair), - [activeTab, selectedPair] - ) - const currentExchangeRate = useMemo( () => transactionData?.exchangeRate ?? undefined, [transactionData?.exchangeRate] @@ -160,7 +248,7 @@ export const BuySellFlow = (props: BuySellFlowProps) => { transactionData, swapResult, activeTab, - selectedPair + selectedPair: currentTokenPair }) // Track swap success when success screen is shown @@ -258,21 +346,37 @@ export const BuySellFlow = (props: BuySellFlowProps) => { {activeTab === 'buy' ? ( t.symbol !== baseTokenSymbol + )} + availableOutputTokens={availableTokens.filter( + (t) => t.symbol !== quoteTokenSymbol + )} + onInputTokenChange={handleInputTokenChange} + onOutputTokenChange={handleOutputTokenChange} /> ) : ( t.symbol !== quoteTokenSymbol + )} + availableOutputTokens={availableTokens.filter( + (t) => t.symbol !== baseTokenSymbol + )} + onInputTokenChange={handleInputTokenChange} + onOutputTokenChange={handleOutputTokenChange} /> )} @@ -325,7 +429,7 @@ export const BuySellFlow = (props: BuySellFlowProps) => { onConfirm={handleConfirmSwap} isConfirming={isConfirmButtonLoading} activeTab={activeTab} - selectedPair={selectedPair} + selectedPair={currentTokenPair} /> ) : null} diff --git a/packages/web/src/components/buy-sell-modal/BuyTab.tsx b/packages/web/src/components/buy-sell-modal/BuyTab.tsx index 4adfdb0694e..e38342125da 100644 --- a/packages/web/src/components/buy-sell-modal/BuyTab.tsx +++ b/packages/web/src/components/buy-sell-modal/BuyTab.tsx @@ -1,8 +1,9 @@ import { useMemo } from 'react' -import { useTokenPrice, useUSDCBalance } from '@audius/common/api' +import { useTokenPrice, useTokenBalance } from '@audius/common/api' import { Status } from '@audius/common/models' -import { TokenPair } from '@audius/common/store' +import { MintName } from '@audius/common/services' +import { TokenPair, TokenInfo } from '@audius/common/store' import { getCurrencyDecimalPlaces } from '@audius/common/utils' import { SwapTab } from './SwapTab' @@ -20,6 +21,10 @@ type BuyTabProps = { errorMessage?: string initialInputValue?: string onInputValueChange?: (value: string) => void + availableInputTokens?: TokenInfo[] + availableOutputTokens?: TokenInfo[] + onInputTokenChange?: (symbol: string) => void + onOutputTokenChange?: (symbol: string) => void } export const BuyTab = ({ @@ -28,10 +33,31 @@ export const BuyTab = ({ error, errorMessage, initialInputValue, - onInputValueChange + onInputValueChange, + availableInputTokens, + availableOutputTokens, + onInputTokenChange, + onOutputTokenChange }: BuyTabProps) => { const { baseToken, quoteToken } = tokenPair - const { status: balanceStatus, data: usdcBalance } = useUSDCBalance() + + // Determine which balance hook to use based on input token + const mintName = useMemo(() => { + switch (quoteToken.symbol) { + case 'USDC': + return 'USDC' as MintName + case 'AUDIO': + return 'wAUDIO' as MintName + case 'BONK': + return 'BONK' as MintName + default: + return 'USDC' as MintName + } + }, [quoteToken.symbol]) + + const { status: balanceStatus, data: tokenBalanceData } = useTokenBalance({ + token: mintName + }) const { data: tokenPriceData, isPending: isTokenPriceLoading } = useTokenPrice(baseToken.address) @@ -43,21 +69,21 @@ export const BuyTab = ({ return getCurrencyDecimalPlaces(parseFloat(tokenPrice)) }, [tokenPrice]) - const getUsdcBalance = useMemo(() => { + const getBalance = useMemo(() => { return () => { - if (balanceStatus === Status.SUCCESS && usdcBalance) { - return parseFloat(usdcBalance.toString()) / 10 ** quoteToken.decimals + if (balanceStatus === Status.SUCCESS && tokenBalanceData) { + return parseFloat(tokenBalanceData.toString()) } return undefined } - }, [balanceStatus, usdcBalance, quoteToken.decimals]) + }, [balanceStatus, tokenBalanceData]) return ( 'Insufficient balance' }} @@ -69,6 +95,8 @@ export const BuyTab = ({ tokenPriceDecimalPlaces={decimalPlaces} initialInputValue={initialInputValue} onInputValueChange={onInputValueChange} + availableOutputTokens={availableOutputTokens} + onOutputTokenChange={onOutputTokenChange} /> ) } diff --git a/packages/web/src/components/buy-sell-modal/SellTab.tsx b/packages/web/src/components/buy-sell-modal/SellTab.tsx index e74c59453e1..cd10fa42446 100644 --- a/packages/web/src/components/buy-sell-modal/SellTab.tsx +++ b/packages/web/src/components/buy-sell-modal/SellTab.tsx @@ -1,7 +1,9 @@ import { useMemo } from 'react' -import { useAudioBalance } from '@audius/common/api' -import { TokenPair } from '@audius/common/store' +import { useAudioBalance, useTokenBalance } from '@audius/common/api' +import { Status } from '@audius/common/models' +import { MintName } from '@audius/common/services' +import { TokenPair, TokenInfo } from '@audius/common/store' import { isNullOrUndefined } from '@audius/common/utils' import { AUDIO } from '@audius/fixed-decimal' @@ -20,6 +22,10 @@ type SellTabProps = { errorMessage?: string initialInputValue?: string onInputValueChange?: (value: string) => void + availableInputTokens?: TokenInfo[] + availableOutputTokens?: TokenInfo[] + onInputTokenChange?: (symbol: string) => void + onOutputTokenChange?: (symbol: string) => void } export const SellTab = ({ @@ -28,29 +34,69 @@ export const SellTab = ({ error, errorMessage, initialInputValue, - onInputValueChange + onInputValueChange, + availableInputTokens, + availableOutputTokens, + onInputTokenChange, + onOutputTokenChange }: SellTabProps) => { // Extract the tokens from the pair const { baseToken, quoteToken } = tokenPair + + // Determine which balance hook to use based on input token + const mintName = useMemo(() => { + switch (baseToken.symbol) { + case 'USDC': + return 'USDC' as MintName + case 'AUDIO': + return 'wAUDIO' as MintName + case 'BONK': + return 'BONK' as MintName + default: + return 'wAUDIO' as MintName + } + }, [baseToken.symbol]) + + // For AUDIO, use the specialized hook for compatibility const { accountBalance } = useAudioBalance({ includeConnectedWallets: false }) - const isBalanceLoading = isNullOrUndefined(accountBalance) + const { data: tokenBalanceData, status: tokenBalanceStatus } = + useTokenBalance({ + token: mintName + }) + + const isBalanceLoading = + baseToken.symbol === 'AUDIO' + ? isNullOrUndefined(accountBalance) + : tokenBalanceStatus === Status.LOADING - // Get AUDIO balance in UI format - const getAudioBalance = useMemo(() => { + // Get balance in UI format + const getBalance = useMemo(() => { return () => { - if (!isBalanceLoading && accountBalance) { - return parseFloat(AUDIO(accountBalance).toString()) + if (baseToken.symbol === 'AUDIO') { + if (!isBalanceLoading && accountBalance) { + return parseFloat(AUDIO(accountBalance).toString()) + } + } else { + if (tokenBalanceStatus === Status.SUCCESS && tokenBalanceData) { + return parseFloat(tokenBalanceData.toString()) + } } return undefined } - }, [accountBalance, isBalanceLoading]) + }, [ + accountBalance, + isBalanceLoading, + baseToken.symbol, + tokenBalanceData, + tokenBalanceStatus + ]) return ( 'Insufficient balance' }} @@ -61,6 +107,10 @@ export const SellTab = ({ tooltipPlacement='right' initialInputValue={initialInputValue} onInputValueChange={onInputValueChange} + availableInputTokens={availableInputTokens} + availableOutputTokens={availableOutputTokens} + onInputTokenChange={onInputTokenChange} + onOutputTokenChange={onOutputTokenChange} /> ) } diff --git a/packages/web/src/components/buy-sell-modal/SwapTab.tsx b/packages/web/src/components/buy-sell-modal/SwapTab.tsx index e3da416f2e9..e1236c1346a 100644 --- a/packages/web/src/components/buy-sell-modal/SwapTab.tsx +++ b/packages/web/src/components/buy-sell-modal/SwapTab.tsx @@ -54,6 +54,10 @@ export type SwapTabProps = { tooltipPlacement?: TooltipPlacement initialInputValue?: string onInputValueChange?: (value: string) => void + availableInputTokens?: TokenInfo[] + availableOutputTokens?: TokenInfo[] + onInputTokenChange?: (symbol: string) => void + onOutputTokenChange?: (symbol: string) => void } export const SwapTab = ({ @@ -71,7 +75,11 @@ export const SwapTab = ({ tokenPriceDecimalPlaces = 2, tooltipPlacement, initialInputValue, - onInputValueChange + onInputValueChange, + availableInputTokens, + availableOutputTokens, + onInputTokenChange, + onOutputTokenChange }: SwapTabProps) => { const { formik, @@ -127,6 +135,8 @@ export const SwapTab = ({ error={error} errorMessage={errorMessage} tooltipPlacement={tooltipPlacement} + availableTokens={availableInputTokens} + onTokenChange={onInputTokenChange} /> )} diff --git a/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx b/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx index 45fecedb74f..e13882bb5f7 100644 --- a/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx +++ b/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { buySellMessages as messages } from '@audius/common/messages' import { TokenAmountSectionProps, TokenInfo } from '@audius/common/store' -import { Button, Divider, Flex, Text, TextInput } from '@audius/harmony' +import { Button, Divider, Flex, Select, Text, TextInput } from '@audius/harmony' import { useTheme } from '@emotion/react' import { TooltipPlacement } from 'antd/lib/tooltip' @@ -156,8 +156,13 @@ export const TokenAmountSection = ({ tokenPrice, isTokenPriceLoading, tokenPriceDecimalPlaces = 2, - tooltipPlacement -}: TokenAmountSectionProps) => { + tooltipPlacement, + availableTokens, + onTokenChange +}: TokenAmountSectionProps & { + availableTokens?: TokenInfo[] + onTokenChange?: (symbol: string) => void +}) => { const { spacing } = useTheme() const { icon: TokenIcon, symbol, isStablecoin } = tokenInfo @@ -174,20 +179,49 @@ export const TokenAmountSection = ({ const priceDisplay = tokenPrice && !isTokenPriceLoading ? messages.tokenPrice(tokenPrice, tokenPriceDecimalPlaces) - : undefined + : null const youPaySection = useMemo(() => { + const showTokenSelector = + availableTokens && availableTokens.length > 1 && onTokenChange + return ( - - - onAmountChange?.(e.target.value)} - error={error} - /> + + + {showTokenSelector ? ( + + + onAmountChange?.(e.target.value)} + error={error} + css={{ flex: 1 }} + /> + ({ + value: token.symbol, + label: token.symbol, + icon: token.icon + }))} + css={{ minWidth: spacing.unit20 }} + /> + + {formattedAmount && ( + + )} + + ) + } + if (isStablecoin) { return ( - {messages.tokenPrice(formattedAmount, 2)} + {messages.tokenPrice(formattedAmount || '0', 2)} ) } return ( ) - }, [formattedAmount, isStablecoin, priceDisplay, tokenInfo]) + }, [ + formattedAmount, + isStablecoin, + priceDisplay, + tokenInfo, + availableTokens, + onTokenChange, + symbol, + spacing.unit20 + ]) const titleText = useMemo(() => { if (isStablecoin && !isInput && TokenIcon) { diff --git a/packages/web/src/components/buy-sell-modal/constants.ts b/packages/web/src/components/buy-sell-modal/constants.ts index 2ea6fef339a..dcdc35cc6d8 100644 --- a/packages/web/src/components/buy-sell-modal/constants.ts +++ b/packages/web/src/components/buy-sell-modal/constants.ts @@ -1,27 +1,52 @@ -import { TOKENS as BASE_TOKENS } from '@audius/common/src/store/ui/buy-sell' +import { + createTokens, + createSupportedTokenPairs +} from '@audius/common/src/store/ui/buy-sell' import { TokenInfo, TokenPair } from '@audius/common/src/store/ui/buy-sell/types' -import { IconLogoCircleUSDC, IconTokenAUDIO } from '@audius/harmony' - -// Token metadata with icons for web -export const TOKENS: Record = { - AUDIO: { - ...BASE_TOKENS.AUDIO, - icon: IconTokenAUDIO - }, - USDC: { - ...BASE_TOKENS.USDC, - icon: IconLogoCircleUSDC +import { + IconLogoCircleUSDC, + IconTokenAUDIO, + IconLogoCircleSOL +} from '@audius/harmony' + +import { env } from 'services/env' + +// Create tokens from centralized configuration with icons for web +const createTokensWithIcons = (): Record => { + const baseTokens = createTokens(env) + const iconMap = { + AUDIO: IconTokenAUDIO, + USDC: IconLogoCircleUSDC, + BONK: IconLogoCircleSOL // Using SOL icon as placeholder for BONK } + + const tokensWithIcons: Record = {} + Object.entries(baseTokens).forEach(([symbol, token]) => { + tokensWithIcons[symbol] = { + ...token, + icon: iconMap[symbol as keyof typeof iconMap] + } + }) + + return tokensWithIcons } -// Define supported token pairs with icons for web -export const SUPPORTED_TOKEN_PAIRS: TokenPair[] = [ - { - baseToken: TOKENS.AUDIO, - quoteToken: TOKENS.USDC, - exchangeRate: null - } -] +export const TOKENS: Record = createTokensWithIcons() + +// Create supported token pairs dynamically with icons for web +const createSupportedTokenPairsWithIcons = (): TokenPair[] => { + const basePairs = createSupportedTokenPairs(env) + const tokensWithIcons = TOKENS + + return basePairs.map((pair) => ({ + ...pair, + baseToken: tokensWithIcons[pair.baseToken.symbol] || pair.baseToken, + quoteToken: tokensWithIcons[pair.quoteToken.symbol] || pair.quoteToken + })) +} + +export const SUPPORTED_TOKEN_PAIRS: TokenPair[] = + createSupportedTokenPairsWithIcons() From 8733dac5d8a37db317259641d9e796a1cc69e84c Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 08:59:30 -0500 Subject: [PATCH 02/10] Remove swap stuff for now --- .../common/src/store/ui/buy-sell/constants.ts | 25 +++- .../src/components/buy-sell-modal/BuyTab.tsx | 24 +--- .../src/components/buy-sell-modal/SellTab.tsx | 25 +--- .../buy-sell-modal/TokenAmountSection.tsx | 114 +++--------------- 4 files changed, 52 insertions(+), 136 deletions(-) diff --git a/packages/common/src/store/ui/buy-sell/constants.ts b/packages/common/src/store/ui/buy-sell/constants.ts index aadc3d74324..8e45e7ae363 100644 --- a/packages/common/src/store/ui/buy-sell/constants.ts +++ b/packages/common/src/store/ui/buy-sell/constants.ts @@ -73,8 +73,29 @@ export const createSupportedTokenPairs = (env: Env): TokenPair[] => { (token) => token.purchasable || token.sellable ).map(tokenConfigToTokenInfo) - // Generate all possible pairs efficiently - const pairs = generateTokenPairs(tradeableTokens) as TokenPair[] + // Find specific tokens for explicit ordering + const audioToken = tradeableTokens.find((token) => token.symbol === 'AUDIO') + const usdcToken = tradeableTokens.find((token) => token.symbol === 'USDC') + + // Create pairs with explicit ordering to ensure USDC -> AUDIO is first + const pairs: TokenPair[] = [] + + // First pair: AUDIO/USDC (for Buy: USDC -> AUDIO, Sell: AUDIO -> USDC) + if (audioToken && usdcToken) { + pairs.push({ + baseToken: audioToken, + quoteToken: usdcToken, + exchangeRate: null + }) + } + + // Generate remaining pairs, excluding the AUDIO/USDC pair we already added + const remainingPairs = generateTokenPairs(tradeableTokens).filter( + (pair) => + !(pair.baseToken.symbol === 'AUDIO' && pair.quoteToken.symbol === 'USDC') + ) as TokenPair[] + + pairs.push(...remainingPairs) // Cache the result tokenPairsCache.set(cacheKey, pairs) diff --git a/packages/web/src/components/buy-sell-modal/BuyTab.tsx b/packages/web/src/components/buy-sell-modal/BuyTab.tsx index e38342125da..0f038a97bb5 100644 --- a/packages/web/src/components/buy-sell-modal/BuyTab.tsx +++ b/packages/web/src/components/buy-sell-modal/BuyTab.tsx @@ -1,10 +1,10 @@ import { useMemo } from 'react' -import { useTokenPrice, useTokenBalance } from '@audius/common/api' +import { useTokenBalance, useTokenPrice } from '@audius/common/api' import { Status } from '@audius/common/models' -import { MintName } from '@audius/common/services' -import { TokenPair, TokenInfo } from '@audius/common/store' +import { TokenInfo, TokenPair } from '@audius/common/store' import { getCurrencyDecimalPlaces } from '@audius/common/utils' +import { FixedDecimal } from '@audius/fixed-decimal' import { SwapTab } from './SwapTab' @@ -41,22 +41,8 @@ export const BuyTab = ({ }: BuyTabProps) => { const { baseToken, quoteToken } = tokenPair - // Determine which balance hook to use based on input token - const mintName = useMemo(() => { - switch (quoteToken.symbol) { - case 'USDC': - return 'USDC' as MintName - case 'AUDIO': - return 'wAUDIO' as MintName - case 'BONK': - return 'BONK' as MintName - default: - return 'USDC' as MintName - } - }, [quoteToken.symbol]) - const { status: balanceStatus, data: tokenBalanceData } = useTokenBalance({ - token: mintName + token: 'USDC' }) const { data: tokenPriceData, isPending: isTokenPriceLoading } = @@ -72,7 +58,7 @@ export const BuyTab = ({ const getBalance = useMemo(() => { return () => { if (balanceStatus === Status.SUCCESS && tokenBalanceData) { - return parseFloat(tokenBalanceData.toString()) + return Number(new FixedDecimal(tokenBalanceData.toString())) } return undefined } diff --git a/packages/web/src/components/buy-sell-modal/SellTab.tsx b/packages/web/src/components/buy-sell-modal/SellTab.tsx index cd10fa42446..025ae66eb97 100644 --- a/packages/web/src/components/buy-sell-modal/SellTab.tsx +++ b/packages/web/src/components/buy-sell-modal/SellTab.tsx @@ -2,10 +2,9 @@ import { useMemo } from 'react' import { useAudioBalance, useTokenBalance } from '@audius/common/api' import { Status } from '@audius/common/models' -import { MintName } from '@audius/common/services' -import { TokenPair, TokenInfo } from '@audius/common/store' +import { TokenInfo, TokenPair } from '@audius/common/store' import { isNullOrUndefined } from '@audius/common/utils' -import { AUDIO } from '@audius/fixed-decimal' +import { AUDIO, FixedDecimal } from '@audius/fixed-decimal' import { SwapTab } from './SwapTab' @@ -43,25 +42,11 @@ export const SellTab = ({ // Extract the tokens from the pair const { baseToken, quoteToken } = tokenPair - // Determine which balance hook to use based on input token - const mintName = useMemo(() => { - switch (baseToken.symbol) { - case 'USDC': - return 'USDC' as MintName - case 'AUDIO': - return 'wAUDIO' as MintName - case 'BONK': - return 'BONK' as MintName - default: - return 'wAUDIO' as MintName - } - }, [baseToken.symbol]) - // For AUDIO, use the specialized hook for compatibility const { accountBalance } = useAudioBalance({ includeConnectedWallets: false }) const { data: tokenBalanceData, status: tokenBalanceStatus } = useTokenBalance({ - token: mintName + token: 'wAUDIO' }) const isBalanceLoading = @@ -74,11 +59,11 @@ export const SellTab = ({ return () => { if (baseToken.symbol === 'AUDIO') { if (!isBalanceLoading && accountBalance) { - return parseFloat(AUDIO(accountBalance).toString()) + return Number(AUDIO(accountBalance).toString()) } } else { if (tokenBalanceStatus === Status.SUCCESS && tokenBalanceData) { - return parseFloat(tokenBalanceData.toString()) + return Number(new FixedDecimal(tokenBalanceData.toString())) } } return undefined diff --git a/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx b/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx index e13882bb5f7..45fecedb74f 100644 --- a/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx +++ b/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { buySellMessages as messages } from '@audius/common/messages' import { TokenAmountSectionProps, TokenInfo } from '@audius/common/store' -import { Button, Divider, Flex, Select, Text, TextInput } from '@audius/harmony' +import { Button, Divider, Flex, Text, TextInput } from '@audius/harmony' import { useTheme } from '@emotion/react' import { TooltipPlacement } from 'antd/lib/tooltip' @@ -156,13 +156,8 @@ export const TokenAmountSection = ({ tokenPrice, isTokenPriceLoading, tokenPriceDecimalPlaces = 2, - tooltipPlacement, - availableTokens, - onTokenChange -}: TokenAmountSectionProps & { - availableTokens?: TokenInfo[] - onTokenChange?: (symbol: string) => void -}) => { + tooltipPlacement +}: TokenAmountSectionProps) => { const { spacing } = useTheme() const { icon: TokenIcon, symbol, isStablecoin } = tokenInfo @@ -179,49 +174,20 @@ export const TokenAmountSection = ({ const priceDisplay = tokenPrice && !isTokenPriceLoading ? messages.tokenPrice(tokenPrice, tokenPriceDecimalPlaces) - : null + : undefined const youPaySection = useMemo(() => { - const showTokenSelector = - availableTokens && availableTokens.length > 1 && onTokenChange - return ( - - - {showTokenSelector ? ( - - - onAmountChange?.(e.target.value)} - error={error} - css={{ flex: 1 }} - /> - ({ - value: token.symbol, - label: token.symbol, - icon: token.icon - }))} - css={{ minWidth: spacing.unit20 }} - /> - - {formattedAmount && ( - - )} - - ) - } - if (isStablecoin) { return ( - {messages.tokenPrice(formattedAmount || '0', 2)} + {messages.tokenPrice(formattedAmount, 2)} ) } return ( ) - }, [ - formattedAmount, - isStablecoin, - priceDisplay, - tokenInfo, - availableTokens, - onTokenChange, - symbol, - spacing.unit20 - ]) + }, [formattedAmount, isStablecoin, priceDisplay, tokenInfo]) const titleText = useMemo(() => { if (isStablecoin && !isInput && TokenIcon) { From f9fa14fb316b12debff93d5b708f80e8fcb8a53a Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 09:01:29 -0500 Subject: [PATCH 03/10] Update props --- packages/web/src/components/buy-sell-modal/SwapTab.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/web/src/components/buy-sell-modal/SwapTab.tsx b/packages/web/src/components/buy-sell-modal/SwapTab.tsx index e1236c1346a..de16a2b6ca0 100644 --- a/packages/web/src/components/buy-sell-modal/SwapTab.tsx +++ b/packages/web/src/components/buy-sell-modal/SwapTab.tsx @@ -135,8 +135,6 @@ export const SwapTab = ({ error={error} errorMessage={errorMessage} tooltipPlacement={tooltipPlacement} - availableTokens={availableInputTokens} - onTokenChange={onInputTokenChange} /> )} From 7127726d5f16fe663edf9d5a72372024114cb7f8 Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 09:08:46 -0500 Subject: [PATCH 04/10] Make more generic --- .../services/audius-backend/AudiusBackend.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/common/src/services/audius-backend/AudiusBackend.ts b/packages/common/src/services/audius-backend/AudiusBackend.ts index 42f05370502..3c2a636e500 100644 --- a/packages/common/src/services/audius-backend/AudiusBackend.ts +++ b/packages/common/src/services/audius-backend/AudiusBackend.ts @@ -43,6 +43,7 @@ import { PushNotifications } from '../../store' import { getErrorMessage, uuid, Maybe, Nullable } from '../../utils' +import { getTokenBySymbol } from '../tokens' import { MintName } from './solana' import { MonitoringCallbacks } from './types' @@ -164,6 +165,14 @@ export const audiusBackend = ({ } } + function getMintAddress(mint: MintName): PublicKey { + const token = getTokenBySymbol(env, mint) + if (!token) { + throw new Error(`Token not found: ${mint}`) + } + return new PublicKey(token.address) + } + async function recordTrackListen({ userId, trackId, @@ -998,12 +1007,7 @@ export const audiusBackend = ({ mint: MintName }) { const solanaTokenProgramKey = new PublicKey(TOKEN_PROGRAM_ID) - const mintKey = - mint === 'wAUDIO' - ? new PublicKey(env.WAUDIO_MINT_ADDRESS) - : mint === 'USDC' - ? new PublicKey(env.USDC_MINT_ADDRESS) - : new PublicKey('DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263') // BONK mint + const mintKey = getMintAddress(mint) const addresses = PublicKey.findProgramAddressSync( [ solanaWalletKey.toBuffer(), @@ -1074,12 +1078,7 @@ export const audiusBackend = ({ solanaWalletKey, mint }) - const mintKey = - mint === 'wAUDIO' - ? new PublicKey(env.WAUDIO_MINT_ADDRESS) - : mint === 'USDC' - ? new PublicKey(env.USDC_MINT_ADDRESS) - : new PublicKey('DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263') // BONK mint + const mintKey = getMintAddress(mint) const accounts = [ // 0. `[sw]` Funding account (must be a system account) { From 0f5fbd7b8112bb858eae7edaf989280a2485f4af Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 09:16:34 -0500 Subject: [PATCH 05/10] Update tanstack --- .../common/src/api/tan-query/queryKeys.ts | 1 + .../api/tan-query/wallets/useTokenBalance.ts | 19 ++++++++++++++++--- .../src/store/ui/buy-sell/useBuySellSwap.ts | 10 +++++----- .../ui/buy-sell/useTokenAmountFormatting.ts | 4 ++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/common/src/api/tan-query/queryKeys.ts b/packages/common/src/api/tan-query/queryKeys.ts index 2bad526be13..f2cc1a1ae50 100644 --- a/packages/common/src/api/tan-query/queryKeys.ts +++ b/packages/common/src/api/tan-query/queryKeys.ts @@ -95,6 +95,7 @@ export const QUERY_KEYS = { eventsByEntityId: 'eventsByEntityId', walletOwner: 'walletOwner', tokenPrice: 'tokenPrice', + tokenBalance: 'tokenBalance', usdcBalance: 'usdcBalance', fileSizes: 'fileSizes', managedAccounts: 'managedAccounts', diff --git a/packages/common/src/api/tan-query/wallets/useTokenBalance.ts b/packages/common/src/api/tan-query/wallets/useTokenBalance.ts index 250a50c2258..8e0b836ee91 100644 --- a/packages/common/src/api/tan-query/wallets/useTokenBalance.ts +++ b/packages/common/src/api/tan-query/wallets/useTokenBalance.ts @@ -11,7 +11,8 @@ import { Status } from '~/models/Status' import { MintName } from '~/services/audius-backend/solana' import { getUserbankAccountInfo } from '~/services/index' -import { QueryOptions } from '../types' +import { QUERY_KEYS } from '../queryKeys' +import { QueryOptions, type QueryKey } from '../types' // Map token symbols to their Fixed Decimal constructors const TOKEN_CONSTRUCTORS = { @@ -22,6 +23,18 @@ const TOKEN_CONSTRUCTORS = { type TokenSymbol = keyof typeof TOKEN_CONSTRUCTORS +export const getTokenBalanceQueryKey = ( + ethAddress: string | null, + token: MintName, + commitment: Commitment +) => + [ + QUERY_KEYS.tokenBalance, + ethAddress, + token, + commitment + ] as unknown as QueryKey + /** * Hook to get the balance for any supported token for the current user. * Uses TanStack Query for data fetching and caching. @@ -48,7 +61,7 @@ export const useTokenBalance = ({ const queryClient = useQueryClient() const result = useQuery({ - queryKey: ['tokenBalance', ethAddress, token, commitment], + queryKey: getTokenBalanceQueryKey(ethAddress, token, commitment), queryFn: async () => { const sdk = await audiusSdk() if (!ethAddress || !token) { @@ -117,7 +130,7 @@ export const useTokenBalance = ({ // This effectively stops the current polling cycle const cancelPolling = useCallback(() => { queryClient.cancelQueries({ - queryKey: ['tokenBalance', ethAddress, token, commitment] + queryKey: getTokenBalanceQueryKey(ethAddress, token, commitment) }) }, [queryClient, ethAddress, token, commitment]) diff --git a/packages/common/src/store/ui/buy-sell/useBuySellSwap.ts b/packages/common/src/store/ui/buy-sell/useBuySellSwap.ts index 86297c4fa4f..ed5026ae0bd 100644 --- a/packages/common/src/store/ui/buy-sell/useBuySellSwap.ts +++ b/packages/common/src/store/ui/buy-sell/useBuySellSwap.ts @@ -60,12 +60,12 @@ export const useBuySellSwap = (props: UseBuySellSwapProps) => { if (activeTab === 'buy') { // Buy: pay with quote token, receive base token - inputMintAddress = selectedPair.quoteToken.address! - outputMintAddress = selectedPair.baseToken.address! + inputMintAddress = selectedPair.quoteToken.address ?? '' + outputMintAddress = selectedPair.baseToken.address ?? '' } else { // Sell: pay with base token, receive quote token - inputMintAddress = selectedPair.baseToken.address! - outputMintAddress = selectedPair.quoteToken.address! + inputMintAddress = selectedPair.baseToken.address ?? '' + outputMintAddress = selectedPair.quoteToken.address ?? '' } swapTokens({ @@ -83,7 +83,7 @@ export const useBuySellSwap = (props: UseBuySellSwapProps) => { queryKey: [QUERY_KEYS.usdcBalance, user.wallet] }) queryClient.invalidateQueries({ - queryKey: ['tokenBalance', user.wallet] + queryKey: [QUERY_KEYS.tokenBalance, user.wallet] }) } if (user?.spl_wallet) { diff --git a/packages/common/src/store/ui/buy-sell/useTokenAmountFormatting.ts b/packages/common/src/store/ui/buy-sell/useTokenAmountFormatting.ts index ba93a90e655..b09044b91b0 100644 --- a/packages/common/src/store/ui/buy-sell/useTokenAmountFormatting.ts +++ b/packages/common/src/store/ui/buy-sell/useTokenAmountFormatting.ts @@ -2,8 +2,8 @@ import { useCallback, useMemo } from 'react' import { AUDIO } from '@audius/fixed-decimal' -import { formatUSDCValue } from '../../../api' -import { getTokenDecimalPlaces, getCurrencyDecimalPlaces } from '../../../utils' +import { formatUSDCValue } from '~/api' +import { getTokenDecimalPlaces, getCurrencyDecimalPlaces } from '~/utils' export type UseTokenAmountFormattingProps = { amount?: string | number From 9086f5afbf5a5946d432fb3a44cf5fb498159768 Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 09:21:11 -0500 Subject: [PATCH 06/10] Remove deprecated --- .../common/src/store/ui/buy-sell/constants.ts | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/packages/common/src/store/ui/buy-sell/constants.ts b/packages/common/src/store/ui/buy-sell/constants.ts index 8e45e7ae363..41a991ecb47 100644 --- a/packages/common/src/store/ui/buy-sell/constants.ts +++ b/packages/common/src/store/ui/buy-sell/constants.ts @@ -6,8 +6,6 @@ import { tokenConfigToTokenInfo } from '~/services/tokens' -import { TOKEN_LISTING_MAP } from '../shared/tokenConstants' - import { TokenInfo, TokenPair } from './types' // USD-based limits that apply to all currencies @@ -19,35 +17,6 @@ export const createTokens = (env: Env): Record => { return createTokenInfoObjects(env) } -// Legacy token metadata without icons (prefer createTokens) -// @deprecated Use createTokens(env) instead for environment-specific tokens -export const TOKENS: Record = { - AUDIO: { - symbol: 'AUDIO', - name: 'Audius', - decimals: TOKEN_LISTING_MAP.AUDIO.decimals, - balance: null, - isStablecoin: false, - address: TOKEN_LISTING_MAP.AUDIO.address - }, - USDC: { - symbol: 'USDC', - name: 'USD Coin', - decimals: TOKEN_LISTING_MAP.USDC.decimals, - balance: null, - isStablecoin: true, - address: TOKEN_LISTING_MAP.USDC.address - }, - BONK: { - symbol: 'BONK', - name: 'Bonk', - decimals: TOKEN_LISTING_MAP.BONK.decimals, - balance: null, - isStablecoin: false, - address: TOKEN_LISTING_MAP.BONK.address - } -} - // Cache for token pairs to avoid repeated computation const tokenPairsCache = new Map() @@ -101,23 +70,3 @@ export const createSupportedTokenPairs = (env: Env): TokenPair[] => { tokenPairsCache.set(cacheKey, pairs) return pairs } - -// Legacy hardcoded supported token pairs (prefer createSupportedTokenPairs) -// @deprecated Use createSupportedTokenPairs(env) instead for dynamic token pairs -export const SUPPORTED_TOKEN_PAIRS: TokenPair[] = [ - { - baseToken: TOKENS.AUDIO, - quoteToken: TOKENS.USDC, - exchangeRate: null - }, - { - baseToken: TOKENS.AUDIO, - quoteToken: TOKENS.BONK, - exchangeRate: null - }, - { - baseToken: TOKENS.USDC, - quoteToken: TOKENS.BONK, - exchangeRate: null - } -] From fa52e289333bf9719389fe518096850ae7248074 Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 09:23:02 -0500 Subject: [PATCH 07/10] Remove unused --- .../src/api/tan-query/jupiter/constants.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/packages/common/src/api/tan-query/jupiter/constants.ts b/packages/common/src/api/tan-query/jupiter/constants.ts index 10b9d7910ce..420dc8ac19c 100644 --- a/packages/common/src/api/tan-query/jupiter/constants.ts +++ b/packages/common/src/api/tan-query/jupiter/constants.ts @@ -2,7 +2,6 @@ import { PublicKey } from '@solana/web3.js' import { Env } from '~/services/env' import { getOrInitializeRegistry, type TokenConfig } from '~/services/tokens' -import { TOKEN_LISTING_MAP } from '~/store/ui/shared/tokenConstants' import { UserBankManagedTokenInfo } from './types' @@ -39,24 +38,3 @@ export const createUserBankManagedTokens = ( return managedTokens } - -export const USER_BANK_MANAGED_TOKENS: Record< - string, - UserBankManagedTokenInfo -> = { - [TOKEN_LISTING_MAP.AUDIO.address.toUpperCase()]: { - mintAddress: TOKEN_LISTING_MAP.AUDIO.address, - claimableTokenMint: 'wAUDIO', - decimals: TOKEN_LISTING_MAP.AUDIO.decimals - }, - [TOKEN_LISTING_MAP.USDC.address.toUpperCase()]: { - mintAddress: TOKEN_LISTING_MAP.USDC.address, - claimableTokenMint: 'USDC', - decimals: TOKEN_LISTING_MAP.USDC.decimals - }, - [TOKEN_LISTING_MAP.BONK.address.toUpperCase()]: { - mintAddress: TOKEN_LISTING_MAP.BONK.address, - claimableTokenMint: 'BONK', - decimals: TOKEN_LISTING_MAP.BONK.decimals - } -} From 1ea356b1c3beeb7d16e01984585b4bf5a1e53ec0 Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 09:23:52 -0500 Subject: [PATCH 08/10] Use BONK Icon --- packages/web/src/components/buy-sell-modal/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/components/buy-sell-modal/constants.ts b/packages/web/src/components/buy-sell-modal/constants.ts index dcdc35cc6d8..a0b8fea5dbc 100644 --- a/packages/web/src/components/buy-sell-modal/constants.ts +++ b/packages/web/src/components/buy-sell-modal/constants.ts @@ -9,7 +9,7 @@ import { import { IconLogoCircleUSDC, IconTokenAUDIO, - IconLogoCircleSOL + IconTokenBonk } from '@audius/harmony' import { env } from 'services/env' @@ -20,7 +20,7 @@ const createTokensWithIcons = (): Record => { const iconMap = { AUDIO: IconTokenAUDIO, USDC: IconLogoCircleUSDC, - BONK: IconLogoCircleSOL // Using SOL icon as placeholder for BONK + BONK: IconTokenBonk } const tokensWithIcons: Record = {} From 740e969dfbe8d35e497ec7e19fb790a7713745a6 Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 09:31:22 -0500 Subject: [PATCH 09/10] Make hook better --- .../api/tan-query/wallets/useTokenBalance.ts | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/common/src/api/tan-query/wallets/useTokenBalance.ts b/packages/common/src/api/tan-query/wallets/useTokenBalance.ts index 8e0b836ee91..de68442a3f4 100644 --- a/packages/common/src/api/tan-query/wallets/useTokenBalance.ts +++ b/packages/common/src/api/tan-query/wallets/useTokenBalance.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { BONK, USDC, wAUDIO } from '@audius/fixed-decimal' +import { FixedDecimal } from '@audius/fixed-decimal' import { TokenAccountNotFoundError } from '@solana/spl-token' import { Commitment } from '@solana/web3.js' import { useQuery, useQueryClient } from '@tanstack/react-query' @@ -9,19 +9,20 @@ import { useCurrentAccountUser } from '~/api' import { useQueryContext } from '~/api/tan-query/utils' import { Status } from '~/models/Status' import { MintName } from '~/services/audius-backend/solana' -import { getUserbankAccountInfo } from '~/services/index' +import { getUserbankAccountInfo, getTokenBySymbol } from '~/services/index' import { QUERY_KEYS } from '../queryKeys' import { QueryOptions, type QueryKey } from '../types' -// Map token symbols to their Fixed Decimal constructors -const TOKEN_CONSTRUCTORS = { - wAUDIO, - USDC, - BONK -} as const - -type TokenSymbol = keyof typeof TOKEN_CONSTRUCTORS +const createTokenBalance = ( + amount: string | number | bigint | null | undefined, + decimals: number +): FixedDecimal | null => { + if (amount === null || amount === undefined) { + return null + } + return new FixedDecimal(BigInt(amount.toString()), decimals) +} export const getTokenBalanceQueryKey = ( ethAddress: string | null, @@ -33,7 +34,7 @@ export const getTokenBalanceQueryKey = ( ethAddress, token, commitment - ] as unknown as QueryKey + ] as unknown as QueryKey /** * Hook to get the balance for any supported token for the current user. @@ -55,7 +56,7 @@ export const useTokenBalance = ({ pollingInterval?: number commitment?: Commitment } & QueryOptions) => { - const { audiusSdk } = useQueryContext() + const { audiusSdk, env } = useQueryContext() const { data: user } = useCurrentAccountUser() const ethAddress = user?.wallet ?? null const queryClient = useQueryClient() @@ -78,27 +79,27 @@ export const useTokenBalance = ({ commitment ) - const TokenConstructor = TOKEN_CONSTRUCTORS[token as TokenSymbol] - if (!TokenConstructor) { - console.warn(`Unsupported token: ${token}, returning null balance`) + // Get token configuration from registry to get decimal places + const tokenConfig = getTokenBySymbol(env, token) + if (!tokenConfig) { + console.warn( + `Token not found in registry: ${token}, returning null balance` + ) return null } - const balance = - account?.amount !== undefined && account?.amount !== null - ? TokenConstructor(BigInt(account.amount.toString())) - : TokenConstructor(BigInt(0)) - - return balance + return createTokenBalance(account?.amount, tokenConfig.decimals) } catch (e) { // If user doesn't have a token account yet, return 0 balance if (e instanceof TokenAccountNotFoundError) { - const TokenConstructor = TOKEN_CONSTRUCTORS[token as TokenSymbol] - if (!TokenConstructor) { - console.warn(`Unsupported token: ${token}, returning null balance`) + const tokenConfig = getTokenBySymbol(env, token) + if (!tokenConfig) { + console.warn( + `Token not found in registry: ${token}, returning null balance` + ) return null } - return TokenConstructor(BigInt(0)) + return createTokenBalance(BigInt(0), tokenConfig.decimals) } console.error(`Error fetching ${token} balance:`, e) // Return null instead of throwing to prevent infinite loading From 50fbf27745944efd50d3f891a6364d3e12968b9f Mon Sep 17 00:00:00 2001 From: Farid Salau Date: Fri, 11 Jul 2025 09:48:46 -0500 Subject: [PATCH 10/10] Use hex --- .../components/buy-sell-modal/TokenAmountSection.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx b/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx index 45fecedb74f..62a5e8417bf 100644 --- a/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx +++ b/packages/web/src/components/buy-sell-modal/TokenAmountSection.tsx @@ -61,7 +61,6 @@ const StackedBalanceSection = ({ tokenInfo, isStablecoin }: BalanceSectionProps) => { - const { cornerRadius } = useTheme() const { icon: TokenIcon, symbol } = tokenInfo if (!formattedAvailableBalance || !TokenIcon) { @@ -89,7 +88,7 @@ const StackedBalanceSection = ({ {/* We need the border radius to be circle here because the AUDIO icon is a square image */} - + ) @@ -106,7 +105,7 @@ const CryptoAmountSection = ({ isStablecoin: boolean priceDisplay?: string }) => { - const { spacing, cornerRadius } = useTheme() + const { spacing } = useTheme() const { icon: TokenIcon, symbol } = tokenInfo const tokenTicker = messages.tokenTicker(symbol, !!isStablecoin) @@ -116,11 +115,7 @@ const CryptoAmountSection = ({ return ( - +