@@ -7,6 +7,40 @@ import { Density } from '@penumbra-zone/ui/Density';
7
7
import { chainsInPenumbraRegistry } from '@/features/cosmos/chain-provider.tsx' ;
8
8
import { useRegistry } from '@/shared/api/registry.ts' ;
9
9
import { useBalances } from '@/features/cosmos/use-augmented-balances.ts' ;
10
+ import { ValueViewComponent } from '@penumbra-zone/ui/ValueView' ;
11
+ import {
12
+ AssetId ,
13
+ Metadata ,
14
+ ValueView ,
15
+ } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb' ;
16
+ import { toValueView } from '@/shared/utils/value-view' ;
17
+ import { Balance } from '@/features/cosmos/types' ;
18
+ import { useQuery } from '@tanstack/react-query' ;
19
+ import { sha256HashStr } from '@penumbra-zone/crypto-web/sha256' ;
20
+ import { Chain } from '@penumbra-labs/registry' ;
21
+
22
+ // Generates penumbra token addresses on counterparty chains.
23
+ // IBC address derived from sha256 has of path: https://tutorials.cosmos.network/tutorials/6-ibc-dev/
24
+ const generatePenumbraIbcDenoms = async ( chains : Chain [ ] ) : Promise < string [ ] > => {
25
+ const ibcAddrs : string [ ] = [ ] ;
26
+ for ( const c of chains ) {
27
+ const ibcStr = `transfer/${ c . counterpartyChannelId } /upenumbra` ;
28
+ const encoder = new TextEncoder ( ) ;
29
+ const encodedString = encoder . encode ( ibcStr ) ;
30
+
31
+ const hash = await sha256HashStr ( encodedString ) ;
32
+ ibcAddrs . push ( `ibc/${ hash . toUpperCase ( ) } ` ) ;
33
+ }
34
+ return ibcAddrs ;
35
+ } ;
36
+
37
+ const usePenumbraIbcDenoms = ( chains : Chain [ ] ) => {
38
+ return useQuery ( {
39
+ queryKey : [ 'penumbraIbcDenoms' , chains ] ,
40
+ queryFn : async ( ) => generatePenumbraIbcDenoms ( chains ) ,
41
+ enabled : chains . length > 0 ,
42
+ } ) ;
43
+ } ;
10
44
11
45
interface CosmosConnectButtonProps {
12
46
actionType ?: ButtonProps [ 'actionType' ] ;
@@ -23,22 +57,113 @@ const CosmosConnectButtonInner = observer(
23
57
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Osmosis is always available
24
58
const { address, disconnect, openView, isWalletConnected } = chains [ 'osmosis' ] ! ;
25
59
60
+ // Get Penumbra IBC denoms
61
+ const { data : penumbraIbcDenoms = [ ] } = usePenumbraIbcDenoms ( registry ?. ibcConnections ?? [ ] ) ;
62
+
26
63
const handleConnect = ( ) => {
27
64
openView ( ) ;
28
65
} ;
29
66
30
67
const { balances } = useBalances ( ) ;
31
68
69
+ // Helper function to convert Cosmos balance to ValueView
70
+ const balanceToValueView = ( balance : Balance ) : ValueView => {
71
+ try {
72
+ // Find matching asset in registry
73
+ if ( ! registry ) {
74
+ throw new Error ( 'Registry not available' ) ;
75
+ }
76
+
77
+ // Try to match by IBC denom or symbol
78
+ let metadata : Metadata | undefined ;
79
+
80
+ // For IBC denoms - check if it's a Penumbra asset
81
+ if ( balance . denom . startsWith ( 'ibc/' ) ) {
82
+ const isPenumbraAsset = penumbraIbcDenoms . includes ( balance . denom ) ;
83
+
84
+ if ( isPenumbraAsset ) {
85
+ // If it's a Penumbra asset, get the Penumbra token metadata
86
+ const allAssets = registry . getAllAssets ( ) ;
87
+ // Find staking token (Penumbra) metadata
88
+ const penumbraMetadata = allAssets . find (
89
+ asset =>
90
+ asset . symbol . toUpperCase ( ) === 'UM' || asset . symbol . toUpperCase ( ) === 'PENUMBRA' ,
91
+ ) ;
92
+
93
+ if ( penumbraMetadata ) {
94
+ metadata = penumbraMetadata ;
95
+ }
96
+ } else {
97
+ // Try to find other IBC assets
98
+ const allAssets = registry . getAllAssets ( ) ;
99
+ // Since we can't directly access properties of asset, we need to check symbol
100
+ // against what we expect from the balance
101
+ if ( balance . symbol ) {
102
+ const matchingAsset = allAssets . find (
103
+ asset => asset . symbol . toLowerCase ( ) === balance . symbol ?. toLowerCase ( ) ,
104
+ ) ;
105
+
106
+ if ( matchingAsset ) {
107
+ metadata = matchingAsset ;
108
+ }
109
+ }
110
+ }
111
+ }
112
+ // For native denoms
113
+ else if ( balance . symbol ) {
114
+ // Get all assets and find one with matching symbol
115
+ const allAssets = registry . getAllAssets ( ) ;
116
+ const matchingAsset = allAssets . find (
117
+ asset => asset . symbol . toLowerCase ( ) === balance . symbol ?. toLowerCase ( ) ,
118
+ ) ;
119
+
120
+ if ( matchingAsset ) {
121
+ metadata = matchingAsset ;
122
+ }
123
+ }
124
+
125
+ if ( metadata ) {
126
+ // Use the toValueView utility to create a KnownAssetId ValueView
127
+ const amountInBaseUnits = parseInt ( balance . amount ) ;
128
+
129
+ return toValueView ( {
130
+ amount : amountInBaseUnits ,
131
+ metadata,
132
+ } ) ;
133
+ }
134
+
135
+ // If no metadata found, create a basic ValueView with an UnknownAssetId
136
+ // Create a placeholder assetId
137
+ const assetId = new AssetId ( { inner : new Uint8Array ( ) } ) ;
138
+
139
+ return toValueView ( {
140
+ amount : parseInt ( balance . amount ) ,
141
+ assetId,
142
+ } ) ;
143
+ } catch ( error ) {
144
+ console . error ( 'Error creating ValueView:' , error ) ;
145
+ // Create a fallback ValueView with UnknownAssetId
146
+ const assetId = new AssetId ( { inner : new Uint8Array ( ) } ) ;
147
+ return toValueView ( {
148
+ amount : parseInt ( balance . amount ) || 0 ,
149
+ assetId,
150
+ } ) ;
151
+ }
152
+ } ;
153
+
32
154
return (
33
155
< Density variant = { variant === 'default' ? 'sparse' : 'compact' } >
34
156
{ isWalletConnected && address ? (
35
157
< >
36
158
< Button actionType = { actionType } onClick = { ( ) => void disconnect ( ) } >
37
159
{ `${ address . slice ( 0 , 8 ) } ...${ address . slice ( - 4 ) } ` }
38
160
</ Button >
39
- < pre className = { 'text-white overflow-scroll' } >
40
- { balances . map ( b => JSON . stringify ( b , undefined , 4 ) ) }
41
- </ pre >
161
+ { balances . map ( ( balance , index ) => (
162
+ < ValueViewComponent
163
+ key = { `${ balance . chain } -${ balance . denom } -${ index } ` }
164
+ valueView = { balanceToValueView ( balance ) }
165
+ />
166
+ ) ) }
42
167
</ >
43
168
) : (
44
169
< Button icon = { Wallet2 } actionType = { actionType } onClick = { handleConnect } >
0 commit comments