From 2f0b4fc80549424a21e7ad73a205e4467e896706 Mon Sep 17 00:00:00 2001 From: KL Date: Tue, 29 Sep 2026 23:15:36 +0800 Subject: [PATCH] fix(widget): distinguish balance read failures from insufficient funds --- .changeset/balance-read-error.md | 5 + packages/widget/src/i18n/en.json | 2 + .../widget/src/utils/getErrorMessage.test.ts | 110 ++++++++++++++++++ packages/widget/src/utils/getErrorMessage.ts | 6 + 4 files changed, 123 insertions(+) create mode 100644 .changeset/balance-read-error.md create mode 100644 packages/widget/src/utils/getErrorMessage.test.ts diff --git a/.changeset/balance-read-error.md b/.changeset/balance-read-error.md new file mode 100644 index 000000000..30f347fef --- /dev/null +++ b/.changeset/balance-read-error.md @@ -0,0 +1,5 @@ +--- +"@lifi/widget": patch +--- + +Distinguish unavailable wallet balances from insufficient funds when a balance check fails. diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index 6cfd97481..66fbde0f4 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -183,6 +183,7 @@ }, "error": { "message": { + "balanceReadFailed": "Your wallet balance could not be checked. Please try again.", "allowanceRequired": "Transfer amount for {{tokenSymbol}} exceeds your current allowance. Please increase your allowance and try again.", "insufficientFunds": "You don't have enough gas to cover the cost of the transaction.", "rateLimitExceeded": "Too many requests have been made in a short period of time. Please wait a moment and try again or contact support if the issue persists.", @@ -203,6 +204,7 @@ "title": { "allowanceRequired": "Insufficient allowance", "balanceIsTooLow": "The balance is too low", + "balanceReadFailed": "Could not read wallet balance", "bookmarkAlreadyExists": "Wallet is already bookmarked as {{name}}", "bookmarkNameRequired": "Bookmark name is required", "chainSwitch": "Chain switch required", diff --git a/packages/widget/src/utils/getErrorMessage.test.ts b/packages/widget/src/utils/getErrorMessage.test.ts new file mode 100644 index 000000000..29d2303c7 --- /dev/null +++ b/packages/widget/src/utils/getErrorMessage.test.ts @@ -0,0 +1,110 @@ +import { + BalanceError, + CheckBalanceTask, + type ExecutionAction, + LiFiErrorCode, + type LiFiStepExtended, + SDKError, + type Token, +} from '@lifi/sdk' +import { createInstance } from 'i18next' +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import en from '../i18n/en.json' +import { getErrorMessage } from './getErrorMessage.js' + +const i18n = createInstance() +const token: Token = { + chainId: 1, + address: '0x1111111111111111111111111111111111111111', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + priceUSD: '1', +} +const step = { + action: { + fromAddress: '0x2222222222222222222222222222222222222222', + fromChainId: 1, + fromToken: token, + fromAmount: '1000000', + }, + estimate: {}, +} as LiFiStepExtended + +beforeAll(async () => { + await i18n.init({ lng: 'en', resources: { en: { translation: en } } }) +}) +afterEach(() => vi.useRealTimers()) + +const renderError = (error: ExecutionAction['error']) => + getErrorMessage(i18n.t, () => undefined, step, { + type: 'SWAP', + status: 'FAILED', + error, + }) + +describe('balance error messages', () => { + it.each(['RPC rejection', 'missing balance', 'timeout', 'shortfall'])( + 'classifies the SDK balance error for %s', + async (scenario) => { + vi.useFakeTimers() + const getBalance = vi.fn(async () => { + if (scenario === 'RPC rejection') { + throw new Error('RPC unavailable') + } + if (scenario === 'timeout') { + return new Promise(() => {}) + } + return [{ ...token, amount: scenario === 'shortfall' ? 0n : undefined }] + }) + const pending = new CheckBalanceTask() + .run({ + client: { + getChainById: async () => ({ id: 1 }), + providers: [{ isAddress: () => true, getBalance }], + }, + step, + statusManager: { initializeAction: vi.fn() }, + isBridgeExecution: false, + } as never) + .catch((error: unknown) => error) + await vi.runAllTimersAsync() + const error = await pending + expect(error).toBeInstanceOf(BalanceError) + const parsed = new SDKError(error as BalanceError) + // BaseStepExecutor stores only these fields on a failed execution action. + const result = renderError({ + code: parsed.code, + message: parsed.cause.message, + }) + + if (scenario === 'shortfall') { + expect(result.title).toBe('The balance is too low') + } else { + expect(result.title).toBe('Could not read wallet balance') + expect(result.message).toBe( + 'Your wallet balance could not be checked. Please try again.' + ) + expect(result.message).not.toContain('remain in your wallet') + } + } + ) + + it('retains the existing handling for other balance errors', () => { + expect( + renderError({ + code: LiFiErrorCode.BalanceError, + message: 'Balance check failed', + }).title + ).toBe('The balance is too low') + }) + + it('does not classify unrelated error codes by their message', () => { + expect( + renderError({ + code: LiFiErrorCode.InsufficientFunds, + message: 'Could not read wallet balance.', + }).title + ).toBe('Insufficient funds') + }) +}) diff --git a/packages/widget/src/utils/getErrorMessage.ts b/packages/widget/src/utils/getErrorMessage.ts index 8b40efd1f..eeff8311d 100644 --- a/packages/widget/src/utils/getErrorMessage.ts +++ b/packages/widget/src/utils/getErrorMessage.ts @@ -44,6 +44,12 @@ export function getErrorMessage( }) break case LiFiErrorCode.BalanceError: + // The SDK uses BalanceError for both read failures and insufficient funds. + if (error.message === 'Could not read wallet balance.') { + title = t('error.title.balanceReadFailed') + message = t('error.message.balanceReadFailed') + break + } title = t('error.title.balanceIsTooLow') message = getDefaultErrorMessage() break