@@ -136,32 +136,59 @@ const NotConnectedNotice = () => {
136
136
) ;
137
137
} ;
138
138
139
- const calculateAssetDistribution = async ( balances : BalancesResponse [ ] ) => {
139
+ interface AssetDistribution {
140
+ percentage : number ;
141
+ color : string ;
142
+ hasError : boolean ;
143
+ isOther : boolean ;
144
+ }
145
+
146
+ interface DistributionResult {
147
+ distribution : AssetDistribution [ ] ;
148
+ sortedBalances : BalancesResponse [ ] ;
149
+ }
150
+
151
+ interface ValueWithMetadata {
152
+ value : number ;
153
+ balance : BalancesResponse ;
154
+ hasError : boolean ;
155
+ }
156
+
157
+ /** Minimum percentage threshold for an asset to be shown individually. Assets below this are grouped into "Other". Unit: % */
158
+ const SMALL_ASSET_THRESHOLD = 2 ;
159
+
160
+ /** HSL color saturation for asset bars. Unit: % */
161
+ const COLOR_SATURATION = 95 ;
162
+
163
+ /** HSL color lightness for asset bars. Unit: % */
164
+ const COLOR_LIGHTNESS = 53 ;
165
+
166
+ /** Angle used in the golden ratio color distribution algorithm to ensure visually distinct colors. Unit: degrees */
167
+ const GOLDEN_RATIO_ANGLE = 137.5 ;
168
+
169
+ const calculateAssetDistribution = ( balances : BalancesResponse [ ] ) : DistributionResult => {
140
170
// First filter out NFTs and special assets
141
171
const displayableBalances = balances . filter ( balance => {
142
172
const metadata = getMetadataFromBalancesResponse . optional ( balance ) ;
173
+ // If we don't have a symbol, we can't display it
143
174
if ( ! metadata ?. symbol ) {
144
175
return false ;
145
176
}
146
- // Filter out LP NFTs, unbonding tokens, and auction tokens
147
- const isSpecialAsset =
148
- metadata . symbol . startsWith ( 'lpNft' ) ||
149
- metadata . symbol . startsWith ( 'unbond' ) ||
150
- metadata . symbol . startsWith ( 'auction' ) ;
151
- return ! isSpecialAsset ;
152
- } ) ;
153
177
154
- const validBalances = displayableBalances . filter ( balance => {
155
- const valueView = getBalanceView . optional ( balance ) ;
156
- const metadata = getMetadataFromBalancesResponse . optional ( balance ) ;
157
- return valueView && metadata ;
178
+ // Filter out LP NFTs, unbonding tokens, and auction tokens
179
+ return (
180
+ ! metadata . symbol . startsWith ( 'lpNft' ) &&
181
+ ! metadata . symbol . startsWith ( 'unbond' ) &&
182
+ ! metadata . symbol . startsWith ( 'auction' )
183
+ ) ;
158
184
} ) ;
159
185
160
- // Calculate values first
161
- const valuesWithMetadata = validBalances . map ( balance => {
186
+ // Calculate values and handle errors
187
+ const valuesWithMetadata : ValueWithMetadata [ ] = displayableBalances . map ( balance => {
162
188
const valueView = getBalanceView . optional ( balance ) ;
163
189
const metadata = getMetadataFromBalancesResponse . optional ( balance ) ;
164
190
const amount = valueView ?. valueView . value ?. amount ;
191
+
165
192
if ( ! amount || ! metadata ) {
166
193
console . warn (
167
194
'Missing amount or metadata for balance' ,
@@ -181,15 +208,7 @@ const calculateAssetDistribution = async (balances: BalancesResponse[]) => {
181
208
) ;
182
209
183
210
if ( Number . isNaN ( formattedAmount ) ) {
184
- console . warn (
185
- 'Failed to format amount for' ,
186
- metadata . symbol ,
187
- 'amount:' ,
188
- amount . toJsonString ( ) ,
189
- 'exponent:' ,
190
- getDisplayDenomExponent ( metadata ) ,
191
- ) ;
192
- return { value : 0 , balance, hasError : true } ;
211
+ throw new Error ( 'Failed to format amount' ) ;
193
212
}
194
213
195
214
return {
@@ -216,70 +235,43 @@ const calculateAssetDistribution = async (balances: BalancesResponse[]) => {
216
235
return { distribution : [ ] , sortedBalances : [ ] } ;
217
236
}
218
237
219
- // Take the first 4 bytes of the hash and convert to an integer
220
- const getHueFromHash = ( buffer : ArrayBuffer ) => {
221
- const view = new DataView ( buffer ) ;
222
- const num = view . getUint32 ( 0 , true ) ; // true for little-endian
223
- // Offset the hue by the hash value to make UM naturally orange
224
- return ( num * 83 ) % 360 ;
225
- } ;
226
-
227
- const distributionWithMetadata = await Promise . all (
228
- valuesWithMetadata . map ( async ( { value, balance, hasError } ) => {
229
- const metadata = getMetadataFromBalancesResponse . optional ( balance ) ;
230
- const assetIdHex = metadata ?. penumbraAssetId ?. inner . toString ( ) ?? '' ;
231
-
232
- // Use Web Crypto API to hash the hex string
233
- const hashBuffer = await crypto . subtle . digest (
234
- 'SHA-256' ,
235
- new TextEncoder ( ) . encode ( assetIdHex ) ,
236
- ) ;
237
- const hue = getHueFromHash ( hashBuffer ) ;
238
-
239
- const percentage = ( value / totalValue ) * 100 ;
240
-
241
- return {
242
- percentage,
243
- color : `hsl(${ hue } , 70%, 50%)` ,
244
- balance,
245
- hasError,
246
- } ;
247
- } ) ,
248
- ) ;
249
-
250
- // Sort by value percentage in descending order
251
- const sorted = distributionWithMetadata . sort ( ( a , b ) => b . percentage - a . percentage ) ;
238
+ // Sort by value percentage in descending order and calculate distribution
239
+ const sorted = valuesWithMetadata
240
+ . map ( ( item , index ) => ( {
241
+ ...item ,
242
+ percentage : ( item . value / totalValue ) * 100 ,
243
+ color : `hsl(${ ( index * GOLDEN_RATIO_ANGLE ) % 360 } , ${ COLOR_SATURATION } %, ${ COLOR_LIGHTNESS } %)` ,
244
+ } ) )
245
+ . sort ( ( a , b ) => b . percentage - a . percentage ) ;
252
246
253
- // Group small assets (less than 2%) into "Other" category for distribution only
254
- const SMALL_ASSET_THRESHOLD = 2 ;
247
+ // Split into main assets and small assets
255
248
const mainAssets = sorted . filter ( asset => asset . percentage >= SMALL_ASSET_THRESHOLD ) ;
256
249
const smallAssets = sorted . filter ( asset => asset . percentage < SMALL_ASSET_THRESHOLD ) ;
257
250
258
251
const otherPercentage = smallAssets . reduce ( ( acc , asset ) => acc + asset . percentage , 0 ) ;
259
252
260
- const distributionWithOther = [
253
+ const distribution : AssetDistribution [ ] = [
261
254
...mainAssets . map ( ( { percentage, color, hasError } ) => ( {
262
255
percentage,
263
256
color,
264
257
hasError,
265
- isOther : false as const ,
258
+ isOther : false ,
266
259
} ) ) ,
267
- // Only add Other if there are small assets
268
260
...( otherPercentage > 0
269
261
? [
270
262
{
271
263
percentage : otherPercentage ,
272
- color : ' hsl(0, 0%, 50%)' , // Gray color for Other
264
+ color : ` hsl(0, 0%, ${ COLOR_LIGHTNESS } %)` ,
273
265
hasError : false ,
274
- isOther : true as const ,
266
+ isOther : true ,
275
267
} ,
276
268
]
277
269
: [ ] ) ,
278
270
] ;
279
271
280
272
return {
281
- distribution : distributionWithOther ,
282
- sortedBalances : sorted . map ( ( { balance } ) => balance ) , // Keep all assets in the table
273
+ distribution,
274
+ sortedBalances : sorted . map ( ( { balance } ) => balance ) ,
283
275
} ;
284
276
} ;
285
277
@@ -290,19 +282,11 @@ export const AssetsTable = observer(() => {
290
282
const { data : balances , isLoading : balancesLoading } = useBalances ( addressIndex . account ) ;
291
283
const { data : assets , isLoading : assetsLoading } = useAssets ( ) ;
292
284
const { data : chainId } = useChainId ( ) ;
293
- const [ distribution , setDistribution ] = useState < {
294
- distribution : {
295
- percentage : number ;
296
- color : string ;
297
- hasError : boolean ;
298
- isOther : boolean ;
299
- } [ ] ;
300
- sortedBalances : BalancesResponse [ ] ;
301
- } > ( ) ;
285
+ const [ distribution , setDistribution ] = useState < DistributionResult > ( ) ;
302
286
303
287
useEffect ( ( ) => {
304
288
if ( balances ) {
305
- void calculateAssetDistribution ( balances ) . then ( setDistribution ) ;
289
+ setDistribution ( calculateAssetDistribution ( balances ) ) ;
306
290
}
307
291
} , [ balances ] ) ;
308
292
@@ -314,7 +298,6 @@ export const AssetsTable = observer(() => {
314
298
if ( isLoading ) {
315
299
return < LoadingState /> ;
316
300
}
317
-
318
301
if ( ! connected ) {
319
302
return < NotConnectedNotice /> ;
320
303
}
@@ -328,15 +311,15 @@ export const AssetsTable = observer(() => {
328
311
</ Text >
329
312
330
313
{ /* Asset distribution bar */ }
331
- < div className = 'flex w-full h-2 mt-4 mb-6 rounded-full overflow-hidden ' >
314
+ < div className = 'flex w-full h-4 mt-4 mb-6 gap-[5px] ' >
332
315
{ distribution . distribution . map ( ( asset , index ) => (
333
316
< div
334
317
key = { index }
335
318
style = { {
336
319
width : `${ asset . percentage } %` ,
337
320
backgroundColor : asset . color ,
338
321
} }
339
- className = 'h-full first: rounded-l-full last:rounded-r-full '
322
+ className = 'h-full rounded'
340
323
/>
341
324
) ) }
342
325
</ div >
0 commit comments