diff --git a/examples/react/src/utils/cofhe.config.tsx b/examples/react/src/utils/cofhe.config.tsx index e035263f..d209f4a6 100644 --- a/examples/react/src/utils/cofhe.config.tsx +++ b/examples/react/src/utils/cofhe.config.tsx @@ -1,17 +1,44 @@ -import { CofheProvider, useInternalQueryClient } from '@cofhe/react'; +import { CofheProvider, createCofheConfig, useInternalQueryClient } from '@cofhe/react'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { usePublicClient, useWalletClient } from 'wagmi'; +import { useAccount, usePublicClient, useWalletClient } from 'wagmi'; +import { baseSepolia, sepolia } from '@cofhe/sdk/chains'; function QueryDebug() { const cofheQueryClient = useInternalQueryClient(); return ; } +const cofheConfig = createCofheConfig({ + supportedChains: [ + sepolia, + // baseSepolia + ], + react: { + // pinnedTokens: { + // 11155111: '0x87A3effB84CBE1E4caB6Ab430139eC41d156D55A', // sepolia weth + // 84532: '0xbED96aa98a49FeA71fcC55d755b915cF022a9159', // base sepolia weth + // }, + // tokenLists: { + // 11155111: ['https://storage.googleapis.com/cofhesdk/sepolia.json'], + // 84532: ['https://storage.googleapis.com/cofhesdk/base-sepolia.json'], + // 421613: ['https://tokens.cofhe.io/arbitrum-sepolia.json'], + // }, + }, +}); export const CofheProviderLocal = ({ children }: { children: React.ReactNode }) => { const wagmiPublicClient = usePublicClient(); const { data: wagmiWalletClient } = useWalletClient(); + + const { chain } = useAccount(); + + const isConnectedToWagmiSupportedChain = chain !== undefined; + return ( - + {children} diff --git a/examples/react/src/utils/wagmi.tsx b/examples/react/src/utils/wagmi.tsx index bdece762..e2a44f41 100644 --- a/examples/react/src/utils/wagmi.tsx +++ b/examples/react/src/utils/wagmi.tsx @@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; export const injectedProvider = injected({ shimDisconnect: true }); const config = createConfig({ - chains: [baseSepolia, sepolia], + chains: [sepolia, baseSepolia], transports: { [sepolia.id]: http(), [baseSepolia.id]: http(), diff --git a/packages/hardhat-plugin-test/test/integration-base-sepolia.test.ts b/packages/hardhat-plugin-test/test/integration-base-sepolia.test.ts index 85a7d549..6d11a1df 100644 --- a/packages/hardhat-plugin-test/test/integration-base-sepolia.test.ts +++ b/packages/hardhat-plugin-test/test/integration-base-sepolia.test.ts @@ -29,7 +29,7 @@ describe('Base Sepolia Integration Tests', () => { before(async function () { // Skip if no private key is provided (for CI/CD) - if (!process.env.BASE_SEPOLIA_PRIVATE_KEY && process.env.CI) { + if (!process.env.TEST_PRIVATE_KEY && process.env.CI) { this.skip(); } @@ -90,7 +90,7 @@ describe('Base Sepolia Integration Tests', () => { it('Should encrypt -> store -> decrypt a value', async function () { // Skip if no private key is provided - if (!process.env.BASE_SEPOLIA_PRIVATE_KEY && process.env.CI) { + if (!process.env.TEST_PRIVATE_KEY && process.env.CI) { this.skip(); } diff --git a/packages/react/src/components/FnxFloatingButton/pages/MainPage/AssetCard.tsx b/packages/react/src/components/FnxFloatingButton/pages/MainPage/AssetCard.tsx index 6e8fc1a4..27335de5 100644 --- a/packages/react/src/components/FnxFloatingButton/pages/MainPage/AssetCard.tsx +++ b/packages/react/src/components/FnxFloatingButton/pages/MainPage/AssetCard.tsx @@ -1,36 +1,28 @@ import { cn } from '@/utils/cn'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; -import { useCofheToken } from '@/hooks/useCofheTokenLists'; import { TokenIcon } from '../../components/TokenIcon'; import { CofheTokenConfidentialBalance } from '../../components/CofheTokenConfidentialBalance'; -import { useCofhePinnedTokenAddress } from '@/hooks/useCofhePinnedTokenAddress'; import { FloatingButtonPage } from '../../pagesConfig/types'; import { usePortalNavigation } from '@/stores'; +import { useCofhePinnedToken } from '@/hooks/useCofhePinnedToken'; +import { useCofheChainId } from '@/hooks/useCofheConnection'; +import { DEFAULT_TOKEN_BY_CHAIN_ID } from '@/types/token'; export const AssetCard: React.FC = () => { - // TODO: show Native token if no pinned token address - const { navigateTo } = usePortalNavigation(); - // const pinnedTokenAddress = "0x8ee52408ED5b0e396aA779Fd52F7fbc20A4b33Fb"; // Base sepolia - // const pinnedTokenAddress = "0xbED96aa98a49FeA71fcC55d755b915cF022a9159"; // Redact (Sepolia) - const pinnedTokenAddress = useCofhePinnedTokenAddress(); - // Find token from token lists to get icon and confidentialityType - const tokenFromList = useCofheToken({ - address: pinnedTokenAddress, - }); + const pinnedToken = useCofhePinnedToken(); + const chainId = useCofheChainId(); + const fallbackToken = chainId ? DEFAULT_TOKEN_BY_CHAIN_ID[chainId] : undefined; + const token = pinnedToken ?? fallbackToken; const handleClick = () => { - // TODO: figure out best handling for this error - if (!tokenFromList) throw new Error('Token not found in token list'); - - if (pinnedTokenAddress) { + if (token) { navigateTo(FloatingButtonPage.TokenInfo, { - pageProps: { token: tokenFromList }, + pageProps: { token }, }); } else { - // TODO: native token support - alert('Native token info navigation is not implemented yet.'); + throw new Error('No token found for AssetCard. No pinned token and no fallback token for current chain.'); } }; @@ -43,11 +35,11 @@ export const AssetCard: React.FC = () => { {/* Left Side: Icon, Ticker, Privacy Metrics */}
{/* Asset Icon */} - + {/* Ticker and Privacy */}
-

{tokenFromList?.symbol}

+

{token?.symbol}

{/* Privacy Metrics Placeholder for future implementation */} {/*
@@ -69,7 +61,7 @@ export const AssetCard: React.FC = () => {
{/* // TODO: add suport for displaying native token balance if no pinned address */} { const { navigateTo } = usePortalNavigation(); const { openPortal } = usePortalUI(); - const defaultToken = useCofhePinnedToken(); + const pinnedToken = useCofhePinnedToken(); + const chainId = useCofheChainId(); + const fallbackToken = chainId ? DEFAULT_TOKEN_BY_CHAIN_ID[chainId] : undefined; + const defaultToken = pinnedToken ?? fallbackToken; const handleNavClick = (page: ElementOf['id']) => { openPortal(); diff --git a/packages/react/src/config.ts b/packages/react/src/config.ts index 2a9d4954..6854f6be 100644 --- a/packages/react/src/config.ts +++ b/packages/react/src/config.ts @@ -35,20 +35,11 @@ export const CofheReactConfigSchema = z.object({ { label: '1 Month', intervalSeconds: 2592000 }, ]), defaultPermitExpirationSeconds: z.number().optional().default(604800), // 1 week - pinnedTokens: z.record(z.string(), addressSchema).optional().default({ - 11155111: '0x87A3effB84CBE1E4caB6Ab430139eC41d156D55A', // sepolia weth - 84532: '0xbED96aa98a49FeA71fcC55d755b915cF022a9159', // base sepolia weth - // 421613: '0x980b62da83eff3d4576c647993b0c1d7faf17c73', // arbitrum sepolia weth - }), + pinnedTokens: z.record(z.string(), addressSchema).optional(), tokenLists: z .record(z.string(), z.array(z.string())) - .optional() - .default({ - 11155111: ['https://storage.googleapis.com/cofhesdk/sepolia.json'], - 84532: ['https://storage.googleapis.com/cofhesdk/base-sepolia.json'], - // 421613: ['https://tokens.cofhe.io/arbitrum-sepolia.json'], - }) - .transform((lists) => lists as Partial>), + .transform((lists) => lists as Partial>) + .optional(), position: z.enum(['bottom-right', 'bottom-left', 'top-right', 'top-left']).optional().default('bottom-right'), initialTheme: z.enum(['dark', 'light']).optional().default('light'), }); diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 092ac203..cbd379ac 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -18,7 +18,13 @@ export { type ClaimableAmountByTokenAddress, } from './useCofheTokensClaimable'; export { useCofheWalletClient } from './useCofheConnection'; -export { useCofheTokens, useCofheTokenLists, ETH_ADDRESS, type Token, type Erc20Pair } from './useCofheTokenLists'; +export { + useCofheTokens, + useCofheTokenLists, + ETH_ADDRESS_LOWERCASE, + type Token, + type Erc20Pair, +} from './useCofheTokenLists'; export { useCofheTokensWithExistingEncryptedBalances, type UseCofheTokensWithExistingBalancesInput, diff --git a/packages/react/src/hooks/useCofheAutoConnect.ts b/packages/react/src/hooks/useCofheAutoConnect.ts index 10a7f81b..89ab8382 100644 --- a/packages/react/src/hooks/useCofheAutoConnect.ts +++ b/packages/react/src/hooks/useCofheAutoConnect.ts @@ -1,6 +1,6 @@ import type { PublicClient, WalletClient } from 'viem'; import { useCofheClient } from './useCofheClient'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useCofheConnect } from './useCofheConnect'; type Input = { @@ -8,23 +8,36 @@ type Input = { walletClient?: WalletClient; }; export const useCofheAutoConnect = ({ walletClient, publicClient }: Input) => { - // TODO: if the user switches in the wallet to a chain that's not supported by the dapp, should show error message or disconnect? const client = useCofheClient(); const connectMutationFn = useCofheConnect().mutate; + const supportedChainIds = useMemo(() => { + return new Set(client.config.supportedChains.map((chain) => chain.id)); + }, [client.config.supportedChains]); + + const disconnectIfConnected = useCallback(() => { + if (client.connected || client.connecting) { + client.disconnect(); + } + }, [client]); + useEffect(() => { // Keep COFHE connection in sync with the upstream (wagmi) connection. // - if wagmi disconnects, it stops providing clients -> disconnect COFHE // - if wagmi provides clients -> ensure COFHE is connected if (!publicClient || !walletClient) { - if (client.connected || client.connecting) { - client.disconnect(); - } + disconnectIfConnected(); + return; + } + + const chainId = walletClient.chain?.id ?? publicClient.chain?.id; + if (chainId !== undefined && !supportedChainIds.has(chainId)) { + disconnectIfConnected(); return; } if (client.connecting) return; connectMutationFn({ publicClient, walletClient }); - }, [publicClient, walletClient, client, client.connected, client.connecting, connectMutationFn]); + }, [publicClient, walletClient, client, connectMutationFn, supportedChainIds, disconnectIfConnected]); }; diff --git a/packages/react/src/hooks/useCofhePinnedTokenAddress.ts b/packages/react/src/hooks/useCofhePinnedTokenAddress.ts index db675fd2..d6f4ccef 100644 --- a/packages/react/src/hooks/useCofhePinnedTokenAddress.ts +++ b/packages/react/src/hooks/useCofhePinnedTokenAddress.ts @@ -8,6 +8,6 @@ import { useCofheChainId } from './useCofheConnection'; export function useCofhePinnedTokenAddress() { const widgetConfig = useCofheContext().client.config.react; const chainId = useCofheChainId(); - const pinnedTokenAddress = chainId ? widgetConfig.pinnedTokens[chainId?.toString()] : undefined; + const pinnedTokenAddress = chainId ? widgetConfig.pinnedTokens?.[chainId?.toString()] : undefined; return pinnedTokenAddress; } diff --git a/packages/react/src/hooks/useCofheTokenLists.ts b/packages/react/src/hooks/useCofheTokenLists.ts index 22260729..31cdb015 100644 --- a/packages/react/src/hooks/useCofheTokenLists.ts +++ b/packages/react/src/hooks/useCofheTokenLists.ts @@ -1,12 +1,12 @@ import { type UseQueryOptions, type UseQueryResult } from '@tanstack/react-query'; import { useCofheContext } from '../providers/CofheProvider'; import { useMemo } from 'react'; -import { ETH_ADDRESS, type Erc20Pair, type Token } from '../types/token.js'; +import { ETH_ADDRESS_LOWERCASE, type Erc20Pair, type Token } from '../types/token.js'; import { useInternalQueries } from '../providers/index.js'; import type { Address } from 'viem'; import { useCofheChainId } from './useCofheConnection'; -export { ETH_ADDRESS, type Token, type Erc20Pair }; +export { ETH_ADDRESS_LOWERCASE, type Token, type Erc20Pair }; type TokenList = { name: string; @@ -30,7 +30,7 @@ export function useCofheTokenLists( queryOptions?: UseTokenListsOptions ): UseTokenListsResult { const widgetConfig = useCofheContext().client.config.react; - const tokensListsUrls = chainId ? widgetConfig.tokenLists[chainId] : []; + const tokensListsUrls = chainId ? widgetConfig.tokenLists?.[chainId] : []; const queriesOptions: UseQueryOptions[] = tokensListsUrls?.map((url) => ({ diff --git a/packages/react/src/hooks/useCofheTokenPublicBalance.ts b/packages/react/src/hooks/useCofheTokenPublicBalance.ts index e7c401ea..3299da50 100644 --- a/packages/react/src/hooks/useCofheTokenPublicBalance.ts +++ b/packages/react/src/hooks/useCofheTokenPublicBalance.ts @@ -1,7 +1,7 @@ import { type UseQueryOptions } from '@tanstack/react-query'; import { type Address } from 'viem'; import { useCofheAccount, useCofhePublicClient } from './useCofheConnection'; -import { type Token, ETH_ADDRESS } from './useCofheTokenLists'; +import { type Token, ETH_ADDRESS_LOWERCASE } from './useCofheTokenLists'; import { ERC20_BALANCE_OF_ABI } from '../constants/erc20ABIs'; import { assert } from 'ts-essentials'; import { useInternalQuery } from '../providers/index'; @@ -92,7 +92,7 @@ export function createPublicTokenBalanceQueryOptions(par assert(publicClient, 'PublicClient is required to fetch token balance'); assert(accountAddress, 'Account address is required to fetch token balance'); - const isNativeToken = tokenAddress.toLowerCase() === ETH_ADDRESS.toLowerCase(); + const isNativeToken = tokenAddress.toLowerCase() === ETH_ADDRESS_LOWERCASE; const balance = isNativeToken ? publicClient.getBalance({ diff --git a/packages/react/src/hooks/useCofheTokenShield.ts b/packages/react/src/hooks/useCofheTokenShield.ts index 84d33f90..a02bd1b3 100644 --- a/packages/react/src/hooks/useCofheTokenShield.ts +++ b/packages/react/src/hooks/useCofheTokenShield.ts @@ -6,7 +6,7 @@ import { } from '@tanstack/react-query'; import { type Address } from 'viem'; import { useCofheWalletClient, useCofheChainId, useCofheAccount, useCofhePublicClient } from './useCofheConnection.js'; -import { type Token, ETH_ADDRESS } from './useCofheTokenLists.js'; +import { type Token, ETH_ADDRESS_LOWERCASE } from './useCofheTokenLists.js'; import { SHIELD_ABIS, UNSHIELD_ABIS, @@ -93,7 +93,7 @@ export function useCofheTokenShield( if (confidentialityType === 'wrapped') { // Check if this is a wrapped ETH token (erc20Pair is ETH_ADDRESS) const erc20PairAddress = input.token.extensions.fhenix.erc20Pair?.address as Address | undefined; - const isEth = erc20PairAddress?.toLowerCase() === ETH_ADDRESS.toLowerCase(); + const isEth = erc20PairAddress?.toLowerCase() === ETH_ADDRESS_LOWERCASE; if (isEth) { // For ETH: use encryptETH(address to) with value diff --git a/packages/react/src/hooks/useTrackPendingTransactions.ts b/packages/react/src/hooks/useTrackPendingTransactions.ts index b35c45fc..9a753b45 100644 --- a/packages/react/src/hooks/useTrackPendingTransactions.ts +++ b/packages/react/src/hooks/useTrackPendingTransactions.ts @@ -11,7 +11,7 @@ import { constructCofheReadContractQueryForInvalidation } from './useCofheReadCo import { QueryClient, type QueriesOptions } from '@tanstack/react-query'; import type { Address, TransactionReceipt } from 'viem'; import { getTokenContractConfig } from '@/constants/confidentialTokenABIs'; -import type { Token } from './useCofheTokenLists'; +import { ETH_ADDRESS_LOWERCASE, type Token } from './useCofheTokenLists'; import { constructPublicTokenBalanceQueryKeyForInvalidation } from './useCofheTokenPublicBalance'; import { constructUnshieldClaimsQueryKeyForInvalidation, invalidateClaimableQueries } from './useCofheTokenClaimable'; import { usePendingTransactions } from './usePendingTransactions'; @@ -131,7 +131,15 @@ function useHandleInvalidations() { const { upsert: upsertDecryptionWatcher, byKey } = useDecryptionWatchersStore(); console.log('Scheduled invalidations store:', byKey); const handleInvalidations = (tx: Transaction) => { - // TODO invalidate gas on all txs since any tx spends gas + // each transaction requires gas, so native token balance changes on every transaction + invalidatePublicTokenBalanceQueries( + { + tokenAddress: ETH_ADDRESS_LOWERCASE, + chainId: tx.chainId, + accountAddress: tx.account, + }, + queryClient + ); if (tx.actionType === TransactionActionType.ShieldSend) { invalidateConfidentialTokenBalanceQueries(tx.token, queryClient); } else if (tx.actionType === TransactionActionType.Shield) { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 43894c4f..0292aced 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -28,7 +28,7 @@ export { useCofheTokenDecryptedBalance, useCofheTokenTransfer, useTransactionReceiptsByHash, - ETH_ADDRESS, + ETH_ADDRESS_LOWERCASE, type Erc20Pair, type UnshieldClaim, type UnshieldClaimsSummary, diff --git a/packages/react/src/types/token.ts b/packages/react/src/types/token.ts index adcd2043..5e4c691c 100644 --- a/packages/react/src/types/token.ts +++ b/packages/react/src/types/token.ts @@ -5,12 +5,25 @@ * to avoid circular dependencies. */ +import { baseSepolia, sepolia } from '@cofhe/sdk/chains'; import type { Address } from 'viem'; /** * Special address representing native ETH (used in erc20Pair for ConfidentialETH tokens) */ -export const ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' as const; +export const ETH_ADDRESS_LOWERCASE = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' as const; + +type TokenWithoutExtensions = Omit; +export function constructNativeToken(chainId: number): TokenWithoutExtensions { + return { + chainId, + address: ETH_ADDRESS_LOWERCASE, + name: 'Ether', + symbol: 'ETH', + decimals: 18, + logoURI: 'https://storage.googleapis.com/cofhesdk/token-icons/eth.webp', + }; +} /** * ERC20 pair information for wrapped confidential tokens @@ -40,7 +53,7 @@ export type Token = { }; }; -// just a sample token for examples and quick tests +// source: https://storage.googleapis.com/cofhesdk/sepolia.json export const WETH_SEPOLIA_TOKEN: Token = { chainId: 11155111, address: '0x87A3effB84CBE1E4caB6Ab430139eC41d156D55A', @@ -61,3 +74,30 @@ export const WETH_SEPOLIA_TOKEN: Token = { }, }, }; + +// source: https://storage.googleapis.com/cofhesdk/base-sepolia.json +const WETH_BASE_SEPOLIA_TOKEN: Token = { + chainId: 84532, + address: '0xbED96aa98a49FeA71fcC55d755b915cF022a9159', + name: 'Redact eETH', + symbol: 'eETH', + decimals: 18, + logoURI: 'https://storage.googleapis.com/cofhesdk/token-icons/eth.webp', + extensions: { + coingeckoId: 'eeth', + fhenix: { + confidentialityType: 'wrapped', + confidentialValueType: 'uint128', + erc20Pair: { + address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + symbol: 'ETH', + decimals: 18, + logoURI: 'https://storage.googleapis.com/cofhesdk/token-icons/eth.webp', + }, + }, + }, +}; +export const DEFAULT_TOKEN_BY_CHAIN_ID: Record = { + [sepolia.id]: WETH_SEPOLIA_TOKEN, + [baseSepolia.id]: WETH_BASE_SEPOLIA_TOKEN, +}; diff --git a/packages/react/src/utils/coingecko.ts b/packages/react/src/utils/coingecko.ts index 4f183728..4dee8dda 100644 --- a/packages/react/src/utils/coingecko.ts +++ b/packages/react/src/utils/coingecko.ts @@ -1,5 +1,5 @@ import type { Address } from 'viem'; -import { ETH_ADDRESS } from '@/types/token'; +import { ETH_ADDRESS_LOWERCASE } from '@/types/token'; export const DEFAULT_COINGECKO_API_BASE_URL = 'https://api.coingecko.com/api/v3' as const; @@ -102,7 +102,7 @@ export function parseCoingeckoSimplePriceUsd({ export function isNativeTokenAddress(address: Address | undefined): boolean { if (!address) return false; - return address.toLowerCase() === ETH_ADDRESS.toLowerCase(); + return address.toLowerCase() === ETH_ADDRESS_LOWERCASE; } export const TMP_WBTC_ON_MAINNET = { diff --git a/packages/react/src/utils/utils.ts b/packages/react/src/utils/utils.ts index 3b333a9c..857b12b3 100644 --- a/packages/react/src/utils/utils.ts +++ b/packages/react/src/utils/utils.ts @@ -1,5 +1,5 @@ import * as viemChains from 'viem/chains'; -import { ETH_ADDRESS, type Token, type Erc20Pair } from '../types/token.js'; +import { ETH_ADDRESS_LOWERCASE, type Token, type Erc20Pair } from '../types/token.js'; import type { FheTypeValue } from '@cofhe/sdk'; import { isValidElement } from 'react'; @@ -68,7 +68,7 @@ export const getBlockExplorerTokenUrl = (chainId: number, tokenAddress: string): */ export const isEthPair = (erc20Pair: Erc20Pair | undefined): boolean => { if (!erc20Pair) return false; - return erc20Pair.address.toLowerCase() === ETH_ADDRESS.toLowerCase(); + return erc20Pair.address.toLowerCase() === ETH_ADDRESS_LOWERCASE; }; /**