@lifi/sdk 3.16.2 → 3.17.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/_cjs/core/EVM/getEVMBalance.js +8 -1
  3. package/src/_cjs/core/EVM/getEVMBalance.js.map +1 -1
  4. package/src/_cjs/core/Solana/getSolanaBalance.js +21 -18
  5. package/src/_cjs/core/Solana/getSolanaBalance.js.map +1 -1
  6. package/src/_cjs/core/Sui/getSuiBalance.js +9 -11
  7. package/src/_cjs/core/Sui/getSuiBalance.js.map +1 -1
  8. package/src/_cjs/core/UTXO/getUTXOBalance.js +10 -3
  9. package/src/_cjs/core/UTXO/getUTXOBalance.js.map +1 -1
  10. package/src/_cjs/core/checkBalance.js +106 -23
  11. package/src/_cjs/core/checkBalance.js.map +1 -1
  12. package/src/_cjs/services/api.js +6 -3
  13. package/src/_cjs/services/api.js.map +1 -1
  14. package/src/_cjs/version.js +1 -1
  15. package/src/_cjs/version.js.map +1 -1
  16. package/src/_esm/core/EVM/getEVMBalance.js +10 -1
  17. package/src/_esm/core/EVM/getEVMBalance.js.map +1 -1
  18. package/src/_esm/core/Solana/getSolanaBalance.js +24 -18
  19. package/src/_esm/core/Solana/getSolanaBalance.js.map +1 -1
  20. package/src/_esm/core/Sui/getSuiBalance.js +11 -11
  21. package/src/_esm/core/Sui/getSuiBalance.js.map +1 -1
  22. package/src/_esm/core/UTXO/getUTXOBalance.js +12 -3
  23. package/src/_esm/core/UTXO/getUTXOBalance.js.map +1 -1
  24. package/src/_esm/core/checkBalance.js +126 -25
  25. package/src/_esm/core/checkBalance.js.map +1 -1
  26. package/src/_esm/services/api.js +6 -3
  27. package/src/_esm/services/api.js.map +1 -1
  28. package/src/_esm/version.js +1 -1
  29. package/src/_esm/version.js.map +1 -1
  30. package/src/_types/core/UTXO/getUTXOBalance.d.ts.map +1 -1
  31. package/src/_types/core/checkBalance.d.ts +13 -1
  32. package/src/_types/core/checkBalance.d.ts.map +1 -1
  33. package/src/_types/services/api.d.ts.map +1 -1
  34. package/src/_types/version.d.ts +1 -1
  35. package/src/_types/version.d.ts.map +1 -1
  36. package/src/core/EVM/getEVMBalance.ts +10 -1
  37. package/src/core/Solana/getSolanaBalance.ts +23 -16
  38. package/src/core/Sui/getSuiBalance.ts +10 -10
  39. package/src/core/UTXO/getUTXOBalance.ts +15 -3
  40. package/src/core/checkBalance.ts +165 -33
  41. package/src/services/api.ts +6 -3
  42. package/src/version.ts +1 -1
@@ -1,45 +1,177 @@
1
- import type { LiFiStep } from '@lifi/types'
2
- import { formatUnits } from 'viem'
1
+ import type { LiFiStep, Token, TokenAmount } from '@lifi/types'
2
+ import { formatUnits, withTimeout } from 'viem'
3
+ import { config } from '../config.js'
3
4
  import { BalanceError } from '../errors/errors.js'
4
- import { getTokenBalance } from '../services/balance.js'
5
5
  import { sleep } from '../utils/sleep.js'
6
6
 
7
+ const MAX_ATTEMPTS = 6
8
+ // Exponential backoff: 150, 300, 600, 1200, 2400 → ≈4.65s of sleep total.
9
+ const BACKOFF_BASE_MS = 150
10
+ const OVERALL_TIMEOUT_MS = 10_000
11
+ const SLIPPAGE_PRECISION = 1_000_000_000n
12
+
13
+ type Requirement = {
14
+ token: Token
15
+ sourcePart: bigint // 0n for pure overhead tokens
16
+ overheadPart: bigint // gas + non-included fees in this token
17
+ }
18
+
19
+ /**
20
+ * Verifies that the wallet holds enough of every token required to execute
21
+ * the step on its source chain — the source-token amount, any gas costs, and
22
+ * any non-included fee costs. Reads all balances in one batched provider
23
+ * call, retries within a bounded budget to absorb transient RPC failures and
24
+ * post-confirmation propagation lag, and applies slippage to the source-token
25
+ * portion only as a last resort (overhead is never trimmed).
26
+ *
27
+ * Throws BalanceError("The balance is too low.") on a genuine shortfall, or
28
+ * BalanceError("Could not read wallet balance.") if the balance can't be read
29
+ * after retries.
30
+ */
7
31
  export const checkBalance = async (
8
32
  walletAddress: string,
9
- step: LiFiStep,
10
- depth = 0
33
+ step: LiFiStep
11
34
  ): Promise<void> => {
12
- const token = await getTokenBalance(walletAddress, step.action.fromToken)
13
- if (token) {
14
- const currentBalance = token.amount ?? 0n
15
- const neededBalance = BigInt(step.action.fromAmount)
16
-
17
- if (currentBalance < neededBalance) {
18
- if (depth <= 3) {
19
- await sleep(200)
20
- await checkBalance(walletAddress, step, depth + 1)
21
- } else if (
22
- (neededBalance *
23
- BigInt((1 - (step.action.slippage ?? 0)) * 1_000_000_000)) /
24
- 1_000_000_000n <=
25
- currentBalance
26
- ) {
27
- // adjust amount in slippage limits
28
- step.action.fromAmount = currentBalance.toString()
29
- } else {
30
- const needed = formatUnits(neededBalance, token.decimals)
31
- const current = formatUnits(currentBalance, token.decimals)
32
- let errorMessage = `Your ${token.symbol} balance is too low, you try to transfer ${needed} ${token.symbol}, but your wallet only holds ${current} ${token.symbol}. No funds have been sent.`
33
-
34
- if (currentBalance !== 0n) {
35
- errorMessage += `If the problem consists, please delete this transfer and start a new one with a maximum of ${current} ${token.symbol}.`
35
+ const fromChainId = step.action.fromChainId
36
+ const requirements = new Map<string, Requirement>()
37
+ const add = (token: Token, amount: bigint, source: boolean): void => {
38
+ if (token.chainId !== fromChainId || amount === 0n) {
39
+ return
40
+ }
41
+ const key = token.address.toLowerCase()
42
+ const req = requirements.get(key) ?? {
43
+ token,
44
+ sourcePart: 0n,
45
+ overheadPart: 0n,
46
+ }
47
+ if (source) {
48
+ req.sourcePart += amount
49
+ } else {
50
+ req.overheadPart += amount
51
+ }
52
+ requirements.set(key, req)
53
+ }
54
+ add(step.action.fromToken, BigInt(step.action.fromAmount), true)
55
+ for (const gas of step.estimate?.gasCosts ?? []) {
56
+ add(gas.token, BigInt(gas.amount), false)
57
+ }
58
+ for (const fee of step.estimate?.feeCosts ?? []) {
59
+ // Included fees are already part of fromAmount — don't count twice.
60
+ if (!fee.included) {
61
+ add(fee.token, BigInt(fee.amount), false)
62
+ }
63
+ }
64
+ if (requirements.size === 0) {
65
+ return
66
+ }
67
+
68
+ // Provider is dispatched by wallet address; all requirements share the
69
+ // source chain, which matches this provider by virtue of the address.
70
+ const provider = config
71
+ .get()
72
+ .providers.find((p) => p.isAddress(walletAddress))
73
+ if (!provider) {
74
+ throw new Error(`SDK Token Provider for ${walletAddress} is not found.`)
75
+ }
76
+
77
+ const reqs = Array.from(requirements.values())
78
+ const tokens = reqs.map((r) => r.token)
79
+ const slippage = step.action.slippage ?? 0
80
+ const slippageScaled = BigInt(
81
+ Math.floor((1 - slippage) * Number(SLIPPAGE_PRECISION))
82
+ )
83
+
84
+ await withTimeout(
85
+ async () => {
86
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
87
+ const isFinal = attempt === MAX_ATTEMPTS - 1
88
+
89
+ let balances: TokenAmount[]
90
+ try {
91
+ balances = await provider.getBalance(walletAddress, tokens)
92
+ } catch (error) {
93
+ if (isFinal) {
94
+ throw new BalanceError(
95
+ 'Could not read wallet balance.',
96
+ error as Error
97
+ )
98
+ }
99
+ await sleep(BACKOFF_BASE_MS * 2 ** attempt)
100
+ continue
36
101
  }
37
102
 
38
- throw new BalanceError(
39
- 'The balance is too low.',
40
- new Error(errorMessage)
103
+ const balanceByAddress = new Map(
104
+ balances.map((b) => [b.address.toLowerCase(), b.amount] as const)
41
105
  )
106
+
107
+ const unknown: Token[] = []
108
+ const insufficient: { req: Requirement; have: bigint }[] = []
109
+ for (const req of reqs) {
110
+ const have = balanceByAddress.get(req.token.address.toLowerCase())
111
+ if (have === undefined) {
112
+ unknown.push(req.token)
113
+ } else if (have < req.sourcePart + req.overheadPart) {
114
+ insufficient.push({ req, have })
115
+ }
116
+ }
117
+
118
+ if (unknown.length === 0 && insufficient.length === 0) {
119
+ return
120
+ }
121
+
122
+ // Final-attempt slippage rescue: only when the sole shortfall is the
123
+ // source-token portion. Trim source down to (balance − overhead) so
124
+ // the overhead reserve is preserved.
125
+ if (
126
+ isFinal &&
127
+ unknown.length === 0 &&
128
+ insufficient.length === 1 &&
129
+ insufficient[0].req.sourcePart > 0n
130
+ ) {
131
+ const { req, have } = insufficient[0]
132
+ const minAcceptable =
133
+ (req.sourcePart * slippageScaled) / SLIPPAGE_PRECISION +
134
+ req.overheadPart
135
+ if (have >= minAcceptable) {
136
+ step.action.fromAmount = (have - req.overheadPart).toString()
137
+ return
138
+ }
139
+ }
140
+
141
+ if (isFinal) {
142
+ if (unknown.length > 0) {
143
+ throw new BalanceError(
144
+ 'Could not read wallet balance.',
145
+ new Error(
146
+ `Could not read balance for: ${unknown
147
+ .map((t) => t.symbol || t.address)
148
+ .join(', ')}.`
149
+ )
150
+ )
151
+ }
152
+ const lines = insufficient.map(({ req, have }) => {
153
+ const needed = formatUnits(
154
+ req.sourcePart + req.overheadPart,
155
+ req.token.decimals
156
+ )
157
+ const current = formatUnits(have, req.token.decimals)
158
+ const symbol = req.token.symbol
159
+ return req.sourcePart > 0n
160
+ ? `Your ${symbol} balance is too low, you try to transfer ${needed} ${symbol}, but your wallet only holds ${current} ${symbol}.`
161
+ : `Insufficient ${symbol} for fees: need ${needed} ${symbol}, have ${current} ${symbol}.`
162
+ })
163
+ throw new BalanceError(
164
+ 'The balance is too low.',
165
+ new Error(`${lines.join(' ')} No funds have been sent.`)
166
+ )
167
+ }
168
+
169
+ await sleep(BACKOFF_BASE_MS * 2 ** attempt)
42
170
  }
171
+ },
172
+ {
173
+ timeout: OVERALL_TIMEOUT_MS,
174
+ errorInstance: new BalanceError('Could not read wallet balance.'),
43
175
  }
44
- }
176
+ )
45
177
  }
@@ -503,7 +503,8 @@ export const getChains = async (
503
503
  ): Promise<ExtendedChain[]> => {
504
504
  if (params) {
505
505
  for (const key of Object.keys(params)) {
506
- if (!params[key as keyof ChainsRequest]) {
506
+ const value = params[key as keyof ChainsRequest]
507
+ if (value === undefined || value === null) {
507
508
  delete params[key as keyof ChainsRequest]
508
509
  }
509
510
  }
@@ -544,7 +545,8 @@ export async function getTokens(
544
545
  ): Promise<TokensResponse> {
545
546
  if (params) {
546
547
  for (const key of Object.keys(params)) {
547
- if (!params[key as keyof TokensRequest]) {
548
+ const value = params[key as keyof TokensRequest]
549
+ if (value === undefined || value === null) {
548
550
  delete params[key as keyof TokensRequest]
549
551
  }
550
552
  }
@@ -611,7 +613,8 @@ export const getTools = async (
611
613
  ): Promise<ToolsResponse> => {
612
614
  if (params) {
613
615
  for (const key of Object.keys(params)) {
614
- if (!params[key as keyof ToolsRequest]) {
616
+ const value = params[key as keyof ToolsRequest]
617
+ if (value === undefined || value === null) {
615
618
  delete params[key as keyof ToolsRequest]
616
619
  }
617
620
  }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/sdk'
2
- export const version = '3.16.2'
2
+ export const version = '3.17.0-beta.0'