UNPKG

@reown/appkit-controllers

Version:

The full stack toolkit to build onchain app UX.

569 lines • 25 kB
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ConstantsUtil } from '@reown/appkit-common'; import { ConnectorController } from '../../exports'; import { BlockchainApiController } from '../../src/controllers/BlockchainApiController'; import {} from '../../src/controllers/ChainController'; import { ChainController } from '../../src/controllers/ChainController'; import { ConnectionController } from '../../src/controllers/ConnectionController'; import { BalanceUtil } from '../../src/utils/BalanceUtil'; import { ERC7811Utils } from '../../src/utils/ERC7811Util'; import { StorageUtil } from '../../src/utils/StorageUtil'; import { ViemUtil } from '../../src/utils/ViemUtil'; vi.mock('../../src/controllers/BlockchainApiController'); vi.mock('../../src/controllers/ChainController'); vi.mock('../../src/controllers/ConnectionController'); vi.mock('../../src/utils/ERC7811Util'); vi.mock('../../src/utils/StorageUtil'); vi.mock('../../src/utils/ViemUtil'); const mockEthereumNetwork = { id: '1', chainNamespace: 'eip155', caipNetworkId: 'eip155:1', name: 'Ethereum', nativeCurrency: { name: 'Ethereum', decimals: 18, symbol: 'ETH' }, rpcUrls: { default: { http: ['https://mainnet.infura.io/v3/YOUR-PROJECT-ID'] } } }; const mockSolanaNetwork = { id: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', caipNetworkId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', chainNamespace: 'solana', name: 'Solana', nativeCurrency: { name: 'Solana', decimals: 9, symbol: 'SOL' }, rpcUrls: { default: { http: ['https://api.mainnet-beta.solana.com'] } } }; const mockEthereumAddress = '0x1234567890123456789012345678901234567890'; const mockEthChainIdAsHex = '0x1'; describe('BalanceUtil', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(ConnectorController, 'getConnectorId').mockReturnValue(ConstantsUtil.CONNECTOR_ID.AUTH); }); describe('getMyTokensWithBalance', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(ChainController, 'getAccountData').mockReturnValue({ address: mockEthereumAddress }); ChainController.state.activeCaipNetwork = mockEthereumNetwork; vi.mocked(ERC7811Utils.getChainIdHexFromCAIP2ChainId).mockReturnValue(mockEthChainIdAsHex); ConnectorController.state.activeConnectorIds = { eip155: ConstantsUtil.CONNECTOR_ID.AUTH, solana: undefined, polkadot: undefined, bip122: undefined, cosmos: undefined, sui: undefined, stacks: undefined, ton: undefined, tron: undefined }; vi.mocked(StorageUtil.getBalanceCacheForCaipAddress).mockReturnValue(undefined); }); it('should return empty array when address is missing', async () => { vi.spyOn(ChainController, 'getAccountData').mockReturnValue({ address: undefined }); const result = await BalanceUtil.getMyTokensWithBalance(); expect(result).toEqual([]); }); it('should use ERC7811 for EIP155 chain when wallet_getAssets is available', async () => { const mockAssetsResponse = { [mockEthChainIdAsHex]: [ { address: mockEthereumAddress, balance: '0xDE0B6B3A7640000', type: 'NATIVE', metadata: { name: 'Ethereum', symbol: 'ETH', decimals: 18, value: 0.0001, price: 3200, iconUrl: 'https://example.com/icon.png' } } ] }; vi.spyOn(ChainController, 'getAccountData').mockReturnValue({ address: mockEthereumAddress }); ChainController.state.activeCaipNetwork = mockEthereumNetwork; const mockBalance = { symbol: 'ETH', quantity: { decimals: '18', numeric: '0.0001' }, name: 'Ethereum', chainId: mockEthChainIdAsHex, price: 3200, iconUrl: 'https://example.com/icon.png' }; vi.mocked(ConnectionController.getCapabilities).mockResolvedValue({ [mockEthChainIdAsHex]: { assetDiscovery: { supported: true } } }); vi.mocked(ConnectionController.walletGetAssets).mockResolvedValue(mockAssetsResponse); vi.mocked(ERC7811Utils.isWalletGetAssetsResponse).mockReturnValue(true); vi.mocked(ERC7811Utils.createBalance).mockReturnValue(mockBalance); const result = await BalanceUtil.getMyTokensWithBalance(); expect(ConnectionController.walletGetAssets).toHaveBeenCalledWith({ account: mockEthereumAddress, chainFilter: [mockEthChainIdAsHex] }); expect(result).toEqual([mockBalance]); }); it('should use BlockchainApi for Solana chain', async () => { const mockSolanaAddress = 'solana123'; const mockBalances = [ { symbol: 'SOL', quantity: { decimals: '9', numeric: '1.0' }, name: 'Solana', chainId: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', price: 0, iconUrl: '' } ]; vi.spyOn(ChainController, 'getAccountData').mockReturnValue({ address: mockSolanaAddress }); ChainController.state.activeCaipNetwork = mockSolanaNetwork; vi.mocked(BlockchainApiController.getBalance).mockResolvedValue({ balances: mockBalances }); const result = await BalanceUtil.getMyTokensWithBalance(); expect(BlockchainApiController.getBalance).toHaveBeenCalledWith(mockSolanaAddress, mockSolanaNetwork.caipNetworkId, undefined); expect(result).toEqual(mockBalances); }); it('should use BlockchainApi if connector is not auth', async () => { const mockBalances = [ { symbol: 'ETH', quantity: { decimals: '18', numeric: '1.0' }, name: 'Ethereum', chainId: mockEthChainIdAsHex, price: 0, iconUrl: '' } ]; vi.spyOn(ChainController, 'getAccountData').mockReturnValue({ address: mockEthereumAddress }); ChainController.state.activeCaipNetwork = mockEthereumNetwork; vi.spyOn(ConnectorController, 'getConnectorId').mockReturnValue(ConstantsUtil.CONNECTOR_ID.INJECTED); vi.mocked(BlockchainApiController.getBalance).mockResolvedValue({ balances: mockBalances }); const result = await BalanceUtil.getMyTokensWithBalance(); expect(BlockchainApiController.getBalance).toHaveBeenCalledWith(mockEthereumAddress, mockEthereumNetwork.caipNetworkId, undefined); expect(result).toEqual(mockBalances); }); it('should use BlockchainApi when wallet_getAssets is not available', async () => { const mockBalances = [ { symbol: 'ETH', quantity: { decimals: '18', numeric: '1.0' }, name: 'Ethereum', chainId: mockEthChainIdAsHex, price: 0, iconUrl: '' } ]; vi.mocked(ConnectionController.getCapabilities).mockResolvedValue({}); vi.mocked(BlockchainApiController.getBalance).mockResolvedValue({ balances: mockBalances }); const result = await BalanceUtil.getMyTokensWithBalance(); expect(BlockchainApiController.getBalance).toHaveBeenCalledWith(mockEthereumAddress, mockEthereumNetwork.caipNetworkId, undefined); expect(result).toEqual(mockBalances); }); it('should filter out zero decimal balances', async () => { const mockBalances = [ { symbol: 'ETH', quantity: { decimals: '18', numeric: '1.0' }, name: 'Ethereum', chainId: mockEthChainIdAsHex, price: 0, iconUrl: '' }, { symbol: 'TEST', quantity: { decimals: '0', numeric: '0' }, name: 'Test Token', chainId: mockEthChainIdAsHex, price: 0, iconUrl: '' } ]; vi.mocked(BlockchainApiController.getBalance).mockResolvedValue({ balances: mockBalances }); const result = await BalanceUtil.getMyTokensWithBalance(); expect(result).toEqual([mockBalances[0]]); }); it('should return cached balance from storage if it exists', async () => { const mockCachedBalance = { balances: [ { symbol: 'ETH', quantity: { decimals: '18', numeric: '1.0' }, name: 'Ethereum', chainId: mockEthChainIdAsHex, price: 3200, iconUrl: 'https://example.com/icon.png' } ], timestamp: Date.now() }; vi.mocked(StorageUtil.getBalanceCacheForCaipAddress).mockReturnValue(mockCachedBalance); const result = await BalanceUtil.getMyTokensWithBalance(); expect(StorageUtil.getBalanceCacheForCaipAddress).toHaveBeenCalledWith(`${mockEthereumNetwork.caipNetworkId}:${mockEthereumAddress}`); expect(result).toEqual(mockCachedBalance.balances); expect(ConnectionController.walletGetAssets).not.toHaveBeenCalled(); expect(BlockchainApiController.getBalance).not.toHaveBeenCalled(); }); }); describe('getEIP155Balances', () => { beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(ChainController, 'getAccountData').mockReturnValue({ address: mockEthereumAddress }); ChainController.state.activeCaipNetwork = mockEthereumNetwork; ConnectorController.state.activeConnectorIds = { eip155: ConstantsUtil.CONNECTOR_ID.AUTH, solana: undefined, polkadot: undefined, bip122: undefined, cosmos: undefined, sui: undefined, stacks: undefined, ton: undefined, tron: undefined }; vi.mocked(ERC7811Utils.getChainIdHexFromCAIP2ChainId).mockReturnValue(mockEthChainIdAsHex); }); it('should return null when walletGetAssetsResponse is invalid', async () => { const invalidResponse = { invalid: 'response' }; vi.mocked(ConnectionController.getCapabilities).mockResolvedValue({ [mockEthChainIdAsHex]: { assetDiscovery: { supported: true } } }); vi.mocked(ConnectionController.walletGetAssets).mockResolvedValue(invalidResponse); vi.mocked(ERC7811Utils.isWalletGetAssetsResponse).mockReturnValue(false); // Mock the type guard to return false const result = await BalanceUtil.getEIP155Balances(mockEthereumAddress, mockEthereumNetwork); expect(result).toBeNull(); }); it('should return null when asset discovery fails', async () => { const errorMessage = 'Network error'; vi.mocked(ConnectionController.getCapabilities).mockResolvedValue({ [mockEthChainIdAsHex]: { assetDiscovery: { supported: true } } }); vi.mocked(ConnectionController.walletGetAssets).mockRejectedValue(new Error(errorMessage)); const result = await BalanceUtil.getEIP155Balances(mockEthereumAddress, mockEthereumNetwork); expect(result).toBeNull(); }); it('should return assets when walletGetAssetsResponse contains chainIdHex', async () => { const mockAssetsResponse = { [mockEthChainIdAsHex]: [ { address: mockEthereumAddress, balance: '0xDE0B6B3A7640000', type: 'NATIVE', metadata: { name: 'Ethereum', symbol: 'ETH', decimals: 18, value: 0.0001, price: 3200, iconUrl: 'https://example.com/icon.png' } } ] }; vi.mocked(ConnectionController.getCapabilities).mockResolvedValue({ [mockEthChainIdAsHex]: { assetDiscovery: { supported: true } } }); vi.mocked(ConnectionController.walletGetAssets).mockResolvedValue(mockAssetsResponse); vi.mocked(ERC7811Utils.isWalletGetAssetsResponse).mockReturnValue(true); // Mock the type guard to return true vi.mocked(ERC7811Utils.createBalance).mockReturnValue({ symbol: 'ETH', quantity: { decimals: '18', numeric: '0.0001' }, name: 'Ethereum', chainId: mockEthChainIdAsHex, price: 3200, iconUrl: 'https://example.com/icon.png' }); const result = await BalanceUtil.getEIP155Balances(mockEthereumAddress, mockEthereumNetwork); expect(result).toBeDefined(); expect(result).toHaveLength(1); expect(result).not.toBeNull(); if (result) { expect(result[0]).toMatchObject({ symbol: 'ETH', quantity: { decimals: '18', numeric: '0.0001' }, name: 'Ethereum', chainId: mockEthChainIdAsHex, price: 3200, iconUrl: 'https://example.com/icon.png' }); } }); it('should return an empty array when walletGetAssetsResponse does not contain chainIdHex', async () => { const mockAssetsResponse = { '0x2': [] }; vi.mocked(ConnectionController.getCapabilities).mockResolvedValue({ [mockEthChainIdAsHex]: { assetDiscovery: { supported: true } } }); vi.mocked(ConnectionController.walletGetAssets).mockResolvedValue(mockAssetsResponse); vi.mocked(ERC7811Utils.isWalletGetAssetsResponse).mockReturnValue(true); const result = await BalanceUtil.getEIP155Balances(mockEthereumAddress, mockEthereumNetwork); expect(result).toEqual([]); }); it('should save balance to storage after fetching', async () => { const mockAssetsResponse = { [mockEthChainIdAsHex]: [ { address: mockEthereumAddress, balance: '0xDE0B6B3A7640000', type: 'NATIVE', metadata: { name: 'Ethereum', symbol: 'ETH', decimals: 18, value: 0.0001, price: 3200, iconUrl: 'https://example.com/icon.png' } } ] }; const mockBalance = { symbol: 'ETH', quantity: { decimals: '18', numeric: '0.0001' }, name: 'Ethereum', chainId: mockEthChainIdAsHex, price: 3200, iconUrl: 'https://example.com/icon.png' }; vi.mocked(ConnectionController.getCapabilities).mockResolvedValue({ [mockEthChainIdAsHex]: { assetDiscovery: { supported: true } } }); vi.mocked(ConnectionController.walletGetAssets).mockResolvedValue(mockAssetsResponse); vi.mocked(ERC7811Utils.isWalletGetAssetsResponse).mockReturnValue(true); vi.mocked(ERC7811Utils.createBalance).mockReturnValue(mockBalance); const result = await BalanceUtil.getEIP155Balances(mockEthereumAddress, mockEthereumNetwork); expect(StorageUtil.updateBalanceCache).toHaveBeenCalledWith({ caipAddress: `${mockEthereumNetwork.caipNetworkId}:${mockEthereumAddress}`, balance: { balances: [mockBalance] }, timestamp: expect.any(Number) }); expect(result).toEqual([mockBalance]); }); }); describe('fetchERC20Balance', () => { const mockCaipAddress = 'eip155:1:0x1234567890123456789012345678901234567890'; const mockAssetAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; const mockCaipNetwork = mockEthereumNetwork; const mockAddress = '0x1234567890123456789012345678901234567890'; let mockPublicClient; beforeEach(() => { vi.restoreAllMocks(); mockPublicClient = { multicall: vi.fn() }; vi.mocked(ViemUtil.createViemPublicClient).mockReturnValue(mockPublicClient); }); it('should successfully fetch ERC20 balance with valid data', async () => { const mockMulticallResult = [ { result: 'Test Token' }, { result: 'TEST' }, { result: BigInt('1000000000000000000') }, { result: 18 } ]; mockPublicClient.multicall.mockResolvedValue(mockMulticallResult); const result = await BalanceUtil.fetchERC20Balance({ caipAddress: mockCaipAddress, assetAddress: mockAssetAddress, caipNetwork: mockCaipNetwork }); expect(ViemUtil.createViemPublicClient).toHaveBeenCalledWith(mockCaipNetwork); expect(mockPublicClient.multicall).toHaveBeenCalledWith({ contracts: [ { address: mockAssetAddress, functionName: 'name', args: [], abi: expect.any(Object) }, { address: mockAssetAddress, functionName: 'symbol', args: [], abi: expect.any(Object) }, { address: mockAssetAddress, functionName: 'balanceOf', args: [mockAddress], abi: expect.any(Object) }, { address: mockAssetAddress, functionName: 'decimals', args: [], abi: expect.any(Object) } ] }); expect(result).toEqual({ name: 'Test Token', symbol: 'TEST', decimals: 18, balance: '1' }); }); it('should return zero balance when balance is zero', async () => { const mockMulticallResult = [ { result: 'Test Token' }, { result: 'TEST' }, { result: BigInt('0') }, { result: 18 } ]; mockPublicClient.multicall.mockResolvedValue(mockMulticallResult); const result = await BalanceUtil.fetchERC20Balance({ caipAddress: mockCaipAddress, assetAddress: mockAssetAddress, caipNetwork: mockCaipNetwork }); expect(result).toEqual({ name: 'Test Token', symbol: 'TEST', decimals: 18, balance: '0' }); }); it('should return zero balance when balance or decimals are null', async () => { const mockMulticallResult = [ { result: 'Test Token' }, { result: 'TEST' }, { result: null }, { result: 18 } ]; mockPublicClient.multicall.mockResolvedValue(mockMulticallResult); const result = await BalanceUtil.fetchERC20Balance({ caipAddress: mockCaipAddress, assetAddress: mockAssetAddress, caipNetwork: mockCaipNetwork }); expect(result).toEqual({ name: 'Test Token', symbol: 'TEST', decimals: 18, balance: '0' }); }); it('should return zero balance when decimals are undefined', async () => { const mockMulticallResult = [ { result: 'Test Token' }, { result: 'TEST' }, { result: BigInt('1000000000000000000') }, { result: undefined } ]; mockPublicClient.multicall.mockResolvedValue(mockMulticallResult); const result = await BalanceUtil.fetchERC20Balance({ caipAddress: mockCaipAddress, assetAddress: mockAssetAddress, caipNetwork: mockCaipNetwork }); expect(result).toEqual({ name: 'Test Token', symbol: 'TEST', decimals: undefined, balance: '0' }); }); it('should handle network errors gracefully', async () => { const networkError = new Error('Network error'); mockPublicClient.multicall.mockRejectedValue(networkError); await expect(BalanceUtil.fetchERC20Balance({ caipAddress: mockCaipAddress, assetAddress: mockAssetAddress, caipNetwork: mockCaipNetwork })).rejects.toThrow('Network error'); expect(ViemUtil.createViemPublicClient).toHaveBeenCalledWith(mockCaipNetwork); expect(mockPublicClient.multicall).toHaveBeenCalled(); }); it('should handle different decimal values correctly', async () => { const mockMulticallResult = [ { result: 'USDC Token' }, { result: 'USDC' }, { result: BigInt('1000000') }, { result: 6 } ]; mockPublicClient.multicall.mockResolvedValue(mockMulticallResult); const result = await BalanceUtil.fetchERC20Balance({ caipAddress: mockCaipAddress, assetAddress: mockAssetAddress, caipNetwork: mockCaipNetwork }); expect(result).toEqual({ name: 'USDC Token', symbol: 'USDC', decimals: 6, balance: '1' }); }); it('should handle large balance values correctly', async () => { const mockMulticallResult = [ { result: 'Large Token' }, { result: 'LARGE' }, { result: BigInt('123456789012345678901234567890') }, { result: 18 } ]; mockPublicClient.multicall.mockResolvedValue(mockMulticallResult); const result = await BalanceUtil.fetchERC20Balance({ caipAddress: mockCaipAddress, assetAddress: mockAssetAddress, caipNetwork: mockCaipNetwork }); expect(result).toEqual({ name: 'Large Token', symbol: 'LARGE', decimals: 18, balance: '123456789012.34567890123456789' }); }); }); }); //# sourceMappingURL=BalanceUtil.test.js.map