@owney/sdk 0.7.25-beta.15 → 0.7.25-beta.16

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.
package/dist/index.cjs CHANGED
@@ -404,20 +404,44 @@ function observeWithdrawalSubmission(sdk, chainId, token) {
404
404
  };
405
405
  const notify = (outcome) => {
406
406
  if (outcome === "rejected" && matching.length !== 1) return;
407
+ const evidence = matching.length === 1 ? { ...request2 } : {
408
+ attempted: request2.attempted,
409
+ method: request2.method,
410
+ path: request2.path,
411
+ ...request2.elapsedMs !== void 0 ? { elapsedMs: request2.elapsedMs } : {}
412
+ };
407
413
  for (const observer of matching) {
408
414
  try {
409
- observer.notify(outcome, { ...request2 });
415
+ observer.notify(outcome, evidence);
410
416
  } catch {
411
417
  }
412
418
  }
413
419
  };
414
420
  notify("attempted");
421
+ let callArgs = args;
422
+ if (withdrawal && client.client?.defaults) {
423
+ const configIndex = method === "get" ? 1 : 2;
424
+ const config = args[configIndex];
425
+ const transforms = config?.transformResponse !== void 0 ? config.transformResponse : client.client.defaults.transformResponse;
426
+ const captureStatus = (data, _headers, status) => {
427
+ if (typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599) request2.httpStatus = status;
428
+ return data;
429
+ };
430
+ callArgs = [...args];
431
+ callArgs[configIndex] = { ...config, transformResponse: [
432
+ ...Array.isArray(transforms) ? transforms : typeof transforms === "function" ? [transforms] : [],
433
+ captureStatus
434
+ ] };
435
+ }
415
436
  try {
416
- return await original.apply(this, args);
437
+ return await original.apply(this, callArgs);
417
438
  } catch (error) {
418
439
  const message = error instanceof Error ? error.message : "";
419
- request2.failure = message === "Unauthorized: Invalid API key" ? "unauthorized" : message === "Forbidden: Access denied" ? "forbidden" : message === "Rate limit exceeded. Please try again later." ? "rate-limited" : /^Idle balance \d+(?:\.\d+)? (USDC|WETH|ETH|EURC) is below the minimum withdrawal of \d+(?:\.\d+)? \1\.?$/.test(message) ? "insufficient-balance" : /^User doesn't have any active session key\.?$/.test(message) ? "session-key-missing" : message === "Network error: Unable to reach the server" ? "network" : "other";
420
- if (["unauthorized", "forbidden", "rate-limited", "insufficient-balance", "session-key-missing"].includes(request2.failure)) notify("rejected");
440
+ const status = error?.response?.status;
441
+ if (request2.httpStatus === void 0 && typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599)
442
+ request2.httpStatus = status;
443
+ request2.failure = message === "Unauthorized: Invalid API key" ? "unauthorized" : message === "Forbidden: Access denied" ? "forbidden" : message === "Rate limit exceeded. Please try again later." ? "rate-limited" : /^Idle balance \d+(?:\.\d+)? (USDC|WETH|ETH|EURC) is below the minimum withdrawal of \d+(?:\.\d+)? \1\.?$/.test(message) ? "insufficient-balance" : /^User doesn't have any active session key\.?$/.test(message) ? "session-key-missing" : method === "post" && request2.httpStatus === 400 && message === "Partial withdrawal already in progress" ? "withdrawal-in-progress" : message === "Network error: Unable to reach the server" ? "network" : "other";
444
+ if (["unauthorized", "forbidden", "rate-limited", "insufficient-balance", "session-key-missing", "withdrawal-in-progress"].includes(request2.failure) && (request2.httpStatus === void 0 || request2.httpStatus >= 400 && request2.httpStatus < 500)) notify("rejected");
421
445
  throw error;
422
446
  } finally {
423
447
  request2.elapsedMs = Math.max(0, Date.now() - startedAt);
@@ -1495,6 +1519,7 @@ var VIEM_CHAIN = { 8453: import_chains.base, 42161: import_chains.arbitrum, 1: i
1495
1519
  var SPLITTING_ASSETS = ["USDC", "WETH"];
1496
1520
  var POOL_POLICY_CHAIN_IDS = [8453, 42161, 1];
1497
1521
  var EARNINGS_REFRESH_COOLDOWN_MS = 60 * 60 * 1e3;
1522
+ var WITHDRAW_BALANCE_TIMEOUT_MS = 2e3;
1498
1523
  var ZyfaiAgent = class _ZyfaiAgent {
1499
1524
  id = "zyfai";
1500
1525
  supportedChainIds = [8453, 42161, 1];
@@ -2409,9 +2434,74 @@ var ZyfaiAgent = class _ZyfaiAgent {
2409
2434
  }
2410
2435
  }
2411
2436
  // --- IAgent: Fund operations ---
2437
+ /** Only after a definitive endpoint rejection, combine a new provider
2438
+ * snapshot with the Safe's exact idle balance. Never parse its rounded error
2439
+ * amount as proof of zero, or use this to skip sending a new MAX request. */
2440
+ async assertNotEmptyRejectedWithdrawal(state, chainId, token, owner) {
2441
+ const internals = this.sdk;
2442
+ const generation = this.earningsGeneration;
2443
+ let expired = false;
2444
+ const unavailable = () => new OwneyError(
2445
+ "WITHDRAW_BALANCE_UNAVAILABLE",
2446
+ "Could not verify Zyfai's balance after its withdrawal rejection.",
2447
+ { chainId, token },
2448
+ this.id
2449
+ );
2450
+ const assertScope = () => {
2451
+ if (expired) throw unavailable();
2452
+ const account = internals.walletClient?.account?.address;
2453
+ if (generation !== this.earningsGeneration || state.chainId !== chainId || state.walletAddress.toLowerCase() !== owner.toLowerCase() || this.connectedAddress?.toLowerCase() !== owner.toLowerCase() || this.connectedChainId !== chainId || account && account.toLowerCase() !== owner.toLowerCase())
2454
+ throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
2455
+ };
2456
+ const read2 = async () => {
2457
+ assertScope();
2458
+ const [wallet, registered, portfolio] = await Promise.all([
2459
+ this.sdk.getSmartWalletAddress(owner, chainId),
2460
+ this.sdk.getSmartWalletByEOA(owner),
2461
+ this.sdk.getPortfolio(owner).catch(() => void 0)
2462
+ ]);
2463
+ assertScope();
2464
+ if (!(0, import_viem4.isAddress)(wallet.address) || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase())
2465
+ throw new OwneyError(
2466
+ "AGENT_AUTH_FAILED",
2467
+ "Could not verify Zyfai's withdrawal wallet.",
2468
+ { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
2469
+ this.id
2470
+ );
2471
+ if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
2472
+ const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
2473
+ const idle = asset ? await this.getPublicClient(chainId).readContract({
2474
+ address: asset.address,
2475
+ abi: import_viem4.erc20Abi,
2476
+ functionName: "balanceOf",
2477
+ args: [wallet.address]
2478
+ }).catch(() => void 0) : void 0;
2479
+ assertScope();
2480
+ if (idle === 0n) throw new OwneyError(
2481
+ "WITHDRAW_INSUFFICIENT_BALANCE",
2482
+ "No funds remain in Zyfai for this asset.",
2483
+ { available: "0", chainId, token },
2484
+ this.id
2485
+ );
2486
+ }
2487
+ };
2488
+ let timer;
2489
+ try {
2490
+ await Promise.race([read2(), new Promise((_, reject) => {
2491
+ timer = setTimeout(() => {
2492
+ expired = true;
2493
+ reject(unavailable());
2494
+ }, WITHDRAW_BALANCE_TIMEOUT_MS);
2495
+ })]);
2496
+ } finally {
2497
+ expired = true;
2498
+ if (timer) clearTimeout(timer);
2499
+ }
2500
+ }
2412
2501
  /** Called only after the execution endpoint explicitly rejected a request.
2413
2502
  * A timeout or an accepted request never authorizes repair and resubmission. */
2414
2503
  async recoverWithdrawalSession(state, chainId, token, owner) {
2504
+ await this.assertNotEmptyRejectedWithdrawal(state, chainId, token, owner);
2415
2505
  const internals = this.sdk;
2416
2506
  const assertScope = () => {
2417
2507
  const account = internals.walletClient?.account?.address;
@@ -2419,35 +2509,6 @@ var ZyfaiAgent = class _ZyfaiAgent {
2419
2509
  throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
2420
2510
  };
2421
2511
  assertScope();
2422
- const [wallet, registered, portfolio] = await Promise.all([
2423
- this.sdk.getSmartWalletAddress(owner, chainId),
2424
- this.sdk.getSmartWalletByEOA(owner),
2425
- this.sdk.getPortfolio(owner).catch(() => void 0)
2426
- ]);
2427
- assertScope();
2428
- if (!(0, import_viem4.isAddress)(wallet.address) || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase())
2429
- throw new OwneyError(
2430
- "AGENT_AUTH_FAILED",
2431
- "Could not verify Zyfai's withdrawal wallet.",
2432
- { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
2433
- this.id
2434
- );
2435
- if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
2436
- const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
2437
- const idle = asset ? await this.getPublicClient(chainId).readContract({
2438
- address: asset.address,
2439
- abi: import_viem4.erc20Abi,
2440
- functionName: "balanceOf",
2441
- args: [wallet.address]
2442
- }).catch(() => void 0) : void 0;
2443
- assertScope();
2444
- if (idle === 0n) throw new OwneyError(
2445
- "WITHDRAW_INSUFFICIENT_BALANCE",
2446
- "No funds remain in Zyfai for this asset.",
2447
- { available: "0", chainId, token },
2448
- this.id
2449
- );
2450
- }
2451
2512
  const walletState = await this.readWalletState(owner);
2452
2513
  assertScope();
2453
2514
  const unavailable = () => new OwneyError(
@@ -2499,6 +2560,13 @@ var ZyfaiAgent = class _ZyfaiAgent {
2499
2560
  raw2 = await this.sdk.withdrawFunds(owner, validChainId, amount, token);
2500
2561
  break;
2501
2562
  } catch (error) {
2563
+ if (!amount && observation.rejected() && observation.request()?.failure === "insufficient-balance") {
2564
+ try {
2565
+ await this.assertNotEmptyRejectedWithdrawal(state, validChainId, token, owner);
2566
+ } catch (proofError) {
2567
+ if (proofError instanceof OwneyError && (proofError.code === "WITHDRAW_INSUFFICIENT_BALANCE" || proofError.code === "NOT_CONNECTED")) throw proofError;
2568
+ }
2569
+ }
2502
2570
  if (!observation.rejected() || observation.request()?.failure !== "session-key-missing") throw error;
2503
2571
  if (attempt === 1)
2504
2572
  throw new OwneyError(
@@ -2592,6 +2660,67 @@ var ZyfaiAgent = class _ZyfaiAgent {
2592
2660
  async getFundingWallet(state, chainId) {
2593
2661
  return (await this.sdk.getSmartWalletAddress(state.walletAddress, isValidChainId(chainId))).address;
2594
2662
  }
2663
+ /** A positive Safe balance is a withdrawal planning floor when the portfolio
2664
+ * index reports zero. Idle zero cannot prove that Zyfai's protocol positions
2665
+ * are empty: its SDK does not expose a complete contract catalog for them. */
2666
+ async getWithdrawableBalance(state, chainId, token) {
2667
+ const validChainId = isValidChainId(chainId);
2668
+ const asset = SupportedAssets.find((value) => value.chainId === validChainId && value.symbol === token);
2669
+ if (!asset) throw new OwneyError("WITHDRAW_NO_PERMITTED_TOKENS", "Asset selected is not supported", void 0, this.id);
2670
+ const owner = state.walletAddress;
2671
+ const generation = this.earningsGeneration;
2672
+ const internals = this.sdk;
2673
+ let expired = false;
2674
+ const unavailable = () => new OwneyError(
2675
+ "WITHDRAW_BALANCE_UNAVAILABLE",
2676
+ "Could not verify Zyfai's withdrawal balance. Its vault positions may still contain funds.",
2677
+ { reason: "ZYFAI_VAULT_BALANCE_UNVERIFIED", chainId, token },
2678
+ this.id
2679
+ );
2680
+ const assertScope = () => {
2681
+ if (expired) throw unavailable();
2682
+ const account = internals.walletClient?.account?.address;
2683
+ if (generation !== this.earningsGeneration || this.connectedAddress?.toLowerCase() !== owner.toLowerCase() || this.connectedChainId !== validChainId || state.walletAddress.toLowerCase() !== owner.toLowerCase() || state.chainId !== validChainId || account && account.toLowerCase() !== owner.toLowerCase())
2684
+ throw new OwneyError("NOT_CONNECTED", "Wallet changed while checking Zyfai's withdrawal balance.", void 0, this.id);
2685
+ };
2686
+ let timer;
2687
+ const read2 = async () => {
2688
+ assertScope();
2689
+ const [wallet, registered] = await Promise.all([
2690
+ this.sdk.getSmartWalletAddress(owner, validChainId),
2691
+ this.sdk.getSmartWalletByEOA(owner)
2692
+ ]);
2693
+ assertScope();
2694
+ if (!(0, import_viem4.isAddress)(wallet.address) || wallet.isDeployed !== true || wallet.isOwner !== true || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase()) throw unavailable();
2695
+ const client = this.getPublicClient(validChainId);
2696
+ const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
2697
+ assertScope();
2698
+ const idle = await client.readContract({
2699
+ address: asset.address,
2700
+ abi: import_viem4.erc20Abi,
2701
+ functionName: "balanceOf",
2702
+ args: [wallet.address],
2703
+ blockNumber
2704
+ });
2705
+ assertScope();
2706
+ if (typeof idle !== "bigint" || idle <= 0n) throw unavailable();
2707
+ return idle;
2708
+ };
2709
+ try {
2710
+ return await Promise.race([read2(), new Promise((_, reject) => {
2711
+ timer = setTimeout(() => {
2712
+ expired = true;
2713
+ reject(unavailable());
2714
+ }, WITHDRAW_BALANCE_TIMEOUT_MS);
2715
+ })]);
2716
+ } catch (error) {
2717
+ if (error instanceof OwneyError) throw error;
2718
+ throw unavailable();
2719
+ } finally {
2720
+ expired = true;
2721
+ if (timer) clearTimeout(timer);
2722
+ }
2723
+ }
2595
2724
  async getBalances(state, chainId) {
2596
2725
  const requestedAt = Date.now();
2597
2726
  const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
@@ -4579,6 +4708,8 @@ var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
4579
4708
  var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
4580
4709
  var YIELDSEEKER_MAX_WITHDRAW_SWEEPS = 3;
4581
4710
  var YIELDSEEKER_MAX_POSITION_VERIFICATION_MS = 2e3;
4711
+ var YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT = 64;
4712
+ var YIELDSEEKER_VAULTS_PER_RPC_BATCH = 10;
4582
4713
  function generateYieldseekerUsername() {
4583
4714
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
4584
4715
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -5102,17 +5233,38 @@ var YieldseekerAgent = class {
5102
5233
  let settlement = null;
5103
5234
  try {
5104
5235
  const maxSettlementBasis = amount === void 0 ? this.snapshotMovement(state, chainId, context, 0n) : null;
5105
- this.advancePortfolioVersion(this.contextKey(state, chainId, asset));
5106
- const portfolioRead = amount === void 0 ? this.loadActivityContext(state, chainId, context, {}).then((value) => ({ positions: value.positions, feeRateBps: 0 })) : this.loadPortfolioContext(state, chainId, context).then((value) => ({ positions: value.positions, feeRateBps: value.snapshot.feeRateBps }));
5236
+ const contextKey = this.contextKey(state, chainId, asset);
5237
+ const priorPositions = this.withdrawalVaultHints(state, chainId, context);
5238
+ this.advancePortfolioVersion(contextKey);
5239
+ const boundedVerification = async (read2, timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
5240
+ let timer;
5241
+ try {
5242
+ return await Promise.race([read2, new Promise((resolve) => {
5243
+ timer = setTimeout(() => resolve(void 0), Math.max(0, timeoutMs));
5244
+ })]);
5245
+ } finally {
5246
+ if (timer) clearTimeout(timer);
5247
+ }
5248
+ };
5249
+ const feeRateRead = amount === void 0 ? Promise.resolve(0) : boundedVerification(
5250
+ (() => {
5251
+ try {
5252
+ return this.portfolioSnapshot(state, chainId, context, contextKey).read.then((value) => value.agentSnapshot.feeRateBps);
5253
+ } catch (error) {
5254
+ return Promise.reject(error);
5255
+ }
5256
+ })().catch(() => void 0)
5257
+ );
5258
+ const positionsRead = this.readWithdrawalPositions(state, chainId, context);
5107
5259
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
5108
5260
  const agentWallet = (0, import_viem11.getAddress)(context.wallet.walletAddress);
5109
- const feeTrackerRead = withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5110
- address: agentWallet,
5111
- abi: YIELDSEEKER_AGENT_WALLET_ABI,
5112
- functionName: "feeTracker"
5113
- }), chainId, "eth_call", this.id);
5114
- const readOwnerFunds = async () => {
5115
- const feeTracker = await feeTrackerRead;
5261
+ const readOwnerFunds = async (blockNumber) => {
5262
+ const feeTracker = await withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5263
+ address: agentWallet,
5264
+ abi: YIELDSEEKER_AGENT_WALLET_ABI,
5265
+ functionName: "feeTracker",
5266
+ blockNumber
5267
+ }), chainId, "eth_call", this.id);
5116
5268
  this.assertSession(generation, chainId, asset);
5117
5269
  if (!(0, import_viem11.isAddress)(feeTracker) || /^0x0{40}$/i.test(feeTracker))
5118
5270
  throw this.invalidResponse("wallet fee tracker");
@@ -5121,61 +5273,58 @@ var YieldseekerAgent = class {
5121
5273
  address: metadata.address,
5122
5274
  abi: import_viem11.erc20Abi,
5123
5275
  functionName: "balanceOf",
5124
- args: [agentWallet]
5276
+ args: [agentWallet],
5277
+ blockNumber
5125
5278
  }), chainId, "eth_call", this.id),
5126
5279
  withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5127
5280
  address: (0, import_viem11.getAddress)(feeTracker),
5128
5281
  abi: YIELDSEEKER_FEE_TRACKER_ABI,
5129
5282
  functionName: "getFeesOwed",
5130
- args: [agentWallet]
5283
+ args: [agentWallet],
5284
+ blockNumber
5131
5285
  }), chainId, "eth_call", this.id)
5132
5286
  ]);
5133
5287
  this.assertSession(generation, chainId, asset);
5134
5288
  if (typeof idle2 !== "bigint" || idle2 < 0n || typeof fees !== "bigint" || fees < 0n)
5135
5289
  throw this.invalidResponse("wallet withdrawal balance");
5136
- return { idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
5290
+ return { blockNumber, idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
5137
5291
  };
5138
- const [portfolio, initialFunds] = await Promise.all([portfolioRead, readOwnerFunds()]);
5292
+ const [funds, feeRateBps] = await Promise.all([
5293
+ positionsRead.then((positions2) => this.resolveWithdrawalLiquidity(
5294
+ state,
5295
+ chainId,
5296
+ context,
5297
+ positions2,
5298
+ priorPositions,
5299
+ amount === void 0 ? void 0 : BigInt(amount)
5300
+ )),
5301
+ feeRateRead
5302
+ ]);
5303
+ const initialFunds = funds;
5139
5304
  const idle = initialFunds.idle;
5140
- let positions = portfolio.positions;
5305
+ const maxLiquidity = amount === void 0 ? funds : void 0;
5306
+ let positions = [...funds.positions, ...funds.indexedPositions.filter((position2) => !funds.verifiedVaults.includes(position2.yieldOption.address.toLowerCase()))];
5141
5307
  let positionsStatus = "empty";
5142
- const validatePositions = (values) => {
5143
- const seen = /* @__PURE__ */ new Set();
5144
- for (const value of values) {
5145
- const address = value?.yieldOption?.address;
5146
- if (typeof address !== "string" || !(0, import_viem11.isAddress)(address) || seen.has(address.toLowerCase()) || typeof value.assetsRaw !== "string" || !/^\d+$/.test(value.assetsRaw) || typeof value.withdrawableAssetsRaw !== "string" || !/^\d+$/.test(value.withdrawableAssetsRaw))
5147
- throw this.invalidResponse("yield positions");
5148
- seen.add(address.toLowerCase());
5149
- }
5150
- return values;
5151
- };
5308
+ const validatePositions = (values) => this.validateWithdrawalPositions(values, chainId, asset);
5309
+ validatePositions(positions);
5152
5310
  const zeroValuePositions = /* @__PURE__ */ new WeakSet();
5153
5311
  const hasPositions = (values) => values.some((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n));
5154
5312
  const knownPositions = /* @__PURE__ */ new Map();
5155
5313
  const rememberPositions = (values) => {
5156
5314
  for (const position2 of values) knownPositions.set(position2.yieldOption.address.toLowerCase(), position2);
5157
5315
  };
5158
- const boundedVerification = async (read2, timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
5159
- let timer;
5160
- try {
5161
- return await Promise.race([read2, new Promise((resolve) => {
5162
- timer = setTimeout(() => resolve(void 0), Math.max(0, timeoutMs));
5163
- })]);
5164
- } finally {
5165
- if (timer) clearTimeout(timer);
5166
- }
5167
- };
5168
5316
  const verifyKnownPositions = async (blockNumber, values = [...knownPositions.values()], timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
5169
5317
  const verifiedPositions = [];
5170
5318
  if (timeoutMs <= 0) return { verified: false, positions: verifiedPositions };
5171
- const reads = values.map(async (position2) => {
5319
+ let active = true;
5320
+ const checks = this.readVaultLiquidity(values.map((position2) => position2.yieldOption), agentWallet, blockNumber).catch(() => []);
5321
+ const reads = values.map(async (position2, index) => {
5172
5322
  const address = (0, import_viem11.getAddress)(position2.yieldOption.address);
5173
5323
  try {
5174
- const [underlying, shares, withdrawable] = await Promise.all([
5175
- this.getReceiptClient().readContract({ address, abi: import_viem11.erc4626Abi, functionName: "asset", blockNumber }),
5176
- this.getReceiptClient().readContract({ address, abi: import_viem11.erc20Abi, functionName: "balanceOf", args: [agentWallet], blockNumber }),
5177
- this.getReceiptClient().readContract({ address, abi: import_viem11.erc4626Abi, functionName: "maxWithdraw", args: [agentWallet], blockNumber })
5178
- ]);
5324
+ const group = (await checks).slice(index * 3, index * 3 + 3);
5325
+ if (!active || group.length !== 3 || group.some((value) => value.status !== "success")) return null;
5326
+ this.assertSession(generation, chainId, asset);
5327
+ const [underlying, shares, withdrawable] = group.map((value) => value.result);
5179
5328
  if (typeof underlying !== "string" || !(0, import_viem11.isAddress)(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n) return null;
5180
5329
  const verified = { ...position2, assetsRaw: shares.toString(), withdrawableAssetsRaw: withdrawable.toString() };
5181
5330
  if (shares > 0n && withdrawable === 0n) {
@@ -5210,6 +5359,7 @@ var YieldseekerAgent = class {
5210
5359
  }
5211
5360
  });
5212
5361
  const result = await boundedVerification(Promise.all(reads), timeoutMs);
5362
+ active = false;
5213
5363
  this.assertSession(generation, chainId, asset);
5214
5364
  return {
5215
5365
  verified: result !== void 0 && result.every((position2) => position2 !== null),
@@ -5228,7 +5378,7 @@ var YieldseekerAgent = class {
5228
5378
  return validatePositions(response.yieldPositions);
5229
5379
  };
5230
5380
  if (amount === void 0) {
5231
- validatePositions(positions);
5381
+ rememberPositions(funds.indexedPositions);
5232
5382
  rememberPositions(positions);
5233
5383
  positionsStatus = hasPositions(positions) ? "remaining" : "empty";
5234
5384
  }
@@ -5244,8 +5394,8 @@ var YieldseekerAgent = class {
5244
5394
  const zeroPayoutCandidates = positions.filter((position2) => BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n);
5245
5395
  if (amount === void 0 && zeroPayoutCandidates.length) {
5246
5396
  const deadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
5247
- const blockNumber = await boundedVerification(
5248
- Promise.resolve().then(() => this.getReceiptClient().getBlockNumber()).catch(() => void 0)
5397
+ const blockNumber = initialFunds.blockNumber ?? await boundedVerification(
5398
+ Promise.resolve().then(() => this.getReceiptClient().getBlockNumber({ cacheTime: 0 })).catch(() => void 0)
5249
5399
  );
5250
5400
  const proof = typeof blockNumber === "bigint" ? await verifyKnownPositions(blockNumber, zeroPayoutCandidates, deadline - Date.now()) : { verified: false, positions: [] };
5251
5401
  positions = proof.positions;
@@ -5369,8 +5519,9 @@ var YieldseekerAgent = class {
5369
5519
  remaining: remaining.toString()
5370
5520
  });
5371
5521
  }
5372
- const availableForOwner = confirmedMovement ? (await readOwnerFunds()).available : initialFunds.available;
5373
- const feeRate = portfolio.feeRateBps;
5522
+ const ownerFunds = confirmedMovement ? await readOwnerFunds(latestUnwindBlock) : initialFunds;
5523
+ const availableForOwner = ownerFunds.available;
5524
+ const feeRate = feeRateBps ?? 0;
5374
5525
  const maxFee = confirmedMovement && Number.isInteger(feeRate) && feeRate >= 0 && feeRate <= 1e3 ? (grossUnwound * BigInt(feeRate) + 9999n) / 10000n : 0n;
5375
5526
  const shortfall = availableForOwner < requested ? requested - availableForOwner : 0n;
5376
5527
  if (availableForOwner === 0n || amount !== void 0 && shortfall > maxFee) {
@@ -5445,7 +5596,7 @@ var YieldseekerAgent = class {
5445
5596
  settlement = plannedMovement;
5446
5597
  const evidence = receipt ? inspectFundingReceipt(receipt, { ...base6, txHash }, metadata.address, "withdraw") : { ...base6, txHash, confirmedAmount: payout.toString(), transfer: "confirmed", delivery: "confirmed" };
5447
5598
  notifyFunding(onProgress, evidence);
5448
- let completion;
5599
+ let completion = maxLiquidity && !maxLiquidity.complete ? "unverified" : void 0;
5449
5600
  if (amount === void 0) {
5450
5601
  const verificationDeadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
5451
5602
  const knownAtPayout = new Set(knownPositions.keys());
@@ -5492,10 +5643,14 @@ var YieldseekerAgent = class {
5492
5643
  { asset, available: "0", txHash },
5493
5644
  this.id
5494
5645
  );
5646
+ const confirmedAmount = evidence.confirmedAmount && /^\d+$/.test(evidence.confirmedAmount) ? BigInt(evidence.confirmedAmount) : void 0;
5647
+ const feeIncrease = ownerFunds.fees - initialFunds.fees;
5648
+ const feeAdjustment = amount !== void 0 && receipt && initialFunds.blockNumber !== void 0 && latestUnwindBlock !== void 0 && initialFunds.blockNumber <= latestUnwindBlock && receipt.blockNumber >= latestUnwindBlock && evidence.transfer === "confirmed" && evidence.delivery === "confirmed" && confirmedAmount === payout && feeIncrease > 0n && feeIncrease <= maxFee && requested - payout === feeIncrease ? { grossAmount: requested.toString(), feeAmount: feeIncrease.toString() } : void 0;
5495
5649
  return {
5496
5650
  txHash,
5497
5651
  type: amount === void 0 && !completion ? "full" : "partial",
5498
5652
  ...completion ? { completion } : {},
5653
+ ...feeAdjustment ? { feeAdjustment } : {},
5499
5654
  amount: evidence.confirmedAmount ?? payout.toString(),
5500
5655
  evidence
5501
5656
  };
@@ -5511,6 +5666,236 @@ var YieldseekerAgent = class {
5511
5666
  }
5512
5667
  }
5513
5668
  }
5669
+ /** A withdrawal-only liquidity floor; display snapshots never authorize it. */
5670
+ async getWithdrawableBalance(state, chainId, asset, minimumAmount) {
5671
+ this.assertChain(chainId);
5672
+ this.assertAsset(asset);
5673
+ const generation = this.readGeneration;
5674
+ const context = await this.findAgent(state, chainId, asset);
5675
+ this.assertSession(generation, chainId, asset);
5676
+ if (!context) throw new OwneyError(
5677
+ "WITHDRAW_BALANCE_UNAVAILABLE",
5678
+ "The Yieldseeker withdrawal wallet could not be verified.",
5679
+ { asset },
5680
+ this.id
5681
+ );
5682
+ const hints = this.withdrawalVaultHints(state, chainId, context);
5683
+ const positions = await this.readWithdrawalPositions(state, chainId, context);
5684
+ const proof = await this.resolveWithdrawalLiquidity(state, chainId, context, positions, hints, minimumAmount);
5685
+ this.assertSession(generation, chainId, asset);
5686
+ if (proof.available === 0n && !proof.complete) throw new OwneyError(
5687
+ "WITHDRAW_BALANCE_UNAVAILABLE",
5688
+ "Yieldseeker withdrawal liquidity could not be verified yet.",
5689
+ { asset },
5690
+ this.id
5691
+ );
5692
+ return proof.available;
5693
+ }
5694
+ async readVaultLiquidity(options, wallet, blockNumber) {
5695
+ const owner = (0, import_viem11.getAddress)(wallet);
5696
+ const batches = Array.from({ length: Math.ceil(options.length / YIELDSEEKER_VAULTS_PER_RPC_BATCH) }, (_, index) => options.slice(index * YIELDSEEKER_VAULTS_PER_RPC_BATCH, (index + 1) * YIELDSEEKER_VAULTS_PER_RPC_BATCH));
5697
+ const results = await Promise.all(batches.map((batch) => this.getReceiptClient().multicall({
5698
+ blockNumber,
5699
+ allowFailure: true,
5700
+ contracts: batch.flatMap((option) => {
5701
+ const address = (0, import_viem11.getAddress)(option.address);
5702
+ return [
5703
+ { address, abi: import_viem11.erc4626Abi, functionName: "asset" },
5704
+ { address, abi: import_viem11.erc20Abi, functionName: "balanceOf", args: [owner] },
5705
+ { address, abi: import_viem11.erc4626Abi, functionName: "maxWithdraw", args: [owner] }
5706
+ ];
5707
+ })
5708
+ })));
5709
+ return results.flat();
5710
+ }
5711
+ validateWithdrawalPositions(values, chainId, asset) {
5712
+ const underlying = YIELDSEEKER_ASSET_METADATA[asset].address.toLowerCase();
5713
+ const seen = /* @__PURE__ */ new Set();
5714
+ for (const value of values) {
5715
+ const option = value?.yieldOption;
5716
+ if (!option || typeof option.address !== "string" || !(0, import_viem11.isAddress)(option.address) || seen.has(option.address.toLowerCase()) || option.chainId !== void 0 && option.chainId !== chainId || option.assetAddress !== void 0 && (typeof option.assetAddress !== "string" || option.assetAddress.toLowerCase() !== underlying) || typeof value.assetsRaw !== "string" || !/^\d+$/.test(value.assetsRaw) || typeof value.withdrawableAssetsRaw !== "string" || !/^\d+$/.test(value.withdrawableAssetsRaw))
5717
+ throw this.invalidResponse("yield positions");
5718
+ seen.add(option.address.toLowerCase());
5719
+ }
5720
+ return values;
5721
+ }
5722
+ withdrawalVaultHints(state, chainId, context) {
5723
+ const key2 = this.contextKey(state, chainId, context.asset);
5724
+ const positions = this.readCache.get(`positions:${key2}`)?.value?.yieldPositions ?? [];
5725
+ const hints = new Map(this.validateWithdrawalPositions(positions, chainId, context.asset).map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
5726
+ const historic = this.readCache.get(`historic:${key2}`)?.value?.position;
5727
+ const receivedAssets = new Set((historic?.movements ?? []).filter((movement) => movement.chainId === chainId && movement.agentWalletId === context.wallet.agentWalletId && movement.toAddress?.toLowerCase() === context.wallet.walletAddress.toLowerCase()).filter((movement) => typeof movement.assetAddress === "string").map((movement) => movement.assetAddress.toLowerCase()));
5728
+ for (const option of this.yieldOptions.get(context.asset)?.value ?? []) {
5729
+ if (option.chainId !== chainId || typeof option.assetAddress !== "string" || option.assetAddress.toLowerCase() !== YIELDSEEKER_ASSET_METADATA[context.asset].address.toLowerCase() || !(0, import_viem11.isAddress)(option.address) || !receivedAssets.has(option.address.toLowerCase()) || hints.has(option.address.toLowerCase())) continue;
5730
+ hints.set(option.address.toLowerCase(), {
5731
+ yieldOption: option,
5732
+ assetsRaw: "0",
5733
+ assetsBase: "0",
5734
+ assetsUsd: 0,
5735
+ withdrawableAssetsRaw: "0",
5736
+ withdrawableAssetsBase: "0"
5737
+ });
5738
+ }
5739
+ return [...hints.values()];
5740
+ }
5741
+ async readWithdrawalPositions(state, chainId, context) {
5742
+ const generation = this.readGeneration;
5743
+ let timer;
5744
+ try {
5745
+ const positions = await Promise.race([
5746
+ this.loadActivityContext(state, chainId, context, {}).then((value) => value.positions).catch((error) => {
5747
+ if (!(error instanceof OwneyError) || !["AGENT_API_ERROR", "AGENT_TIMEOUT"].includes(error.code)) throw error;
5748
+ return void 0;
5749
+ }),
5750
+ new Promise((resolve) => {
5751
+ timer = setTimeout(() => resolve(void 0), YIELDSEEKER_MAX_POSITION_VERIFICATION_MS);
5752
+ })
5753
+ ]);
5754
+ this.assertSession(generation, chainId, context.asset);
5755
+ return positions === void 0 ? void 0 : this.validateWithdrawalPositions(positions, chainId, context.asset);
5756
+ } finally {
5757
+ if (timer) clearTimeout(timer);
5758
+ }
5759
+ }
5760
+ async resolveWithdrawalLiquidity(state, chainId, context, positions, previousPositions = [], requested) {
5761
+ const generation = this.readGeneration;
5762
+ const contextKey = this.contextKey(state, chainId, context.asset);
5763
+ const version = this.portfolioVersion(contextKey);
5764
+ const metadata = YIELDSEEKER_ASSET_METADATA[context.asset];
5765
+ const unavailable = () => new OwneyError(
5766
+ "WITHDRAW_BALANCE_UNAVAILABLE",
5767
+ "Yieldseeker withdrawal liquidity could not be verified yet.",
5768
+ { asset: context.asset, reason: "YIELDSEEKER_LIQUIDITY_UNVERIFIED" },
5769
+ this.id
5770
+ );
5771
+ let active = true;
5772
+ let knownProof;
5773
+ const hasUnverifiedIndexedFunds = (proof) => proof.indexedPositions.some((position2) => !proof.verifiedVaults.includes(position2.yieldOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) > 0n);
5774
+ const assertReadCurrent = () => {
5775
+ this.assertSession(generation, chainId, context.asset);
5776
+ if (!active || this.portfolioVersion(contextKey) !== version) throw unavailable();
5777
+ };
5778
+ const read2 = async () => {
5779
+ const client = this.getReceiptClient();
5780
+ const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
5781
+ const wallet = (0, import_viem11.getAddress)(context.wallet.walletAddress);
5782
+ assertReadCurrent();
5783
+ if (typeof blockNumber !== "bigint") throw unavailable();
5784
+ const indexedPositions = this.validateWithdrawalPositions(positions ?? [], chainId, context.asset);
5785
+ const candidates = /* @__PURE__ */ new Map();
5786
+ for (const position2 of this.validateWithdrawalPositions([...previousPositions], chainId, context.asset))
5787
+ candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
5788
+ for (const position2 of indexedPositions) candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
5789
+ const options = [...candidates.values()];
5790
+ if (options.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
5791
+ const [checks, idle, feeTracker] = await Promise.all([
5792
+ this.readVaultLiquidity(options, wallet, blockNumber),
5793
+ client.readContract({
5794
+ address: metadata.address,
5795
+ abi: import_viem11.erc20Abi,
5796
+ functionName: "balanceOf",
5797
+ args: [wallet],
5798
+ blockNumber
5799
+ }),
5800
+ client.readContract({
5801
+ address: wallet,
5802
+ abi: YIELDSEEKER_AGENT_WALLET_ABI,
5803
+ functionName: "feeTracker",
5804
+ blockNumber
5805
+ })
5806
+ ]);
5807
+ if (typeof idle !== "bigint" || idle < 0n || !(0, import_viem11.isAddress)(feeTracker) || /^0x0{40}$/i.test(feeTracker)) throw unavailable();
5808
+ assertReadCurrent();
5809
+ const fees = await client.readContract({
5810
+ address: (0, import_viem11.getAddress)(feeTracker),
5811
+ abi: YIELDSEEKER_FEE_TRACKER_ABI,
5812
+ functionName: "getFeesOwed",
5813
+ args: [wallet],
5814
+ blockNumber
5815
+ });
5816
+ if (typeof fees !== "bigint" || fees < 0n) throw unavailable();
5817
+ const verified = [];
5818
+ const verifiedVaults = [];
5819
+ let deployed = 0n;
5820
+ const collect = (options2, results) => {
5821
+ let valid2 = true;
5822
+ options2.forEach((option, index) => {
5823
+ const group = results.slice(index * 3, index * 3 + 3);
5824
+ if (group.length !== 3 || group.some((value) => value.status !== "success")) {
5825
+ valid2 = false;
5826
+ return;
5827
+ }
5828
+ const [underlying, shares, withdrawable] = group.map((value) => value.result);
5829
+ if (typeof underlying !== "string" || !(0, import_viem11.isAddress)(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n || shares === 0n && withdrawable > 0n) {
5830
+ valid2 = false;
5831
+ return;
5832
+ }
5833
+ verifiedVaults.push(option.address.toLowerCase());
5834
+ deployed += withdrawable;
5835
+ verified.push({
5836
+ yieldOption: option,
5837
+ assetsRaw: shares.toString(),
5838
+ assetsBase: "0",
5839
+ assetsUsd: 0,
5840
+ withdrawableAssetsRaw: withdrawable.toString(),
5841
+ withdrawableAssetsBase: withdrawable.toString()
5842
+ });
5843
+ });
5844
+ return valid2;
5845
+ };
5846
+ const allKnownVerified = collect(options, checks);
5847
+ const proof = (complete) => ({
5848
+ blockNumber,
5849
+ idle,
5850
+ fees,
5851
+ available: idle + deployed > fees ? idle + deployed - fees : 0n,
5852
+ positions: [...verified],
5853
+ verifiedVaults: [...verifiedVaults],
5854
+ indexedPositions,
5855
+ complete
5856
+ });
5857
+ knownProof = proof(false);
5858
+ const indexed = new Map(indexedPositions.map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
5859
+ const discrepancy = verified.some((position2) => {
5860
+ const row = indexed.get(position2.yieldOption.address.toLowerCase());
5861
+ return row ? BigInt(position2.withdrawableAssetsRaw) < BigInt(row.withdrawableAssetsRaw) : BigInt(position2.assetsRaw) > 0n;
5862
+ });
5863
+ const typedCoveredByIdle = requested !== void 0 && idle >= requested + fees;
5864
+ const expand = positions === void 0 || indexedPositions.length === 0 && !typedCoveredByIdle || !allKnownVerified || discrepancy || knownProof.available === 0n || requested !== void 0 && requested > knownProof.available;
5865
+ if (!expand) {
5866
+ assertReadCurrent();
5867
+ return proof(true);
5868
+ }
5869
+ const catalog = await this.loadYieldOptions(context.asset).catch(() => void 0);
5870
+ assertReadCurrent();
5871
+ const eligible = /* @__PURE__ */ new Map();
5872
+ for (const option of catalog ?? []) {
5873
+ if (option?.chainId === chainId && typeof option.assetAddress === "string" && option.assetAddress.toLowerCase() === metadata.address.toLowerCase() && (0, import_viem11.isAddress)(option.address))
5874
+ eligible.set(option.address.toLowerCase(), option);
5875
+ }
5876
+ const extra = [...eligible].filter(([address]) => !candidates.has(address)).map(([, option]) => option);
5877
+ if (options.length + extra.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
5878
+ const allExtraVerified = collect(extra, await this.readVaultLiquidity(extra, wallet, blockNumber));
5879
+ assertReadCurrent();
5880
+ const result = proof(eligible.size > 0 && allKnownVerified && allExtraVerified);
5881
+ if (result.available === 0n && !result.complete && !hasUnverifiedIndexedFunds(result)) throw unavailable();
5882
+ return result;
5883
+ };
5884
+ let timer;
5885
+ try {
5886
+ return await Promise.race([read2(), new Promise((_, reject) => {
5887
+ timer = setTimeout(() => reject(unavailable()), YIELDSEEKER_MAX_POSITION_VERIFICATION_MS);
5888
+ })]);
5889
+ } catch (error) {
5890
+ assertReadCurrent();
5891
+ if (knownProof && (knownProof.available > 0n || hasUnverifiedIndexedFunds(knownProof)) && (!(error instanceof OwneyError) || error.code === "WITHDRAW_BALANCE_UNAVAILABLE")) return knownProof;
5892
+ if (error instanceof OwneyError) throw error;
5893
+ throw unavailable();
5894
+ } finally {
5895
+ active = false;
5896
+ if (timer) clearTimeout(timer);
5897
+ }
5898
+ }
5514
5899
  async getBalances(state, chainId, asset) {
5515
5900
  this.assertChain(chainId);
5516
5901
  if (asset !== void 0) this.assertAsset(asset);
@@ -8672,11 +9057,10 @@ var OwneySDK = class {
8672
9057
  };
8673
9058
  }
8674
9059
  const requested = BigInt(amount);
8675
- const aggregated = await this.getBalances(void 0, { chainId, asset: token });
9060
+ const { balances: aggregated, failures: balanceFailures } = await this.readAggregatedBalances({ chainId, asset: token });
8676
9061
  const unavailableAgents = eligibleAgents.filter(
8677
9062
  (agent) => !(agent.id in aggregated.agentBalances)
8678
9063
  );
8679
- const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
8680
9064
  const balances = projectAgentBalancesForAsset(
8681
9065
  eligibleAgents,
8682
9066
  aggregated.agentBalances,
@@ -8684,6 +9068,44 @@ var OwneySDK = class {
8684
9068
  asset,
8685
9069
  assetInfo.decimals
8686
9070
  );
9071
+ const indexedAvailable = balances.reduce((sum, entry) => sum + entry.balance, 0n);
9072
+ await Promise.all(balances.map(async (entry) => {
9073
+ if (entry.balance !== 0n && indexedAvailable >= requested || !entry.agent.getWithdrawableBalance) return;
9074
+ const originalFailure = balanceFailures.get(entry.agent.id);
9075
+ if (originalFailure instanceof OwneyError && ![
9076
+ "AGENT_API_ERROR",
9077
+ "AGENT_TIMEOUT",
9078
+ "AGENT_RATE_LIMITED"
9079
+ ].includes(originalFailure.code)) return;
9080
+ if (this.isUserRejectedWithdrawal(originalFailure)) throw originalFailure;
9081
+ if (entry.balance !== 0n && entry.agent.id !== "yieldseeker") {
9082
+ const total = aggregated.agentBalances[entry.agent.id]?.assetBalances?.find((value) => value.chainId === chainId && value.asset.toUpperCase() === token);
9083
+ if (!total || (0, import_viem16.parseUnits)(total.amount, assetInfo.decimals) <= entry.balance) return;
9084
+ }
9085
+ try {
9086
+ const otherAvailable = indexedAvailable - entry.balance;
9087
+ const minimumAmount = requested > otherAvailable ? requested - otherAvailable : void 0;
9088
+ const verified = await entry.agent.getWithdrawableBalance(state, chainId, token, minimumAmount);
9089
+ if (typeof verified !== "bigint" || verified < 0n)
9090
+ throw new OwneyError("WITHDRAW_BALANCE_UNAVAILABLE", "Invalid withdrawal balance proof.", {}, entry.agent.id);
9091
+ if (this.state !== state || this.state.walletAddress !== state.walletAddress || this.state.chainId !== chainId)
9092
+ throw new OwneyError("NOT_CONNECTED", "Account changed while planning withdrawal.");
9093
+ entry.balance = verified;
9094
+ const unavailableIndex = unavailableAgents.indexOf(entry.agent);
9095
+ if (unavailableIndex !== -1) unavailableAgents.splice(unavailableIndex, 1);
9096
+ if (aggregated.agentErrors) delete aggregated.agentErrors[entry.agent.id];
9097
+ } catch (error) {
9098
+ entry.balance = 0n;
9099
+ if (!unavailableAgents.includes(entry.agent)) unavailableAgents.push(entry.agent);
9100
+ aggregated.agentErrors = {
9101
+ ...aggregated.agentErrors,
9102
+ [entry.agent.id]: error instanceof Error ? error.message : String(error)
9103
+ };
9104
+ }
9105
+ }));
9106
+ if (this.state !== state || this.state.chainId !== chainId)
9107
+ throw new OwneyError("NOT_CONNECTED", "Account changed while planning withdrawal.");
9108
+ const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
8687
9109
  const totalAvailable = balances.reduce((sum, x) => sum + x.balance, 0n);
8688
9110
  if (totalAvailable === 0n && unavailableAgents.length > 0) {
8689
9111
  throw new OwneyError(
@@ -8768,7 +9190,18 @@ var OwneySDK = class {
8768
9190
  );
8769
9191
  }
8770
9192
  const withdrawn = sumWithdrawnAmount(results);
8771
- if (withdrawn < requested) {
9193
+ const verifiedFeeDeductions = orderedPlans.reduce((total, plan) => {
9194
+ const result = results[plan.agent.id];
9195
+ const adjustment = result?.feeAdjustment;
9196
+ const evidence = result?.evidence;
9197
+ const submitted = submissionEvidence.get(plan.agent.id);
9198
+ if (plan.agent.id !== "yieldseeker" || !adjustment || !evidence || !submitted || typeof result.amount !== "string" || !/^\d+$/.test(result.amount) || typeof adjustment.grossAmount !== "string" || !/^\d+$/.test(adjustment.grossAmount) || typeof adjustment.feeAmount !== "string" || !/^\d+$/.test(adjustment.feeAmount) || evidence.agentId !== plan.agent.id || evidence.chainId !== chainId || evidence.token !== token || typeof evidence.walletAddress !== "string" || evidence.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || typeof evidence.agentWallet !== "string" || !/^0x[\da-f]{40}$/i.test(evidence.agentWallet) || typeof result.txHash !== "string" || !/^0x[\da-f]{64}$/i.test(result.txHash) || typeof evidence.txHash !== "string" || typeof submitted.txHash !== "string" || evidence.txHash?.toLowerCase() !== result.txHash.toLowerCase() || submitted.txHash?.toLowerCase() !== result.txHash.toLowerCase() || submitted.agentId !== evidence.agentId || submitted.chainId !== chainId || submitted.token !== token || typeof submitted.walletAddress !== "string" || typeof submitted.agentWallet !== "string" || submitted.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || submitted.agentWallet?.toLowerCase() !== evidence.agentWallet.toLowerCase() || evidence.transfer !== "confirmed" || evidence.delivery !== "confirmed" || submitted.transfer !== "confirmed" || submitted.delivery !== "confirmed" || evidence.confirmedAmount !== result.amount || submitted.confirmedAmount !== result.amount || evidence.intendedAmount !== result.amount || submitted.intendedAmount !== result.amount) return total;
9199
+ const fee = BigInt(adjustment.feeAmount);
9200
+ const net = BigInt(result.amount);
9201
+ const gross = BigInt(adjustment.grossAmount);
9202
+ return fee > 0n && net > 0n && gross === plan.planned && net + fee === gross ? total + fee : total;
9203
+ }, 0n);
9204
+ if (withdrawn + verifiedFeeDeductions < requested) {
8772
9205
  throw new OwneyError(
8773
9206
  "WITHDRAW_PARTIAL_FAILURE",
8774
9207
  `Withdrew "${withdrawn.toString()}" of requested "${amount}" of "${asset}". One or more agents failed; remaining balance could not cover the shortfall.`,
@@ -8792,23 +9225,42 @@ var OwneySDK = class {
8792
9225
  * @returns {AgentBalance} for a single agent, or {OwneyBalances} with totalBalance and per-agent breakdown
8793
9226
  */
8794
9227
  async getBalances(agentId, options) {
8795
- const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
8796
- const asset = options?.asset;
8797
- const readBalance = (agent) => this.readAgent(
8798
- agent,
8799
- "balances",
8800
- () => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
8801
- asset ? { asset } : null
8802
- );
8803
9228
  if (agentId) {
9229
+ const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
9230
+ const asset = options?.asset;
8804
9231
  const agent = this.getAgent(agentId);
8805
- const result = await readBalance(agent);
9232
+ const result = await this.readAgent(
9233
+ agent,
9234
+ "balances",
9235
+ () => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
9236
+ asset ? { asset } : null
9237
+ );
8806
9238
  assertCurrent();
8807
9239
  return {
8808
9240
  ...result,
8809
9241
  balanceComposition: agent.balanceComposition ?? "tokens-include-positions"
8810
9242
  };
8811
9243
  }
9244
+ const { balances, failures } = await this.readAggregatedBalances(options);
9245
+ if (Object.keys(balances.agentBalances).length === 0) {
9246
+ throw new OwneyError(
9247
+ "BALANCE_ALL_FAILED",
9248
+ "Failed to fetch balances for all active agents.",
9249
+ { agentErrors: balances.agentErrors ?? {}, failures: [...failures.values()] }
9250
+ );
9251
+ }
9252
+ return balances;
9253
+ }
9254
+ /** Preserve failure objects for withdrawal recovery without changing public balance results. */
9255
+ async readAggregatedBalances(options) {
9256
+ const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
9257
+ const asset = options?.asset;
9258
+ const readBalance = (agent) => this.readAgent(
9259
+ agent,
9260
+ "balances",
9261
+ () => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
9262
+ asset ? { asset } : null
9263
+ );
8812
9264
  let totalBalance = 0;
8813
9265
  const results = {};
8814
9266
  const entries = [...this.getActiveAgents().entries()];
@@ -8825,39 +9277,33 @@ var OwneySDK = class {
8825
9277
  })
8826
9278
  );
8827
9279
  assertCurrent();
8828
- let successCount = 0;
8829
9280
  const agentErrors = {};
8830
9281
  const agentRetryAt = {};
8831
- const agentFailures = [];
9282
+ const agentFailures = /* @__PURE__ */ new Map();
8832
9283
  for (let i = 0; i < balanceResults.length; i++) {
8833
9284
  const settledResult = balanceResults[i];
8834
- const [agentId2] = entries[i];
9285
+ const [agentId] = entries[i];
8835
9286
  if (settledResult.status === "fulfilled") {
8836
9287
  const [id, b] = settledResult.value;
8837
9288
  results[id] = b;
8838
9289
  totalBalance += Number(b.totalBalance);
8839
- successCount += 1;
8840
9290
  continue;
8841
9291
  }
8842
9292
  const reason = settledResult.reason;
8843
- agentFailures.push(reason);
9293
+ agentFailures.set(agentId, reason);
8844
9294
  const retryDelay = rateLimitDelay(reason);
8845
- if (retryDelay !== void 0) agentRetryAt[agentId2] = Date.now() + retryDelay;
8846
- agentErrors[agentId2] = reason instanceof Error ? reason.message : String(reason);
8847
- console.error(`[owney-sdk] Balance fetch failed for agent "${agentId2}":`, reason);
8848
- }
8849
- if (successCount === 0) {
8850
- throw new OwneyError(
8851
- "BALANCE_ALL_FAILED",
8852
- "Failed to fetch balances for all active agents.",
8853
- { agentErrors, failures: agentFailures }
8854
- );
9295
+ if (retryDelay !== void 0) agentRetryAt[agentId] = Date.now() + retryDelay;
9296
+ agentErrors[agentId] = reason instanceof Error ? reason.message : String(reason);
9297
+ console.error(`[owney-sdk] Balance fetch failed for agent "${agentId}":`, reason);
8855
9298
  }
8856
9299
  return {
8857
- totalBalance: String(totalBalance),
8858
- totalBalanceAsset: "usdc",
8859
- agentBalances: results,
8860
- ...Object.keys(agentErrors).length > 0 ? { agentErrors, agentRetryAt } : {}
9300
+ balances: {
9301
+ totalBalance: String(totalBalance),
9302
+ totalBalanceAsset: "usdc",
9303
+ agentBalances: results,
9304
+ ...Object.keys(agentErrors).length > 0 ? { agentErrors, agentRetryAt } : {}
9305
+ },
9306
+ failures: agentFailures
8861
9307
  };
8862
9308
  }
8863
9309
  /**