From 2a79fa4106324a08c21555c545238f20986dd250 Mon Sep 17 00:00:00 2001 From: Dusk1e Date: Fri, 14 Aug 2026 00:44:14 +0300 Subject: [PATCH] fix(nextjs): parse the unshield amount in confidential decimals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `useUpdateEncryptDecryptValue` parsed the input with `pair.publicToken.decimals` in both directions. In the decrypt direction that value goes straight into `unshield(from, to, uint64 amount)`, which takes the amount in the confidential token's own decimals — the FHERC20 wrapper caps those at 6. The call site in MainTokenSwapping already labels it as such (`tokenDecimals: pair.confidentialToken.decimals`), so only the parse was wrong. For the ETH pair (public 18, confidential 6) typing 1 produced 1e18 where the contract expects 1e6. In practice the form blocked first: `useEncryptDecryptValueError` compares the input against the decrypted confidential balance, which is in 6 decimals, so with 1 eETH held the comparison was 1000000000000000000 > 1000000 and every amount read as insufficient balance. The percent buttons showed the same split from the other side. They stored the correct raw value but `useEncryptDecryptInputValue` rendered it with public decimals, so 50% of 1 eETH appeared as 0.0000000000005 instead of 0.5. `useUpdateEncryptDecryptValueByPercent` already selected decimals by direction, so that expression is now an `inputDecimals(pair, isEncrypt)` helper used by all three. Shield is unchanged: `shield(to, uint256 amount)` does take underlying units and that call site already passes `publicToken.decimals`. Only USDC and EURC ship in the token list and both are 6 decimals, so public and confidential coincide and nothing looks wrong there. --- .../nextjs/services/store/encryptDecrypt.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/nextjs/services/store/encryptDecrypt.ts b/packages/nextjs/services/store/encryptDecrypt.ts index ca564a7..21e8592 100644 --- a/packages/nextjs/services/store/encryptDecrypt.ts +++ b/packages/nextjs/services/store/encryptDecrypt.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from "react"; import { useDecryptValue } from "./decrypted"; -import { useConfidentialTokenPair, useConfidentialTokenPairBalances } from "./tokenStore"; +import { ConfidentialTokenPair, useConfidentialTokenPair, useConfidentialTokenPairBalances } from "./tokenStore"; import { FheTypes } from "@cofhe/sdk"; import { Address, formatUnits, parseUnits } from "viem"; import { useAccount, useChainId } from "wagmi"; @@ -29,6 +29,14 @@ export const useEncryptDecryptStore = create()( })), ); +/** + * Decimals of the amount being entered. Encrypting (shield) moves the underlying token, so the + * amount is in public decimals. Decrypting (unshield) moves the confidential balance, which the + * FHERC20 denominates in its own (capped) decimals. + */ +const inputDecimals = (pair: ConfidentialTokenPair | undefined, isEncrypt: boolean) => + isEncrypt ? (pair?.publicToken.decimals ?? 18) : (pair?.confidentialToken?.decimals ?? 6); + // Actions const selectToken = (chain: number, account: Address, pairPublicToken: Address | null, isEncrypt?: boolean) => { @@ -105,7 +113,7 @@ export const useUpdateEncryptDecryptValue = () => { // Allow only numbers and optional single decimal point, no negatives if (/^\d*\.?\d*$/.test(sanitized)) { try { - const amount = parseUnits(sanitized, pair.publicToken.decimals); + const amount = parseUnits(sanitized, inputDecimals(pair, state.isEncrypt)); if (state.isEncrypt) { state.encryptValue = amount; } else { @@ -134,11 +142,12 @@ export const useEncryptDecryptRawInputValue = () => { export const useEncryptDecryptInputValue = () => { const rawInputValue = useEncryptDecryptRawInputValue(); const pair = useEncryptDecryptPair(); + const isEncrypt = useEncryptDecryptIsEncrypt(); return useMemo(() => { if (pair == null) return ""; - return formatUnits(rawInputValue, pair.publicToken.decimals); - }, [pair, rawInputValue]); + return formatUnits(rawInputValue, inputDecimals(pair, isEncrypt)); + }, [pair, isEncrypt, rawInputValue]); }; export const useEncryptDecryptInputString = () => { @@ -164,10 +173,7 @@ export const useUpdateEncryptDecryptValueByPercent = () => { state.decryptValue = amount; } // Update the input string with the formatted amount - const currentTokenDecimals = !state.isEncrypt - ? (pair?.confidentialToken?.decimals ?? 6) - : (pair?.publicToken.decimals ?? 18); - state.inputString = formatUnits(amount, currentTokenDecimals); + state.inputString = formatUnits(amount, inputDecimals(pair, state.isEncrypt)); }); }, [pair, balances],