@lifi/sdk 4.0.0-beta.5 → 4.0.0-beta.7

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 (36) hide show
  1. package/dist/cjs/client/getClientStorage.js +2 -1
  2. package/dist/cjs/client/getClientStorage.js.map +1 -1
  3. package/dist/cjs/core/tasks/helpers/checkBalance.d.ts +13 -1
  4. package/dist/cjs/core/tasks/helpers/checkBalance.js +87 -17
  5. package/dist/cjs/core/tasks/helpers/checkBalance.js.map +1 -1
  6. package/dist/cjs/index.d.ts +2 -1
  7. package/dist/cjs/index.js +2 -0
  8. package/dist/cjs/utils/checkPackageUpdates.js +1 -1
  9. package/dist/cjs/utils/withTimeout.d.ts +26 -0
  10. package/dist/cjs/utils/withTimeout.js +35 -0
  11. package/dist/cjs/utils/withTimeout.js.map +1 -0
  12. package/dist/cjs/version.d.ts +1 -1
  13. package/dist/cjs/version.js +1 -1
  14. package/dist/cjs/version.js.map +1 -1
  15. package/dist/esm/client/getClientStorage.js +2 -1
  16. package/dist/esm/client/getClientStorage.js.map +1 -1
  17. package/dist/esm/core/tasks/helpers/checkBalance.d.ts +13 -1
  18. package/dist/esm/core/tasks/helpers/checkBalance.d.ts.map +1 -1
  19. package/dist/esm/core/tasks/helpers/checkBalance.js +87 -17
  20. package/dist/esm/core/tasks/helpers/checkBalance.js.map +1 -1
  21. package/dist/esm/index.d.ts +2 -1
  22. package/dist/esm/index.js +2 -1
  23. package/dist/esm/utils/checkPackageUpdates.js +1 -1
  24. package/dist/esm/utils/withTimeout.d.ts +26 -0
  25. package/dist/esm/utils/withTimeout.d.ts.map +1 -0
  26. package/dist/esm/utils/withTimeout.js +34 -0
  27. package/dist/esm/utils/withTimeout.js.map +1 -0
  28. package/dist/esm/version.d.ts +1 -1
  29. package/dist/esm/version.js +1 -1
  30. package/dist/esm/version.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/client/getClientStorage.ts +1 -0
  33. package/src/core/tasks/helpers/checkBalance.ts +161 -35
  34. package/src/index.ts +1 -0
  35. package/src/utils/withTimeout.ts +50 -0
  36. package/src/version.ts +1 -1
@@ -28,7 +28,8 @@ const getClientStorage = (config) => {
28
28
  _lifi_types.ChainType.EVM,
29
29
  _lifi_types.ChainType.SVM,
30
30
  _lifi_types.ChainType.UTXO,
31
- _lifi_types.ChainType.MVM
31
+ _lifi_types.ChainType.MVM,
32
+ _lifi_types.ChainType.TVM
32
33
  ] });
33
34
  _chainsUpdatedAt = Date.now();
34
35
  updateRpcUrls();
@@ -1 +1 @@
1
- {"version":3,"file":"getClientStorage.js","names":["getRpcUrlsFromChains","ChainId","_getChains","ChainType"],"sources":["../../../src/client/getClientStorage.ts"],"sourcesContent":["import { ChainId, ChainType, type ExtendedChain } from '@lifi/types'\nimport { _getChains } from '../actions/getChains.js'\nimport { getRpcUrlsFromChains } from '../core/utils.js'\nimport type { RPCUrls, SDKBaseConfig } from '../types/core.js'\n\n// 6 hours in milliseconds\nconst chainsRefreshInterval = 1000 * 60 * 60 * 6\n\nexport interface ClientStorage {\n readonly needReset: boolean\n setChains(chains: ExtendedChain[]): void\n getChains(): Promise<ExtendedChain[]>\n getRpcUrls(): Promise<RPCUrls>\n}\n\nexport const getClientStorage = (config: SDKBaseConfig): ClientStorage => {\n let _chains = [] as ExtendedChain[]\n let _rpcUrls = { ...config.rpcUrls } as RPCUrls\n let _chainsUpdatedAt: number | undefined\n\n const updateRpcUrls = () => {\n _rpcUrls = { ...config.rpcUrls }\n _rpcUrls = getRpcUrlsFromChains(_rpcUrls, _chains, [ChainId.SOL])\n }\n\n return {\n get needReset() {\n return (\n !_chainsUpdatedAt ||\n Date.now() - _chainsUpdatedAt >= chainsRefreshInterval\n )\n },\n setChains(chains: ExtendedChain[]) {\n _chains = chains\n _chainsUpdatedAt = Date.now()\n updateRpcUrls()\n },\n async getChains() {\n // When preloadChains is false, SDK does not auto-fetch chains\n // External consumer is responsible for calling setChains\n if (!config.preloadChains) {\n return _chains\n }\n\n if (this.needReset || !_chains.length) {\n _chains = await _getChains(config, {\n chainTypes: [\n ChainType.EVM,\n ChainType.SVM,\n ChainType.UTXO,\n ChainType.MVM,\n ],\n })\n _chainsUpdatedAt = Date.now()\n updateRpcUrls()\n }\n return _chains\n },\n async getRpcUrls() {\n await this.getChains() // _rpcUrls is updated when needed\n return _rpcUrls\n },\n }\n}\n"],"mappings":";;;;;AAMA,MAAM,wBAAwB,MAAO,KAAK,KAAK;AAS/C,MAAa,oBAAoB,WAAyC;CACxE,IAAI,UAAU,EAAE;CAChB,IAAI,WAAW,EAAE,GAAG,OAAO,SAAS;CACpC,IAAI;CAEJ,MAAM,sBAAsB;AAC1B,aAAW,EAAE,GAAG,OAAO,SAAS;AAChC,aAAWA,mBAAAA,qBAAqB,UAAU,SAAS,CAACC,YAAAA,QAAQ,IAAI,CAAC;;AAGnE,QAAO;EACL,IAAI,YAAY;AACd,UACE,CAAC,oBACD,KAAK,KAAK,GAAG,oBAAoB;;EAGrC,UAAU,QAAyB;AACjC,aAAU;AACV,sBAAmB,KAAK,KAAK;AAC7B,kBAAe;;EAEjB,MAAM,YAAY;AAGhB,OAAI,CAAC,OAAO,cACV,QAAO;AAGT,OAAI,KAAK,aAAa,CAAC,QAAQ,QAAQ;AACrC,cAAU,MAAMC,0BAAAA,WAAW,QAAQ,EACjC,YAAY;KACVC,YAAAA,UAAU;KACVA,YAAAA,UAAU;KACVA,YAAAA,UAAU;KACVA,YAAAA,UAAU;KACX,EACF,CAAC;AACF,uBAAmB,KAAK,KAAK;AAC7B,mBAAe;;AAEjB,UAAO;;EAET,MAAM,aAAa;AACjB,SAAM,KAAK,WAAW;AACtB,UAAO;;EAEV"}
1
+ {"version":3,"file":"getClientStorage.js","names":["getRpcUrlsFromChains","ChainId","_getChains","ChainType"],"sources":["../../../src/client/getClientStorage.ts"],"sourcesContent":["import { ChainId, ChainType, type ExtendedChain } from '@lifi/types'\nimport { _getChains } from '../actions/getChains.js'\nimport { getRpcUrlsFromChains } from '../core/utils.js'\nimport type { RPCUrls, SDKBaseConfig } from '../types/core.js'\n\n// 6 hours in milliseconds\nconst chainsRefreshInterval = 1000 * 60 * 60 * 6\n\nexport interface ClientStorage {\n readonly needReset: boolean\n setChains(chains: ExtendedChain[]): void\n getChains(): Promise<ExtendedChain[]>\n getRpcUrls(): Promise<RPCUrls>\n}\n\nexport const getClientStorage = (config: SDKBaseConfig): ClientStorage => {\n let _chains = [] as ExtendedChain[]\n let _rpcUrls = { ...config.rpcUrls } as RPCUrls\n let _chainsUpdatedAt: number | undefined\n\n const updateRpcUrls = () => {\n _rpcUrls = { ...config.rpcUrls }\n _rpcUrls = getRpcUrlsFromChains(_rpcUrls, _chains, [ChainId.SOL])\n }\n\n return {\n get needReset() {\n return (\n !_chainsUpdatedAt ||\n Date.now() - _chainsUpdatedAt >= chainsRefreshInterval\n )\n },\n setChains(chains: ExtendedChain[]) {\n _chains = chains\n _chainsUpdatedAt = Date.now()\n updateRpcUrls()\n },\n async getChains() {\n // When preloadChains is false, SDK does not auto-fetch chains\n // External consumer is responsible for calling setChains\n if (!config.preloadChains) {\n return _chains\n }\n\n if (this.needReset || !_chains.length) {\n _chains = await _getChains(config, {\n chainTypes: [\n ChainType.EVM,\n ChainType.SVM,\n ChainType.UTXO,\n ChainType.MVM,\n ChainType.TVM,\n ],\n })\n _chainsUpdatedAt = Date.now()\n updateRpcUrls()\n }\n return _chains\n },\n async getRpcUrls() {\n await this.getChains() // _rpcUrls is updated when needed\n return _rpcUrls\n },\n }\n}\n"],"mappings":";;;;;AAMA,MAAM,wBAAwB,MAAO,KAAK,KAAK;AAS/C,MAAa,oBAAoB,WAAyC;CACxE,IAAI,UAAU,EAAE;CAChB,IAAI,WAAW,EAAE,GAAG,OAAO,SAAS;CACpC,IAAI;CAEJ,MAAM,sBAAsB;AAC1B,aAAW,EAAE,GAAG,OAAO,SAAS;AAChC,aAAWA,mBAAAA,qBAAqB,UAAU,SAAS,CAACC,YAAAA,QAAQ,IAAI,CAAC;;AAGnE,QAAO;EACL,IAAI,YAAY;AACd,UACE,CAAC,oBACD,KAAK,KAAK,GAAG,oBAAoB;;EAGrC,UAAU,QAAyB;AACjC,aAAU;AACV,sBAAmB,KAAK,KAAK;AAC7B,kBAAe;;EAEjB,MAAM,YAAY;AAGhB,OAAI,CAAC,OAAO,cACV,QAAO;AAGT,OAAI,KAAK,aAAa,CAAC,QAAQ,QAAQ;AACrC,cAAU,MAAMC,0BAAAA,WAAW,QAAQ,EACjC,YAAY;KACVC,YAAAA,UAAU;KACVA,YAAAA,UAAU;KACVA,YAAAA,UAAU;KACVA,YAAAA,UAAU;KACVA,YAAAA,UAAU;KACX,EACF,CAAC;AACF,uBAAmB,KAAK,KAAK;AAC7B,mBAAe;;AAEjB,UAAO;;EAET,MAAM,aAAa;AACjB,SAAM,KAAK,WAAW;AACtB,UAAO;;EAEV"}
@@ -2,7 +2,19 @@ import { SDKClient } from "../../../types/core.js";
2
2
  import { LiFiStep } from "@lifi/types";
3
3
 
4
4
  //#region src/core/tasks/helpers/checkBalance.d.ts
5
- declare const checkBalance: (client: SDKClient, walletAddress: string, step: LiFiStep, depth?: number) => Promise<void>;
5
+ /**
6
+ * Verifies that the wallet holds enough of every token required to execute
7
+ * the step on its source chain — the source-token amount, any gas costs, and
8
+ * any non-included fee costs. Reads all balances in one batched provider
9
+ * call, retries within a bounded budget to absorb transient RPC failures and
10
+ * post-confirmation propagation lag, and applies slippage to the source-token
11
+ * portion only as a last resort (overhead is never trimmed).
12
+ *
13
+ * Throws BalanceError("The balance is too low.") on a genuine shortfall, or
14
+ * BalanceError("Could not read wallet balance.") if the balance can't be read
15
+ * after retries.
16
+ */
17
+ declare const checkBalance: (client: SDKClient, walletAddress: string, step: LiFiStep) => Promise<void>;
6
18
  //#endregion
7
19
  export { checkBalance };
8
20
  //# sourceMappingURL=checkBalance.d.ts.map
@@ -1,26 +1,96 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_errors_errors = require("../../../errors/errors.js");
3
3
  const require_utils_sleep = require("../../../utils/sleep.js");
4
- const require_actions_getTokenBalance = require("../../../actions/getTokenBalance.js");
5
4
  const require_utils_formatUnits = require("../../../utils/formatUnits.js");
5
+ const require_utils_withTimeout = require("../../../utils/withTimeout.js");
6
6
  //#region src/core/tasks/helpers/checkBalance.ts
7
- const checkBalance = async (client, walletAddress, step, depth = 0) => {
8
- const token = await require_actions_getTokenBalance.getTokenBalance(client, walletAddress, step.action.fromToken);
9
- if (token) {
10
- const currentBalance = token.amount ?? 0n;
11
- const neededBalance = BigInt(step.action.fromAmount);
12
- if (currentBalance < neededBalance) if (depth <= 3) {
13
- await require_utils_sleep.sleep(200);
14
- await checkBalance(client, walletAddress, step, depth + 1);
15
- } else if (neededBalance * BigInt((1 - (step.action.slippage ?? 0)) * 1e9) / 1000000000n <= currentBalance) step.action.fromAmount = currentBalance.toString();
16
- else {
17
- const needed = require_utils_formatUnits.formatUnits(neededBalance, token.decimals);
18
- const current = require_utils_formatUnits.formatUnits(currentBalance, token.decimals);
19
- 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.`;
20
- if (currentBalance !== 0n) errorMessage += `If the problem consists, please delete this transfer and start a new one with a maximum of ${current} ${token.symbol}.`;
21
- throw new require_errors_errors.BalanceError("The balance is too low.", new Error(errorMessage));
7
+ const MAX_ATTEMPTS = 6;
8
+ const BACKOFF_BASE_MS = 150;
9
+ const OVERALL_TIMEOUT_MS = 1e4;
10
+ const SLIPPAGE_PRECISION = 1000000000n;
11
+ /**
12
+ * Verifies that the wallet holds enough of every token required to execute
13
+ * the step on its source chain — the source-token amount, any gas costs, and
14
+ * any non-included fee costs. Reads all balances in one batched provider
15
+ * call, retries within a bounded budget to absorb transient RPC failures and
16
+ * post-confirmation propagation lag, and applies slippage to the source-token
17
+ * portion only as a last resort (overhead is never trimmed).
18
+ *
19
+ * Throws BalanceError("The balance is too low.") on a genuine shortfall, or
20
+ * BalanceError("Could not read wallet balance.") if the balance can't be read
21
+ * after retries.
22
+ */
23
+ const checkBalance = async (client, walletAddress, step) => {
24
+ const fromChainId = step.action.fromChainId;
25
+ const requirements = /* @__PURE__ */ new Map();
26
+ const add = (token, amount, source) => {
27
+ if (token.chainId !== fromChainId || amount === 0n) return;
28
+ const key = token.address.toLowerCase();
29
+ const req = requirements.get(key) ?? {
30
+ token,
31
+ sourcePart: 0n,
32
+ overheadPart: 0n
33
+ };
34
+ if (source) req.sourcePart += amount;
35
+ else req.overheadPart += amount;
36
+ requirements.set(key, req);
37
+ };
38
+ add(step.action.fromToken, BigInt(step.action.fromAmount), true);
39
+ for (const gas of step.estimate?.gasCosts ?? []) add(gas.token, BigInt(gas.amount), false);
40
+ for (const fee of step.estimate?.feeCosts ?? []) if (!fee.included) add(fee.token, BigInt(fee.amount), false);
41
+ if (requirements.size === 0) return;
42
+ const provider = client.providers.find((p) => p.isAddress(walletAddress));
43
+ if (!provider) throw new Error(`SDK Token Provider for ${walletAddress} is not found.`);
44
+ const reqs = Array.from(requirements.values());
45
+ const tokens = reqs.map((r) => r.token);
46
+ const slippage = step.action.slippage ?? 0;
47
+ const slippageScaled = BigInt(Math.floor((1 - slippage) * Number(SLIPPAGE_PRECISION)));
48
+ await require_utils_withTimeout.withTimeout(async () => {
49
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
50
+ const isFinal = attempt === MAX_ATTEMPTS - 1;
51
+ let balances;
52
+ try {
53
+ balances = await provider.getBalance(client, walletAddress, tokens);
54
+ } catch (error) {
55
+ if (isFinal) throw new require_errors_errors.BalanceError("Could not read wallet balance.", error);
56
+ await require_utils_sleep.sleep(BACKOFF_BASE_MS * 2 ** attempt);
57
+ continue;
58
+ }
59
+ const balanceByAddress = new Map(balances.map((b) => [b.address.toLowerCase(), b.amount]));
60
+ const unknown = [];
61
+ const insufficient = [];
62
+ for (const req of reqs) {
63
+ const have = balanceByAddress.get(req.token.address.toLowerCase());
64
+ if (have === void 0) unknown.push(req.token);
65
+ else if (have < req.sourcePart + req.overheadPart) insufficient.push({
66
+ req,
67
+ have
68
+ });
69
+ }
70
+ if (unknown.length === 0 && insufficient.length === 0) return;
71
+ if (isFinal && unknown.length === 0 && insufficient.length === 1 && insufficient[0].req.sourcePart > 0n) {
72
+ const { req, have } = insufficient[0];
73
+ if (have >= req.sourcePart * slippageScaled / SLIPPAGE_PRECISION + req.overheadPart) {
74
+ step.action.fromAmount = (have - req.overheadPart).toString();
75
+ return;
76
+ }
77
+ }
78
+ if (isFinal) {
79
+ if (unknown.length > 0) throw new require_errors_errors.BalanceError("Could not read wallet balance.", /* @__PURE__ */ new Error(`Could not read balance for: ${unknown.map((t) => t.symbol || t.address).join(", ")}.`));
80
+ const lines = insufficient.map(({ req, have }) => {
81
+ const needed = require_utils_formatUnits.formatUnits(req.sourcePart + req.overheadPart, req.token.decimals);
82
+ const current = require_utils_formatUnits.formatUnits(have, req.token.decimals);
83
+ const symbol = req.token.symbol;
84
+ return req.sourcePart > 0n ? `Your ${symbol} balance is too low, you try to transfer ${needed} ${symbol}, but your wallet only holds ${current} ${symbol}.` : `Insufficient ${symbol} for fees: need ${needed} ${symbol}, have ${current} ${symbol}.`;
85
+ });
86
+ throw new require_errors_errors.BalanceError("The balance is too low.", /* @__PURE__ */ new Error(`${lines.join(" ")} No funds have been sent.`));
87
+ }
88
+ await require_utils_sleep.sleep(BACKOFF_BASE_MS * 2 ** attempt);
22
89
  }
23
- }
90
+ }, {
91
+ timeout: OVERALL_TIMEOUT_MS,
92
+ errorInstance: new require_errors_errors.BalanceError("Could not read wallet balance.")
93
+ });
24
94
  };
25
95
  //#endregion
26
96
  exports.checkBalance = checkBalance;
@@ -1 +1 @@
1
- {"version":3,"file":"checkBalance.js","names":["getTokenBalance","sleep","formatUnits","BalanceError"],"sources":["../../../../../src/core/tasks/helpers/checkBalance.ts"],"sourcesContent":["import type { LiFiStep } from '@lifi/types'\nimport { getTokenBalance } from '../../../actions/getTokenBalance.js'\nimport { BalanceError } from '../../../errors/errors.js'\nimport type { SDKClient } from '../../../types/core.js'\nimport { formatUnits } from '../../../utils/formatUnits.js'\nimport { sleep } from '../../../utils/sleep.js'\n\nexport const checkBalance = async (\n client: SDKClient,\n walletAddress: string,\n step: LiFiStep,\n depth = 0\n): Promise<void> => {\n const token = await getTokenBalance(\n client,\n walletAddress,\n step.action.fromToken\n )\n if (token) {\n const currentBalance = token.amount ?? 0n\n const neededBalance = BigInt(step.action.fromAmount)\n\n if (currentBalance < neededBalance) {\n if (depth <= 3) {\n await sleep(200)\n await checkBalance(client, walletAddress, step, depth + 1)\n } else if (\n (neededBalance *\n BigInt((1 - (step.action.slippage ?? 0)) * 1_000_000_000)) /\n 1_000_000_000n <=\n currentBalance\n ) {\n // adjust amount in slippage limits\n step.action.fromAmount = currentBalance.toString()\n } else {\n const needed = formatUnits(neededBalance, token.decimals)\n const current = formatUnits(currentBalance, token.decimals)\n 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.`\n\n if (currentBalance !== 0n) {\n errorMessage += `If the problem consists, please delete this transfer and start a new one with a maximum of ${current} ${token.symbol}.`\n }\n\n throw new BalanceError(\n 'The balance is too low.',\n new Error(errorMessage)\n )\n }\n }\n }\n}\n"],"mappings":";;;;;;AAOA,MAAa,eAAe,OAC1B,QACA,eACA,MACA,QAAQ,MACU;CAClB,MAAM,QAAQ,MAAMA,gCAAAA,gBAClB,QACA,eACA,KAAK,OAAO,UACb;AACD,KAAI,OAAO;EACT,MAAM,iBAAiB,MAAM,UAAU;EACvC,MAAM,gBAAgB,OAAO,KAAK,OAAO,WAAW;AAEpD,MAAI,iBAAiB,cACnB,KAAI,SAAS,GAAG;AACd,SAAMC,oBAAAA,MAAM,IAAI;AAChB,SAAM,aAAa,QAAQ,eAAe,MAAM,QAAQ,EAAE;aAEzD,gBACC,QAAQ,KAAK,KAAK,OAAO,YAAY,MAAM,IAAc,GACzD,eACF,eAGA,MAAK,OAAO,aAAa,eAAe,UAAU;OAC7C;GACL,MAAM,SAASC,0BAAAA,YAAY,eAAe,MAAM,SAAS;GACzD,MAAM,UAAUA,0BAAAA,YAAY,gBAAgB,MAAM,SAAS;GAC3D,IAAI,eAAe,QAAQ,MAAM,OAAO,2CAA2C,OAAO,GAAG,MAAM,OAAO,+BAA+B,QAAQ,GAAG,MAAM,OAAO;AAEjK,OAAI,mBAAmB,GACrB,iBAAgB,8FAA8F,QAAQ,GAAG,MAAM,OAAO;AAGxI,SAAM,IAAIC,sBAAAA,aACR,2BACA,IAAI,MAAM,aAAa,CACxB"}
1
+ {"version":3,"file":"checkBalance.js","names":["withTimeout","BalanceError","sleep","formatUnits"],"sources":["../../../../../src/core/tasks/helpers/checkBalance.ts"],"sourcesContent":["import type { LiFiStep, Token, TokenAmount } from '@lifi/types'\nimport { BalanceError } from '../../../errors/errors.js'\nimport type { SDKClient } from '../../../types/core.js'\nimport { formatUnits } from '../../../utils/formatUnits.js'\nimport { sleep } from '../../../utils/sleep.js'\nimport { withTimeout } from '../../../utils/withTimeout.js'\n\nconst MAX_ATTEMPTS = 6\n// Exponential backoff: 150, 300, 600, 1200, 2400 → ≈4.65s of sleep total.\nconst BACKOFF_BASE_MS = 150\nconst OVERALL_TIMEOUT_MS = 10_000\nconst SLIPPAGE_PRECISION = 1_000_000_000n\n\ntype Requirement = {\n token: Token\n sourcePart: bigint // 0n for pure overhead tokens\n overheadPart: bigint // gas + non-included fees in this token\n}\n\n/**\n * Verifies that the wallet holds enough of every token required to execute\n * the step on its source chain — the source-token amount, any gas costs, and\n * any non-included fee costs. Reads all balances in one batched provider\n * call, retries within a bounded budget to absorb transient RPC failures and\n * post-confirmation propagation lag, and applies slippage to the source-token\n * portion only as a last resort (overhead is never trimmed).\n *\n * Throws BalanceError(\"The balance is too low.\") on a genuine shortfall, or\n * BalanceError(\"Could not read wallet balance.\") if the balance can't be read\n * after retries.\n */\nexport const checkBalance = async (\n client: SDKClient,\n walletAddress: string,\n step: LiFiStep\n): Promise<void> => {\n const fromChainId = step.action.fromChainId\n const requirements = new Map<string, Requirement>()\n const add = (token: Token, amount: bigint, source: boolean): void => {\n if (token.chainId !== fromChainId || amount === 0n) {\n return\n }\n const key = token.address.toLowerCase()\n const req = requirements.get(key) ?? {\n token,\n sourcePart: 0n,\n overheadPart: 0n,\n }\n if (source) {\n req.sourcePart += amount\n } else {\n req.overheadPart += amount\n }\n requirements.set(key, req)\n }\n add(step.action.fromToken, BigInt(step.action.fromAmount), true)\n for (const gas of step.estimate?.gasCosts ?? []) {\n add(gas.token, BigInt(gas.amount), false)\n }\n for (const fee of step.estimate?.feeCosts ?? []) {\n // Included fees are already part of fromAmount — don't count twice.\n if (!fee.included) {\n add(fee.token, BigInt(fee.amount), false)\n }\n }\n if (requirements.size === 0) {\n return\n }\n\n // Provider is dispatched by wallet address; all requirements share the\n // source chain, which matches this provider by virtue of the address.\n const provider = client.providers.find((p) => p.isAddress(walletAddress))\n if (!provider) {\n throw new Error(`SDK Token Provider for ${walletAddress} is not found.`)\n }\n\n const reqs = Array.from(requirements.values())\n const tokens = reqs.map((r) => r.token)\n const slippage = step.action.slippage ?? 0\n const slippageScaled = BigInt(\n Math.floor((1 - slippage) * Number(SLIPPAGE_PRECISION))\n )\n\n await withTimeout(\n async () => {\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n const isFinal = attempt === MAX_ATTEMPTS - 1\n\n let balances: TokenAmount[]\n try {\n balances = await provider.getBalance(client, walletAddress, tokens)\n } catch (error) {\n if (isFinal) {\n throw new BalanceError(\n 'Could not read wallet balance.',\n error as Error\n )\n }\n await sleep(BACKOFF_BASE_MS * 2 ** attempt)\n continue\n }\n\n const balanceByAddress = new Map(\n balances.map((b) => [b.address.toLowerCase(), b.amount] as const)\n )\n\n const unknown: Token[] = []\n const insufficient: { req: Requirement; have: bigint }[] = []\n for (const req of reqs) {\n const have = balanceByAddress.get(req.token.address.toLowerCase())\n if (have === undefined) {\n unknown.push(req.token)\n } else if (have < req.sourcePart + req.overheadPart) {\n insufficient.push({ req, have })\n }\n }\n\n if (unknown.length === 0 && insufficient.length === 0) {\n return\n }\n\n // Final-attempt slippage rescue: only when the sole shortfall is the\n // source-token portion. Trim source down to (balance − overhead) so\n // the overhead reserve is preserved.\n if (\n isFinal &&\n unknown.length === 0 &&\n insufficient.length === 1 &&\n insufficient[0].req.sourcePart > 0n\n ) {\n const { req, have } = insufficient[0]\n const minAcceptable =\n (req.sourcePart * slippageScaled) / SLIPPAGE_PRECISION +\n req.overheadPart\n if (have >= minAcceptable) {\n step.action.fromAmount = (have - req.overheadPart).toString()\n return\n }\n }\n\n if (isFinal) {\n if (unknown.length > 0) {\n throw new BalanceError(\n 'Could not read wallet balance.',\n new Error(\n `Could not read balance for: ${unknown\n .map((t) => t.symbol || t.address)\n .join(', ')}.`\n )\n )\n }\n const lines = insufficient.map(({ req, have }) => {\n const needed = formatUnits(\n req.sourcePart + req.overheadPart,\n req.token.decimals\n )\n const current = formatUnits(have, req.token.decimals)\n const symbol = req.token.symbol\n return req.sourcePart > 0n\n ? `Your ${symbol} balance is too low, you try to transfer ${needed} ${symbol}, but your wallet only holds ${current} ${symbol}.`\n : `Insufficient ${symbol} for fees: need ${needed} ${symbol}, have ${current} ${symbol}.`\n })\n throw new BalanceError(\n 'The balance is too low.',\n new Error(`${lines.join(' ')} No funds have been sent.`)\n )\n }\n\n await sleep(BACKOFF_BASE_MS * 2 ** attempt)\n }\n },\n {\n timeout: OVERALL_TIMEOUT_MS,\n errorInstance: new BalanceError('Could not read wallet balance.'),\n }\n )\n}\n"],"mappings":";;;;;;AAOA,MAAM,eAAe;AAErB,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;;;;;;;;;;;;;AAoB3B,MAAa,eAAe,OAC1B,QACA,eACA,SACkB;CAClB,MAAM,cAAc,KAAK,OAAO;CAChC,MAAM,+BAAe,IAAI,KAA0B;CACnD,MAAM,OAAO,OAAc,QAAgB,WAA0B;AACnE,MAAI,MAAM,YAAY,eAAe,WAAW,GAC9C;EAEF,MAAM,MAAM,MAAM,QAAQ,aAAa;EACvC,MAAM,MAAM,aAAa,IAAI,IAAI,IAAI;GACnC;GACA,YAAY;GACZ,cAAc;GACf;AACD,MAAI,OACF,KAAI,cAAc;MAElB,KAAI,gBAAgB;AAEtB,eAAa,IAAI,KAAK,IAAI;;AAE5B,KAAI,KAAK,OAAO,WAAW,OAAO,KAAK,OAAO,WAAW,EAAE,KAAK;AAChE,MAAK,MAAM,OAAO,KAAK,UAAU,YAAY,EAAE,CAC7C,KAAI,IAAI,OAAO,OAAO,IAAI,OAAO,EAAE,MAAM;AAE3C,MAAK,MAAM,OAAO,KAAK,UAAU,YAAY,EAAE,CAE7C,KAAI,CAAC,IAAI,SACP,KAAI,IAAI,OAAO,OAAO,IAAI,OAAO,EAAE,MAAM;AAG7C,KAAI,aAAa,SAAS,EACxB;CAKF,MAAM,WAAW,OAAO,UAAU,MAAM,MAAM,EAAE,UAAU,cAAc,CAAC;AACzE,KAAI,CAAC,SACH,OAAM,IAAI,MAAM,0BAA0B,cAAc,gBAAgB;CAG1E,MAAM,OAAO,MAAM,KAAK,aAAa,QAAQ,CAAC;CAC9C,MAAM,SAAS,KAAK,KAAK,MAAM,EAAE,MAAM;CACvC,MAAM,WAAW,KAAK,OAAO,YAAY;CACzC,MAAM,iBAAiB,OACrB,KAAK,OAAO,IAAI,YAAY,OAAO,mBAAmB,CAAC,CACxD;AAED,OAAMA,0BAAAA,YACJ,YAAY;AACV,OAAK,IAAI,UAAU,GAAG,UAAU,cAAc,WAAW;GACvD,MAAM,UAAU,YAAY,eAAe;GAE3C,IAAI;AACJ,OAAI;AACF,eAAW,MAAM,SAAS,WAAW,QAAQ,eAAe,OAAO;YAC5D,OAAO;AACd,QAAI,QACF,OAAM,IAAIC,sBAAAA,aACR,kCACA,MACD;AAEH,UAAMC,oBAAAA,MAAM,kBAAkB,KAAK,QAAQ;AAC3C;;GAGF,MAAM,mBAAmB,IAAI,IAC3B,SAAS,KAAK,MAAM,CAAC,EAAE,QAAQ,aAAa,EAAE,EAAE,OAAO,CAAU,CAClE;GAED,MAAM,UAAmB,EAAE;GAC3B,MAAM,eAAqD,EAAE;AAC7D,QAAK,MAAM,OAAO,MAAM;IACtB,MAAM,OAAO,iBAAiB,IAAI,IAAI,MAAM,QAAQ,aAAa,CAAC;AAClE,QAAI,SAAS,KAAA,EACX,SAAQ,KAAK,IAAI,MAAM;aACd,OAAO,IAAI,aAAa,IAAI,aACrC,cAAa,KAAK;KAAE;KAAK;KAAM,CAAC;;AAIpC,OAAI,QAAQ,WAAW,KAAK,aAAa,WAAW,EAClD;AAMF,OACE,WACA,QAAQ,WAAW,KACnB,aAAa,WAAW,KACxB,aAAa,GAAG,IAAI,aAAa,IACjC;IACA,MAAM,EAAE,KAAK,SAAS,aAAa;AAInC,QAAI,QAFD,IAAI,aAAa,iBAAkB,qBACpC,IAAI,cACqB;AACzB,UAAK,OAAO,cAAc,OAAO,IAAI,cAAc,UAAU;AAC7D;;;AAIJ,OAAI,SAAS;AACX,QAAI,QAAQ,SAAS,EACnB,OAAM,IAAID,sBAAAA,aACR,kDACA,IAAI,MACF,+BAA+B,QAC5B,KAAK,MAAM,EAAE,UAAU,EAAE,QAAQ,CACjC,KAAK,KAAK,CAAC,GACf,CACF;IAEH,MAAM,QAAQ,aAAa,KAAK,EAAE,KAAK,WAAW;KAChD,MAAM,SAASE,0BAAAA,YACb,IAAI,aAAa,IAAI,cACrB,IAAI,MAAM,SACX;KACD,MAAM,UAAUA,0BAAAA,YAAY,MAAM,IAAI,MAAM,SAAS;KACrD,MAAM,SAAS,IAAI,MAAM;AACzB,YAAO,IAAI,aAAa,KACpB,QAAQ,OAAO,2CAA2C,OAAO,GAAG,OAAO,+BAA+B,QAAQ,GAAG,OAAO,KAC5H,gBAAgB,OAAO,kBAAkB,OAAO,GAAG,OAAO,SAAS,QAAQ,GAAG,OAAO;MACzF;AACF,UAAM,IAAIF,sBAAAA,aACR,2CACA,IAAI,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC,2BAA2B,CACzD;;AAGH,SAAMC,oBAAAA,MAAM,kBAAkB,KAAK,QAAQ;;IAG/C;EACE,SAAS;EACT,eAAe,IAAID,sBAAAA,aAAa,iCAAiC;EAClE,CACF"}
@@ -50,5 +50,6 @@ import { parseUnits } from "./utils/parseUnits.js";
50
50
  import { sleep } from "./utils/sleep.js";
51
51
  import { waitForResult } from "./utils/waitForResult.js";
52
52
  import { LruMap, withDedupe } from "./utils/withDedupe.js";
53
+ import { withTimeout } from "./utils/withTimeout.js";
53
54
  export * from "@lifi/types";
54
- export { type AcceptExchangeRateUpdateHook, type AcceptSlippageUpdateHook, type AcceptSlippageUpdateHookParams, BalanceError, BaseError, BaseStepExecutionTask, BaseStepExecutor, CheckBalanceTask, type ContractCallParams, type ContractTool, type ErrorCode, ErrorMessage, ErrorName, type ExchangeRateUpdateParams, ExecuteStepRetryError, type ExecuteStepRetryParams, type Execution, type ExecutionAction, type ExecutionActionStatus, type ExecutionActionType, type ExecutionOptions, type ExecutionStatus, type GetContractCallsHook, type GetContractCallsResult, HTTPError, InMemoryStorage, type InteractionSettings, LiFiErrorCode, type LiFiStepExtended, LocalStorageAdapter, LruMap, PrepareTransactionTask, ProviderError, RPCError, type RPCUrls, type RequestInterceptor, type RouteExecutionData, type RouteExecutionDataDictionary, type RouteExecutionDictionary, type RouteExtended, type SDKBaseConfig, type SDKClient, type SDKConfig, SDKError, type SDKProvider, type SDKStorage, ServerError, StatusManager, type StepExecutor, type StepExecutorBaseContext, type StepExecutorContext, type StepExecutorOptions, type StepExtended, TaskPipeline, type TaskResult, type TaskStatus, TransactionError, type TransactionMethodType, type TransactionParameters, type TransactionRequestParameters, type TransactionRequestUpdateHook, UnknownError, type UpdateRouteHook, ValidationError, WaitForTransactionStatusTask, actions, checkBalance, checkPackageUpdates, convertQuoteToRoute, createClient, createDefaultStorage, executeRoute, fetchTxErrorDetails, formatUnits, getActionMessage, getActiveRoute, getActiveRoutes, getChains, getConnections, getContractCallsQuote, getGasRecommendation, getNameServiceAddress, getQuote, getRelayedTransactionStatus, getRelayerQuote, getRoutes, getStatus, getStepTransaction, getSubstatusMessage, getToken, getTokenBalance, getTokenBalances, getTokenBalancesByChain, getTokens, getTools, getTransactionHistory, getTransactionRequestData, getWalletBalances, isHex, parseUnits, patchContractCalls, relayTransaction, resumeRoute, sleep, stepComparison, stopRouteExecution, updateRouteExecution, waitForResult, withDedupe };
55
+ export { type AcceptExchangeRateUpdateHook, type AcceptSlippageUpdateHook, type AcceptSlippageUpdateHookParams, BalanceError, BaseError, BaseStepExecutionTask, BaseStepExecutor, CheckBalanceTask, type ContractCallParams, type ContractTool, type ErrorCode, ErrorMessage, ErrorName, type ExchangeRateUpdateParams, ExecuteStepRetryError, type ExecuteStepRetryParams, type Execution, type ExecutionAction, type ExecutionActionStatus, type ExecutionActionType, type ExecutionOptions, type ExecutionStatus, type GetContractCallsHook, type GetContractCallsResult, HTTPError, InMemoryStorage, type InteractionSettings, LiFiErrorCode, type LiFiStepExtended, LocalStorageAdapter, LruMap, PrepareTransactionTask, ProviderError, RPCError, type RPCUrls, type RequestInterceptor, type RouteExecutionData, type RouteExecutionDataDictionary, type RouteExecutionDictionary, type RouteExtended, type SDKBaseConfig, type SDKClient, type SDKConfig, SDKError, type SDKProvider, type SDKStorage, ServerError, StatusManager, type StepExecutor, type StepExecutorBaseContext, type StepExecutorContext, type StepExecutorOptions, type StepExtended, TaskPipeline, type TaskResult, type TaskStatus, TransactionError, type TransactionMethodType, type TransactionParameters, type TransactionRequestParameters, type TransactionRequestUpdateHook, UnknownError, type UpdateRouteHook, ValidationError, WaitForTransactionStatusTask, actions, checkBalance, checkPackageUpdates, convertQuoteToRoute, createClient, createDefaultStorage, executeRoute, fetchTxErrorDetails, formatUnits, getActionMessage, getActiveRoute, getActiveRoutes, getChains, getConnections, getContractCallsQuote, getGasRecommendation, getNameServiceAddress, getQuote, getRelayedTransactionStatus, getRelayerQuote, getRoutes, getStatus, getStepTransaction, getSubstatusMessage, getToken, getTokenBalance, getTokenBalances, getTokenBalancesByChain, getTokens, getTools, getTransactionHistory, getTransactionRequestData, getWalletBalances, isHex, parseUnits, patchContractCalls, relayTransaction, resumeRoute, sleep, stepComparison, stopRouteExecution, updateRouteExecution, waitForResult, withDedupe, withTimeout };
package/dist/cjs/index.js CHANGED
@@ -38,6 +38,7 @@ const require_core_execution = require("./core/execution.js");
38
38
  const require_core_storage = require("./core/storage.js");
39
39
  const require_core_TaskPipeline = require("./core/TaskPipeline.js");
40
40
  const require_utils_formatUnits = require("./utils/formatUnits.js");
41
+ const require_utils_withTimeout = require("./utils/withTimeout.js");
41
42
  const require_core_tasks_helpers_checkBalance = require("./core/tasks/helpers/checkBalance.js");
42
43
  const require_core_tasks_CheckBalanceTask = require("./core/tasks/CheckBalanceTask.js");
43
44
  const require_core_tasks_helpers_getTransactionRequestData = require("./core/tasks/helpers/getTransactionRequestData.js");
@@ -117,6 +118,7 @@ exports.stopRouteExecution = require_core_execution.stopRouteExecution;
117
118
  exports.updateRouteExecution = require_core_execution.updateRouteExecution;
118
119
  exports.waitForResult = require_utils_waitForResult.waitForResult;
119
120
  exports.withDedupe = require_utils_withDedupe.withDedupe;
121
+ exports.withTimeout = require_utils_withTimeout.withTimeout;
120
122
  var _lifi_types = require("@lifi/types");
121
123
  Object.keys(_lifi_types).forEach(function(k) {
122
124
  if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
@@ -5,7 +5,7 @@ const checkPackageUpdates = async (packageName, packageVersion) => {
5
5
  try {
6
6
  const pkgName = packageName ?? "@lifi/sdk";
7
7
  const latestVersion = (await (await fetch(`https://registry.npmjs.org/${pkgName}/latest`)).json()).version;
8
- const currentVersion = packageVersion ?? "4.0.0-beta.5";
8
+ const currentVersion = packageVersion ?? "4.0.0-beta.7";
9
9
  if (latestVersion > currentVersion) console.warn(`${pkgName}: new package version is available. Please update as soon as possible to enjoy the newest features. Current version: ${currentVersion}. Latest version: ${latestVersion}.`);
10
10
  } catch (_error) {}
11
11
  };
@@ -0,0 +1,26 @@
1
+ //#region src/utils/withTimeout.d.ts
2
+ /**
3
+ * Wraps a function in a timeout.
4
+ * Based on viem's withTimeout implementation.
5
+ * @param fn - The function to wrap.
6
+ * @param timeout - The timeout in milliseconds.
7
+ * @param errorInstance - The error instance to throw when the timeout is reached.
8
+ * @param signal - Whether or not the timeout should use an abort signal.
9
+ * @returns The result of the function.
10
+ */
11
+ declare function withTimeout<T>(fn: ({
12
+ signal
13
+ }: {
14
+ signal: AbortController["signal"] | null;
15
+ }) => Promise<T>, {
16
+ errorInstance,
17
+ timeout,
18
+ signal
19
+ }: {
20
+ errorInstance?: Error | undefined;
21
+ timeout: number;
22
+ signal?: boolean | undefined;
23
+ }): Promise<T>;
24
+ //#endregion
25
+ export { withTimeout };
26
+ //# sourceMappingURL=withTimeout.d.ts.map
@@ -0,0 +1,35 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/utils/withTimeout.ts
3
+ /**
4
+ * Wraps a function in a timeout.
5
+ * Based on viem's withTimeout implementation.
6
+ * @param fn - The function to wrap.
7
+ * @param timeout - The timeout in milliseconds.
8
+ * @param errorInstance - The error instance to throw when the timeout is reached.
9
+ * @param signal - Whether or not the timeout should use an abort signal.
10
+ * @returns The result of the function.
11
+ */
12
+ function withTimeout(fn, { errorInstance = /* @__PURE__ */ new Error("Timed out after waiting for too long."), timeout, signal }) {
13
+ return new Promise((resolve, reject) => {
14
+ (async () => {
15
+ let timeoutId;
16
+ try {
17
+ const controller = new AbortController();
18
+ if (timeout > 0) timeoutId = setTimeout(() => {
19
+ if (signal) controller.abort();
20
+ else reject(errorInstance);
21
+ }, timeout);
22
+ resolve(await fn({ signal: controller?.signal || null }));
23
+ } catch (err) {
24
+ if (err?.name === "AbortError") reject(errorInstance);
25
+ reject(err);
26
+ } finally {
27
+ clearTimeout(timeoutId);
28
+ }
29
+ })();
30
+ });
31
+ }
32
+ //#endregion
33
+ exports.withTimeout = withTimeout;
34
+
35
+ //# sourceMappingURL=withTimeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTimeout.js","names":[],"sources":["../../../src/utils/withTimeout.ts"],"sourcesContent":["/**\n * Wraps a function in a timeout.\n * Based on viem's withTimeout implementation.\n * @param fn - The function to wrap.\n * @param timeout - The timeout in milliseconds.\n * @param errorInstance - The error instance to throw when the timeout is reached.\n * @param signal - Whether or not the timeout should use an abort signal.\n * @returns The result of the function.\n */\nexport function withTimeout<T>(\n fn: ({ signal }: { signal: AbortController['signal'] | null }) => Promise<T>,\n {\n errorInstance = new Error('Timed out after waiting for too long.'),\n timeout,\n signal,\n }: {\n // The error instance to throw when the timeout is reached.\n errorInstance?: Error | undefined\n // The timeout (in ms).\n timeout: number\n // Whether or not the timeout should use an abort signal.\n signal?: boolean | undefined\n }\n): Promise<T> {\n return new Promise((resolve, reject) => {\n ;(async () => {\n let timeoutId!: NodeJS.Timeout\n try {\n const controller = new AbortController()\n if (timeout > 0) {\n timeoutId = setTimeout(() => {\n if (signal) {\n controller.abort()\n } else {\n reject(errorInstance)\n }\n }, timeout) as NodeJS.Timeout // need to cast because bun globals.d.ts overrides @types/node\n }\n resolve(await fn({ signal: controller?.signal || null }))\n } catch (err) {\n if ((err as Error)?.name === 'AbortError') {\n reject(errorInstance)\n }\n reject(err)\n } finally {\n clearTimeout(timeoutId)\n }\n })()\n })\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAgB,YACd,IACA,EACE,gCAAgB,IAAI,MAAM,wCAAwC,EAClE,SACA,UASU;AACZ,QAAO,IAAI,SAAS,SAAS,WAAW;AACrC,GAAC,YAAY;GACZ,IAAI;AACJ,OAAI;IACF,MAAM,aAAa,IAAI,iBAAiB;AACxC,QAAI,UAAU,EACZ,aAAY,iBAAiB;AAC3B,SAAI,OACF,YAAW,OAAO;SAElB,QAAO,cAAc;OAEtB,QAAQ;AAEb,YAAQ,MAAM,GAAG,EAAE,QAAQ,YAAY,UAAU,MAAM,CAAC,CAAC;YAClD,KAAK;AACZ,QAAK,KAAe,SAAS,aAC3B,QAAO,cAAc;AAEvB,WAAO,IAAI;aACH;AACR,iBAAa,UAAU;;MAEvB;GACJ"}
@@ -1,6 +1,6 @@
1
1
  //#region src/version.d.ts
2
2
  declare const name = "@lifi/sdk";
3
- declare const version = "4.0.0-beta.5";
3
+ declare const version = "4.0.0-beta.7";
4
4
  //#endregion
5
5
  export { name, version };
6
6
  //# sourceMappingURL=version.d.ts.map
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/version.ts
3
3
  const name = "@lifi/sdk";
4
- const version = "4.0.0-beta.5";
4
+ const version = "4.0.0-beta.7";
5
5
  //#endregion
6
6
  exports.name = name;
7
7
  exports.version = version;
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","names":[],"sources":["../../src/version.ts"],"sourcesContent":["export const name = '@lifi/sdk'\nexport const version = '4.0.0-beta.5'\n"],"mappings":";;AAAA,MAAa,OAAO;AACpB,MAAa,UAAU"}
1
+ {"version":3,"file":"version.js","names":[],"sources":["../../src/version.ts"],"sourcesContent":["export const name = '@lifi/sdk'\nexport const version = '4.0.0-beta.7'\n"],"mappings":";;AAAA,MAAa,OAAO;AACpB,MAAa,UAAU"}
@@ -27,7 +27,8 @@ const getClientStorage = (config) => {
27
27
  ChainType.EVM,
28
28
  ChainType.SVM,
29
29
  ChainType.UTXO,
30
- ChainType.MVM
30
+ ChainType.MVM,
31
+ ChainType.TVM
31
32
  ] });
32
33
  _chainsUpdatedAt = Date.now();
33
34
  updateRpcUrls();
@@ -1 +1 @@
1
- {"version":3,"file":"getClientStorage.js","names":[],"sources":["../../../src/client/getClientStorage.ts"],"sourcesContent":["import { ChainId, ChainType, type ExtendedChain } from '@lifi/types'\nimport { _getChains } from '../actions/getChains.js'\nimport { getRpcUrlsFromChains } from '../core/utils.js'\nimport type { RPCUrls, SDKBaseConfig } from '../types/core.js'\n\n// 6 hours in milliseconds\nconst chainsRefreshInterval = 1000 * 60 * 60 * 6\n\nexport interface ClientStorage {\n readonly needReset: boolean\n setChains(chains: ExtendedChain[]): void\n getChains(): Promise<ExtendedChain[]>\n getRpcUrls(): Promise<RPCUrls>\n}\n\nexport const getClientStorage = (config: SDKBaseConfig): ClientStorage => {\n let _chains = [] as ExtendedChain[]\n let _rpcUrls = { ...config.rpcUrls } as RPCUrls\n let _chainsUpdatedAt: number | undefined\n\n const updateRpcUrls = () => {\n _rpcUrls = { ...config.rpcUrls }\n _rpcUrls = getRpcUrlsFromChains(_rpcUrls, _chains, [ChainId.SOL])\n }\n\n return {\n get needReset() {\n return (\n !_chainsUpdatedAt ||\n Date.now() - _chainsUpdatedAt >= chainsRefreshInterval\n )\n },\n setChains(chains: ExtendedChain[]) {\n _chains = chains\n _chainsUpdatedAt = Date.now()\n updateRpcUrls()\n },\n async getChains() {\n // When preloadChains is false, SDK does not auto-fetch chains\n // External consumer is responsible for calling setChains\n if (!config.preloadChains) {\n return _chains\n }\n\n if (this.needReset || !_chains.length) {\n _chains = await _getChains(config, {\n chainTypes: [\n ChainType.EVM,\n ChainType.SVM,\n ChainType.UTXO,\n ChainType.MVM,\n ],\n })\n _chainsUpdatedAt = Date.now()\n updateRpcUrls()\n }\n return _chains\n },\n async getRpcUrls() {\n await this.getChains() // _rpcUrls is updated when needed\n return _rpcUrls\n },\n }\n}\n"],"mappings":";;;;AAMA,MAAM,wBAAwB,MAAO,KAAK,KAAK;AAS/C,MAAa,oBAAoB,WAAyC;CACxE,IAAI,UAAU,EAAE;CAChB,IAAI,WAAW,EAAE,GAAG,OAAO,SAAS;CACpC,IAAI;CAEJ,MAAM,sBAAsB;AAC1B,aAAW,EAAE,GAAG,OAAO,SAAS;AAChC,aAAW,qBAAqB,UAAU,SAAS,CAAC,QAAQ,IAAI,CAAC;;AAGnE,QAAO;EACL,IAAI,YAAY;AACd,UACE,CAAC,oBACD,KAAK,KAAK,GAAG,oBAAoB;;EAGrC,UAAU,QAAyB;AACjC,aAAU;AACV,sBAAmB,KAAK,KAAK;AAC7B,kBAAe;;EAEjB,MAAM,YAAY;AAGhB,OAAI,CAAC,OAAO,cACV,QAAO;AAGT,OAAI,KAAK,aAAa,CAAC,QAAQ,QAAQ;AACrC,cAAU,MAAM,WAAW,QAAQ,EACjC,YAAY;KACV,UAAU;KACV,UAAU;KACV,UAAU;KACV,UAAU;KACX,EACF,CAAC;AACF,uBAAmB,KAAK,KAAK;AAC7B,mBAAe;;AAEjB,UAAO;;EAET,MAAM,aAAa;AACjB,SAAM,KAAK,WAAW;AACtB,UAAO;;EAEV"}
1
+ {"version":3,"file":"getClientStorage.js","names":[],"sources":["../../../src/client/getClientStorage.ts"],"sourcesContent":["import { ChainId, ChainType, type ExtendedChain } from '@lifi/types'\nimport { _getChains } from '../actions/getChains.js'\nimport { getRpcUrlsFromChains } from '../core/utils.js'\nimport type { RPCUrls, SDKBaseConfig } from '../types/core.js'\n\n// 6 hours in milliseconds\nconst chainsRefreshInterval = 1000 * 60 * 60 * 6\n\nexport interface ClientStorage {\n readonly needReset: boolean\n setChains(chains: ExtendedChain[]): void\n getChains(): Promise<ExtendedChain[]>\n getRpcUrls(): Promise<RPCUrls>\n}\n\nexport const getClientStorage = (config: SDKBaseConfig): ClientStorage => {\n let _chains = [] as ExtendedChain[]\n let _rpcUrls = { ...config.rpcUrls } as RPCUrls\n let _chainsUpdatedAt: number | undefined\n\n const updateRpcUrls = () => {\n _rpcUrls = { ...config.rpcUrls }\n _rpcUrls = getRpcUrlsFromChains(_rpcUrls, _chains, [ChainId.SOL])\n }\n\n return {\n get needReset() {\n return (\n !_chainsUpdatedAt ||\n Date.now() - _chainsUpdatedAt >= chainsRefreshInterval\n )\n },\n setChains(chains: ExtendedChain[]) {\n _chains = chains\n _chainsUpdatedAt = Date.now()\n updateRpcUrls()\n },\n async getChains() {\n // When preloadChains is false, SDK does not auto-fetch chains\n // External consumer is responsible for calling setChains\n if (!config.preloadChains) {\n return _chains\n }\n\n if (this.needReset || !_chains.length) {\n _chains = await _getChains(config, {\n chainTypes: [\n ChainType.EVM,\n ChainType.SVM,\n ChainType.UTXO,\n ChainType.MVM,\n ChainType.TVM,\n ],\n })\n _chainsUpdatedAt = Date.now()\n updateRpcUrls()\n }\n return _chains\n },\n async getRpcUrls() {\n await this.getChains() // _rpcUrls is updated when needed\n return _rpcUrls\n },\n }\n}\n"],"mappings":";;;;AAMA,MAAM,wBAAwB,MAAO,KAAK,KAAK;AAS/C,MAAa,oBAAoB,WAAyC;CACxE,IAAI,UAAU,EAAE;CAChB,IAAI,WAAW,EAAE,GAAG,OAAO,SAAS;CACpC,IAAI;CAEJ,MAAM,sBAAsB;AAC1B,aAAW,EAAE,GAAG,OAAO,SAAS;AAChC,aAAW,qBAAqB,UAAU,SAAS,CAAC,QAAQ,IAAI,CAAC;;AAGnE,QAAO;EACL,IAAI,YAAY;AACd,UACE,CAAC,oBACD,KAAK,KAAK,GAAG,oBAAoB;;EAGrC,UAAU,QAAyB;AACjC,aAAU;AACV,sBAAmB,KAAK,KAAK;AAC7B,kBAAe;;EAEjB,MAAM,YAAY;AAGhB,OAAI,CAAC,OAAO,cACV,QAAO;AAGT,OAAI,KAAK,aAAa,CAAC,QAAQ,QAAQ;AACrC,cAAU,MAAM,WAAW,QAAQ,EACjC,YAAY;KACV,UAAU;KACV,UAAU;KACV,UAAU;KACV,UAAU;KACV,UAAU;KACX,EACF,CAAC;AACF,uBAAmB,KAAK,KAAK;AAC7B,mBAAe;;AAEjB,UAAO;;EAET,MAAM,aAAa;AACjB,SAAM,KAAK,WAAW;AACtB,UAAO;;EAEV"}
@@ -2,7 +2,19 @@ import { SDKClient } from "../../../types/core.js";
2
2
  import { LiFiStep } from "@lifi/types";
3
3
 
4
4
  //#region src/core/tasks/helpers/checkBalance.d.ts
5
- declare const checkBalance: (client: SDKClient, walletAddress: string, step: LiFiStep, depth?: number) => Promise<void>;
5
+ /**
6
+ * Verifies that the wallet holds enough of every token required to execute
7
+ * the step on its source chain — the source-token amount, any gas costs, and
8
+ * any non-included fee costs. Reads all balances in one batched provider
9
+ * call, retries within a bounded budget to absorb transient RPC failures and
10
+ * post-confirmation propagation lag, and applies slippage to the source-token
11
+ * portion only as a last resort (overhead is never trimmed).
12
+ *
13
+ * Throws BalanceError("The balance is too low.") on a genuine shortfall, or
14
+ * BalanceError("Could not read wallet balance.") if the balance can't be read
15
+ * after retries.
16
+ */
17
+ declare const checkBalance: (client: SDKClient, walletAddress: string, step: LiFiStep) => Promise<void>;
6
18
  //#endregion
7
19
  export { checkBalance };
8
20
  //# sourceMappingURL=checkBalance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkBalance.d.ts","names":[],"sources":["../../../../../src/core/tasks/helpers/checkBalance.ts"],"mappings":";;;;cAOa,YAAA,GACX,MAAA,EAAQ,SAAA,EACR,aAAA,UACA,IAAA,EAAM,QAAA,EACN,KAAA,cACC,OAAA"}
1
+ {"version":3,"file":"checkBalance.d.ts","names":[],"sources":["../../../../../src/core/tasks/helpers/checkBalance.ts"],"mappings":";;;;;;AA+BA;;;;;;;;;;cAAa,YAAA,GACX,MAAA,EAAQ,SAAA,EACR,aAAA,UACA,IAAA,EAAM,QAAA,KACL,OAAA"}
@@ -1,25 +1,95 @@
1
1
  import { BalanceError } from "../../../errors/errors.js";
2
2
  import { sleep } from "../../../utils/sleep.js";
3
- import { getTokenBalance } from "../../../actions/getTokenBalance.js";
4
3
  import { formatUnits } from "../../../utils/formatUnits.js";
4
+ import { withTimeout } from "../../../utils/withTimeout.js";
5
5
  //#region src/core/tasks/helpers/checkBalance.ts
6
- const checkBalance = async (client, walletAddress, step, depth = 0) => {
7
- const token = await getTokenBalance(client, walletAddress, step.action.fromToken);
8
- if (token) {
9
- const currentBalance = token.amount ?? 0n;
10
- const neededBalance = BigInt(step.action.fromAmount);
11
- if (currentBalance < neededBalance) if (depth <= 3) {
12
- await sleep(200);
13
- await checkBalance(client, walletAddress, step, depth + 1);
14
- } else if (neededBalance * BigInt((1 - (step.action.slippage ?? 0)) * 1e9) / 1000000000n <= currentBalance) step.action.fromAmount = currentBalance.toString();
15
- else {
16
- const needed = formatUnits(neededBalance, token.decimals);
17
- const current = formatUnits(currentBalance, token.decimals);
18
- 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.`;
19
- if (currentBalance !== 0n) errorMessage += `If the problem consists, please delete this transfer and start a new one with a maximum of ${current} ${token.symbol}.`;
20
- throw new BalanceError("The balance is too low.", new Error(errorMessage));
6
+ const MAX_ATTEMPTS = 6;
7
+ const BACKOFF_BASE_MS = 150;
8
+ const OVERALL_TIMEOUT_MS = 1e4;
9
+ const SLIPPAGE_PRECISION = 1000000000n;
10
+ /**
11
+ * Verifies that the wallet holds enough of every token required to execute
12
+ * the step on its source chain — the source-token amount, any gas costs, and
13
+ * any non-included fee costs. Reads all balances in one batched provider
14
+ * call, retries within a bounded budget to absorb transient RPC failures and
15
+ * post-confirmation propagation lag, and applies slippage to the source-token
16
+ * portion only as a last resort (overhead is never trimmed).
17
+ *
18
+ * Throws BalanceError("The balance is too low.") on a genuine shortfall, or
19
+ * BalanceError("Could not read wallet balance.") if the balance can't be read
20
+ * after retries.
21
+ */
22
+ const checkBalance = async (client, walletAddress, step) => {
23
+ const fromChainId = step.action.fromChainId;
24
+ const requirements = /* @__PURE__ */ new Map();
25
+ const add = (token, amount, source) => {
26
+ if (token.chainId !== fromChainId || amount === 0n) return;
27
+ const key = token.address.toLowerCase();
28
+ const req = requirements.get(key) ?? {
29
+ token,
30
+ sourcePart: 0n,
31
+ overheadPart: 0n
32
+ };
33
+ if (source) req.sourcePart += amount;
34
+ else req.overheadPart += amount;
35
+ requirements.set(key, req);
36
+ };
37
+ add(step.action.fromToken, BigInt(step.action.fromAmount), true);
38
+ for (const gas of step.estimate?.gasCosts ?? []) add(gas.token, BigInt(gas.amount), false);
39
+ for (const fee of step.estimate?.feeCosts ?? []) if (!fee.included) add(fee.token, BigInt(fee.amount), false);
40
+ if (requirements.size === 0) return;
41
+ const provider = client.providers.find((p) => p.isAddress(walletAddress));
42
+ if (!provider) throw new Error(`SDK Token Provider for ${walletAddress} is not found.`);
43
+ const reqs = Array.from(requirements.values());
44
+ const tokens = reqs.map((r) => r.token);
45
+ const slippage = step.action.slippage ?? 0;
46
+ const slippageScaled = BigInt(Math.floor((1 - slippage) * Number(SLIPPAGE_PRECISION)));
47
+ await withTimeout(async () => {
48
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
49
+ const isFinal = attempt === MAX_ATTEMPTS - 1;
50
+ let balances;
51
+ try {
52
+ balances = await provider.getBalance(client, walletAddress, tokens);
53
+ } catch (error) {
54
+ if (isFinal) throw new BalanceError("Could not read wallet balance.", error);
55
+ await sleep(BACKOFF_BASE_MS * 2 ** attempt);
56
+ continue;
57
+ }
58
+ const balanceByAddress = new Map(balances.map((b) => [b.address.toLowerCase(), b.amount]));
59
+ const unknown = [];
60
+ const insufficient = [];
61
+ for (const req of reqs) {
62
+ const have = balanceByAddress.get(req.token.address.toLowerCase());
63
+ if (have === void 0) unknown.push(req.token);
64
+ else if (have < req.sourcePart + req.overheadPart) insufficient.push({
65
+ req,
66
+ have
67
+ });
68
+ }
69
+ if (unknown.length === 0 && insufficient.length === 0) return;
70
+ if (isFinal && unknown.length === 0 && insufficient.length === 1 && insufficient[0].req.sourcePart > 0n) {
71
+ const { req, have } = insufficient[0];
72
+ if (have >= req.sourcePart * slippageScaled / SLIPPAGE_PRECISION + req.overheadPart) {
73
+ step.action.fromAmount = (have - req.overheadPart).toString();
74
+ return;
75
+ }
76
+ }
77
+ if (isFinal) {
78
+ if (unknown.length > 0) throw new BalanceError("Could not read wallet balance.", /* @__PURE__ */ new Error(`Could not read balance for: ${unknown.map((t) => t.symbol || t.address).join(", ")}.`));
79
+ const lines = insufficient.map(({ req, have }) => {
80
+ const needed = formatUnits(req.sourcePart + req.overheadPart, req.token.decimals);
81
+ const current = formatUnits(have, req.token.decimals);
82
+ const symbol = req.token.symbol;
83
+ return req.sourcePart > 0n ? `Your ${symbol} balance is too low, you try to transfer ${needed} ${symbol}, but your wallet only holds ${current} ${symbol}.` : `Insufficient ${symbol} for fees: need ${needed} ${symbol}, have ${current} ${symbol}.`;
84
+ });
85
+ throw new BalanceError("The balance is too low.", /* @__PURE__ */ new Error(`${lines.join(" ")} No funds have been sent.`));
86
+ }
87
+ await sleep(BACKOFF_BASE_MS * 2 ** attempt);
21
88
  }
22
- }
89
+ }, {
90
+ timeout: OVERALL_TIMEOUT_MS,
91
+ errorInstance: new BalanceError("Could not read wallet balance.")
92
+ });
23
93
  };
24
94
  //#endregion
25
95
  export { checkBalance };
@@ -1 +1 @@
1
- {"version":3,"file":"checkBalance.js","names":[],"sources":["../../../../../src/core/tasks/helpers/checkBalance.ts"],"sourcesContent":["import type { LiFiStep } from '@lifi/types'\nimport { getTokenBalance } from '../../../actions/getTokenBalance.js'\nimport { BalanceError } from '../../../errors/errors.js'\nimport type { SDKClient } from '../../../types/core.js'\nimport { formatUnits } from '../../../utils/formatUnits.js'\nimport { sleep } from '../../../utils/sleep.js'\n\nexport const checkBalance = async (\n client: SDKClient,\n walletAddress: string,\n step: LiFiStep,\n depth = 0\n): Promise<void> => {\n const token = await getTokenBalance(\n client,\n walletAddress,\n step.action.fromToken\n )\n if (token) {\n const currentBalance = token.amount ?? 0n\n const neededBalance = BigInt(step.action.fromAmount)\n\n if (currentBalance < neededBalance) {\n if (depth <= 3) {\n await sleep(200)\n await checkBalance(client, walletAddress, step, depth + 1)\n } else if (\n (neededBalance *\n BigInt((1 - (step.action.slippage ?? 0)) * 1_000_000_000)) /\n 1_000_000_000n <=\n currentBalance\n ) {\n // adjust amount in slippage limits\n step.action.fromAmount = currentBalance.toString()\n } else {\n const needed = formatUnits(neededBalance, token.decimals)\n const current = formatUnits(currentBalance, token.decimals)\n 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.`\n\n if (currentBalance !== 0n) {\n errorMessage += `If the problem consists, please delete this transfer and start a new one with a maximum of ${current} ${token.symbol}.`\n }\n\n throw new BalanceError(\n 'The balance is too low.',\n new Error(errorMessage)\n )\n }\n }\n }\n}\n"],"mappings":";;;;;AAOA,MAAa,eAAe,OAC1B,QACA,eACA,MACA,QAAQ,MACU;CAClB,MAAM,QAAQ,MAAM,gBAClB,QACA,eACA,KAAK,OAAO,UACb;AACD,KAAI,OAAO;EACT,MAAM,iBAAiB,MAAM,UAAU;EACvC,MAAM,gBAAgB,OAAO,KAAK,OAAO,WAAW;AAEpD,MAAI,iBAAiB,cACnB,KAAI,SAAS,GAAG;AACd,SAAM,MAAM,IAAI;AAChB,SAAM,aAAa,QAAQ,eAAe,MAAM,QAAQ,EAAE;aAEzD,gBACC,QAAQ,KAAK,KAAK,OAAO,YAAY,MAAM,IAAc,GACzD,eACF,eAGA,MAAK,OAAO,aAAa,eAAe,UAAU;OAC7C;GACL,MAAM,SAAS,YAAY,eAAe,MAAM,SAAS;GACzD,MAAM,UAAU,YAAY,gBAAgB,MAAM,SAAS;GAC3D,IAAI,eAAe,QAAQ,MAAM,OAAO,2CAA2C,OAAO,GAAG,MAAM,OAAO,+BAA+B,QAAQ,GAAG,MAAM,OAAO;AAEjK,OAAI,mBAAmB,GACrB,iBAAgB,8FAA8F,QAAQ,GAAG,MAAM,OAAO;AAGxI,SAAM,IAAI,aACR,2BACA,IAAI,MAAM,aAAa,CACxB"}
1
+ {"version":3,"file":"checkBalance.js","names":[],"sources":["../../../../../src/core/tasks/helpers/checkBalance.ts"],"sourcesContent":["import type { LiFiStep, Token, TokenAmount } from '@lifi/types'\nimport { BalanceError } from '../../../errors/errors.js'\nimport type { SDKClient } from '../../../types/core.js'\nimport { formatUnits } from '../../../utils/formatUnits.js'\nimport { sleep } from '../../../utils/sleep.js'\nimport { withTimeout } from '../../../utils/withTimeout.js'\n\nconst MAX_ATTEMPTS = 6\n// Exponential backoff: 150, 300, 600, 1200, 2400 → ≈4.65s of sleep total.\nconst BACKOFF_BASE_MS = 150\nconst OVERALL_TIMEOUT_MS = 10_000\nconst SLIPPAGE_PRECISION = 1_000_000_000n\n\ntype Requirement = {\n token: Token\n sourcePart: bigint // 0n for pure overhead tokens\n overheadPart: bigint // gas + non-included fees in this token\n}\n\n/**\n * Verifies that the wallet holds enough of every token required to execute\n * the step on its source chain — the source-token amount, any gas costs, and\n * any non-included fee costs. Reads all balances in one batched provider\n * call, retries within a bounded budget to absorb transient RPC failures and\n * post-confirmation propagation lag, and applies slippage to the source-token\n * portion only as a last resort (overhead is never trimmed).\n *\n * Throws BalanceError(\"The balance is too low.\") on a genuine shortfall, or\n * BalanceError(\"Could not read wallet balance.\") if the balance can't be read\n * after retries.\n */\nexport const checkBalance = async (\n client: SDKClient,\n walletAddress: string,\n step: LiFiStep\n): Promise<void> => {\n const fromChainId = step.action.fromChainId\n const requirements = new Map<string, Requirement>()\n const add = (token: Token, amount: bigint, source: boolean): void => {\n if (token.chainId !== fromChainId || amount === 0n) {\n return\n }\n const key = token.address.toLowerCase()\n const req = requirements.get(key) ?? {\n token,\n sourcePart: 0n,\n overheadPart: 0n,\n }\n if (source) {\n req.sourcePart += amount\n } else {\n req.overheadPart += amount\n }\n requirements.set(key, req)\n }\n add(step.action.fromToken, BigInt(step.action.fromAmount), true)\n for (const gas of step.estimate?.gasCosts ?? []) {\n add(gas.token, BigInt(gas.amount), false)\n }\n for (const fee of step.estimate?.feeCosts ?? []) {\n // Included fees are already part of fromAmount — don't count twice.\n if (!fee.included) {\n add(fee.token, BigInt(fee.amount), false)\n }\n }\n if (requirements.size === 0) {\n return\n }\n\n // Provider is dispatched by wallet address; all requirements share the\n // source chain, which matches this provider by virtue of the address.\n const provider = client.providers.find((p) => p.isAddress(walletAddress))\n if (!provider) {\n throw new Error(`SDK Token Provider for ${walletAddress} is not found.`)\n }\n\n const reqs = Array.from(requirements.values())\n const tokens = reqs.map((r) => r.token)\n const slippage = step.action.slippage ?? 0\n const slippageScaled = BigInt(\n Math.floor((1 - slippage) * Number(SLIPPAGE_PRECISION))\n )\n\n await withTimeout(\n async () => {\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n const isFinal = attempt === MAX_ATTEMPTS - 1\n\n let balances: TokenAmount[]\n try {\n balances = await provider.getBalance(client, walletAddress, tokens)\n } catch (error) {\n if (isFinal) {\n throw new BalanceError(\n 'Could not read wallet balance.',\n error as Error\n )\n }\n await sleep(BACKOFF_BASE_MS * 2 ** attempt)\n continue\n }\n\n const balanceByAddress = new Map(\n balances.map((b) => [b.address.toLowerCase(), b.amount] as const)\n )\n\n const unknown: Token[] = []\n const insufficient: { req: Requirement; have: bigint }[] = []\n for (const req of reqs) {\n const have = balanceByAddress.get(req.token.address.toLowerCase())\n if (have === undefined) {\n unknown.push(req.token)\n } else if (have < req.sourcePart + req.overheadPart) {\n insufficient.push({ req, have })\n }\n }\n\n if (unknown.length === 0 && insufficient.length === 0) {\n return\n }\n\n // Final-attempt slippage rescue: only when the sole shortfall is the\n // source-token portion. Trim source down to (balance − overhead) so\n // the overhead reserve is preserved.\n if (\n isFinal &&\n unknown.length === 0 &&\n insufficient.length === 1 &&\n insufficient[0].req.sourcePart > 0n\n ) {\n const { req, have } = insufficient[0]\n const minAcceptable =\n (req.sourcePart * slippageScaled) / SLIPPAGE_PRECISION +\n req.overheadPart\n if (have >= minAcceptable) {\n step.action.fromAmount = (have - req.overheadPart).toString()\n return\n }\n }\n\n if (isFinal) {\n if (unknown.length > 0) {\n throw new BalanceError(\n 'Could not read wallet balance.',\n new Error(\n `Could not read balance for: ${unknown\n .map((t) => t.symbol || t.address)\n .join(', ')}.`\n )\n )\n }\n const lines = insufficient.map(({ req, have }) => {\n const needed = formatUnits(\n req.sourcePart + req.overheadPart,\n req.token.decimals\n )\n const current = formatUnits(have, req.token.decimals)\n const symbol = req.token.symbol\n return req.sourcePart > 0n\n ? `Your ${symbol} balance is too low, you try to transfer ${needed} ${symbol}, but your wallet only holds ${current} ${symbol}.`\n : `Insufficient ${symbol} for fees: need ${needed} ${symbol}, have ${current} ${symbol}.`\n })\n throw new BalanceError(\n 'The balance is too low.',\n new Error(`${lines.join(' ')} No funds have been sent.`)\n )\n }\n\n await sleep(BACKOFF_BASE_MS * 2 ** attempt)\n }\n },\n {\n timeout: OVERALL_TIMEOUT_MS,\n errorInstance: new BalanceError('Could not read wallet balance.'),\n }\n )\n}\n"],"mappings":"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"}
@@ -50,5 +50,6 @@ import { parseUnits } from "./utils/parseUnits.js";
50
50
  import { sleep } from "./utils/sleep.js";
51
51
  import { waitForResult } from "./utils/waitForResult.js";
52
52
  import { LruMap, withDedupe } from "./utils/withDedupe.js";
53
+ import { withTimeout } from "./utils/withTimeout.js";
53
54
  export * from "@lifi/types";
54
- export { type AcceptExchangeRateUpdateHook, type AcceptSlippageUpdateHook, type AcceptSlippageUpdateHookParams, BalanceError, BaseError, BaseStepExecutionTask, BaseStepExecutor, CheckBalanceTask, type ContractCallParams, type ContractTool, type ErrorCode, ErrorMessage, ErrorName, type ExchangeRateUpdateParams, ExecuteStepRetryError, type ExecuteStepRetryParams, type Execution, type ExecutionAction, type ExecutionActionStatus, type ExecutionActionType, type ExecutionOptions, type ExecutionStatus, type GetContractCallsHook, type GetContractCallsResult, HTTPError, InMemoryStorage, type InteractionSettings, LiFiErrorCode, type LiFiStepExtended, LocalStorageAdapter, LruMap, PrepareTransactionTask, ProviderError, RPCError, type RPCUrls, type RequestInterceptor, type RouteExecutionData, type RouteExecutionDataDictionary, type RouteExecutionDictionary, type RouteExtended, type SDKBaseConfig, type SDKClient, type SDKConfig, SDKError, type SDKProvider, type SDKStorage, ServerError, StatusManager, type StepExecutor, type StepExecutorBaseContext, type StepExecutorContext, type StepExecutorOptions, type StepExtended, TaskPipeline, type TaskResult, type TaskStatus, TransactionError, type TransactionMethodType, type TransactionParameters, type TransactionRequestParameters, type TransactionRequestUpdateHook, UnknownError, type UpdateRouteHook, ValidationError, WaitForTransactionStatusTask, actions, checkBalance, checkPackageUpdates, convertQuoteToRoute, createClient, createDefaultStorage, executeRoute, fetchTxErrorDetails, formatUnits, getActionMessage, getActiveRoute, getActiveRoutes, getChains, getConnections, getContractCallsQuote, getGasRecommendation, getNameServiceAddress, getQuote, getRelayedTransactionStatus, getRelayerQuote, getRoutes, getStatus, getStepTransaction, getSubstatusMessage, getToken, getTokenBalance, getTokenBalances, getTokenBalancesByChain, getTokens, getTools, getTransactionHistory, getTransactionRequestData, getWalletBalances, isHex, parseUnits, patchContractCalls, relayTransaction, resumeRoute, sleep, stepComparison, stopRouteExecution, updateRouteExecution, waitForResult, withDedupe };
55
+ export { type AcceptExchangeRateUpdateHook, type AcceptSlippageUpdateHook, type AcceptSlippageUpdateHookParams, BalanceError, BaseError, BaseStepExecutionTask, BaseStepExecutor, CheckBalanceTask, type ContractCallParams, type ContractTool, type ErrorCode, ErrorMessage, ErrorName, type ExchangeRateUpdateParams, ExecuteStepRetryError, type ExecuteStepRetryParams, type Execution, type ExecutionAction, type ExecutionActionStatus, type ExecutionActionType, type ExecutionOptions, type ExecutionStatus, type GetContractCallsHook, type GetContractCallsResult, HTTPError, InMemoryStorage, type InteractionSettings, LiFiErrorCode, type LiFiStepExtended, LocalStorageAdapter, LruMap, PrepareTransactionTask, ProviderError, RPCError, type RPCUrls, type RequestInterceptor, type RouteExecutionData, type RouteExecutionDataDictionary, type RouteExecutionDictionary, type RouteExtended, type SDKBaseConfig, type SDKClient, type SDKConfig, SDKError, type SDKProvider, type SDKStorage, ServerError, StatusManager, type StepExecutor, type StepExecutorBaseContext, type StepExecutorContext, type StepExecutorOptions, type StepExtended, TaskPipeline, type TaskResult, type TaskStatus, TransactionError, type TransactionMethodType, type TransactionParameters, type TransactionRequestParameters, type TransactionRequestUpdateHook, UnknownError, type UpdateRouteHook, ValidationError, WaitForTransactionStatusTask, actions, checkBalance, checkPackageUpdates, convertQuoteToRoute, createClient, createDefaultStorage, executeRoute, fetchTxErrorDetails, formatUnits, getActionMessage, getActiveRoute, getActiveRoutes, getChains, getConnections, getContractCallsQuote, getGasRecommendation, getNameServiceAddress, getQuote, getRelayedTransactionStatus, getRelayerQuote, getRoutes, getStatus, getStepTransaction, getSubstatusMessage, getToken, getTokenBalance, getTokenBalances, getTokenBalancesByChain, getTokens, getTools, getTransactionHistory, getTransactionRequestData, getWalletBalances, isHex, parseUnits, patchContractCalls, relayTransaction, resumeRoute, sleep, stepComparison, stopRouteExecution, updateRouteExecution, waitForResult, withDedupe, withTimeout };
package/dist/esm/index.js CHANGED
@@ -37,6 +37,7 @@ import { executeRoute, getActiveRoute, getActiveRoutes, resumeRoute, stopRouteEx
37
37
  import { InMemoryStorage, LocalStorageAdapter, createDefaultStorage } from "./core/storage.js";
38
38
  import { TaskPipeline } from "./core/TaskPipeline.js";
39
39
  import { formatUnits } from "./utils/formatUnits.js";
40
+ import { withTimeout } from "./utils/withTimeout.js";
40
41
  import { checkBalance } from "./core/tasks/helpers/checkBalance.js";
41
42
  import { CheckBalanceTask } from "./core/tasks/CheckBalanceTask.js";
42
43
  import { getTransactionRequestData } from "./core/tasks/helpers/getTransactionRequestData.js";
@@ -49,4 +50,4 @@ import { fetchTxErrorDetails } from "./utils/fetchTxErrorDetails.js";
49
50
  import { isHex } from "./utils/isHex.js";
50
51
  import { parseUnits } from "./utils/parseUnits.js";
51
52
  export * from "@lifi/types";
52
- export { BalanceError, BaseError, BaseStepExecutionTask, BaseStepExecutor, CheckBalanceTask, ErrorMessage, ErrorName, ExecuteStepRetryError, HTTPError, InMemoryStorage, LiFiErrorCode, LocalStorageAdapter, LruMap, PrepareTransactionTask, ProviderError, RPCError, SDKError, ServerError, StatusManager, TaskPipeline, TransactionError, UnknownError, ValidationError, WaitForTransactionStatusTask, actions, checkBalance, checkPackageUpdates, convertQuoteToRoute, createClient, createDefaultStorage, executeRoute, fetchTxErrorDetails, formatUnits, getActionMessage, getActiveRoute, getActiveRoutes, getChains, getConnections, getContractCallsQuote, getGasRecommendation, getNameServiceAddress, getQuote, getRelayedTransactionStatus, getRelayerQuote, getRoutes, getStatus, getStepTransaction, getSubstatusMessage, getToken, getTokenBalance, getTokenBalances, getTokenBalancesByChain, getTokens, getTools, getTransactionHistory, getTransactionRequestData, getWalletBalances, isHex, parseUnits, patchContractCalls, relayTransaction, resumeRoute, sleep, stepComparison, stopRouteExecution, updateRouteExecution, waitForResult, withDedupe };
53
+ export { BalanceError, BaseError, BaseStepExecutionTask, BaseStepExecutor, CheckBalanceTask, ErrorMessage, ErrorName, ExecuteStepRetryError, HTTPError, InMemoryStorage, LiFiErrorCode, LocalStorageAdapter, LruMap, PrepareTransactionTask, ProviderError, RPCError, SDKError, ServerError, StatusManager, TaskPipeline, TransactionError, UnknownError, ValidationError, WaitForTransactionStatusTask, actions, checkBalance, checkPackageUpdates, convertQuoteToRoute, createClient, createDefaultStorage, executeRoute, fetchTxErrorDetails, formatUnits, getActionMessage, getActiveRoute, getActiveRoutes, getChains, getConnections, getContractCallsQuote, getGasRecommendation, getNameServiceAddress, getQuote, getRelayedTransactionStatus, getRelayerQuote, getRoutes, getStatus, getStepTransaction, getSubstatusMessage, getToken, getTokenBalance, getTokenBalances, getTokenBalancesByChain, getTokens, getTools, getTransactionHistory, getTransactionRequestData, getWalletBalances, isHex, parseUnits, patchContractCalls, relayTransaction, resumeRoute, sleep, stepComparison, stopRouteExecution, updateRouteExecution, waitForResult, withDedupe, withTimeout };
@@ -4,7 +4,7 @@ const checkPackageUpdates = async (packageName, packageVersion) => {
4
4
  try {
5
5
  const pkgName = packageName ?? "@lifi/sdk";
6
6
  const latestVersion = (await (await fetch(`https://registry.npmjs.org/${pkgName}/latest`)).json()).version;
7
- const currentVersion = packageVersion ?? "4.0.0-beta.5";
7
+ const currentVersion = packageVersion ?? "4.0.0-beta.7";
8
8
  if (latestVersion > currentVersion) console.warn(`${pkgName}: new package version is available. Please update as soon as possible to enjoy the newest features. Current version: ${currentVersion}. Latest version: ${latestVersion}.`);
9
9
  } catch (_error) {}
10
10
  };
@@ -0,0 +1,26 @@
1
+ //#region src/utils/withTimeout.d.ts
2
+ /**
3
+ * Wraps a function in a timeout.
4
+ * Based on viem's withTimeout implementation.
5
+ * @param fn - The function to wrap.
6
+ * @param timeout - The timeout in milliseconds.
7
+ * @param errorInstance - The error instance to throw when the timeout is reached.
8
+ * @param signal - Whether or not the timeout should use an abort signal.
9
+ * @returns The result of the function.
10
+ */
11
+ declare function withTimeout<T>(fn: ({
12
+ signal
13
+ }: {
14
+ signal: AbortController["signal"] | null;
15
+ }) => Promise<T>, {
16
+ errorInstance,
17
+ timeout,
18
+ signal
19
+ }: {
20
+ errorInstance?: Error | undefined;
21
+ timeout: number;
22
+ signal?: boolean | undefined;
23
+ }): Promise<T>;
24
+ //#endregion
25
+ export { withTimeout };
26
+ //# sourceMappingURL=withTimeout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTimeout.d.ts","names":[],"sources":["../../../src/utils/withTimeout.ts"],"mappings":";;AASA;;;;;;;;iBAAgB,WAAA,GAAA,CACd,EAAA;EAAO;AAAA;EAAY,MAAA,EAAQ,eAAA;AAAA,MAAuC,OAAA,CAAQ,CAAA;EAExE,aAAA;EACA,OAAA;EACA;AAAA;EAGA,aAAA,GAAgB,KAAA;EAEhB,OAAA;EAEA,MAAA;AAAA,IAED,OAAA,CAAQ,CAAA"}
@@ -0,0 +1,34 @@
1
+ //#region src/utils/withTimeout.ts
2
+ /**
3
+ * Wraps a function in a timeout.
4
+ * Based on viem's withTimeout implementation.
5
+ * @param fn - The function to wrap.
6
+ * @param timeout - The timeout in milliseconds.
7
+ * @param errorInstance - The error instance to throw when the timeout is reached.
8
+ * @param signal - Whether or not the timeout should use an abort signal.
9
+ * @returns The result of the function.
10
+ */
11
+ function withTimeout(fn, { errorInstance = /* @__PURE__ */ new Error("Timed out after waiting for too long."), timeout, signal }) {
12
+ return new Promise((resolve, reject) => {
13
+ (async () => {
14
+ let timeoutId;
15
+ try {
16
+ const controller = new AbortController();
17
+ if (timeout > 0) timeoutId = setTimeout(() => {
18
+ if (signal) controller.abort();
19
+ else reject(errorInstance);
20
+ }, timeout);
21
+ resolve(await fn({ signal: controller?.signal || null }));
22
+ } catch (err) {
23
+ if (err?.name === "AbortError") reject(errorInstance);
24
+ reject(err);
25
+ } finally {
26
+ clearTimeout(timeoutId);
27
+ }
28
+ })();
29
+ });
30
+ }
31
+ //#endregion
32
+ export { withTimeout };
33
+
34
+ //# sourceMappingURL=withTimeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTimeout.js","names":[],"sources":["../../../src/utils/withTimeout.ts"],"sourcesContent":["/**\n * Wraps a function in a timeout.\n * Based on viem's withTimeout implementation.\n * @param fn - The function to wrap.\n * @param timeout - The timeout in milliseconds.\n * @param errorInstance - The error instance to throw when the timeout is reached.\n * @param signal - Whether or not the timeout should use an abort signal.\n * @returns The result of the function.\n */\nexport function withTimeout<T>(\n fn: ({ signal }: { signal: AbortController['signal'] | null }) => Promise<T>,\n {\n errorInstance = new Error('Timed out after waiting for too long.'),\n timeout,\n signal,\n }: {\n // The error instance to throw when the timeout is reached.\n errorInstance?: Error | undefined\n // The timeout (in ms).\n timeout: number\n // Whether or not the timeout should use an abort signal.\n signal?: boolean | undefined\n }\n): Promise<T> {\n return new Promise((resolve, reject) => {\n ;(async () => {\n let timeoutId!: NodeJS.Timeout\n try {\n const controller = new AbortController()\n if (timeout > 0) {\n timeoutId = setTimeout(() => {\n if (signal) {\n controller.abort()\n } else {\n reject(errorInstance)\n }\n }, timeout) as NodeJS.Timeout // need to cast because bun globals.d.ts overrides @types/node\n }\n resolve(await fn({ signal: controller?.signal || null }))\n } catch (err) {\n if ((err as Error)?.name === 'AbortError') {\n reject(errorInstance)\n }\n reject(err)\n } finally {\n clearTimeout(timeoutId)\n }\n })()\n })\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,YACd,IACA,EACE,gCAAgB,IAAI,MAAM,wCAAwC,EAClE,SACA,UASU;AACZ,QAAO,IAAI,SAAS,SAAS,WAAW;AACrC,GAAC,YAAY;GACZ,IAAI;AACJ,OAAI;IACF,MAAM,aAAa,IAAI,iBAAiB;AACxC,QAAI,UAAU,EACZ,aAAY,iBAAiB;AAC3B,SAAI,OACF,YAAW,OAAO;SAElB,QAAO,cAAc;OAEtB,QAAQ;AAEb,YAAQ,MAAM,GAAG,EAAE,QAAQ,YAAY,UAAU,MAAM,CAAC,CAAC;YAClD,KAAK;AACZ,QAAK,KAAe,SAAS,aAC3B,QAAO,cAAc;AAEvB,WAAO,IAAI;aACH;AACR,iBAAa,UAAU;;MAEvB;GACJ"}
@@ -1,6 +1,6 @@
1
1
  //#region src/version.d.ts
2
2
  declare const name = "@lifi/sdk";
3
- declare const version = "4.0.0-beta.5";
3
+ declare const version = "4.0.0-beta.7";
4
4
  //#endregion
5
5
  export { name, version };
6
6
  //# sourceMappingURL=version.d.ts.map
@@ -1,6 +1,6 @@
1
1
  //#region src/version.ts
2
2
  const name = "@lifi/sdk";
3
- const version = "4.0.0-beta.5";
3
+ const version = "4.0.0-beta.7";
4
4
  //#endregion
5
5
  export { name, version };
6
6
 
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","names":[],"sources":["../../src/version.ts"],"sourcesContent":["export const name = '@lifi/sdk'\nexport const version = '4.0.0-beta.5'\n"],"mappings":";AAAA,MAAa,OAAO;AACpB,MAAa,UAAU"}
1
+ {"version":3,"file":"version.js","names":[],"sources":["../../src/version.ts"],"sourcesContent":["export const name = '@lifi/sdk'\nexport const version = '4.0.0-beta.7'\n"],"mappings":";AAAA,MAAa,OAAO;AACpB,MAAa,UAAU"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/sdk",
3
- "version": "4.0.0-beta.5",
3
+ "version": "4.0.0-beta.7",
4
4
  "description": "LI.FI SDK for Any-to-Any Cross-Chain-Swap",
5
5
  "homepage": "https://github.com/lifinance/sdk",
6
6
  "bugs": {
@@ -28,7 +28,7 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "@lifi/types": "17.75.0"
31
+ "@lifi/types": "17.76.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"
@@ -49,6 +49,7 @@ export const getClientStorage = (config: SDKBaseConfig): ClientStorage => {
49
49
  ChainType.SVM,
50
50
  ChainType.UTXO,
51
51
  ChainType.MVM,
52
+ ChainType.TVM,
52
53
  ],
53
54
  })
54
55
  _chainsUpdatedAt = Date.now()
@@ -1,51 +1,177 @@
1
- import type { LiFiStep } from '@lifi/types'
2
- import { getTokenBalance } from '../../../actions/getTokenBalance.js'
1
+ import type { LiFiStep, Token, TokenAmount } from '@lifi/types'
3
2
  import { BalanceError } from '../../../errors/errors.js'
4
3
  import type { SDKClient } from '../../../types/core.js'
5
4
  import { formatUnits } from '../../../utils/formatUnits.js'
6
5
  import { sleep } from '../../../utils/sleep.js'
6
+ import { withTimeout } from '../../../utils/withTimeout.js'
7
7
 
8
+ const MAX_ATTEMPTS = 6
9
+ // Exponential backoff: 150, 300, 600, 1200, 2400 → ≈4.65s of sleep total.
10
+ const BACKOFF_BASE_MS = 150
11
+ const OVERALL_TIMEOUT_MS = 10_000
12
+ const SLIPPAGE_PRECISION = 1_000_000_000n
13
+
14
+ type Requirement = {
15
+ token: Token
16
+ sourcePart: bigint // 0n for pure overhead tokens
17
+ overheadPart: bigint // gas + non-included fees in this token
18
+ }
19
+
20
+ /**
21
+ * Verifies that the wallet holds enough of every token required to execute
22
+ * the step on its source chain — the source-token amount, any gas costs, and
23
+ * any non-included fee costs. Reads all balances in one batched provider
24
+ * call, retries within a bounded budget to absorb transient RPC failures and
25
+ * post-confirmation propagation lag, and applies slippage to the source-token
26
+ * portion only as a last resort (overhead is never trimmed).
27
+ *
28
+ * Throws BalanceError("The balance is too low.") on a genuine shortfall, or
29
+ * BalanceError("Could not read wallet balance.") if the balance can't be read
30
+ * after retries.
31
+ */
8
32
  export const checkBalance = async (
9
33
  client: SDKClient,
10
34
  walletAddress: string,
11
- step: LiFiStep,
12
- depth = 0
35
+ step: LiFiStep
13
36
  ): Promise<void> => {
14
- const token = await getTokenBalance(
15
- client,
16
- walletAddress,
17
- step.action.fromToken
37
+ const fromChainId = step.action.fromChainId
38
+ const requirements = new Map<string, Requirement>()
39
+ const add = (token: Token, amount: bigint, source: boolean): void => {
40
+ if (token.chainId !== fromChainId || amount === 0n) {
41
+ return
42
+ }
43
+ const key = token.address.toLowerCase()
44
+ const req = requirements.get(key) ?? {
45
+ token,
46
+ sourcePart: 0n,
47
+ overheadPart: 0n,
48
+ }
49
+ if (source) {
50
+ req.sourcePart += amount
51
+ } else {
52
+ req.overheadPart += amount
53
+ }
54
+ requirements.set(key, req)
55
+ }
56
+ add(step.action.fromToken, BigInt(step.action.fromAmount), true)
57
+ for (const gas of step.estimate?.gasCosts ?? []) {
58
+ add(gas.token, BigInt(gas.amount), false)
59
+ }
60
+ for (const fee of step.estimate?.feeCosts ?? []) {
61
+ // Included fees are already part of fromAmount — don't count twice.
62
+ if (!fee.included) {
63
+ add(fee.token, BigInt(fee.amount), false)
64
+ }
65
+ }
66
+ if (requirements.size === 0) {
67
+ return
68
+ }
69
+
70
+ // Provider is dispatched by wallet address; all requirements share the
71
+ // source chain, which matches this provider by virtue of the address.
72
+ const provider = client.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))
18
82
  )
19
- if (token) {
20
- const currentBalance = token.amount ?? 0n
21
- const neededBalance = BigInt(step.action.fromAmount)
22
-
23
- if (currentBalance < neededBalance) {
24
- if (depth <= 3) {
25
- await sleep(200)
26
- await checkBalance(client, walletAddress, step, depth + 1)
27
- } else if (
28
- (neededBalance *
29
- BigInt((1 - (step.action.slippage ?? 0)) * 1_000_000_000)) /
30
- 1_000_000_000n <=
31
- currentBalance
32
- ) {
33
- // adjust amount in slippage limits
34
- step.action.fromAmount = currentBalance.toString()
35
- } else {
36
- const needed = formatUnits(neededBalance, token.decimals)
37
- const current = formatUnits(currentBalance, token.decimals)
38
- 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.`
39
-
40
- if (currentBalance !== 0n) {
41
- errorMessage += `If the problem consists, please delete this transfer and start a new one with a maximum of ${current} ${token.symbol}.`
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(client, 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
42
101
  }
43
102
 
44
- throw new BalanceError(
45
- 'The balance is too low.',
46
- new Error(errorMessage)
103
+ const balanceByAddress = new Map(
104
+ balances.map((b) => [b.address.toLowerCase(), b.amount] as const)
47
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)
48
170
  }
171
+ },
172
+ {
173
+ timeout: OVERALL_TIMEOUT_MS,
174
+ errorInstance: new BalanceError('Could not read wallet balance.'),
49
175
  }
50
- }
176
+ )
51
177
  }
package/src/index.ts CHANGED
@@ -116,3 +116,4 @@ export { parseUnits } from './utils/parseUnits.js'
116
116
  export { sleep } from './utils/sleep.js'
117
117
  export { waitForResult } from './utils/waitForResult.js'
118
118
  export { LruMap, withDedupe } from './utils/withDedupe.js'
119
+ export { withTimeout } from './utils/withTimeout.js'
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Wraps a function in a timeout.
3
+ * Based on viem's withTimeout implementation.
4
+ * @param fn - The function to wrap.
5
+ * @param timeout - The timeout in milliseconds.
6
+ * @param errorInstance - The error instance to throw when the timeout is reached.
7
+ * @param signal - Whether or not the timeout should use an abort signal.
8
+ * @returns The result of the function.
9
+ */
10
+ export function withTimeout<T>(
11
+ fn: ({ signal }: { signal: AbortController['signal'] | null }) => Promise<T>,
12
+ {
13
+ errorInstance = new Error('Timed out after waiting for too long.'),
14
+ timeout,
15
+ signal,
16
+ }: {
17
+ // The error instance to throw when the timeout is reached.
18
+ errorInstance?: Error | undefined
19
+ // The timeout (in ms).
20
+ timeout: number
21
+ // Whether or not the timeout should use an abort signal.
22
+ signal?: boolean | undefined
23
+ }
24
+ ): Promise<T> {
25
+ return new Promise((resolve, reject) => {
26
+ ;(async () => {
27
+ let timeoutId!: NodeJS.Timeout
28
+ try {
29
+ const controller = new AbortController()
30
+ if (timeout > 0) {
31
+ timeoutId = setTimeout(() => {
32
+ if (signal) {
33
+ controller.abort()
34
+ } else {
35
+ reject(errorInstance)
36
+ }
37
+ }, timeout) as NodeJS.Timeout // need to cast because bun globals.d.ts overrides @types/node
38
+ }
39
+ resolve(await fn({ signal: controller?.signal || null }))
40
+ } catch (err) {
41
+ if ((err as Error)?.name === 'AbortError') {
42
+ reject(errorInstance)
43
+ }
44
+ reject(err)
45
+ } finally {
46
+ clearTimeout(timeoutId)
47
+ }
48
+ })()
49
+ })
50
+ }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/sdk'
2
- export const version = '4.0.0-beta.5'
2
+ export const version = '4.0.0-beta.7'