@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.js CHANGED
@@ -370,20 +370,44 @@ function observeWithdrawalSubmission(sdk, chainId, token) {
370
370
  };
371
371
  const notify = (outcome) => {
372
372
  if (outcome === "rejected" && matching.length !== 1) return;
373
+ const evidence = matching.length === 1 ? { ...request2 } : {
374
+ attempted: request2.attempted,
375
+ method: request2.method,
376
+ path: request2.path,
377
+ ...request2.elapsedMs !== void 0 ? { elapsedMs: request2.elapsedMs } : {}
378
+ };
373
379
  for (const observer of matching) {
374
380
  try {
375
- observer.notify(outcome, { ...request2 });
381
+ observer.notify(outcome, evidence);
376
382
  } catch {
377
383
  }
378
384
  }
379
385
  };
380
386
  notify("attempted");
387
+ let callArgs = args;
388
+ if (withdrawal && client.client?.defaults) {
389
+ const configIndex = method === "get" ? 1 : 2;
390
+ const config = args[configIndex];
391
+ const transforms = config?.transformResponse !== void 0 ? config.transformResponse : client.client.defaults.transformResponse;
392
+ const captureStatus = (data, _headers, status) => {
393
+ if (typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599) request2.httpStatus = status;
394
+ return data;
395
+ };
396
+ callArgs = [...args];
397
+ callArgs[configIndex] = { ...config, transformResponse: [
398
+ ...Array.isArray(transforms) ? transforms : typeof transforms === "function" ? [transforms] : [],
399
+ captureStatus
400
+ ] };
401
+ }
381
402
  try {
382
- return await original.apply(this, args);
403
+ return await original.apply(this, callArgs);
383
404
  } catch (error) {
384
405
  const message = error instanceof Error ? error.message : "";
385
- 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";
386
- if (["unauthorized", "forbidden", "rate-limited", "insufficient-balance", "session-key-missing"].includes(request2.failure)) notify("rejected");
406
+ const status = error?.response?.status;
407
+ if (request2.httpStatus === void 0 && typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599)
408
+ request2.httpStatus = status;
409
+ 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";
410
+ 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");
387
411
  throw error;
388
412
  } finally {
389
413
  request2.elapsedMs = Math.max(0, Date.now() - startedAt);
@@ -1464,6 +1488,7 @@ var VIEM_CHAIN = { 8453: base, 42161: arbitrum, 1: mainnet };
1464
1488
  var SPLITTING_ASSETS = ["USDC", "WETH"];
1465
1489
  var POOL_POLICY_CHAIN_IDS = [8453, 42161, 1];
1466
1490
  var EARNINGS_REFRESH_COOLDOWN_MS = 60 * 60 * 1e3;
1491
+ var WITHDRAW_BALANCE_TIMEOUT_MS = 2e3;
1467
1492
  var ZyfaiAgent = class _ZyfaiAgent {
1468
1493
  id = "zyfai";
1469
1494
  supportedChainIds = [8453, 42161, 1];
@@ -2378,9 +2403,74 @@ var ZyfaiAgent = class _ZyfaiAgent {
2378
2403
  }
2379
2404
  }
2380
2405
  // --- IAgent: Fund operations ---
2406
+ /** Only after a definitive endpoint rejection, combine a new provider
2407
+ * snapshot with the Safe's exact idle balance. Never parse its rounded error
2408
+ * amount as proof of zero, or use this to skip sending a new MAX request. */
2409
+ async assertNotEmptyRejectedWithdrawal(state, chainId, token, owner) {
2410
+ const internals = this.sdk;
2411
+ const generation = this.earningsGeneration;
2412
+ let expired = false;
2413
+ const unavailable = () => new OwneyError(
2414
+ "WITHDRAW_BALANCE_UNAVAILABLE",
2415
+ "Could not verify Zyfai's balance after its withdrawal rejection.",
2416
+ { chainId, token },
2417
+ this.id
2418
+ );
2419
+ const assertScope = () => {
2420
+ if (expired) throw unavailable();
2421
+ const account = internals.walletClient?.account?.address;
2422
+ 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())
2423
+ throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
2424
+ };
2425
+ const read2 = async () => {
2426
+ assertScope();
2427
+ const [wallet, registered, portfolio] = await Promise.all([
2428
+ this.sdk.getSmartWalletAddress(owner, chainId),
2429
+ this.sdk.getSmartWalletByEOA(owner),
2430
+ this.sdk.getPortfolio(owner).catch(() => void 0)
2431
+ ]);
2432
+ assertScope();
2433
+ if (!isAddress(wallet.address) || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase())
2434
+ throw new OwneyError(
2435
+ "AGENT_AUTH_FAILED",
2436
+ "Could not verify Zyfai's withdrawal wallet.",
2437
+ { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
2438
+ this.id
2439
+ );
2440
+ if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
2441
+ const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
2442
+ const idle = asset ? await this.getPublicClient(chainId).readContract({
2443
+ address: asset.address,
2444
+ abi: erc20Abi2,
2445
+ functionName: "balanceOf",
2446
+ args: [wallet.address]
2447
+ }).catch(() => void 0) : void 0;
2448
+ assertScope();
2449
+ if (idle === 0n) throw new OwneyError(
2450
+ "WITHDRAW_INSUFFICIENT_BALANCE",
2451
+ "No funds remain in Zyfai for this asset.",
2452
+ { available: "0", chainId, token },
2453
+ this.id
2454
+ );
2455
+ }
2456
+ };
2457
+ let timer;
2458
+ try {
2459
+ await Promise.race([read2(), new Promise((_, reject) => {
2460
+ timer = setTimeout(() => {
2461
+ expired = true;
2462
+ reject(unavailable());
2463
+ }, WITHDRAW_BALANCE_TIMEOUT_MS);
2464
+ })]);
2465
+ } finally {
2466
+ expired = true;
2467
+ if (timer) clearTimeout(timer);
2468
+ }
2469
+ }
2381
2470
  /** Called only after the execution endpoint explicitly rejected a request.
2382
2471
  * A timeout or an accepted request never authorizes repair and resubmission. */
2383
2472
  async recoverWithdrawalSession(state, chainId, token, owner) {
2473
+ await this.assertNotEmptyRejectedWithdrawal(state, chainId, token, owner);
2384
2474
  const internals = this.sdk;
2385
2475
  const assertScope = () => {
2386
2476
  const account = internals.walletClient?.account?.address;
@@ -2388,35 +2478,6 @@ var ZyfaiAgent = class _ZyfaiAgent {
2388
2478
  throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
2389
2479
  };
2390
2480
  assertScope();
2391
- const [wallet, registered, portfolio] = await Promise.all([
2392
- this.sdk.getSmartWalletAddress(owner, chainId),
2393
- this.sdk.getSmartWalletByEOA(owner),
2394
- this.sdk.getPortfolio(owner).catch(() => void 0)
2395
- ]);
2396
- assertScope();
2397
- if (!isAddress(wallet.address) || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase())
2398
- throw new OwneyError(
2399
- "AGENT_AUTH_FAILED",
2400
- "Could not verify Zyfai's withdrawal wallet.",
2401
- { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
2402
- this.id
2403
- );
2404
- if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
2405
- const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
2406
- const idle = asset ? await this.getPublicClient(chainId).readContract({
2407
- address: asset.address,
2408
- abi: erc20Abi2,
2409
- functionName: "balanceOf",
2410
- args: [wallet.address]
2411
- }).catch(() => void 0) : void 0;
2412
- assertScope();
2413
- if (idle === 0n) throw new OwneyError(
2414
- "WITHDRAW_INSUFFICIENT_BALANCE",
2415
- "No funds remain in Zyfai for this asset.",
2416
- { available: "0", chainId, token },
2417
- this.id
2418
- );
2419
- }
2420
2481
  const walletState = await this.readWalletState(owner);
2421
2482
  assertScope();
2422
2483
  const unavailable = () => new OwneyError(
@@ -2468,6 +2529,13 @@ var ZyfaiAgent = class _ZyfaiAgent {
2468
2529
  raw2 = await this.sdk.withdrawFunds(owner, validChainId, amount, token);
2469
2530
  break;
2470
2531
  } catch (error) {
2532
+ if (!amount && observation.rejected() && observation.request()?.failure === "insufficient-balance") {
2533
+ try {
2534
+ await this.assertNotEmptyRejectedWithdrawal(state, validChainId, token, owner);
2535
+ } catch (proofError) {
2536
+ if (proofError instanceof OwneyError && (proofError.code === "WITHDRAW_INSUFFICIENT_BALANCE" || proofError.code === "NOT_CONNECTED")) throw proofError;
2537
+ }
2538
+ }
2471
2539
  if (!observation.rejected() || observation.request()?.failure !== "session-key-missing") throw error;
2472
2540
  if (attempt === 1)
2473
2541
  throw new OwneyError(
@@ -2561,6 +2629,67 @@ var ZyfaiAgent = class _ZyfaiAgent {
2561
2629
  async getFundingWallet(state, chainId) {
2562
2630
  return (await this.sdk.getSmartWalletAddress(state.walletAddress, isValidChainId(chainId))).address;
2563
2631
  }
2632
+ /** A positive Safe balance is a withdrawal planning floor when the portfolio
2633
+ * index reports zero. Idle zero cannot prove that Zyfai's protocol positions
2634
+ * are empty: its SDK does not expose a complete contract catalog for them. */
2635
+ async getWithdrawableBalance(state, chainId, token) {
2636
+ const validChainId = isValidChainId(chainId);
2637
+ const asset = SupportedAssets.find((value) => value.chainId === validChainId && value.symbol === token);
2638
+ if (!asset) throw new OwneyError("WITHDRAW_NO_PERMITTED_TOKENS", "Asset selected is not supported", void 0, this.id);
2639
+ const owner = state.walletAddress;
2640
+ const generation = this.earningsGeneration;
2641
+ const internals = this.sdk;
2642
+ let expired = false;
2643
+ const unavailable = () => new OwneyError(
2644
+ "WITHDRAW_BALANCE_UNAVAILABLE",
2645
+ "Could not verify Zyfai's withdrawal balance. Its vault positions may still contain funds.",
2646
+ { reason: "ZYFAI_VAULT_BALANCE_UNVERIFIED", chainId, token },
2647
+ this.id
2648
+ );
2649
+ const assertScope = () => {
2650
+ if (expired) throw unavailable();
2651
+ const account = internals.walletClient?.account?.address;
2652
+ 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())
2653
+ throw new OwneyError("NOT_CONNECTED", "Wallet changed while checking Zyfai's withdrawal balance.", void 0, this.id);
2654
+ };
2655
+ let timer;
2656
+ const read2 = async () => {
2657
+ assertScope();
2658
+ const [wallet, registered] = await Promise.all([
2659
+ this.sdk.getSmartWalletAddress(owner, validChainId),
2660
+ this.sdk.getSmartWalletByEOA(owner)
2661
+ ]);
2662
+ assertScope();
2663
+ if (!isAddress(wallet.address) || wallet.isDeployed !== true || wallet.isOwner !== true || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase()) throw unavailable();
2664
+ const client = this.getPublicClient(validChainId);
2665
+ const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
2666
+ assertScope();
2667
+ const idle = await client.readContract({
2668
+ address: asset.address,
2669
+ abi: erc20Abi2,
2670
+ functionName: "balanceOf",
2671
+ args: [wallet.address],
2672
+ blockNumber
2673
+ });
2674
+ assertScope();
2675
+ if (typeof idle !== "bigint" || idle <= 0n) throw unavailable();
2676
+ return idle;
2677
+ };
2678
+ try {
2679
+ return await Promise.race([read2(), new Promise((_, reject) => {
2680
+ timer = setTimeout(() => {
2681
+ expired = true;
2682
+ reject(unavailable());
2683
+ }, WITHDRAW_BALANCE_TIMEOUT_MS);
2684
+ })]);
2685
+ } catch (error) {
2686
+ if (error instanceof OwneyError) throw error;
2687
+ throw unavailable();
2688
+ } finally {
2689
+ expired = true;
2690
+ if (timer) clearTimeout(timer);
2691
+ }
2692
+ }
2564
2693
  async getBalances(state, chainId) {
2565
2694
  const requestedAt = Date.now();
2566
2695
  const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
@@ -4573,6 +4702,8 @@ var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
4573
4702
  var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
4574
4703
  var YIELDSEEKER_MAX_WITHDRAW_SWEEPS = 3;
4575
4704
  var YIELDSEEKER_MAX_POSITION_VERIFICATION_MS = 2e3;
4705
+ var YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT = 64;
4706
+ var YIELDSEEKER_VAULTS_PER_RPC_BATCH = 10;
4576
4707
  function generateYieldseekerUsername() {
4577
4708
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
4578
4709
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -5096,17 +5227,38 @@ var YieldseekerAgent = class {
5096
5227
  let settlement = null;
5097
5228
  try {
5098
5229
  const maxSettlementBasis = amount === void 0 ? this.snapshotMovement(state, chainId, context, 0n) : null;
5099
- this.advancePortfolioVersion(this.contextKey(state, chainId, asset));
5100
- 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 }));
5230
+ const contextKey = this.contextKey(state, chainId, asset);
5231
+ const priorPositions = this.withdrawalVaultHints(state, chainId, context);
5232
+ this.advancePortfolioVersion(contextKey);
5233
+ const boundedVerification = async (read2, timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
5234
+ let timer;
5235
+ try {
5236
+ return await Promise.race([read2, new Promise((resolve) => {
5237
+ timer = setTimeout(() => resolve(void 0), Math.max(0, timeoutMs));
5238
+ })]);
5239
+ } finally {
5240
+ if (timer) clearTimeout(timer);
5241
+ }
5242
+ };
5243
+ const feeRateRead = amount === void 0 ? Promise.resolve(0) : boundedVerification(
5244
+ (() => {
5245
+ try {
5246
+ return this.portfolioSnapshot(state, chainId, context, contextKey).read.then((value) => value.agentSnapshot.feeRateBps);
5247
+ } catch (error) {
5248
+ return Promise.reject(error);
5249
+ }
5250
+ })().catch(() => void 0)
5251
+ );
5252
+ const positionsRead = this.readWithdrawalPositions(state, chainId, context);
5101
5253
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
5102
5254
  const agentWallet = getAddress4(context.wallet.walletAddress);
5103
- const feeTrackerRead = withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5104
- address: agentWallet,
5105
- abi: YIELDSEEKER_AGENT_WALLET_ABI,
5106
- functionName: "feeTracker"
5107
- }), chainId, "eth_call", this.id);
5108
- const readOwnerFunds = async () => {
5109
- const feeTracker = await feeTrackerRead;
5255
+ const readOwnerFunds = async (blockNumber) => {
5256
+ const feeTracker = await withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5257
+ address: agentWallet,
5258
+ abi: YIELDSEEKER_AGENT_WALLET_ABI,
5259
+ functionName: "feeTracker",
5260
+ blockNumber
5261
+ }), chainId, "eth_call", this.id);
5110
5262
  this.assertSession(generation, chainId, asset);
5111
5263
  if (!isAddress3(feeTracker) || /^0x0{40}$/i.test(feeTracker))
5112
5264
  throw this.invalidResponse("wallet fee tracker");
@@ -5115,61 +5267,58 @@ var YieldseekerAgent = class {
5115
5267
  address: metadata.address,
5116
5268
  abi: erc20Abi3,
5117
5269
  functionName: "balanceOf",
5118
- args: [agentWallet]
5270
+ args: [agentWallet],
5271
+ blockNumber
5119
5272
  }), chainId, "eth_call", this.id),
5120
5273
  withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5121
5274
  address: getAddress4(feeTracker),
5122
5275
  abi: YIELDSEEKER_FEE_TRACKER_ABI,
5123
5276
  functionName: "getFeesOwed",
5124
- args: [agentWallet]
5277
+ args: [agentWallet],
5278
+ blockNumber
5125
5279
  }), chainId, "eth_call", this.id)
5126
5280
  ]);
5127
5281
  this.assertSession(generation, chainId, asset);
5128
5282
  if (typeof idle2 !== "bigint" || idle2 < 0n || typeof fees !== "bigint" || fees < 0n)
5129
5283
  throw this.invalidResponse("wallet withdrawal balance");
5130
- return { idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
5284
+ return { blockNumber, idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
5131
5285
  };
5132
- const [portfolio, initialFunds] = await Promise.all([portfolioRead, readOwnerFunds()]);
5286
+ const [funds, feeRateBps] = await Promise.all([
5287
+ positionsRead.then((positions2) => this.resolveWithdrawalLiquidity(
5288
+ state,
5289
+ chainId,
5290
+ context,
5291
+ positions2,
5292
+ priorPositions,
5293
+ amount === void 0 ? void 0 : BigInt(amount)
5294
+ )),
5295
+ feeRateRead
5296
+ ]);
5297
+ const initialFunds = funds;
5133
5298
  const idle = initialFunds.idle;
5134
- let positions = portfolio.positions;
5299
+ const maxLiquidity = amount === void 0 ? funds : void 0;
5300
+ let positions = [...funds.positions, ...funds.indexedPositions.filter((position2) => !funds.verifiedVaults.includes(position2.yieldOption.address.toLowerCase()))];
5135
5301
  let positionsStatus = "empty";
5136
- const validatePositions = (values) => {
5137
- const seen = /* @__PURE__ */ new Set();
5138
- for (const value of values) {
5139
- const address = value?.yieldOption?.address;
5140
- if (typeof address !== "string" || !isAddress3(address) || seen.has(address.toLowerCase()) || typeof value.assetsRaw !== "string" || !/^\d+$/.test(value.assetsRaw) || typeof value.withdrawableAssetsRaw !== "string" || !/^\d+$/.test(value.withdrawableAssetsRaw))
5141
- throw this.invalidResponse("yield positions");
5142
- seen.add(address.toLowerCase());
5143
- }
5144
- return values;
5145
- };
5302
+ const validatePositions = (values) => this.validateWithdrawalPositions(values, chainId, asset);
5303
+ validatePositions(positions);
5146
5304
  const zeroValuePositions = /* @__PURE__ */ new WeakSet();
5147
5305
  const hasPositions = (values) => values.some((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n));
5148
5306
  const knownPositions = /* @__PURE__ */ new Map();
5149
5307
  const rememberPositions = (values) => {
5150
5308
  for (const position2 of values) knownPositions.set(position2.yieldOption.address.toLowerCase(), position2);
5151
5309
  };
5152
- const boundedVerification = async (read2, timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
5153
- let timer;
5154
- try {
5155
- return await Promise.race([read2, new Promise((resolve) => {
5156
- timer = setTimeout(() => resolve(void 0), Math.max(0, timeoutMs));
5157
- })]);
5158
- } finally {
5159
- if (timer) clearTimeout(timer);
5160
- }
5161
- };
5162
5310
  const verifyKnownPositions = async (blockNumber, values = [...knownPositions.values()], timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
5163
5311
  const verifiedPositions = [];
5164
5312
  if (timeoutMs <= 0) return { verified: false, positions: verifiedPositions };
5165
- const reads = values.map(async (position2) => {
5313
+ let active = true;
5314
+ const checks = this.readVaultLiquidity(values.map((position2) => position2.yieldOption), agentWallet, blockNumber).catch(() => []);
5315
+ const reads = values.map(async (position2, index) => {
5166
5316
  const address = getAddress4(position2.yieldOption.address);
5167
5317
  try {
5168
- const [underlying, shares, withdrawable] = await Promise.all([
5169
- this.getReceiptClient().readContract({ address, abi: erc4626Abi, functionName: "asset", blockNumber }),
5170
- this.getReceiptClient().readContract({ address, abi: erc20Abi3, functionName: "balanceOf", args: [agentWallet], blockNumber }),
5171
- this.getReceiptClient().readContract({ address, abi: erc4626Abi, functionName: "maxWithdraw", args: [agentWallet], blockNumber })
5172
- ]);
5318
+ const group = (await checks).slice(index * 3, index * 3 + 3);
5319
+ if (!active || group.length !== 3 || group.some((value) => value.status !== "success")) return null;
5320
+ this.assertSession(generation, chainId, asset);
5321
+ const [underlying, shares, withdrawable] = group.map((value) => value.result);
5173
5322
  if (typeof underlying !== "string" || !isAddress3(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n) return null;
5174
5323
  const verified = { ...position2, assetsRaw: shares.toString(), withdrawableAssetsRaw: withdrawable.toString() };
5175
5324
  if (shares > 0n && withdrawable === 0n) {
@@ -5204,6 +5353,7 @@ var YieldseekerAgent = class {
5204
5353
  }
5205
5354
  });
5206
5355
  const result = await boundedVerification(Promise.all(reads), timeoutMs);
5356
+ active = false;
5207
5357
  this.assertSession(generation, chainId, asset);
5208
5358
  return {
5209
5359
  verified: result !== void 0 && result.every((position2) => position2 !== null),
@@ -5222,7 +5372,7 @@ var YieldseekerAgent = class {
5222
5372
  return validatePositions(response.yieldPositions);
5223
5373
  };
5224
5374
  if (amount === void 0) {
5225
- validatePositions(positions);
5375
+ rememberPositions(funds.indexedPositions);
5226
5376
  rememberPositions(positions);
5227
5377
  positionsStatus = hasPositions(positions) ? "remaining" : "empty";
5228
5378
  }
@@ -5238,8 +5388,8 @@ var YieldseekerAgent = class {
5238
5388
  const zeroPayoutCandidates = positions.filter((position2) => BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n);
5239
5389
  if (amount === void 0 && zeroPayoutCandidates.length) {
5240
5390
  const deadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
5241
- const blockNumber = await boundedVerification(
5242
- Promise.resolve().then(() => this.getReceiptClient().getBlockNumber()).catch(() => void 0)
5391
+ const blockNumber = initialFunds.blockNumber ?? await boundedVerification(
5392
+ Promise.resolve().then(() => this.getReceiptClient().getBlockNumber({ cacheTime: 0 })).catch(() => void 0)
5243
5393
  );
5244
5394
  const proof = typeof blockNumber === "bigint" ? await verifyKnownPositions(blockNumber, zeroPayoutCandidates, deadline - Date.now()) : { verified: false, positions: [] };
5245
5395
  positions = proof.positions;
@@ -5363,8 +5513,9 @@ var YieldseekerAgent = class {
5363
5513
  remaining: remaining.toString()
5364
5514
  });
5365
5515
  }
5366
- const availableForOwner = confirmedMovement ? (await readOwnerFunds()).available : initialFunds.available;
5367
- const feeRate = portfolio.feeRateBps;
5516
+ const ownerFunds = confirmedMovement ? await readOwnerFunds(latestUnwindBlock) : initialFunds;
5517
+ const availableForOwner = ownerFunds.available;
5518
+ const feeRate = feeRateBps ?? 0;
5368
5519
  const maxFee = confirmedMovement && Number.isInteger(feeRate) && feeRate >= 0 && feeRate <= 1e3 ? (grossUnwound * BigInt(feeRate) + 9999n) / 10000n : 0n;
5369
5520
  const shortfall = availableForOwner < requested ? requested - availableForOwner : 0n;
5370
5521
  if (availableForOwner === 0n || amount !== void 0 && shortfall > maxFee) {
@@ -5439,7 +5590,7 @@ var YieldseekerAgent = class {
5439
5590
  settlement = plannedMovement;
5440
5591
  const evidence = receipt ? inspectFundingReceipt(receipt, { ...base6, txHash }, metadata.address, "withdraw") : { ...base6, txHash, confirmedAmount: payout.toString(), transfer: "confirmed", delivery: "confirmed" };
5441
5592
  notifyFunding(onProgress, evidence);
5442
- let completion;
5593
+ let completion = maxLiquidity && !maxLiquidity.complete ? "unverified" : void 0;
5443
5594
  if (amount === void 0) {
5444
5595
  const verificationDeadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
5445
5596
  const knownAtPayout = new Set(knownPositions.keys());
@@ -5486,10 +5637,14 @@ var YieldseekerAgent = class {
5486
5637
  { asset, available: "0", txHash },
5487
5638
  this.id
5488
5639
  );
5640
+ const confirmedAmount = evidence.confirmedAmount && /^\d+$/.test(evidence.confirmedAmount) ? BigInt(evidence.confirmedAmount) : void 0;
5641
+ const feeIncrease = ownerFunds.fees - initialFunds.fees;
5642
+ 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;
5489
5643
  return {
5490
5644
  txHash,
5491
5645
  type: amount === void 0 && !completion ? "full" : "partial",
5492
5646
  ...completion ? { completion } : {},
5647
+ ...feeAdjustment ? { feeAdjustment } : {},
5493
5648
  amount: evidence.confirmedAmount ?? payout.toString(),
5494
5649
  evidence
5495
5650
  };
@@ -5505,6 +5660,236 @@ var YieldseekerAgent = class {
5505
5660
  }
5506
5661
  }
5507
5662
  }
5663
+ /** A withdrawal-only liquidity floor; display snapshots never authorize it. */
5664
+ async getWithdrawableBalance(state, chainId, asset, minimumAmount) {
5665
+ this.assertChain(chainId);
5666
+ this.assertAsset(asset);
5667
+ const generation = this.readGeneration;
5668
+ const context = await this.findAgent(state, chainId, asset);
5669
+ this.assertSession(generation, chainId, asset);
5670
+ if (!context) throw new OwneyError(
5671
+ "WITHDRAW_BALANCE_UNAVAILABLE",
5672
+ "The Yieldseeker withdrawal wallet could not be verified.",
5673
+ { asset },
5674
+ this.id
5675
+ );
5676
+ const hints = this.withdrawalVaultHints(state, chainId, context);
5677
+ const positions = await this.readWithdrawalPositions(state, chainId, context);
5678
+ const proof = await this.resolveWithdrawalLiquidity(state, chainId, context, positions, hints, minimumAmount);
5679
+ this.assertSession(generation, chainId, asset);
5680
+ if (proof.available === 0n && !proof.complete) throw new OwneyError(
5681
+ "WITHDRAW_BALANCE_UNAVAILABLE",
5682
+ "Yieldseeker withdrawal liquidity could not be verified yet.",
5683
+ { asset },
5684
+ this.id
5685
+ );
5686
+ return proof.available;
5687
+ }
5688
+ async readVaultLiquidity(options, wallet, blockNumber) {
5689
+ const owner = getAddress4(wallet);
5690
+ 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));
5691
+ const results = await Promise.all(batches.map((batch) => this.getReceiptClient().multicall({
5692
+ blockNumber,
5693
+ allowFailure: true,
5694
+ contracts: batch.flatMap((option) => {
5695
+ const address = getAddress4(option.address);
5696
+ return [
5697
+ { address, abi: erc4626Abi, functionName: "asset" },
5698
+ { address, abi: erc20Abi3, functionName: "balanceOf", args: [owner] },
5699
+ { address, abi: erc4626Abi, functionName: "maxWithdraw", args: [owner] }
5700
+ ];
5701
+ })
5702
+ })));
5703
+ return results.flat();
5704
+ }
5705
+ validateWithdrawalPositions(values, chainId, asset) {
5706
+ const underlying = YIELDSEEKER_ASSET_METADATA[asset].address.toLowerCase();
5707
+ const seen = /* @__PURE__ */ new Set();
5708
+ for (const value of values) {
5709
+ const option = value?.yieldOption;
5710
+ if (!option || typeof option.address !== "string" || !isAddress3(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))
5711
+ throw this.invalidResponse("yield positions");
5712
+ seen.add(option.address.toLowerCase());
5713
+ }
5714
+ return values;
5715
+ }
5716
+ withdrawalVaultHints(state, chainId, context) {
5717
+ const key2 = this.contextKey(state, chainId, context.asset);
5718
+ const positions = this.readCache.get(`positions:${key2}`)?.value?.yieldPositions ?? [];
5719
+ const hints = new Map(this.validateWithdrawalPositions(positions, chainId, context.asset).map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
5720
+ const historic = this.readCache.get(`historic:${key2}`)?.value?.position;
5721
+ 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()));
5722
+ for (const option of this.yieldOptions.get(context.asset)?.value ?? []) {
5723
+ if (option.chainId !== chainId || typeof option.assetAddress !== "string" || option.assetAddress.toLowerCase() !== YIELDSEEKER_ASSET_METADATA[context.asset].address.toLowerCase() || !isAddress3(option.address) || !receivedAssets.has(option.address.toLowerCase()) || hints.has(option.address.toLowerCase())) continue;
5724
+ hints.set(option.address.toLowerCase(), {
5725
+ yieldOption: option,
5726
+ assetsRaw: "0",
5727
+ assetsBase: "0",
5728
+ assetsUsd: 0,
5729
+ withdrawableAssetsRaw: "0",
5730
+ withdrawableAssetsBase: "0"
5731
+ });
5732
+ }
5733
+ return [...hints.values()];
5734
+ }
5735
+ async readWithdrawalPositions(state, chainId, context) {
5736
+ const generation = this.readGeneration;
5737
+ let timer;
5738
+ try {
5739
+ const positions = await Promise.race([
5740
+ this.loadActivityContext(state, chainId, context, {}).then((value) => value.positions).catch((error) => {
5741
+ if (!(error instanceof OwneyError) || !["AGENT_API_ERROR", "AGENT_TIMEOUT"].includes(error.code)) throw error;
5742
+ return void 0;
5743
+ }),
5744
+ new Promise((resolve) => {
5745
+ timer = setTimeout(() => resolve(void 0), YIELDSEEKER_MAX_POSITION_VERIFICATION_MS);
5746
+ })
5747
+ ]);
5748
+ this.assertSession(generation, chainId, context.asset);
5749
+ return positions === void 0 ? void 0 : this.validateWithdrawalPositions(positions, chainId, context.asset);
5750
+ } finally {
5751
+ if (timer) clearTimeout(timer);
5752
+ }
5753
+ }
5754
+ async resolveWithdrawalLiquidity(state, chainId, context, positions, previousPositions = [], requested) {
5755
+ const generation = this.readGeneration;
5756
+ const contextKey = this.contextKey(state, chainId, context.asset);
5757
+ const version = this.portfolioVersion(contextKey);
5758
+ const metadata = YIELDSEEKER_ASSET_METADATA[context.asset];
5759
+ const unavailable = () => new OwneyError(
5760
+ "WITHDRAW_BALANCE_UNAVAILABLE",
5761
+ "Yieldseeker withdrawal liquidity could not be verified yet.",
5762
+ { asset: context.asset, reason: "YIELDSEEKER_LIQUIDITY_UNVERIFIED" },
5763
+ this.id
5764
+ );
5765
+ let active = true;
5766
+ let knownProof;
5767
+ const hasUnverifiedIndexedFunds = (proof) => proof.indexedPositions.some((position2) => !proof.verifiedVaults.includes(position2.yieldOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) > 0n);
5768
+ const assertReadCurrent = () => {
5769
+ this.assertSession(generation, chainId, context.asset);
5770
+ if (!active || this.portfolioVersion(contextKey) !== version) throw unavailable();
5771
+ };
5772
+ const read2 = async () => {
5773
+ const client = this.getReceiptClient();
5774
+ const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
5775
+ const wallet = getAddress4(context.wallet.walletAddress);
5776
+ assertReadCurrent();
5777
+ if (typeof blockNumber !== "bigint") throw unavailable();
5778
+ const indexedPositions = this.validateWithdrawalPositions(positions ?? [], chainId, context.asset);
5779
+ const candidates = /* @__PURE__ */ new Map();
5780
+ for (const position2 of this.validateWithdrawalPositions([...previousPositions], chainId, context.asset))
5781
+ candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
5782
+ for (const position2 of indexedPositions) candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
5783
+ const options = [...candidates.values()];
5784
+ if (options.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
5785
+ const [checks, idle, feeTracker] = await Promise.all([
5786
+ this.readVaultLiquidity(options, wallet, blockNumber),
5787
+ client.readContract({
5788
+ address: metadata.address,
5789
+ abi: erc20Abi3,
5790
+ functionName: "balanceOf",
5791
+ args: [wallet],
5792
+ blockNumber
5793
+ }),
5794
+ client.readContract({
5795
+ address: wallet,
5796
+ abi: YIELDSEEKER_AGENT_WALLET_ABI,
5797
+ functionName: "feeTracker",
5798
+ blockNumber
5799
+ })
5800
+ ]);
5801
+ if (typeof idle !== "bigint" || idle < 0n || !isAddress3(feeTracker) || /^0x0{40}$/i.test(feeTracker)) throw unavailable();
5802
+ assertReadCurrent();
5803
+ const fees = await client.readContract({
5804
+ address: getAddress4(feeTracker),
5805
+ abi: YIELDSEEKER_FEE_TRACKER_ABI,
5806
+ functionName: "getFeesOwed",
5807
+ args: [wallet],
5808
+ blockNumber
5809
+ });
5810
+ if (typeof fees !== "bigint" || fees < 0n) throw unavailable();
5811
+ const verified = [];
5812
+ const verifiedVaults = [];
5813
+ let deployed = 0n;
5814
+ const collect = (options2, results) => {
5815
+ let valid2 = true;
5816
+ options2.forEach((option, index) => {
5817
+ const group = results.slice(index * 3, index * 3 + 3);
5818
+ if (group.length !== 3 || group.some((value) => value.status !== "success")) {
5819
+ valid2 = false;
5820
+ return;
5821
+ }
5822
+ const [underlying, shares, withdrawable] = group.map((value) => value.result);
5823
+ if (typeof underlying !== "string" || !isAddress3(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n || shares === 0n && withdrawable > 0n) {
5824
+ valid2 = false;
5825
+ return;
5826
+ }
5827
+ verifiedVaults.push(option.address.toLowerCase());
5828
+ deployed += withdrawable;
5829
+ verified.push({
5830
+ yieldOption: option,
5831
+ assetsRaw: shares.toString(),
5832
+ assetsBase: "0",
5833
+ assetsUsd: 0,
5834
+ withdrawableAssetsRaw: withdrawable.toString(),
5835
+ withdrawableAssetsBase: withdrawable.toString()
5836
+ });
5837
+ });
5838
+ return valid2;
5839
+ };
5840
+ const allKnownVerified = collect(options, checks);
5841
+ const proof = (complete) => ({
5842
+ blockNumber,
5843
+ idle,
5844
+ fees,
5845
+ available: idle + deployed > fees ? idle + deployed - fees : 0n,
5846
+ positions: [...verified],
5847
+ verifiedVaults: [...verifiedVaults],
5848
+ indexedPositions,
5849
+ complete
5850
+ });
5851
+ knownProof = proof(false);
5852
+ const indexed = new Map(indexedPositions.map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
5853
+ const discrepancy = verified.some((position2) => {
5854
+ const row = indexed.get(position2.yieldOption.address.toLowerCase());
5855
+ return row ? BigInt(position2.withdrawableAssetsRaw) < BigInt(row.withdrawableAssetsRaw) : BigInt(position2.assetsRaw) > 0n;
5856
+ });
5857
+ const typedCoveredByIdle = requested !== void 0 && idle >= requested + fees;
5858
+ const expand = positions === void 0 || indexedPositions.length === 0 && !typedCoveredByIdle || !allKnownVerified || discrepancy || knownProof.available === 0n || requested !== void 0 && requested > knownProof.available;
5859
+ if (!expand) {
5860
+ assertReadCurrent();
5861
+ return proof(true);
5862
+ }
5863
+ const catalog = await this.loadYieldOptions(context.asset).catch(() => void 0);
5864
+ assertReadCurrent();
5865
+ const eligible = /* @__PURE__ */ new Map();
5866
+ for (const option of catalog ?? []) {
5867
+ if (option?.chainId === chainId && typeof option.assetAddress === "string" && option.assetAddress.toLowerCase() === metadata.address.toLowerCase() && isAddress3(option.address))
5868
+ eligible.set(option.address.toLowerCase(), option);
5869
+ }
5870
+ const extra = [...eligible].filter(([address]) => !candidates.has(address)).map(([, option]) => option);
5871
+ if (options.length + extra.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
5872
+ const allExtraVerified = collect(extra, await this.readVaultLiquidity(extra, wallet, blockNumber));
5873
+ assertReadCurrent();
5874
+ const result = proof(eligible.size > 0 && allKnownVerified && allExtraVerified);
5875
+ if (result.available === 0n && !result.complete && !hasUnverifiedIndexedFunds(result)) throw unavailable();
5876
+ return result;
5877
+ };
5878
+ let timer;
5879
+ try {
5880
+ return await Promise.race([read2(), new Promise((_, reject) => {
5881
+ timer = setTimeout(() => reject(unavailable()), YIELDSEEKER_MAX_POSITION_VERIFICATION_MS);
5882
+ })]);
5883
+ } catch (error) {
5884
+ assertReadCurrent();
5885
+ if (knownProof && (knownProof.available > 0n || hasUnverifiedIndexedFunds(knownProof)) && (!(error instanceof OwneyError) || error.code === "WITHDRAW_BALANCE_UNAVAILABLE")) return knownProof;
5886
+ if (error instanceof OwneyError) throw error;
5887
+ throw unavailable();
5888
+ } finally {
5889
+ active = false;
5890
+ if (timer) clearTimeout(timer);
5891
+ }
5892
+ }
5508
5893
  async getBalances(state, chainId, asset) {
5509
5894
  this.assertChain(chainId);
5510
5895
  if (asset !== void 0) this.assertAsset(asset);
@@ -6702,6 +7087,7 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
6702
7087
 
6703
7088
  // src/client.ts
6704
7089
  import {
7090
+ parseUnits as parseUnits2,
6705
7091
  createPublicClient as createPublicClient4,
6706
7092
  createWalletClient as createWalletClient4,
6707
7093
  custom as custom4
@@ -8674,11 +9060,10 @@ var OwneySDK = class {
8674
9060
  };
8675
9061
  }
8676
9062
  const requested = BigInt(amount);
8677
- const aggregated = await this.getBalances(void 0, { chainId, asset: token });
9063
+ const { balances: aggregated, failures: balanceFailures } = await this.readAggregatedBalances({ chainId, asset: token });
8678
9064
  const unavailableAgents = eligibleAgents.filter(
8679
9065
  (agent) => !(agent.id in aggregated.agentBalances)
8680
9066
  );
8681
- const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
8682
9067
  const balances = projectAgentBalancesForAsset(
8683
9068
  eligibleAgents,
8684
9069
  aggregated.agentBalances,
@@ -8686,6 +9071,44 @@ var OwneySDK = class {
8686
9071
  asset,
8687
9072
  assetInfo.decimals
8688
9073
  );
9074
+ const indexedAvailable = balances.reduce((sum, entry) => sum + entry.balance, 0n);
9075
+ await Promise.all(balances.map(async (entry) => {
9076
+ if (entry.balance !== 0n && indexedAvailable >= requested || !entry.agent.getWithdrawableBalance) return;
9077
+ const originalFailure = balanceFailures.get(entry.agent.id);
9078
+ if (originalFailure instanceof OwneyError && ![
9079
+ "AGENT_API_ERROR",
9080
+ "AGENT_TIMEOUT",
9081
+ "AGENT_RATE_LIMITED"
9082
+ ].includes(originalFailure.code)) return;
9083
+ if (this.isUserRejectedWithdrawal(originalFailure)) throw originalFailure;
9084
+ if (entry.balance !== 0n && entry.agent.id !== "yieldseeker") {
9085
+ const total = aggregated.agentBalances[entry.agent.id]?.assetBalances?.find((value) => value.chainId === chainId && value.asset.toUpperCase() === token);
9086
+ if (!total || parseUnits2(total.amount, assetInfo.decimals) <= entry.balance) return;
9087
+ }
9088
+ try {
9089
+ const otherAvailable = indexedAvailable - entry.balance;
9090
+ const minimumAmount = requested > otherAvailable ? requested - otherAvailable : void 0;
9091
+ const verified = await entry.agent.getWithdrawableBalance(state, chainId, token, minimumAmount);
9092
+ if (typeof verified !== "bigint" || verified < 0n)
9093
+ throw new OwneyError("WITHDRAW_BALANCE_UNAVAILABLE", "Invalid withdrawal balance proof.", {}, entry.agent.id);
9094
+ if (this.state !== state || this.state.walletAddress !== state.walletAddress || this.state.chainId !== chainId)
9095
+ throw new OwneyError("NOT_CONNECTED", "Account changed while planning withdrawal.");
9096
+ entry.balance = verified;
9097
+ const unavailableIndex = unavailableAgents.indexOf(entry.agent);
9098
+ if (unavailableIndex !== -1) unavailableAgents.splice(unavailableIndex, 1);
9099
+ if (aggregated.agentErrors) delete aggregated.agentErrors[entry.agent.id];
9100
+ } catch (error) {
9101
+ entry.balance = 0n;
9102
+ if (!unavailableAgents.includes(entry.agent)) unavailableAgents.push(entry.agent);
9103
+ aggregated.agentErrors = {
9104
+ ...aggregated.agentErrors,
9105
+ [entry.agent.id]: error instanceof Error ? error.message : String(error)
9106
+ };
9107
+ }
9108
+ }));
9109
+ if (this.state !== state || this.state.chainId !== chainId)
9110
+ throw new OwneyError("NOT_CONNECTED", "Account changed while planning withdrawal.");
9111
+ const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
8689
9112
  const totalAvailable = balances.reduce((sum, x) => sum + x.balance, 0n);
8690
9113
  if (totalAvailable === 0n && unavailableAgents.length > 0) {
8691
9114
  throw new OwneyError(
@@ -8770,7 +9193,18 @@ var OwneySDK = class {
8770
9193
  );
8771
9194
  }
8772
9195
  const withdrawn = sumWithdrawnAmount(results);
8773
- if (withdrawn < requested) {
9196
+ const verifiedFeeDeductions = orderedPlans.reduce((total, plan) => {
9197
+ const result = results[plan.agent.id];
9198
+ const adjustment = result?.feeAdjustment;
9199
+ const evidence = result?.evidence;
9200
+ const submitted = submissionEvidence.get(plan.agent.id);
9201
+ 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;
9202
+ const fee = BigInt(adjustment.feeAmount);
9203
+ const net = BigInt(result.amount);
9204
+ const gross = BigInt(adjustment.grossAmount);
9205
+ return fee > 0n && net > 0n && gross === plan.planned && net + fee === gross ? total + fee : total;
9206
+ }, 0n);
9207
+ if (withdrawn + verifiedFeeDeductions < requested) {
8774
9208
  throw new OwneyError(
8775
9209
  "WITHDRAW_PARTIAL_FAILURE",
8776
9210
  `Withdrew "${withdrawn.toString()}" of requested "${amount}" of "${asset}". One or more agents failed; remaining balance could not cover the shortfall.`,
@@ -8794,23 +9228,42 @@ var OwneySDK = class {
8794
9228
  * @returns {AgentBalance} for a single agent, or {OwneyBalances} with totalBalance and per-agent breakdown
8795
9229
  */
8796
9230
  async getBalances(agentId, options) {
8797
- const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
8798
- const asset = options?.asset;
8799
- const readBalance = (agent) => this.readAgent(
8800
- agent,
8801
- "balances",
8802
- () => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
8803
- asset ? { asset } : null
8804
- );
8805
9231
  if (agentId) {
9232
+ const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
9233
+ const asset = options?.asset;
8806
9234
  const agent = this.getAgent(agentId);
8807
- const result = await readBalance(agent);
9235
+ const result = await this.readAgent(
9236
+ agent,
9237
+ "balances",
9238
+ () => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
9239
+ asset ? { asset } : null
9240
+ );
8808
9241
  assertCurrent();
8809
9242
  return {
8810
9243
  ...result,
8811
9244
  balanceComposition: agent.balanceComposition ?? "tokens-include-positions"
8812
9245
  };
8813
9246
  }
9247
+ const { balances, failures } = await this.readAggregatedBalances(options);
9248
+ if (Object.keys(balances.agentBalances).length === 0) {
9249
+ throw new OwneyError(
9250
+ "BALANCE_ALL_FAILED",
9251
+ "Failed to fetch balances for all active agents.",
9252
+ { agentErrors: balances.agentErrors ?? {}, failures: [...failures.values()] }
9253
+ );
9254
+ }
9255
+ return balances;
9256
+ }
9257
+ /** Preserve failure objects for withdrawal recovery without changing public balance results. */
9258
+ async readAggregatedBalances(options) {
9259
+ const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
9260
+ const asset = options?.asset;
9261
+ const readBalance = (agent) => this.readAgent(
9262
+ agent,
9263
+ "balances",
9264
+ () => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
9265
+ asset ? { asset } : null
9266
+ );
8814
9267
  let totalBalance = 0;
8815
9268
  const results = {};
8816
9269
  const entries = [...this.getActiveAgents().entries()];
@@ -8827,39 +9280,33 @@ var OwneySDK = class {
8827
9280
  })
8828
9281
  );
8829
9282
  assertCurrent();
8830
- let successCount = 0;
8831
9283
  const agentErrors = {};
8832
9284
  const agentRetryAt = {};
8833
- const agentFailures = [];
9285
+ const agentFailures = /* @__PURE__ */ new Map();
8834
9286
  for (let i = 0; i < balanceResults.length; i++) {
8835
9287
  const settledResult = balanceResults[i];
8836
- const [agentId2] = entries[i];
9288
+ const [agentId] = entries[i];
8837
9289
  if (settledResult.status === "fulfilled") {
8838
9290
  const [id, b] = settledResult.value;
8839
9291
  results[id] = b;
8840
9292
  totalBalance += Number(b.totalBalance);
8841
- successCount += 1;
8842
9293
  continue;
8843
9294
  }
8844
9295
  const reason = settledResult.reason;
8845
- agentFailures.push(reason);
9296
+ agentFailures.set(agentId, reason);
8846
9297
  const retryDelay = rateLimitDelay(reason);
8847
- if (retryDelay !== void 0) agentRetryAt[agentId2] = Date.now() + retryDelay;
8848
- agentErrors[agentId2] = reason instanceof Error ? reason.message : String(reason);
8849
- console.error(`[owney-sdk] Balance fetch failed for agent "${agentId2}":`, reason);
8850
- }
8851
- if (successCount === 0) {
8852
- throw new OwneyError(
8853
- "BALANCE_ALL_FAILED",
8854
- "Failed to fetch balances for all active agents.",
8855
- { agentErrors, failures: agentFailures }
8856
- );
9298
+ if (retryDelay !== void 0) agentRetryAt[agentId] = Date.now() + retryDelay;
9299
+ agentErrors[agentId] = reason instanceof Error ? reason.message : String(reason);
9300
+ console.error(`[owney-sdk] Balance fetch failed for agent "${agentId}":`, reason);
8857
9301
  }
8858
9302
  return {
8859
- totalBalance: String(totalBalance),
8860
- totalBalanceAsset: "usdc",
8861
- agentBalances: results,
8862
- ...Object.keys(agentErrors).length > 0 ? { agentErrors, agentRetryAt } : {}
9303
+ balances: {
9304
+ totalBalance: String(totalBalance),
9305
+ totalBalanceAsset: "usdc",
9306
+ agentBalances: results,
9307
+ ...Object.keys(agentErrors).length > 0 ? { agentErrors, agentRetryAt } : {}
9308
+ },
9309
+ failures: agentFailures
8863
9310
  };
8864
9311
  }
8865
9312
  /**