diff --git a/apps/web/package.json b/apps/web/package.json index a1b3aadc9291..0d23c4fc82b4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,6 +22,9 @@ "@headlessui/react": "^1.7.16", "@heroicons/react": "v1", "@hookform/resolvers": "^3.1.1", + "@injective-labs/sdk-ts": "^1.10.45", + "@injective-labs/ts-types": "^1.10.45", + "@injective-labs/wallet-strategy": "^1.10.45", "@lens-protocol/sdk-gated": "^1.2.0", "@lenster/abis": "workspace:*", "@lenster/data": "workspace:*", diff --git a/apps/web/src/components/Common/Providers/index.tsx b/apps/web/src/components/Common/Providers/index.tsx index fded9ca1172f..748dc3bd3c3a 100644 --- a/apps/web/src/components/Common/Providers/index.tsx +++ b/apps/web/src/components/Common/Providers/index.tsx @@ -55,6 +55,8 @@ const queryClient = new QueryClient({ }); const apolloClient = webClient; +import { InjectiveWalletProvider } from '@lib/InjectiveWalletProvider'; + const Providers = ({ children }: { children: ReactNode }) => { return ( @@ -65,7 +67,9 @@ const Providers = ({ children }: { children: ReactNode }) => { - {children} + + {children} + diff --git a/apps/web/src/components/Profile/FeedType.tsx b/apps/web/src/components/Profile/FeedType.tsx index dc7e3e86fcb7..353033ac1ecf 100644 --- a/apps/web/src/components/Profile/FeedType.tsx +++ b/apps/web/src/components/Profile/FeedType.tsx @@ -80,6 +80,13 @@ const FeedType: FC = ({ setFeedType, feedType }) => { onClick={() => switchTab(ProfileFeedType.Stats)} /> )} + } + active={feedType === ProfileFeedType.Injective} + type={ProfileFeedType.Injective.toLowerCase()} + onClick={() => switchTab(ProfileFeedType.Injective)} + />
{feedType === ProfileFeedType.Media && }
diff --git a/apps/web/src/components/Profile/InjectivePortfolio.tsx b/apps/web/src/components/Profile/InjectivePortfolio.tsx new file mode 100644 index 000000000000..87eae94010a8 --- /dev/null +++ b/apps/web/src/components/Profile/InjectivePortfolio.tsx @@ -0,0 +1,109 @@ +import { useInjectiveWallet } from '@lib/InjectiveWalletProvider'; +import { Card, EmptyState, Spinner, ErrorMessage } from '@lenster/ui'; +import { t, Trans } from '@lingui/macro'; +import { useEffect, useState } from 'react'; +import { IndexerGrpcAccountApi } from '@injective-labs/sdk-ts'; +import { INJECTIVE_ENDPOINTS } from '@lenster/data/constants'; + +// Helper to get bank balances +const fetchBalances = async (address: string) => { + const indexerAccountApi = new IndexerGrpcAccountApi(INJECTIVE_ENDPOINTS.indexerApi); + const accountPortfolio = await indexerAccountApi.fetchAccountPortfolio(address); + + return accountPortfolio; +}; + +interface Coin { + denom: string; + amount: string; +} + +interface Portfolio { + bankBalancesList: Coin[]; + subaccountsList: string[]; +} + +const InjectivePortfolio = () => { + const { address } = useInjectiveWallet(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [portfolio, setPortfolio] = useState(null); + + useEffect(() => { + if (!address) return; + + const loadBalances = async () => { + setLoading(true); + setError(null); + try { + const data = await fetchBalances(address); + setPortfolio(data); + } catch (err: any) { + setError(new Error(err?.message || 'Failed to fetch balances')); + console.error('Failed to fetch Injective balances', err); + } finally { + setLoading(false); + } + }; + + loadBalances(); + }, [address]); + + if (!address) { + return ( + 🏦} + hideCard + /> + ); + } + + if (loading) { + return ( + +
+ +
+
+ ); + } + + if (error) { + return ( + + ); + } + + const bankBalances = portfolio?.bankBalancesList || []; + const subaccountBalances = portfolio?.subaccountsList || []; + + return ( +
+ +
+ Injective Assets +
+ {bankBalances.length === 0 && subaccountBalances.length === 0 ? ( +
+ No assets found. +
+ ) : ( +
+ {bankBalances.map((balance: Coin, i: number) => ( +
+ {balance.denom} + {balance.amount} +
+ ))} +
+ )} +
+
+ ); +}; + +export default InjectivePortfolio; diff --git a/apps/web/src/components/Profile/index.tsx b/apps/web/src/components/Profile/index.tsx index 6272cd48d13a..033f0ca68f8d 100644 --- a/apps/web/src/components/Profile/index.tsx +++ b/apps/web/src/components/Profile/index.tsx @@ -30,6 +30,7 @@ import Feed from './Feed'; import FeedType from './FeedType'; import FollowDialog from './FollowDialog'; import NftGallery from './NftGallery'; +import InjectivePortfolio from './InjectivePortfolio'; import ProfilePageShimmer from './Shimmer'; const ViewProfile: NextPage = () => { @@ -157,6 +158,7 @@ const ViewProfile: NextPage = () => { {feedType === ProfileFeedType.Stats && IS_MAINNET ? ( ) : null} + {feedType === ProfileFeedType.Injective ? : null} diff --git a/apps/web/src/components/Shared/Login/WalletSelector.tsx b/apps/web/src/components/Shared/Login/WalletSelector.tsx index 8dd6ff0375c8..42efba1d3947 100644 --- a/apps/web/src/components/Shared/Login/WalletSelector.tsx +++ b/apps/web/src/components/Shared/Login/WalletSelector.tsx @@ -30,6 +30,8 @@ import { useNetwork, useSignMessage } from 'wagmi'; +import { useInjectiveWallet } from '@lib/InjectiveWalletProvider'; +import { Wallet } from '@injective-labs/wallet-strategy'; interface WalletSelectorProps { setHasConnected: Dispatch; @@ -46,6 +48,7 @@ const WalletSelector: FC = ({ const setShowAuthModal = useGlobalModalStateStore( (state) => state.setShowAuthModal ); + const { connect: connectInjective } = useInjectiveWallet(); const [isLoading, setIsLoading] = useState(false); const onError = (error: any) => { @@ -225,6 +228,21 @@ const WalletSelector: FC = ({ ); })} + {error?.message ? (
diff --git a/apps/web/src/enums.ts b/apps/web/src/enums.ts index 1c283c33d19a..eac22967a000 100644 --- a/apps/web/src/enums.ts +++ b/apps/web/src/enums.ts @@ -17,7 +17,8 @@ export enum ProfileFeedType { Media = 'MEDIA', Collects = 'COLLECTS', Nft = 'NFT', - Stats = 'STATS' + Stats = 'STATS', + Injective = 'INJECTIVE' } export enum MessageTabs { diff --git a/apps/web/src/lib/InjectiveWalletProvider.tsx b/apps/web/src/lib/InjectiveWalletProvider.tsx new file mode 100644 index 000000000000..8fda749ef508 --- /dev/null +++ b/apps/web/src/lib/InjectiveWalletProvider.tsx @@ -0,0 +1,71 @@ +import { ChainId } from '@injective-labs/ts-types'; +import { Wallet, WalletStrategy } from '@injective-labs/wallet-strategy'; +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; + +interface InjectiveWalletContextType { + walletStrategy: WalletStrategy; + address: string; + chainId: ChainId; + connect: (wallet: Wallet) => Promise; + disconnect: () => void; + isLoading: boolean; +} + +const InjectiveWalletContext = createContext(null); + +export const useInjectiveWallet = () => { + const context = useContext(InjectiveWalletContext); + if (!context) { + throw new Error('useInjectiveWallet must be used within an InjectiveWalletProvider'); + } + return context; +}; + +export const InjectiveWalletProvider = ({ children }: { children: React.ReactNode }) => { + const [address, setAddress] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + // Initialize Wallet Strategy + // TODO: Make chainId configurable via env + const chainId = (process.env.NEXT_PUBLIC_INJECTIVE_CHAIN_ID as unknown as ChainId) || ChainId.Mainnet; + + const walletStrategy = useMemo(() => new WalletStrategy({ + chainId, + // Add compatible wallets here + wallet: Wallet.Keplr + }), [chainId]); + + const connect = async (wallet: Wallet) => { + setIsLoading(true); + try { + walletStrategy.setWallet(wallet); + const addresses = await walletStrategy.getAddresses(); + if (addresses.length > 0) { + setAddress(addresses[0]); + } + } catch (error) { + console.error('Failed to connect Injective wallet:', error); + } finally { + setIsLoading(false); + } + }; + + const disconnect = () => { + setAddress(''); + // WalletStrategy doesn't strictly "disconnect" in the same way, + // but we clear local state. + }; + + return ( + + {children} + + ); +}; diff --git a/packages/data/constants.ts b/packages/data/constants.ts index 8b1781c58d68..57403437c414 100644 --- a/packages/data/constants.ts +++ b/packages/data/constants.ts @@ -14,6 +14,12 @@ export const LENS_PERIPHERY = getEnvConfig().lensPeripheryAddress; export const DEFAULT_COLLECT_TOKEN = getEnvConfig().defaultCollectToken; export const LIT_PROTOCOL_ENVIRONMENT = getEnvConfig().litProtocolEnvironment; +export const INJECTIVE_ENDPOINTS = { + indexerApi: 'https://sentry.exchange.grpc-web.injective.network', + grpc: 'https://sentry.chain.grpc-web.injective.network', + rest: 'https://sentry.lcd.injective.network' +}; + export const IS_MAINNET = API_URL === LensEndpoint.Mainnet; // XMTP