-
Notifications
You must be signed in to change notification settings - Fork 131
feat: Add comprehensive BONK token support with multi-token swap func… #12502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9c4a513
2f34105
71fdbc2
8733dac
f9fa14f
7127726
0f5fbd7
9086f5a
fa52e28
1ea356b
740e969
50fbf27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,47 +1,40 @@ | ||
| 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 { 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' | ||
| ) | ||
|
|
||
| export const createUserBankManagedTokens = ( | ||
| env: Env | ||
| ): Record<string, UserBankManagedTokenInfo> => { | ||
| 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<string, UserBankManagedTokenInfo> = {} | ||
|
|
||
| // Convert registry tokens to UserBankManagedTokenInfo format | ||
| userbankTokens.forEach((token: TokenConfig) => { | ||
| if (isClaimableTokenMint(token.symbol)) { | ||
| managedTokens[token.address.toUpperCase()] = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think solana addresses are case sensitive? |
||
| mintAddress: token.address, | ||
| claimableTokenMint: token.symbol, | ||
| decimals: token.decimals | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| 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 | ||
| } | ||
| return managedTokens | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import { useCallback } from 'react' | ||
|
|
||
| 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' | ||
|
|
||
| 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, getTokenBySymbol } from '~/services/index' | ||
|
|
||
| import { QUERY_KEYS } from '../queryKeys' | ||
| import { QueryOptions, type QueryKey } from '../types' | ||
|
|
||
| const createTokenBalance = ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: is this rly the cleanest way? can we not do something like:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idk i guess that's kind of annoying too.. |
||
| 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, | ||
| token: MintName, | ||
| commitment: Commitment | ||
| ) => | ||
| [ | ||
| QUERY_KEYS.tokenBalance, | ||
| ethAddress, | ||
| token, | ||
| commitment | ||
| ] as unknown as QueryKey<FixedDecimal | null> | ||
|
|
||
| /** | ||
| * 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, env } = useQueryContext() | ||
| const { data: user } = useCurrentAccountUser() | ||
| const ethAddress = user?.wallet ?? null | ||
| const queryClient = useQueryClient() | ||
|
|
||
| const result = useQuery({ | ||
| queryKey: getTokenBalanceQueryKey(ethAddress, token, commitment), | ||
| queryFn: async () => { | ||
| const sdk = await audiusSdk() | ||
| if (!ethAddress || !token) { | ||
| return null | ||
| } | ||
|
|
||
| try { | ||
| const account = await getUserbankAccountInfo( | ||
| sdk, | ||
| { | ||
| ethAddress, | ||
| mint: token | ||
| }, | ||
| commitment | ||
| ) | ||
|
|
||
| // 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 | ||
| } | ||
|
|
||
| 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 tokenConfig = getTokenBySymbol(env, token) | ||
| if (!tokenConfig) { | ||
| console.warn( | ||
| `Token not found in registry: ${token}, returning null balance` | ||
| ) | ||
| return null | ||
| } | ||
| return createTokenBalance(BigInt(0), tokenConfig.decimals) | ||
| } | ||
| 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. smart |
||
| ...queryOptions | ||
| }) | ||
|
|
||
| // Map TanStack Query states to the Status enum for API compatibility | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. damn is this another migration we need to do |
||
| 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: getTokenBalanceQueryKey(ethAddress, token, commitment) | ||
| }) | ||
| }, [queryClient, ethAddress, token, commitment]) | ||
|
|
||
| return { | ||
| status, | ||
| data, | ||
| error: result.error, | ||
| refresh: result.refetch, | ||
| cancelPolling | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whoa cool