feat(sdk): generic balance check with native fee + rent coverage#372
Merged
feat(sdk): generic balance check with native fee + rent coverage#372
Conversation
EMB-334 Rewrite checkBalance to verify the wallet holds enough of every token required to execute the step on its source chain: source amount, gas costs, and non-included fee costs. Reads all balances in one batched provider call and retries within a bounded budget (6 attempts, exponential backoff ≈4.65s, 10s outer timeout) to absorb transient RPC failures and post-confirmation propagation lag. Slippage is applied to the source-token portion only and never trims the overhead reserve. Providers now distinguish "known zero" (RPC succeeded, amount is 0n) from "unknown" (RPC failed, amount is undefined) so the retry loop can tell a flaky RPC apart from a genuinely empty wallet. Applies across EVM multicall sub-calls, Solana Token/Token2022 partial failures, Bitcoin RPC failures, and Sui getAllBalances rejections. Move withTimeout from sdk-provider-solana to @lifi/sdk and re-export it so both the new checkBalance retry wrapper and the Solana sign-and- execute task use the shared utility. Delete dead execution.unit.spec.ts and its handlers (describe.skip'd suite with commented-out assertions and a vi.mock targeting a non- existent path).
Align Solana and Sui int tests with the new provider semantics: a mint/coin-type the wallet doesn't hold resolves to amount: 0n when the underlying RPC call succeeds (previously the tests asserted amount: undefined, which conflated "wallet has none" with "RPC failed"). The inline comments already said "invalid tokens should be returned with balance 0" — the assertions now match the comments. EMB-334
chybisov
added a commit
that referenced
this pull request
Apr 15, 2026
EMB-334 Backport of #372 (v4 main) to v3. Rewrite checkBalance to verify the wallet holds enough of every token required to execute the step on its source chain: source amount, gas costs, and non-included fee costs. Reads all balances in one batched provider call and retries within a bounded budget (6 attempts, exponential backoff, 10s outer timeout). Slippage is applied to the source-token portion only and never trims the overhead reserve. Providers now distinguish "known zero" (amount: 0n) from "unknown" (amount: undefined) so the retry loop can tell a flaky RPC apart from a genuinely empty wallet: - EVM: multicall sub-call failures → undefined - Solana: per-program ok flags; only report 0n when both Token and Token2022 queries succeeded - UTXO: Promise.allSettled; omit amount on RPC failure - Sui: distinguish getAllBalances rejection from "no coins" Update Solana/Sui int tests to assert 0n for not-held tokens (was toBeUndefined — contradicted the inline comments).
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which Linear task is linked to this PR?
EMB-334
Why was it implemented this way?
The SDK's
checkBalanceonly validated the source-token amount. It never verified the wallet had enough of the chain's native gas token to pay fees, and on Solana it didn't check ATA rent. Wallets with sufficient swap-token balance but insufficient native gas slipped past the SDK and hit cryptic "transaction will fail" warnings during wallet simulation (Slack-reported case: ~0.001 SOL when ~0.002 SOL of rent was required).A second bug surfaced alongside: every provider's balance reader silently treated RPC failures as zero balance, so a flaky RPC on a well-funded wallet produced false "Insufficient balance" errors. Fixed in the same change.
Approach
Core (
packages/sdk) — one chain-agnostic helper:provider.getBalancecall.150 * 2^attempt≈ 4.65s total sleep), wrapped in a 10s outerwithTimeout. Slippage applies to the source-token portion only and only on the final attempt; overhead (gas/fees) is never trimmed.Providers — each balance reader now distinguishes:
amount: 0n.amount: undefined.This drives the core helper's retry / error decisions:
bigint).0nfor SPL mints when both Token and Token2022 program queries succeeded (handles PYUSD-on-Token2022 when only Token responded).Promise.allSettledinstead ofPromise.all— an RPC flake no longer sinks the read.getAllBalancesrejection from "wallet holds no coin of this type".withTimeoutconsolidation — moved fromsdk-provider-solanato@lifi/sdk, re-exported. Shared by the new core retry wrapper and the Solana sign-and-execute path.Decisions locked with the user
TokenAmount.amount === undefined⇒ unknown;0n⇒ known zero. No public-type change; fixes the prior?? 0nbug at callsites.BalanceErrorwith distinct message"Could not read wallet balance."vs"The balance is too low.".Out of scope (follow-ups)
AbortSignalthrough provider RPC calls so timed-out reads actually cancel instead of running detached..int.spec.tsagainst live RPCs).Visual showcase (Screenshots or Videos)
N/A — backend logic change.
Checklist before requesting a review
Test plan
pnpm check— greenpnpm check:types— greenpnpm test:unit— 313 passing, 5 skipped (no regressions); includes 18 newcheckBalancetests, 4 newwithTimeouttests, 2 new EVM multicall tests*.int.spec.tsagainst live RPCs to verify the "known zero" changes don't regress live readsBalanceErrorbefore wallet simulation. Top up to ≥0.005 SOL → flow proceeds. Kill RPC mid-check → expect "Could not read wallet balance." (not a false "balance too low")