Skip to content

Commit afeceae

Browse files
authored
Fix AUDIO breakdown (#13298)
### Description - Show ETH & SPL AUDIO Balance for AUDIO - Update Buy/Sell flow to only show SPL balance ### How Has This Been Tested? On Leontis's account <img width="727" height="408" alt="image" src="https://github.com/user-attachments/assets/8ceb6556-389d-4ebc-9a53-df402169672c" />
1 parent 68eace6 commit afeceae

File tree

5 files changed

+350
-110
lines changed

5 files changed

+350
-110
lines changed

packages/common/src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export * from './tan-query/wallets/useWalletOwner'
161161
export * from './tan-query/wallets/useUSDCBalance'
162162
export * from './tan-query/wallets/useExternalWalletBalance'
163163
export * from './tan-query/wallets/useCoinBalance'
164+
export * from './tan-query/wallets/useCoinBalanceBreakdown'
164165
export * from './tan-query/wallets/useSendCoins'
165166
export * from './tan-query/jupiter/useSwapCoins'
166167
export * from './tan-query/jupiter/useCoinExchangeRate'
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { useMemo } from 'react'
2+
3+
import { AUDIO } from '@audius/fixed-decimal'
4+
5+
import {
6+
useAssociatedWallets,
7+
useCurrentAccountUser,
8+
useUserCoin,
9+
useWalletAudioBalances
10+
} from '~/api'
11+
import { Chain } from '~/models'
12+
13+
type UseCoinBalanceBreakdownParams = {
14+
mint: string
15+
isAudio: boolean
16+
}
17+
18+
export type WalletAccount = {
19+
owner: string
20+
balance: number
21+
isInAppWallet?: boolean
22+
}
23+
24+
export type AudioWalletBalance = {
25+
balance: bigint | null
26+
chain: Chain
27+
address: string
28+
}
29+
30+
/**
31+
* Hook that provides coin balance breakdown logic for both AUDIO and other coins.
32+
* Consolidates the logic previously duplicated between BalanceSection and BalanceCard components.
33+
*/
34+
export const useCoinBalanceBreakdown = ({
35+
mint,
36+
isAudio
37+
}: UseCoinBalanceBreakdownParams) => {
38+
// Fetch wallet accounts for balance breakdown (for non-AUDIO coins)
39+
const { data: userCoins } = useUserCoin({ mint }, { enabled: !isAudio })
40+
const { accounts: unsortedAccounts = [], decimals } = userCoins ?? {}
41+
42+
// For AUDIO, fetch ERC and SPL balances separately for built-in wallet
43+
const { data: currentUser } = useCurrentAccountUser()
44+
const audioBalances = useWalletAudioBalances(
45+
{
46+
wallets: [
47+
...(currentUser?.erc_wallet
48+
? [{ address: currentUser.erc_wallet, chain: Chain.Eth }]
49+
: []),
50+
...(currentUser?.spl_wallet
51+
? [{ address: currentUser.spl_wallet, chain: Chain.Sol }]
52+
: [])
53+
],
54+
includeStaked: false
55+
},
56+
{ enabled: isAudio }
57+
)
58+
59+
// For AUDIO, fetch associated/linked wallets (both ETH and SOL)
60+
const { data: associatedWallets = [] } = useAssociatedWallets({
61+
enabled: isAudio
62+
})
63+
const associatedAudioBalances = useWalletAudioBalances(
64+
{
65+
wallets: associatedWallets,
66+
includeStaked: false
67+
},
68+
{ enabled: isAudio && associatedWallets.length > 0 }
69+
)
70+
71+
// Sort accounts by balance (descending) for non-AUDIO coins
72+
const accounts = useMemo(
73+
() => [...unsortedAccounts].sort((a, b) => b.balance - a.balance),
74+
[unsortedAccounts]
75+
)
76+
77+
// Separate built-in wallet from linked wallets for non-AUDIO coins
78+
const inAppWallet = useMemo(
79+
() => accounts.find((account) => account.isInAppWallet),
80+
[accounts]
81+
)
82+
const linkedWallets = useMemo(
83+
() => accounts.filter((account) => !account.isInAppWallet),
84+
[accounts]
85+
)
86+
87+
// For AUDIO, calculate the combined ERC + SPL balance for built-in wallet
88+
const audioBuiltInBalance = useMemo(() => {
89+
if (!isAudio) return null
90+
// AudioWei is a bigint - use bigint arithmetic to avoid precision loss
91+
// Wrap addition in AUDIO().value to maintain AudioWei branded type
92+
let totalWei = AUDIO(0).value
93+
for (const balanceData of audioBalances.data) {
94+
if (balanceData.balance) {
95+
totalWei = AUDIO(totalWei + balanceData.balance).value
96+
}
97+
}
98+
// AUDIO constructor handles bigint in wei for display formatting
99+
return AUDIO(totalWei).toLocaleString('en-US', {
100+
maximumFractionDigits: 2,
101+
roundingMode: 'trunc'
102+
})
103+
}, [isAudio, audioBalances.data])
104+
105+
return {
106+
// For non-AUDIO coins
107+
accounts,
108+
decimals,
109+
inAppWallet,
110+
linkedWallets,
111+
// For AUDIO coins
112+
audioBuiltInBalance,
113+
associatedAudioBalances,
114+
// Common data
115+
hasBreakdown:
116+
linkedWallets.length > 0 ||
117+
(isAudio && audioBuiltInBalance) ||
118+
(isAudio && associatedAudioBalances.data.length > 0)
119+
}
120+
}

packages/mobile/src/screens/coin-details-screen/components/BalanceCard.tsx

Lines changed: 95 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { useCallback, useMemo } from 'react'
1+
import { useCallback } from 'react'
22

3-
import { useCoinBalance, useArtistCoin, useUserCoin } from '@audius/common/api'
3+
import {
4+
useCoinBalance,
5+
useArtistCoin,
6+
useCoinBalanceBreakdown
7+
} from '@audius/common/api'
48
import {
59
useFormattedCoinBalance,
610
useIsManagedAccount,
@@ -12,6 +16,7 @@ import {
1216
sendTokensModalActions
1317
} from '@audius/common/store'
1418
import { shortenSPLAddress } from '@audius/common/utils'
19+
import { AUDIO } from '@audius/fixed-decimal'
1520
import { useDispatch } from 'react-redux'
1621

1722
import {
@@ -24,6 +29,7 @@ import {
2429
} from '@audius/harmony-native'
2530
import { TokenIcon } from 'app/components/core'
2631
import { useNavigation } from 'app/hooks/useNavigation'
32+
import { env } from 'app/services/env'
2733

2834
const messages = coinDetailsMessages.balance
2935

@@ -95,31 +101,25 @@ const HasBalanceState = ({
95101
onSend,
96102
onReceive,
97103
mint,
98-
coinName
99-
}: BalanceStateProps & { mint: string; coinName: string }) => {
104+
coinName,
105+
isAudio
106+
}: BalanceStateProps & {
107+
mint: string
108+
coinName: string
109+
isAudio: boolean
110+
}) => {
100111
const isManagerMode = useIsManagedAccount()
101112
const { coinBalanceFormatted, formattedHeldValue } =
102113
useFormattedCoinBalance(mint)
103114

104-
// Fetch wallet accounts for balance breakdown
105-
const { data: userCoins } = useUserCoin({ mint })
106-
const { accounts: unsortedAccounts = [], decimals } = userCoins ?? {}
107-
108-
// Sort accounts by balance (descending)
109-
const accounts = useMemo(
110-
() => [...unsortedAccounts].sort((a, b) => b.balance - a.balance),
111-
[unsortedAccounts]
112-
)
113-
114-
// Separate built-in wallet from linked wallets
115-
const inAppWallet = useMemo(
116-
() => accounts.find((account) => account.isInAppWallet),
117-
[accounts]
118-
)
119-
const linkedWallets = useMemo(
120-
() => accounts.filter((account) => !account.isInAppWallet),
121-
[accounts]
122-
)
115+
const {
116+
decimals,
117+
inAppWallet,
118+
linkedWallets,
119+
audioBuiltInBalance,
120+
associatedAudioBalances,
121+
hasBreakdown
122+
} = useCoinBalanceBreakdown({ mint, isAudio })
123123

124124
return (
125125
<Flex column gap='l' w='100%'>
@@ -162,15 +162,15 @@ const HasBalanceState = ({
162162
</Text>
163163
</Flex>
164164
</Flex>
165-
{linkedWallets.length > 0 && (
165+
{hasBreakdown && (
166166
<>
167167
<Divider />
168168
<Flex column gap='s' w='100%'>
169169
<Text variant='title' size='l'>
170170
{coinDetailsMessages.externalWallets.hasBalanceTitle}
171171
</Text>
172172
<Flex column gap='s' w='100%'>
173-
{inAppWallet && (
173+
{(inAppWallet || (isAudio && audioBuiltInBalance)) && (
174174
<Flex
175175
direction='row'
176176
alignItems='center'
@@ -182,36 +182,77 @@ const HasBalanceState = ({
182182
{coinDetailsMessages.externalWallets.builtIn}
183183
</Text>
184184
<Text variant='body' size='l'>
185-
{Math.trunc(
186-
inAppWallet.balance / Math.pow(10, decimals ?? 0)
187-
).toLocaleString()}
185+
{isAudio
186+
? audioBuiltInBalance
187+
: Math.trunc(
188+
inAppWallet!.balance / Math.pow(10, decimals ?? 0)
189+
).toLocaleString()}
188190
</Text>
189191
</Flex>
190192
)}
191-
{linkedWallets.map((wallet, index) => (
192-
<Flex
193-
key={wallet.owner}
194-
direction='row'
195-
alignItems='center'
196-
justifyContent='space-between'
197-
w='100%'
198-
pv='2xs'
199-
>
200-
<Flex gap='xs' alignItems='center' row>
201-
<Text variant='body' size='l'>
202-
{walletMessages.linkedWallets.wallet(index)}
203-
</Text>
204-
<Text variant='body' size='l' color='subdued'>
205-
({shortenSPLAddress(wallet.owner)})
206-
</Text>
207-
</Flex>
208-
<Text variant='body' size='l'>
209-
{Math.trunc(
210-
wallet.balance / Math.pow(10, decimals ?? 0)
211-
).toLocaleString()}
212-
</Text>
213-
</Flex>
214-
))}
193+
{isAudio
194+
? // For AUDIO, show associated wallets (both ETH and SOL)
195+
associatedAudioBalances.data.map((walletBalance, index) => {
196+
if (
197+
!walletBalance.balance ||
198+
walletBalance.balance === AUDIO(0).value
199+
)
200+
return null
201+
// Use AUDIO constructor to format bigint balance properly
202+
const balanceFormatted = AUDIO(
203+
walletBalance.balance
204+
).toLocaleString('en-US', {
205+
maximumFractionDigits: 2,
206+
roundingMode: 'trunc'
207+
})
208+
return (
209+
<Flex
210+
key={`${walletBalance.chain}-${walletBalance.address}`}
211+
direction='row'
212+
alignItems='center'
213+
justifyContent='space-between'
214+
w='100%'
215+
pv='2xs'
216+
>
217+
<Flex gap='xs' alignItems='center' row>
218+
<Text variant='body' size='l'>
219+
{walletMessages.linkedWallets.wallet(index)}
220+
</Text>
221+
<Text variant='body' size='l' color='subdued'>
222+
({shortenSPLAddress(walletBalance.address)})
223+
</Text>
224+
</Flex>
225+
<Text variant='body' size='l'>
226+
{balanceFormatted}
227+
</Text>
228+
</Flex>
229+
)
230+
})
231+
: // For other coins, show SPL linked wallets
232+
linkedWallets.map((wallet, index) => (
233+
<Flex
234+
key={wallet.owner}
235+
direction='row'
236+
alignItems='center'
237+
justifyContent='space-between'
238+
w='100%'
239+
pv='2xs'
240+
>
241+
<Flex gap='xs' alignItems='center' row>
242+
<Text variant='body' size='l'>
243+
{walletMessages.linkedWallets.wallet(index)}
244+
</Text>
245+
<Text variant='body' size='l' color='subdued'>
246+
({shortenSPLAddress(wallet.owner)})
247+
</Text>
248+
</Flex>
249+
<Text variant='body' size='l'>
250+
{Math.trunc(
251+
wallet.balance / Math.pow(10, decimals ?? 0)
252+
).toLocaleString()}
253+
</Text>
254+
</Flex>
255+
))}
215256
</Flex>
216257
</Flex>
217258
<Divider />
@@ -248,6 +289,7 @@ export const BalanceCard = ({ mint }: { mint: string }) => {
248289
const { data: coin, isPending: coinsLoading } = useArtistCoin(mint)
249290
const { data: tokenBalance } = useCoinBalance({ mint })
250291
const initialTab = useBuySellInitialTab()
292+
const isAudio = mint === env.WAUDIO_MINT_ADDRESS
251293

252294
const handleBuy = useCallback(() => {
253295
navigation.navigate('BuySell', {
@@ -293,6 +335,7 @@ export const BalanceCard = ({ mint }: { mint: string }) => {
293335
onReceive={handleReceive}
294336
mint={mint}
295337
coinName={coinName}
338+
isAudio={isAudio}
296339
/>
297340
)}
298341
</Paper>

0 commit comments

Comments
 (0)