Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/balance-read-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lifi/widget": patch
---

Distinguish unavailable wallet balances from insufficient funds when a balance check fails.
2 changes: 2 additions & 0 deletions packages/widget/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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",
Expand Down
110 changes: 110 additions & 0 deletions packages/widget/src/utils/getErrorMessage.test.ts
Original file line number Diff line number Diff line change
@@ -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<never>(() => {})
}
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')
})
})
6 changes: 6 additions & 0 deletions packages/widget/src/utils/getErrorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down