@owney/sdk 0.7.25-beta.14 → 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
@@ -68,6 +68,8 @@ function inspectFundingReceipt(receipt, evidence, tokenAddress, direction) {
68
68
  const attributed = /* @__PURE__ */ new Map();
69
69
  const agent = evidence.agentWallet?.toLowerCase();
70
70
  const owner = evidence.walletAddress.toLowerCase();
71
+ const source = direction === "deposit" ? owner : agent;
72
+ const destination = direction === "deposit" ? agent : owner;
71
73
  const logIndexes = [];
72
74
  const transferLogs = [];
73
75
  const zeroTransferLogs = [];
@@ -82,17 +84,13 @@ function inspectFundingReceipt(receipt, evidence, tokenAddress, direction) {
82
84
  if (direction === "withdraw" && value === 0n && from.toLowerCase() === agent && to.toLowerCase() === owner)
83
85
  zeroTransferLogs.push(log.logIndex);
84
86
  let matched = 0n;
85
- if (direction === "deposit") {
86
- if (to.toLowerCase() === agent) matched = value;
87
- } else {
88
- const sender = from.toLowerCase();
89
- const recipient = to.toLowerCase();
90
- const available = sender === agent ? value : attributed.get(sender) ?? 0n;
91
- const carried = available < value ? available : value;
92
- if (sender !== agent) attributed.set(sender, available - carried);
93
- if (recipient === owner) matched = carried;
94
- else if (recipient !== agent) attributed.set(recipient, (attributed.get(recipient) ?? 0n) + carried);
95
- }
87
+ const sender = from.toLowerCase();
88
+ const recipient = to.toLowerCase();
89
+ const available = sender === source ? value : attributed.get(sender) ?? 0n;
90
+ const carried = available < value ? available : value;
91
+ if (sender !== source) attributed.set(sender, available - carried);
92
+ if (recipient === destination) matched = carried;
93
+ else if (recipient !== source) attributed.set(recipient, (attributed.get(recipient) ?? 0n) + carried);
96
94
  if (matched === 0n) continue;
97
95
  amount += matched;
98
96
  transferLogs.push({ logIndex: log.logIndex, amountRaw: matched.toString() });
@@ -372,20 +370,44 @@ function observeWithdrawalSubmission(sdk, chainId, token) {
372
370
  };
373
371
  const notify = (outcome) => {
374
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
+ };
375
379
  for (const observer of matching) {
376
380
  try {
377
- observer.notify(outcome, { ...request2 });
381
+ observer.notify(outcome, evidence);
378
382
  } catch {
379
383
  }
380
384
  }
381
385
  };
382
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
+ }
383
402
  try {
384
- return await original.apply(this, args);
403
+ return await original.apply(this, callArgs);
385
404
  } catch (error) {
386
405
  const message = error instanceof Error ? error.message : "";
387
- request2.failure = message === "Unauthorized: Invalid API key" ? "unauthorized" : message === "Forbidden: Access denied" ? "forbidden" : message === "Rate limit exceeded. Please try again later." ? "rate-limited" : message === "Network error: Unable to reach the server" ? "network" : "other";
388
- if (["unauthorized", "forbidden", "rate-limited"].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");
389
411
  throw error;
390
412
  } finally {
391
413
  request2.elapsedMs = Math.max(0, Date.now() - startedAt);
@@ -408,9 +430,49 @@ function observeWithdrawalSubmission(sdk, chainId, token) {
408
430
  } };
409
431
  }
410
432
 
433
+ // src/agents/zyfai/zyfai.withdraw-empty.ts
434
+ function hasEmptyWithdrawalSnapshot(raw2, chainId, token) {
435
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
436
+ const response = record(raw2);
437
+ const portfolio = record(response?.portfolio);
438
+ const chains = record(portfolio?.portfolioByChain);
439
+ if (response?.success === false || !chains || !Array.isArray(portfolio?.positions) || !Array.isArray(portfolio.staleBalances)) return false;
440
+ if (portfolio.pendingAsyncWithdrawals !== void 0 && (!Array.isArray(portfolio.pendingAsyncWithdrawals) || portfolio.pendingAsyncWithdrawals.length > 0)) return false;
441
+ const symbol = (value) => typeof value === "string" ? value.toUpperCase() === "ETH" ? "WETH" : value.toUpperCase() : void 0;
442
+ const chain = (value) => {
443
+ if (typeof value === "number") return value;
444
+ if (typeof value !== "string") return void 0;
445
+ return /^\d+$/.test(value) ? Number(value) : { base: 8453, arbitrum: 42161, ethereum: 1, mainnet: 1 }[value.trim().toLowerCase()];
446
+ };
447
+ const zero = (value) => typeof value === "string" && /^(?:0x)?0+$/i.test(value);
448
+ for (const [id, assets] of Object.entries(chains)) {
449
+ const idNumber = chain(id);
450
+ if (idNumber === void 0) return false;
451
+ if (idNumber !== chainId) continue;
452
+ const entries = record(assets);
453
+ if (!entries) return false;
454
+ for (const [asset, value] of Object.entries(entries)) {
455
+ if (symbol(asset) !== token) continue;
456
+ const balance = record(value);
457
+ if (!balance || !zero(balance.balance)) return false;
458
+ }
459
+ }
460
+ for (const value of [...portfolio.positions, ...portfolio.staleBalances]) {
461
+ const entry = record(value);
462
+ if (!entry) return false;
463
+ const id = chain(entry.chainId ?? entry.chain);
464
+ const asset = symbol(entry.tokenSymbol ?? entry.token_symbol);
465
+ if (id === void 0 || asset === void 0) return false;
466
+ if (id !== chainId || asset !== token) continue;
467
+ const amounts = [entry.balance, entry.amount, entry.underlyingAmount].filter((value2) => value2 !== void 0);
468
+ if (!amounts.length || !amounts.every(zero)) return false;
469
+ }
470
+ return true;
471
+ }
472
+
411
473
  // src/agents/zyfai/zyfai.agent.ts
412
474
  import { SMART_SESSIONS_VALIDATOR, ZyfaiSDK } from "@zyfai/sdk";
413
- import { formatUnits, parseAbi as parseAbi2 } from "viem";
475
+ import { erc20Abi as erc20Abi2, formatUnits as formatUnits2, isAddress, parseAbi as parseAbi2 } from "viem";
414
476
  import { base, arbitrum, mainnet } from "viem/chains";
415
477
 
416
478
  // src/types/config.ts
@@ -497,14 +559,6 @@ function hexToDecimal(hex, decimals = 6) {
497
559
  const remainder = parsed % divisor;
498
560
  return Number(whole) + Number(remainder) / Number(divisor);
499
561
  }
500
- function normalizeDecimalPlace(value, decimals = 6) {
501
- const parsed = BigInt(value);
502
- if (decimals <= 0) return Number(parsed);
503
- const divisor = BigInt(10 ** decimals);
504
- const whole = parsed / divisor;
505
- const remainder = parsed % divisor;
506
- return Number(whole) + Number(remainder) / Number(divisor);
507
- }
508
562
  var DayFilterMapping = {
509
563
  "30D": 30,
510
564
  "14D": 14,
@@ -512,6 +566,8 @@ var DayFilterMapping = {
512
566
  };
513
567
 
514
568
  // src/agents/zyfai/zyfai.mapper.ts
569
+ import { formatUnits } from "viem";
570
+ var exactHexAmount = (raw2, decimals) => formatUnits(BigInt(/^0x/i.test(raw2) ? raw2 : `0x${raw2}`), decimals);
515
571
  var DECIMALS_BY_SYMBOL = {
516
572
  usdc: 6,
517
573
  weth: 18,
@@ -566,15 +622,15 @@ function mapPendingAllocations(raw2) {
566
622
  if (typeof e.balance !== "string") continue;
567
623
  let amount;
568
624
  try {
569
- amount = hexToDecimal(e.balance, tokenDecimals(e.tokenSymbol));
625
+ if (BigInt(/^0x/i.test(e.balance) ? e.balance : `0x${e.balance}`) <= 0n) continue;
626
+ amount = exactHexAmount(e.balance, tokenDecimals(e.tokenSymbol));
570
627
  } catch {
571
628
  continue;
572
629
  }
573
- if (!Number.isFinite(amount) || amount <= 0) continue;
574
630
  pending.push({
575
631
  chainId: e.chainId,
576
632
  asset: e.tokenSymbol,
577
- amount: String(amount),
633
+ amount,
578
634
  isPending: e.isPending === true,
579
635
  ...typeof e.updatedAt === "string" && e.updatedAt !== "" ? { since: e.updatedAt } : {}
580
636
  });
@@ -597,13 +653,13 @@ function mapBalances(raw2, _chainId, smartWallet) {
597
653
  { balance, balanceWithFee, decimals }
598
654
  ] of Object.entries(tokensBySymbol ?? {})) {
599
655
  if (balanceWithFee === void 0 && balance === void 0) throw new Error("Zyfai asset balance is unavailable.");
600
- const amount = hexToDecimal(balanceWithFee ?? balance, decimals);
601
- totalBalance += amount;
656
+ const amount = exactHexAmount(balanceWithFee ?? balance, tokenDecimals(symbol, decimals));
657
+ totalBalance += Number(amount);
602
658
  tokens.push({
603
659
  chain: CHAIN_ID_TO_NAME[positionChainId] ?? "BASE",
604
660
  chainId: positionChainId,
605
661
  asset: symbol,
606
- amount: String(amount)
662
+ amount
607
663
  });
608
664
  }
609
665
  }
@@ -617,19 +673,17 @@ function mapBalances(raw2, _chainId, smartWallet) {
617
673
  // legacy `amount` fallback is hex. Runtime
618
674
  // getBalances payloads often carry one (e.g. 18 for WETH) — prefer that
619
675
  // when present, otherwise fall back to the per-symbol lookup.
620
- amount: String(
621
- p.underlyingAmountWithFee ?? p.underlyingAmount ? normalizeDecimalPlace(
622
- p.underlyingAmountWithFee ?? p.underlyingAmount ?? "0",
623
- tokenDecimals(
624
- p.token_symbol,
625
- p.decimals
626
- )
627
- ) : hexToDecimal(
628
- p.amount || "0",
629
- tokenDecimals(
630
- p.token_symbol,
631
- p.decimals
632
- )
676
+ amount: p.underlyingAmountWithFee ?? p.underlyingAmount ? formatUnits(
677
+ BigInt(p.underlyingAmountWithFee ?? p.underlyingAmount ?? "0"),
678
+ tokenDecimals(
679
+ p.token_symbol,
680
+ p.decimals
681
+ )
682
+ ) : exactHexAmount(
683
+ p.amount || "0",
684
+ tokenDecimals(
685
+ p.token_symbol,
686
+ p.decimals
633
687
  )
634
688
  ),
635
689
  apy: netApy(p.pool_apy_withFee, p.pool_apy, "pool_apy_withFee"),
@@ -1434,6 +1488,7 @@ var VIEM_CHAIN = { 8453: base, 42161: arbitrum, 1: mainnet };
1434
1488
  var SPLITTING_ASSETS = ["USDC", "WETH"];
1435
1489
  var POOL_POLICY_CHAIN_IDS = [8453, 42161, 1];
1436
1490
  var EARNINGS_REFRESH_COOLDOWN_MS = 60 * 60 * 1e3;
1491
+ var WITHDRAW_BALANCE_TIMEOUT_MS = 2e3;
1437
1492
  var ZyfaiAgent = class _ZyfaiAgent {
1438
1493
  id = "zyfai";
1439
1494
  supportedChainIds = [8453, 42161, 1];
@@ -1573,7 +1628,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1573
1628
  }, asset.address, entry.action === "Withdraw" ? "withdraw" : "deposit");
1574
1629
  if (evidence.transfer === "confirmed" && evidence.confirmedAmount) {
1575
1630
  Object.assign(tx, {
1576
- amount: formatUnits(BigInt(evidence.confirmedAmount), asset.decimals),
1631
+ amount: formatUnits2(BigInt(evidence.confirmedAmount), asset.decimals),
1577
1632
  amountRaw: evidence.confirmedAmount,
1578
1633
  blockNumber: evidence.blockNumber,
1579
1634
  logIndexes: evidence.logIndexes,
@@ -2300,6 +2355,8 @@ var ZyfaiAgent = class _ZyfaiAgent {
2300
2355
  evidence2 = { ...evidence2, transfer: "unknown" };
2301
2356
  }
2302
2357
  notifyFunding(onProgress, evidence2);
2358
+ if (evidence2.transfer === "failed")
2359
+ throw new OwneyError("AGENT_TRANSACTION_REVERTED", "Zyfai deposit transaction reverted.", { txHash }, this.id);
2303
2360
  try {
2304
2361
  const tokenAddress = asset === "WETH" ? WETH_ADDRESS_BY_CHAIN[validChainId] : void 0;
2305
2362
  await (tokenAddress ? this.sdk.logDeposit(validChainId, txHash, amount, tokenAddress) : this.sdk.logDeposit(validChainId, txHash, amount));
@@ -2338,12 +2395,113 @@ var ZyfaiAgent = class _ZyfaiAgent {
2338
2395
  } catch {
2339
2396
  }
2340
2397
  notifyFunding(onProgress, evidence);
2398
+ if (evidence.transfer === "failed")
2399
+ throw new OwneyError("AGENT_TRANSACTION_REVERTED", "Zyfai deposit transaction reverted.", { txHash: raw2.txHash }, this.id);
2341
2400
  return { ...mapDeposit(raw2), evidence };
2342
2401
  } catch (error) {
2343
2402
  throw error;
2344
2403
  }
2345
2404
  }
2346
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
+ }
2470
+ /** Called only after the execution endpoint explicitly rejected a request.
2471
+ * A timeout or an accepted request never authorizes repair and resubmission. */
2472
+ async recoverWithdrawalSession(state, chainId, token, owner) {
2473
+ await this.assertNotEmptyRejectedWithdrawal(state, chainId, token, owner);
2474
+ const internals = this.sdk;
2475
+ const assertScope = () => {
2476
+ const account = internals.walletClient?.account?.address;
2477
+ if (state.walletAddress.toLowerCase() !== owner.toLowerCase() || this.connectedAddress?.toLowerCase() !== owner.toLowerCase() || this.connectedChainId !== chainId || account && account.toLowerCase() !== owner.toLowerCase())
2478
+ throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
2479
+ };
2480
+ assertScope();
2481
+ const walletState = await this.readWalletState(owner);
2482
+ assertScope();
2483
+ const unavailable = () => new OwneyError(
2484
+ "AGENT_AUTH_FAILED",
2485
+ "Zyfai has no active withdrawal session. Its withdrawal was not submitted.",
2486
+ { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
2487
+ this.id
2488
+ );
2489
+ if (walletState.predeployed !== false || internals.isPredeployed === true) throw unavailable();
2490
+ internals.hasActiveSessionKey = false;
2491
+ const key2 = `${owner.toLowerCase()}:${chainId}`;
2492
+ this.sessionKeyAttempts.delete(key2);
2493
+ try {
2494
+ const result = await this.sdk.createSessionKey(owner, chainId);
2495
+ assertScope();
2496
+ if (result.success === false || result.alreadyActive === true) throw unavailable();
2497
+ this.sessionKeyAttempts.add(key2);
2498
+ } catch (error) {
2499
+ assertScope();
2500
+ const code = error?.code;
2501
+ if (code === 4001 || code === "ACTION_REJECTED" || error instanceof Error && /user (rejected|denied)|request rejected|action rejected/i.test(error.message)) throw error;
2502
+ throw unavailable();
2503
+ }
2504
+ }
2347
2505
  async withdraw(state, chainId, token, amount, onProgress) {
2348
2506
  const validChainId = isValidChainId(chainId);
2349
2507
  try {
@@ -2361,11 +2519,42 @@ var ZyfaiAgent = class _ZyfaiAgent {
2361
2519
  });
2362
2520
  throw error;
2363
2521
  }
2364
- const observation = observeWithdrawalSubmission(this.sdk, validChainId, token);
2522
+ let observation = observeWithdrawalSubmission(this.sdk, validChainId, token);
2365
2523
  const authToken = this.sdk.httpClient?.authToken;
2524
+ const owner = this.getAddress();
2366
2525
  let raw2;
2367
2526
  try {
2368
- raw2 = await this.sdk.withdrawFunds(this.getAddress(), validChainId, amount, token);
2527
+ for (let attempt = 0; attempt < 2; attempt++) {
2528
+ try {
2529
+ raw2 = await this.sdk.withdrawFunds(owner, validChainId, amount, token);
2530
+ break;
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
+ }
2539
+ if (!observation.rejected() || observation.request()?.failure !== "session-key-missing") throw error;
2540
+ if (attempt === 1)
2541
+ throw new OwneyError(
2542
+ "AGENT_AUTH_FAILED",
2543
+ "Zyfai has no active withdrawal session. Its withdrawal was not submitted.",
2544
+ { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
2545
+ this.id
2546
+ );
2547
+ await this.recoverWithdrawalSession(state, validChainId, token, owner);
2548
+ observation.stop();
2549
+ observation = observeWithdrawalSubmission(this.sdk, validChainId, token);
2550
+ }
2551
+ }
2552
+ if (!raw2) throw new OwneyError(
2553
+ "AGENT_INVALID_RESPONSE",
2554
+ "Zyfai returned no withdrawal result.",
2555
+ { chainId, token, phase: "observe" },
2556
+ this.id
2557
+ );
2369
2558
  } catch (error) {
2370
2559
  this.invalidateRejectedWithdrawalSession(error, state, validChainId, authToken);
2371
2560
  if (observation.attempted() !== void 0) {
@@ -2440,6 +2629,67 @@ var ZyfaiAgent = class _ZyfaiAgent {
2440
2629
  async getFundingWallet(state, chainId) {
2441
2630
  return (await this.sdk.getSmartWalletAddress(state.walletAddress, isValidChainId(chainId))).address;
2442
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
+ }
2443
2693
  async getBalances(state, chainId) {
2444
2694
  const requestedAt = Date.now();
2445
2695
  const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
@@ -2648,10 +2898,13 @@ import {
2648
2898
  createPublicClient as createPublicClient3,
2649
2899
  createWalletClient as createWalletClient3,
2650
2900
  custom as custom3,
2901
+ decodeEventLog as decodeEventLog2,
2902
+ encodeEventTopics,
2651
2903
  encodeFunctionData,
2652
- erc20Abi as erc20Abi2,
2904
+ erc20Abi as erc20Abi3,
2905
+ erc4626Abi,
2653
2906
  getAddress as getAddress4,
2654
- isAddress as isAddress2
2907
+ isAddress as isAddress3
2655
2908
  } from "viem";
2656
2909
  import { base as base4 } from "viem/chains";
2657
2910
 
@@ -3270,7 +3523,7 @@ var YieldseekerApiClient = class {
3270
3523
  };
3271
3524
 
3272
3525
  // src/agents/yieldseeker/yieldseeker.mapper.ts
3273
- import { formatUnits as formatUnits2, isAddress } from "viem";
3526
+ import { formatUnits as formatUnits3, isAddress as isAddress2 } from "viem";
3274
3527
 
3275
3528
  // src/lib/helpers/snapshot-apy.ts
3276
3529
  var DAY_MS = 864e5;
@@ -3337,10 +3590,10 @@ function raw(value, endpoint) {
3337
3590
  return BigInt(value);
3338
3591
  }
3339
3592
  function decimal(value, decimals, endpoint) {
3340
- return formatUnits2(raw(value, endpoint), decimals);
3593
+ return formatUnits3(raw(value, endpoint), decimals);
3341
3594
  }
3342
3595
  function usd(rawAmount, decimals, price) {
3343
- return Number(formatUnits2(rawAmount, decimals)) * price;
3596
+ return Number(formatUnits3(rawAmount, decimals)) * price;
3344
3597
  }
3345
3598
  function percent(value) {
3346
3599
  const result = Number(value);
@@ -3366,7 +3619,7 @@ function assetAddressValue(record, address) {
3366
3619
  }
3367
3620
  function position(value, asset, baseAssetDecimals) {
3368
3621
  const option = value?.yieldOption;
3369
- if (!option || typeof option.provider !== "string" || typeof option.name !== "string" || typeof option.address !== "string" || !isAddress(option.address)) {
3622
+ if (!option || typeof option.provider !== "string" || typeof option.name !== "string" || typeof option.address !== "string" || !isAddress2(option.address)) {
3370
3623
  return invalid("yield positions", "missing vault metadata");
3371
3624
  }
3372
3625
  return {
@@ -3458,7 +3711,7 @@ function mapYieldseekerEarnings(contexts) {
3458
3711
  chain: "BASE",
3459
3712
  chainId: 8453,
3460
3713
  asset: context.asset,
3461
- amount: formatUnits2(amount, context.snapshot.baseAssetDecimals)
3714
+ amount: formatUnits3(amount, context.snapshot.baseAssetDecimals)
3462
3715
  });
3463
3716
  lifetimeEarnings += usd(
3464
3717
  amount,
@@ -3587,7 +3840,7 @@ function allocationEntry(action, context) {
3587
3840
  const leg = deposit;
3588
3841
  const symbol = typeof leg.assetSymbol === "string" ? leg.assetSymbol.toUpperCase() : "";
3589
3842
  if (symbol !== context.asset && !(context.asset === "WETH" && symbol === "ETH")) return void 0;
3590
- if (typeof leg.vaultAddress !== "string" || !isAddress(leg.vaultAddress)) return void 0;
3843
+ if (typeof leg.vaultAddress !== "string" || !isAddress2(leg.vaultAddress)) return void 0;
3591
3844
  const rawAmount = typeof leg.rawAmount === "string" && /^\d+$/.test(leg.rawAmount) ? BigInt(leg.rawAmount) : typeof leg.rawAmount === "number" && Number.isSafeInteger(leg.rawAmount) ? BigInt(leg.rawAmount) : void 0;
3592
3845
  if (rawAmount === void 0 || rawAmount <= 0n) return void 0;
3593
3846
  const option = context.positions.find((position2) => position2.yieldOption.address.toLowerCase() === leg.vaultAddress.toLowerCase())?.yieldOption;
@@ -3595,7 +3848,7 @@ function allocationEntry(action, context) {
3595
3848
  protocol: typeof leg.vaultProvider === "string" && leg.vaultProvider ? leg.vaultProvider : option?.provider || "Vault",
3596
3849
  pool: typeof leg.vaultName === "string" && leg.vaultName ? leg.vaultName : option?.name,
3597
3850
  tokenSymbol: context.asset,
3598
- amount: formatUnits2(rawAmount, YIELDSEEKER_ASSET_METADATA[context.asset].decimals)
3851
+ amount: formatUnits3(rawAmount, YIELDSEEKER_ASSET_METADATA[context.asset].decimals)
3599
3852
  });
3600
3853
  amountRaw += rawAmount;
3601
3854
  }
@@ -3614,7 +3867,7 @@ function allocationEntry(action, context) {
3614
3867
  tokenSymbol: context.asset,
3615
3868
  agentWallet: context.wallet.walletAddress,
3616
3869
  amountRaw: String(amountRaw),
3617
- amount: formatUnits2(amountRaw, YIELDSEEKER_ASSET_METADATA[context.asset].decimals)
3870
+ amount: formatUnits3(amountRaw, YIELDSEEKER_ASSET_METADATA[context.asset].decimals)
3618
3871
  }]
3619
3872
  };
3620
3873
  }
@@ -3738,7 +3991,7 @@ function mapYieldseekerHistory(contexts, options) {
3738
3991
  const known = new Set(target.logIndexes ?? []);
3739
3992
  if (tx.logIndexes?.some((index) => !known.has(index))) {
3740
3993
  target.amountRaw = String(BigInt(target.amountRaw ?? "0") + BigInt(tx.amountRaw));
3741
- target.amount = formatUnits2(
3994
+ target.amount = formatUnits3(
3742
3995
  BigInt(target.amountRaw),
3743
3996
  YIELDSEEKER_ASSET_METADATA[target.tokenSymbol].decimals
3744
3997
  );
@@ -3833,7 +4086,8 @@ var walletAbi = parseAbi3([
3833
4086
  "function ownerAgentIndex() view returns (uint256)",
3834
4087
  "function baseAsset() view returns (address)",
3835
4088
  "function FACTORY() view returns (address)",
3836
- "function upgradeToLatest()"
4089
+ "function upgradeToLatest()",
4090
+ "event Upgraded(address indexed implementation)"
3837
4091
  ]);
3838
4092
  var factoryAbi = parseAbi3([
3839
4093
  "function userWallets(address owner, uint256 index) view returns (address)",
@@ -3842,19 +4096,19 @@ var factoryAbi = parseAbi3([
3842
4096
  function yieldseekerOwnerPath(code) {
3843
4097
  return !code || code === "0x" || /^0xef0100[0-9a-f]{40}$/i.test(code) ? "eoa" : "smart";
3844
4098
  }
3845
- async function readYieldseekerWalletStatus(client, wallet, owner, asset) {
4099
+ async function readYieldseekerWalletStatus(client, wallet, owner, asset, blockNumber) {
3846
4100
  const agentWallet = getAddress2(wallet);
3847
4101
  const expectedOwner = getAddress2(owner);
3848
4102
  const expectedAsset = getAddress2(asset);
3849
4103
  const [code, ownerCode, slot, actualOwner, index, actualAsset, actualFactory, latest] = await Promise.all([
3850
- client.getBytecode({ address: agentWallet }),
3851
- client.getBytecode({ address: expectedOwner }),
3852
- client.getStorageAt({ address: agentWallet, slot: ERC1967_IMPLEMENTATION_SLOT }),
3853
- client.readContract({ address: agentWallet, abi: walletAbi, functionName: "owner" }),
3854
- client.readContract({ address: agentWallet, abi: walletAbi, functionName: "ownerAgentIndex" }),
3855
- client.readContract({ address: agentWallet, abi: walletAbi, functionName: "baseAsset" }),
3856
- client.readContract({ address: agentWallet, abi: walletAbi, functionName: "FACTORY" }),
3857
- client.readContract({ address: YIELDSEEKER_FACTORY, abi: factoryAbi, functionName: "agentWalletImplementation" })
4104
+ client.getBytecode({ address: agentWallet, blockNumber }),
4105
+ client.getBytecode({ address: expectedOwner, blockNumber }),
4106
+ client.getStorageAt({ address: agentWallet, slot: ERC1967_IMPLEMENTATION_SLOT, blockNumber }),
4107
+ client.readContract({ address: agentWallet, abi: walletAbi, functionName: "owner", blockNumber }),
4108
+ client.readContract({ address: agentWallet, abi: walletAbi, functionName: "ownerAgentIndex", blockNumber }),
4109
+ client.readContract({ address: agentWallet, abi: walletAbi, functionName: "baseAsset", blockNumber }),
4110
+ client.readContract({ address: agentWallet, abi: walletAbi, functionName: "FACTORY", blockNumber }),
4111
+ client.readContract({ address: YIELDSEEKER_FACTORY, abi: factoryAbi, functionName: "agentWalletImplementation", blockNumber })
3858
4112
  ]);
3859
4113
  if (!code || code === "0x" || !slot || /^0x0+$/.test(slot)) {
3860
4114
  throw new Error("Yieldseeker wallet is not a deployed ERC1967 proxy");
@@ -3864,7 +4118,8 @@ async function readYieldseekerWalletStatus(client, wallet, owner, asset) {
3864
4118
  address: YIELDSEEKER_FACTORY,
3865
4119
  abi: factoryAbi,
3866
4120
  functionName: "userWallets",
3867
- args: [expectedOwner, index]
4121
+ args: [expectedOwner, index],
4122
+ blockNumber
3868
4123
  });
3869
4124
  const verified = isAddressEqual(actualOwner, expectedOwner) && isAddressEqual(actualAsset, expectedAsset) && isAddressEqual(actualFactory, YIELDSEEKER_FACTORY) && isAddressEqual(mapped, agentWallet);
3870
4125
  const version = !verified ? "unknown" : isAddressEqual(implementation, YIELDSEEKER_V2_IMPLEMENTATION) ? "v2" : isAddressEqual(implementation, YIELDSEEKER_V1_IMPLEMENTATION) ? "v1" : "unknown";
@@ -3893,6 +4148,8 @@ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
3893
4148
  import { base as base3 } from "viem/chains";
3894
4149
  var ENTRY_POINT_V06 = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
3895
4150
  var USER_OP_FEE_HEADROOM = 2n;
4151
+ var OBSERVATION_WINDOW_MS = 6e4;
4152
+ var OBSERVATION_POLL_MS = 1500;
3896
4153
  var ENTRY_POINT_NONCE_ABI = [
3897
4154
  { type: "function", name: "getNonce", stateMutability: "view", inputs: [
3898
4155
  { name: "sender", type: "address" },
@@ -3925,10 +4182,119 @@ function sponsorError(message, safeToFallback, details = {}) {
3925
4182
  return new OwneyError(
3926
4183
  "SPONSOR_REQUEST_FAILED",
3927
4184
  message,
3928
- { safeToFallback, ...details },
4185
+ {
4186
+ safeToFallback,
4187
+ phase: safeToFallback ? "prepare" : "observe",
4188
+ failureCode: safeToFallback ? "SPONSOR_PREPARATION_FAILED" : "SPONSOR_OUTCOME_UNRESOLVED",
4189
+ ...safeToFallback ? { rejectedBeforeSubmission: true } : {},
4190
+ ...details
4191
+ },
3929
4192
  "yieldseeker"
3930
4193
  );
3931
4194
  }
4195
+ function providerReason(value) {
4196
+ if (typeof value !== "string") return void 0;
4197
+ return value.replace(/https?:\/\/\S+/gi, "[url]").replace(/0x[0-9a-fA-F]{40,}/g, "[hex]").replace(/[A-Za-z0-9_+=\/-]{48,}/g, "[token]").slice(0, 300);
4198
+ }
4199
+ function rpcDiagnostics(data) {
4200
+ const details = {};
4201
+ if (typeof data?.failureCode === "string" && [
4202
+ "SPONSOR_RATE_LIMIT",
4203
+ "SPONSOR_UPSTREAM_UNAVAILABLE",
4204
+ "SPONSOR_RPC_UNSUPPORTED",
4205
+ "SPONSOR_POLICY_REJECTED",
4206
+ "SPONSOR_BUDGET_REJECTED",
4207
+ "SPONSOR_EXECUTION_REVERTED"
4208
+ ].includes(data.failureCode))
4209
+ details.failureCode = data.failureCode;
4210
+ if (typeof data?.revertSelector === "string" && /^0x[0-9a-fA-F]{8}$/.test(data.revertSelector))
4211
+ details.revertSelector = data.revertSelector;
4212
+ if (typeof data?.revertName === "string" && [
4213
+ "InsufficientBalance",
4214
+ "InvalidAsset",
4215
+ "Unauthorized",
4216
+ "Error",
4217
+ "Panic",
4218
+ "FailedOp",
4219
+ "FailedOpWithRevert"
4220
+ ].includes(data.revertName))
4221
+ details.revertName = data.revertName;
4222
+ if (typeof data?.aaCode === "string" && /^AA[0-9]{2}$/.test(data.aaCode)) details.aaCode = data.aaCode;
4223
+ if (typeof data?.panicCode === "string" && /^0x[0-9a-fA-F]{1,2}$/.test(data.panicCode)) {
4224
+ const panicCode = toHex(BigInt(data.panicCode));
4225
+ if (["0x0", "0x1", "0x11", "0x12", "0x21", "0x22", "0x31", "0x32", "0x41", "0x51"].includes(panicCode))
4226
+ details.panicCode = panicCode;
4227
+ }
4228
+ return details;
4229
+ }
4230
+ function isHash(value) {
4231
+ return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value);
4232
+ }
4233
+ async function assertOwnerSession(state, owner) {
4234
+ const [chain, accounts] = await Promise.all([
4235
+ state.provider.request({ method: "eth_chainId" }),
4236
+ state.provider.request({ method: "eth_accounts" })
4237
+ ]);
4238
+ if (Number(chain) !== 8453)
4239
+ throw new OwneyError(
4240
+ "CHAIN_MISMATCH",
4241
+ "Switch back to Base before withdrawing.",
4242
+ { rejectedBeforeSubmission: true, phase: "prepare" },
4243
+ "yieldseeker"
4244
+ );
4245
+ if (!Array.isArray(accounts) || typeof accounts[0] !== "string" || accounts[0].toLowerCase() !== owner.toLowerCase())
4246
+ throw new OwneyError(
4247
+ "NOT_CONNECTED",
4248
+ "The connected withdrawal account changed.",
4249
+ { rejectedBeforeSubmission: true, phase: "prepare" },
4250
+ "yieldseeker"
4251
+ );
4252
+ }
4253
+ function savePending(onPending, evidence, beforeSubmission = false) {
4254
+ try {
4255
+ onPending?.(evidence);
4256
+ } catch {
4257
+ if (beforeSubmission)
4258
+ throw sponsorError(
4259
+ "Withdrawal status cannot be saved before submission.",
4260
+ true,
4261
+ { phase: "prepare", rejectedBeforeSubmission: true, failureCode: "RECOVERY_STORAGE_UNAVAILABLE" }
4262
+ );
4263
+ }
4264
+ }
4265
+ async function checkpoint(callback, pending) {
4266
+ try {
4267
+ await callback?.(pending);
4268
+ } catch {
4269
+ throw sponsorError(
4270
+ "Withdrawal status cannot be saved before submission.",
4271
+ true,
4272
+ { phase: "prepare", rejectedBeforeSubmission: true, failureCode: "RECOVERY_STORAGE_UNAVAILABLE" }
4273
+ );
4274
+ }
4275
+ }
4276
+ function finalPaymasterFields(value) {
4277
+ const result = {};
4278
+ for (const key2 of [
4279
+ "paymasterAndData",
4280
+ "callGasLimit",
4281
+ "verificationGasLimit",
4282
+ "preVerificationGas",
4283
+ "maxFeePerGas",
4284
+ "maxPriorityFeePerGas"
4285
+ ]) {
4286
+ const field = value[key2];
4287
+ if (field === void 0) continue;
4288
+ if (typeof field !== "string" || !/^0x[0-9a-fA-F]+$/.test(field))
4289
+ throw sponsorError(
4290
+ "Paymaster returned an invalid final operation.",
4291
+ true,
4292
+ { phase: "prepare", rejectedBeforeSubmission: true, failureCode: "INVALID_PAYMASTER_QUOTE" }
4293
+ );
4294
+ result[key2] = field;
4295
+ }
4296
+ return result;
4297
+ }
3932
4298
  async function requestIntent(input) {
3933
4299
  const baseUrl = (input.routingApiBaseUrl ?? DEFAULT_ROUTING_API).replace(/\/$/, "");
3934
4300
  let response;
@@ -3950,18 +4316,35 @@ async function requestIntent(input) {
3950
4316
  action: input.action,
3951
4317
  yieldseekerUserId: input.userId,
3952
4318
  yieldseekerAgentId: input.agentId
3953
- })
4319
+ }),
4320
+ signal: AbortSignal.timeout(15e3)
3954
4321
  });
3955
4322
  } catch {
3956
- throw sponsorError("Withdrawal sponsorship could not be requested.", true);
4323
+ throw sponsorError(
4324
+ "Withdrawal sponsorship could not be requested.",
4325
+ true,
4326
+ { phase: "prepare", rejectedBeforeSubmission: true, failureCode: "INTENT_NETWORK_ERROR" }
4327
+ );
3957
4328
  }
3958
4329
  const envelope = await response.json().catch(() => null);
3959
4330
  const intent = envelope?.data;
3960
4331
  if (!response.ok || !envelope?.success || !intent?.intent || !["eoa", "smart"].includes(intent.ownerPath)) {
4332
+ const rawCode = typeof envelope?.failureCode === "string" ? envelope.failureCode : envelope?.code;
4333
+ const failureCode = typeof rawCode === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(rawCode) ? rawCode : void 0;
4334
+ if (input.refreshYieldseekerSignature && (failureCode === "YIELDSEEKER_AUTH_REJECTED" || failureCode === "AUTH_REJECTED" || envelope?.sponsorshipStage === "provider-auth" && (envelope.providerStatus === 401 || envelope.providerStatus === 403))) {
4335
+ const signature = await input.refreshYieldseekerSignature(input.yieldseekerSignature);
4336
+ return requestIntent({ ...input, yieldseekerSignature: signature, refreshYieldseekerSignature: void 0 });
4337
+ }
3961
4338
  throw sponsorError(
3962
4339
  "Withdrawal sponsorship is currently unavailable.",
3963
4340
  true,
3964
- { statusCode: response.status }
4341
+ {
4342
+ statusCode: response.status,
4343
+ phase: "prepare",
4344
+ rejectedBeforeSubmission: true,
4345
+ failureCode: failureCode ?? "INTENT_REJECTED",
4346
+ providerReason: providerReason(envelope?.message)
4347
+ }
3965
4348
  );
3966
4349
  }
3967
4350
  const url = new URL(`${baseUrl}/api/v1/sponsor/yieldseeker-withdraw/rpc`);
@@ -3974,19 +4357,55 @@ async function rpc(url, method, params, phase) {
3974
4357
  response = await fetch(url, {
3975
4358
  method: "POST",
3976
4359
  headers: { "content-type": "application/json" },
3977
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
4360
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
4361
+ signal: AbortSignal.timeout(15e3)
3978
4362
  });
3979
4363
  } catch {
3980
- throw sponsorError("Withdrawal sponsorship request status is unknown.", phase === "prepare");
4364
+ throw sponsorError(
4365
+ "Withdrawal sponsorship request status is unknown.",
4366
+ phase === "prepare",
4367
+ {
4368
+ phase,
4369
+ rpcMethod: method,
4370
+ ...phase === "prepare" ? { rejectedBeforeSubmission: true } : {},
4371
+ failureCode: "RPC_NETWORK_ERROR"
4372
+ }
4373
+ );
3981
4374
  }
3982
4375
  const payload = await response.json().catch(() => null);
3983
4376
  if (!response.ok || payload?.error || payload?.result === void 0) {
3984
4377
  const rejection = payload?.error;
3985
- const definitiveRejection = phase === "send" && response.status < 500 && !!rejection && (rejection.data?.rejectedBeforeSubmission === true || [-32501, -32502, -32503, -32504, -32505, -32507].includes(rejection.code ?? 0) || /maxFeePerGas.*(?:too low|lower than|less than|must be at least)/i.test(rejection.message ?? ""));
4378
+ const diagnostics = rpcDiagnostics(rejection?.data);
4379
+ const executionReverted = rejection?.code === -32521 || diagnostics.failureCode === "SPONSOR_EXECUTION_REVERTED";
4380
+ const details = {
4381
+ statusCode: response.status,
4382
+ rpcCode: rejection?.code,
4383
+ rpcMethod: method,
4384
+ phase,
4385
+ providerReason: providerReason(rejection?.data?.providerReason) ?? providerReason(rejection?.message),
4386
+ failureCode: "RPC_REJECTED",
4387
+ ...diagnostics
4388
+ };
4389
+ if (phase === "prepare" && executionReverted)
4390
+ throw new OwneyError(
4391
+ "WITHDRAW_FAILED",
4392
+ "Yieldseeker withdrawal simulation reverted before submission.",
4393
+ {
4394
+ ...details,
4395
+ failureCode: "SPONSOR_EXECUTION_REVERTED",
4396
+ safeToFallback: false,
4397
+ rejectedBeforeSubmission: true
4398
+ },
4399
+ "yieldseeker"
4400
+ );
4401
+ const definitiveRejection = phase === "send" && !executionReverted && response.status < 500 && !!rejection && (rejection.data?.rejectedBeforeSubmission === true || [-32501, -32502, -32503, -32504, -32505, -32507].includes(rejection.code ?? 0) || /maxFeePerGas.*(?:too low|lower than|less than|must be at least)/i.test(rejection.message ?? ""));
3986
4402
  throw sponsorError(
3987
4403
  phase !== "prepare" && !definitiveRejection ? "Sponsored withdrawal submission status is unknown." : "Withdrawal sponsorship was declined before submission.",
3988
4404
  phase === "prepare" || definitiveRejection,
3989
- { statusCode: response.status, rpcCode: payload?.error?.code, rpcMethod: method }
4405
+ {
4406
+ ...details,
4407
+ ...phase === "prepare" || definitiveRejection ? { rejectedBeforeSubmission: true } : {}
4408
+ }
3990
4409
  );
3991
4410
  }
3992
4411
  return payload.result;
@@ -4006,13 +4425,15 @@ async function submitEoa(input) {
4006
4425
  let nonce;
4007
4426
  let fees;
4008
4427
  try {
4009
- nonce = await input.client.readContract({
4010
- address: ENTRY_POINT_V06,
4011
- abi: ENTRY_POINT_NONCE_ABI,
4012
- functionName: "getNonce",
4013
- args: [input.agentWallet, 0n]
4014
- });
4015
- fees = await input.client.estimateFeesPerGas();
4428
+ [nonce, fees] = await Promise.all([
4429
+ input.client.readContract({
4430
+ address: ENTRY_POINT_V06,
4431
+ abi: ENTRY_POINT_NONCE_ABI,
4432
+ functionName: "getNonce",
4433
+ args: [input.agentWallet, 0n]
4434
+ }),
4435
+ input.client.estimateFeesPerGas()
4436
+ ]);
4016
4437
  } catch {
4017
4438
  throw sponsorError("Could not prepare the sponsored withdrawal.", true);
4018
4439
  }
@@ -4055,9 +4476,10 @@ async function submitEoa(input) {
4055
4476
  );
4056
4477
  if (!final.paymasterAndData?.startsWith("0x"))
4057
4478
  throw sponsorError("CDP did not return final paymaster data.", true);
4058
- operation = { ...operation, paymasterAndData: final.paymasterAndData };
4479
+ operation = { ...operation, ...finalPaymasterFields(final) };
4059
4480
  }
4060
4481
  const userOpHash = operationHash(operation);
4482
+ await assertOwnerSession(input.state, input.owner);
4061
4483
  const wallet = createWalletClient2({
4062
4484
  account: input.owner,
4063
4485
  chain: base3,
@@ -4065,24 +4487,47 @@ async function submitEoa(input) {
4065
4487
  });
4066
4488
  const signature = await wallet.signMessage({ message: { raw: userOpHash } });
4067
4489
  operation = { ...operation, signature };
4068
- input.onPending?.({ userOpHash });
4490
+ await assertOwnerSession(input.state, input.owner);
4491
+ const userOperation = {
4492
+ sender: operation.sender,
4493
+ nonce: operation.nonce,
4494
+ initCode: operation.initCode,
4495
+ callData: operation.callData,
4496
+ callGasLimit: operation.callGasLimit,
4497
+ verificationGasLimit: operation.verificationGasLimit,
4498
+ preVerificationGas: operation.preVerificationGas,
4499
+ maxFeePerGas: operation.maxFeePerGas,
4500
+ maxPriorityFeePerGas: operation.maxPriorityFeePerGas,
4501
+ paymasterAndData: operation.paymasterAndData
4502
+ };
4503
+ savePending(input.onPending, { userOpHash, userOperation }, true);
4504
+ await checkpoint(input.onBeforeSubmission, { userOpHash, userOperation });
4069
4505
  const submittedHash = await rpc(
4070
4506
  input.rpcUrl,
4071
4507
  "eth_sendUserOperation",
4072
4508
  [operation, ENTRY_POINT_V06],
4073
4509
  "send"
4074
4510
  );
4075
- if (submittedHash.toLowerCase() !== userOpHash.toLowerCase())
4511
+ if (!isHash(submittedHash) || submittedHash.toLowerCase() !== userOpHash.toLowerCase())
4076
4512
  throw sponsorError(
4077
4513
  "Bundler returned a different UserOperation hash.",
4078
4514
  false,
4079
- { userOpHash, submittedHash }
4515
+ { userOpHash, submittedHash, phase: "send", failureCode: "BUNDLER_HASH_MISMATCH" }
4080
4516
  );
4081
- for (let poll = 0; poll < 40; poll++) {
4082
- const response = await rpc(input.rpcUrl, "eth_getUserOperationReceipt", [userOpHash], "observe");
4517
+ const deadline = Date.now() + OBSERVATION_WINDOW_MS;
4518
+ let lastReadError;
4519
+ for (let poll = 0; poll < 40 && Date.now() < deadline; poll++) {
4520
+ let response;
4521
+ try {
4522
+ response = await rpc(input.rpcUrl, "eth_getUserOperationReceipt", [userOpHash], "observe");
4523
+ } catch (error) {
4524
+ lastReadError = error;
4525
+ await new Promise((resolve) => setTimeout(resolve, Math.min(OBSERVATION_POLL_MS, Math.max(0, deadline - Date.now()))));
4526
+ continue;
4527
+ }
4083
4528
  const txHash = response?.receipt?.transactionHash;
4084
- if (txHash) {
4085
- input.onPending?.({ userOpHash, txHash });
4529
+ if (isHash(txHash)) {
4530
+ savePending(input.onPending, { userOpHash, txHash });
4086
4531
  if (response?.success === false)
4087
4532
  throw new OwneyError(
4088
4533
  "AGENT_TRANSACTION_REVERTED",
@@ -4092,12 +4537,17 @@ async function submitEoa(input) {
4092
4537
  );
4093
4538
  return { txHash, receipt: await waitForTransaction(input.client, txHash) };
4094
4539
  }
4095
- await new Promise((resolve) => setTimeout(resolve, 1500));
4540
+ await new Promise((resolve) => setTimeout(resolve, OBSERVATION_POLL_MS));
4096
4541
  }
4097
4542
  throw sponsorError(
4098
4543
  "Sponsored withdrawal is still pending; check status before retrying.",
4099
4544
  false,
4100
- { userOpHash }
4545
+ {
4546
+ userOpHash,
4547
+ phase: "observe",
4548
+ failureCode: "RECEIPT_UNRESOLVED",
4549
+ ...lastReadError instanceof OwneyError ? { lastObservationError: lastReadError.details } : {}
4550
+ }
4101
4551
  );
4102
4552
  }
4103
4553
  async function submitSmart(input) {
@@ -4110,14 +4560,14 @@ async function submitSmart(input) {
4110
4560
  } catch {
4111
4561
  throw sponsorError("This account cannot check paymaster support on Base.", true);
4112
4562
  }
4113
- if (!capabilities?.[toHex(8453)]?.paymasterService?.supported && !capabilities?.["8453"]?.paymasterService?.supported)
4563
+ const supportsPaymaster = capabilities?.[toHex(8453)]?.paymasterService?.supported ?? capabilities?.["8453"]?.paymasterService?.supported ?? capabilities?.["0x0"]?.paymasterService?.supported;
4564
+ if (supportsPaymaster !== true)
4114
4565
  throw sponsorError("This account cannot use a paymaster on Base.", true);
4566
+ await assertOwnerSession(input.state, input.owner);
4115
4567
  let sent;
4116
- try {
4117
- input.onPending?.({});
4118
- } catch {
4119
- throw sponsorError("Upgrade or withdrawal status cannot be saved.", true);
4120
- }
4568
+ const requestedId = toHex(globalThis.crypto.getRandomValues(new Uint8Array(32)));
4569
+ savePending(input.onPending, { callsId: requestedId }, true);
4570
+ await checkpoint(input.onBeforeSubmission, { callsId: requestedId });
4121
4571
  try {
4122
4572
  sent = await input.state.provider.request({
4123
4573
  method: "wallet_sendCalls",
@@ -4125,6 +4575,7 @@ async function submitSmart(input) {
4125
4575
  version: "2.0.0",
4126
4576
  from: input.owner,
4127
4577
  chainId: toHex(8453),
4578
+ id: requestedId,
4128
4579
  atomicRequired: false,
4129
4580
  calls: [{ to: input.agentWallet, value: "0x0", data: input.callData }],
4130
4581
  capabilities: { paymasterService: { url: input.rpcUrl } }
@@ -4133,17 +4584,27 @@ async function submitSmart(input) {
4133
4584
  } catch (error) {
4134
4585
  if (error?.code === 4001) throw error;
4135
4586
  if ([4200, -32601].includes(Number(error?.code)))
4136
- throw sponsorError("This account does not support sponsored calls.", true);
4137
- throw sponsorError("Wallet submission status is unknown. Check activity before retrying.", false);
4587
+ throw sponsorError(
4588
+ "This account does not support sponsored calls.",
4589
+ true,
4590
+ { phase: "send", failureCode: "WALLET_CALLS_UNSUPPORTED" }
4591
+ );
4592
+ throw sponsorError(
4593
+ "Wallet submission status is unknown. Check activity before retrying.",
4594
+ false,
4595
+ { callsId: requestedId, phase: "send", failureCode: "WALLET_SEND_UNRESOLVED" }
4596
+ );
4138
4597
  }
4139
4598
  const callsId = typeof sent === "string" ? sent : sent?.id;
4140
- if (!callsId) throw sponsorError("Wallet returned no calls ID; submission status is unknown.", false);
4141
- try {
4142
- input.onPending?.({ callsId });
4143
- } catch {
4144
- throw sponsorError("Submitted call status could not be saved.", false, { callsId });
4145
- }
4146
- for (let poll = 0; poll < 40; poll++) {
4599
+ if (typeof callsId !== "string" || callsId.length === 0 || callsId.length > 8194)
4600
+ throw sponsorError(
4601
+ "Wallet returned no calls ID; submission status is unknown.",
4602
+ false,
4603
+ { callsId: requestedId, phase: "send", failureCode: "WALLET_SEND_UNRESOLVED" }
4604
+ );
4605
+ savePending(input.onPending, { callsId });
4606
+ const deadline = Date.now() + OBSERVATION_WINDOW_MS;
4607
+ for (let poll = 0; poll < 40 && Date.now() < deadline; poll++) {
4147
4608
  let status;
4148
4609
  try {
4149
4610
  status = await input.state.provider.request({
@@ -4151,33 +4612,40 @@ async function submitSmart(input) {
4151
4612
  params: [callsId]
4152
4613
  });
4153
4614
  } catch {
4154
- throw sponsorError("Sponsored withdrawal status is unknown.", false, { callsId });
4615
+ await new Promise((resolve) => setTimeout(resolve, Math.min(OBSERVATION_POLL_MS, Math.max(0, deadline - Date.now()))));
4616
+ continue;
4155
4617
  }
4156
- if (status?.status === 400 || status?.status === 500)
4618
+ if (status?.id !== void 0 && status.id !== callsId || status?.chainId !== void 0 && Number(status.chainId) !== 8453)
4619
+ throw sponsorError(
4620
+ "Wallet returned a different sponsored operation status.",
4621
+ false,
4622
+ { callsId, phase: "observe", failureCode: "WALLET_STATUS_MISMATCH" }
4623
+ );
4624
+ if (Number(status?.status) === 400 || Number(status?.status) === 500)
4157
4625
  throw sponsorError(
4158
4626
  "Sponsored withdrawal failed in the wallet.",
4159
4627
  false,
4160
- { callsId, executionFailed: true }
4628
+ { callsId, executionFailed: true, phase: "observe", failureCode: "WALLET_EXECUTION_FAILED" }
4161
4629
  );
4162
- const txHash = status?.receipts?.[0]?.transactionHash;
4630
+ const hashes = [...new Set((status?.receipts ?? []).map((receipt) => receipt.transactionHash).filter(isHash))];
4631
+ const txHash = hashes.length === 1 ? hashes[0] : void 0;
4163
4632
  if (txHash) {
4164
- try {
4165
- input.onPending?.({ callsId, txHash });
4166
- } catch {
4167
- throw sponsorError(
4168
- "Submitted call receipt could not be saved.",
4169
- false,
4170
- { callsId, txHash }
4633
+ savePending(input.onPending, { callsId, txHash });
4634
+ if (status?.receipts?.some((receipt) => receipt.transactionHash === txHash && /^(?:0x0+|0|failed|reverted)$/i.test(String(receipt.status))))
4635
+ throw new OwneyError(
4636
+ "AGENT_TRANSACTION_REVERTED",
4637
+ "Sponsored withdrawal execution reverted.",
4638
+ { callsId, transactionHash: txHash },
4639
+ "yieldseeker"
4171
4640
  );
4172
- }
4173
4641
  return { txHash, receipt: await waitForTransaction(input.client, txHash) };
4174
4642
  }
4175
- await new Promise((resolve) => setTimeout(resolve, 1500));
4643
+ await new Promise((resolve) => setTimeout(resolve, OBSERVATION_POLL_MS));
4176
4644
  }
4177
4645
  throw sponsorError(
4178
4646
  "Sponsored withdrawal is still pending; check status before retrying.",
4179
4647
  false,
4180
- { callsId }
4648
+ { callsId, phase: "observe", failureCode: "RECEIPT_UNRESOLVED" }
4181
4649
  );
4182
4650
  }
4183
4651
  async function submitYieldseekerSponsoredWithdrawal(input) {
@@ -4192,7 +4660,8 @@ async function submitYieldseekerSponsoredWithdrawal(input) {
4192
4660
  agentWallet: input.agentWallet,
4193
4661
  asset: input.asset,
4194
4662
  callData: input.callData,
4195
- action: "withdraw"
4663
+ action: "withdraw",
4664
+ refreshYieldseekerSignature: input.refreshYieldseekerSignature
4196
4665
  });
4197
4666
  const args = { ...input, owner, rpcUrl };
4198
4667
  return intent.ownerPath === "eoa" ? submitEoa(args) : submitSmart(args);
@@ -4209,7 +4678,8 @@ async function submitYieldseekerSponsoredUpgrade(input) {
4209
4678
  agentWallet: input.agentWallet,
4210
4679
  asset: input.asset,
4211
4680
  callData: input.callData,
4212
- action: "upgrade"
4681
+ action: "upgrade",
4682
+ refreshYieldseekerSignature: input.refreshYieldseekerSignature
4213
4683
  });
4214
4684
  if (intent.ownerPath !== "smart")
4215
4685
  throw sponsorError("V1 upgrade sponsorship requires a Base Account owner.", true);
@@ -4231,6 +4701,9 @@ var YIELDSEEKER_READ_FAILURE_COOLDOWN_MS = 6e4;
4231
4701
  var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
4232
4702
  var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
4233
4703
  var YIELDSEEKER_MAX_WITHDRAW_SWEEPS = 3;
4704
+ var YIELDSEEKER_MAX_POSITION_VERIFICATION_MS = 2e3;
4705
+ var YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT = 64;
4706
+ var YIELDSEEKER_VAULTS_PER_RPC_BATCH = 10;
4234
4707
  function generateYieldseekerUsername() {
4235
4708
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
4236
4709
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -4241,6 +4714,13 @@ function isUsernameConflict(error) {
4241
4714
  return code.includes("USERNAME") && (code.includes("TAKEN") || code.includes("EXIST") || code.includes("UNAVAILABLE") || code.includes("IN_USE"));
4242
4715
  }
4243
4716
  var YIELDSEEKER_AGENT_WALLET_ABI = [
4717
+ {
4718
+ type: "function",
4719
+ name: "feeTracker",
4720
+ stateMutability: "view",
4721
+ inputs: [],
4722
+ outputs: [{ name: "", type: "address" }]
4723
+ },
4244
4724
  {
4245
4725
  type: "function",
4246
4726
  name: "withdrawAssetToUser",
@@ -4263,6 +4743,13 @@ var YIELDSEEKER_AGENT_WALLET_ABI = [
4263
4743
  outputs: []
4264
4744
  }
4265
4745
  ];
4746
+ var YIELDSEEKER_FEE_TRACKER_ABI = [{
4747
+ type: "function",
4748
+ name: "getFeesOwed",
4749
+ stateMutability: "view",
4750
+ inputs: [{ name: "wallet", type: "address" }],
4751
+ outputs: [{ name: "", type: "uint256" }]
4752
+ }];
4266
4753
  function query(params) {
4267
4754
  const search = new URLSearchParams();
4268
4755
  for (const [key2, value] of Object.entries(params)) {
@@ -4346,17 +4833,22 @@ var YieldseekerAgent = class {
4346
4833
  getAddress4(state.walletAddress),
4347
4834
  YIELDSEEKER_ASSET_METADATA[asset].address
4348
4835
  );
4836
+ this.rememberWalletStatus(key2, status);
4837
+ return status;
4838
+ }
4839
+ rememberWalletStatus(key2, status) {
4349
4840
  if (status.verified && status.version === "v2") {
4350
4841
  this.withdrawalVersionCache.set(key2, { at: Date.now(), status: { ...status } });
4351
4842
  if (this.withdrawalVersionCache.size > 128)
4352
4843
  this.withdrawalVersionCache.delete(this.withdrawalVersionCache.keys().next().value);
4353
4844
  } else this.withdrawalVersionCache.delete(key2);
4354
- return status;
4355
4845
  }
4356
4846
  /** Explicit owner action. This never runs as a side effect of withdrawal. */
4357
4847
  async upgradeWallet(state, chainId, asset, gasMode = "paid") {
4848
+ const generation = this.readGeneration;
4358
4849
  const before = await this.getWalletStatus(state, chainId, asset);
4359
- if (!before?.verified || !before.upgradeAvailable) {
4850
+ this.assertSession(generation, chainId);
4851
+ if (!before?.verified || before.version === "unknown") {
4360
4852
  throw new OwneyError(
4361
4853
  "WITHDRAW_FAILED",
4362
4854
  "This Yieldseeker wallet cannot be upgraded to the reviewed V2 implementation.",
@@ -4364,48 +4856,77 @@ var YieldseekerAgent = class {
4364
4856
  this.id
4365
4857
  );
4366
4858
  }
4367
- const data = encodeFunctionData({ abi: YIELDSEEKER_UPGRADE_ABI, functionName: "upgradeToLatest" });
4368
- let txHash;
4369
- if (gasMode === "sponsored") {
4370
- if (before.ownerPath !== "smart")
4371
- throw new OwneyError(
4372
- "SPONSOR_REQUEST_FAILED",
4373
- "EOA owners pay Base gas for the V1 upgrade.",
4374
- { safeToFallback: true },
4375
- this.id
4376
- );
4377
- let storage5;
4378
- try {
4379
- storage5 = typeof window === "undefined" ? void 0 : window.localStorage;
4380
- } catch {
4381
- storage5 = void 0;
4382
- }
4383
- if (!storage5)
4859
+ const versionKey = `${chainId}:${before.owner.toLowerCase()}:${before.agentWallet.toLowerCase()}:${asset}`;
4860
+ const pendingKey2 = `owney.yieldseeker.upgrade:${before.owner.toLowerCase()}:${before.agentWallet.toLowerCase()}`;
4861
+ const confirmUpgrade = async (hash, receipt2) => {
4862
+ this.withdrawalVersionCache.delete(versionKey);
4863
+ const upgradeTopic = encodeEventTopics({ abi: YIELDSEEKER_UPGRADE_ABI, eventName: "Upgraded" })[0];
4864
+ const lastUpgrade = receipt2?.logs.filter((log) => log.address.toLowerCase() === before.agentWallet.toLowerCase() && log.topics[0] === upgradeTopic).at(-1);
4865
+ const expectedUpgrade = (() => {
4866
+ if (!lastUpgrade || lastUpgrade.removed) return false;
4867
+ try {
4868
+ const event = decodeEventLog2({ abi: YIELDSEEKER_UPGRADE_ABI, eventName: "Upgraded", data: lastUpgrade.data, topics: lastUpgrade.topics });
4869
+ return event.args.implementation.toLowerCase() === YIELDSEEKER_V2_IMPLEMENTATION.toLowerCase();
4870
+ } catch {
4871
+ return false;
4872
+ }
4873
+ })();
4874
+ if (!receipt2 || receipt2.status !== "success" || receipt2.transactionHash.toLowerCase() !== hash.toLowerCase() || typeof receipt2.blockNumber !== "bigint" || !expectedUpgrade)
4384
4875
  throw new OwneyError(
4385
- "SPONSOR_REQUEST_FAILED",
4386
- "Upgrade status cannot be saved in this browser.",
4387
- { safeToFallback: true },
4876
+ "WITHDRAW_FAILED",
4877
+ "Yieldseeker wallet upgrade receipt could not be verified.",
4878
+ { txHash: hash, reason: "UPGRADE_RECEIPT_MISMATCH" },
4388
4879
  this.id
4389
4880
  );
4390
- const pendingKey = `owney.yieldseeker.upgrade:${state.walletAddress.toLowerCase()}:${before.agentWallet.toLowerCase()}`;
4391
- let prior = null;
4392
- let saved;
4881
+ this.assertSession(generation, chainId);
4882
+ const status = {
4883
+ ...before,
4884
+ implementation: YIELDSEEKER_V2_IMPLEMENTATION,
4885
+ version: "v2",
4886
+ upgradeAvailable: false
4887
+ };
4888
+ this.rememberWalletStatus(versionKey, status);
4889
+ return { txHash: hash, status };
4890
+ };
4891
+ let storage5;
4892
+ try {
4893
+ storage5 = typeof window === "undefined" ? void 0 : window.localStorage;
4894
+ } catch {
4895
+ storage5 = void 0;
4896
+ }
4897
+ const clearConfirmedUpgrade = () => {
4393
4898
  try {
4394
- saved = storage5.getItem(pendingKey);
4899
+ storage5?.removeItem(pendingKey2);
4395
4900
  } catch {
4396
- throw new OwneyError(
4397
- "SPONSOR_REQUEST_FAILED",
4398
- "Upgrade status cannot be read in this browser.",
4399
- { safeToFallback: true },
4400
- this.id
4401
- );
4402
4901
  }
4902
+ };
4903
+ if (!storage5 && (gasMode === "sponsored" || before.ownerPath === "smart"))
4904
+ throw new OwneyError(
4905
+ "SPONSOR_REQUEST_FAILED",
4906
+ "Upgrade status cannot be saved in this browser.",
4907
+ { safeToFallback: false },
4908
+ this.id
4909
+ );
4910
+ let prior = null;
4911
+ try {
4912
+ const saved = storage5?.getItem(pendingKey2);
4403
4913
  try {
4404
4914
  prior = JSON.parse(saved ?? "null");
4405
4915
  } catch {
4406
4916
  prior = { callsId: void 0 };
4407
4917
  }
4408
- if (prior) {
4918
+ } catch {
4919
+ if (gasMode === "paid" && before.ownerPath === "eoa") storage5 = void 0;
4920
+ else throw new OwneyError(
4921
+ "SPONSOR_REQUEST_FAILED",
4922
+ "Upgrade status cannot be read in this browser.",
4923
+ { safeToFallback: false },
4924
+ this.id
4925
+ );
4926
+ }
4927
+ if (prior) {
4928
+ let earlierHash = this.isTransactionHash(prior.txHash) ? prior.txHash : void 0;
4929
+ if (!earlierHash) {
4409
4930
  if (!prior.callsId)
4410
4931
  throw new OwneyError(
4411
4932
  "SPONSOR_REQUEST_FAILED",
@@ -4415,10 +4936,7 @@ var YieldseekerAgent = class {
4415
4936
  );
4416
4937
  let callsStatus;
4417
4938
  try {
4418
- callsStatus = await state.provider.request({
4419
- method: "wallet_getCallsStatus",
4420
- params: [prior.callsId]
4421
- });
4939
+ callsStatus = await state.provider.request({ method: "wallet_getCallsStatus", params: [prior.callsId] });
4422
4940
  } catch {
4423
4941
  throw new OwneyError(
4424
4942
  "SPONSOR_REQUEST_FAILED",
@@ -4427,8 +4945,15 @@ var YieldseekerAgent = class {
4427
4945
  this.id
4428
4946
  );
4429
4947
  }
4430
- if (callsStatus?.status === 400 || callsStatus?.status === 500) {
4431
- storage5.removeItem(pendingKey);
4948
+ if (callsStatus?.id !== void 0 && callsStatus.id !== prior.callsId || callsStatus?.chainId !== void 0 && Number(callsStatus.chainId) !== chainId)
4949
+ throw new OwneyError(
4950
+ "SPONSOR_REQUEST_FAILED",
4951
+ "The earlier upgrade returned a different operation status.",
4952
+ { safeToFallback: false, callsId: prior.callsId },
4953
+ this.id
4954
+ );
4955
+ if (Number(callsStatus?.status) === 400 || Number(callsStatus?.status) === 500) {
4956
+ storage5.removeItem(pendingKey2);
4432
4957
  throw new OwneyError(
4433
4958
  "SPONSOR_REQUEST_FAILED",
4434
4959
  "The earlier sponsored upgrade failed.",
@@ -4436,22 +4961,39 @@ var YieldseekerAgent = class {
4436
4961
  this.id
4437
4962
  );
4438
4963
  }
4439
- const earlierHash = callsStatus?.receipts?.[0]?.transactionHash;
4440
- if (earlierHash) {
4441
- await this.waitForReceipt(state, chainId, earlierHash);
4442
- const earlierStatus = await this.getWalletStatus(state, chainId, asset);
4443
- if (earlierStatus?.version === "v2" && earlierStatus.verified) {
4444
- storage5.removeItem(pendingKey);
4445
- return { txHash: earlierHash, status: earlierStatus };
4446
- }
4447
- }
4964
+ const hashes = [...new Set((callsStatus?.receipts ?? []).map((value) => value.transactionHash).filter(this.isTransactionHash))];
4965
+ earlierHash = hashes.length === 1 ? hashes[0] : void 0;
4966
+ }
4967
+ if (earlierHash) {
4968
+ const recovered = await confirmUpgrade(earlierHash, await this.waitForReceipt(state, chainId, earlierHash));
4969
+ clearConfirmedUpgrade();
4970
+ return recovered;
4971
+ }
4972
+ throw new OwneyError(
4973
+ "SPONSOR_REQUEST_FAILED",
4974
+ "An earlier upgrade is still pending.",
4975
+ { safeToFallback: false, callsId: prior.callsId },
4976
+ this.id
4977
+ );
4978
+ }
4979
+ if (!before.upgradeAvailable)
4980
+ throw new OwneyError(
4981
+ "WITHDRAW_FAILED",
4982
+ "This Yieldseeker wallet cannot be upgraded to the reviewed V2 implementation.",
4983
+ { version: before.version },
4984
+ this.id
4985
+ );
4986
+ const data = encodeFunctionData({ abi: YIELDSEEKER_UPGRADE_ABI, functionName: "upgradeToLatest" });
4987
+ let txHash;
4988
+ let receipt;
4989
+ if (gasMode === "sponsored") {
4990
+ if (before.ownerPath !== "smart")
4448
4991
  throw new OwneyError(
4449
4992
  "SPONSOR_REQUEST_FAILED",
4450
- "An earlier upgrade is still pending.",
4451
- { safeToFallback: false, callsId: prior.callsId },
4993
+ "EOA owners pay Base gas for the V1 upgrade.",
4994
+ { safeToFallback: true },
4452
4995
  this.id
4453
4996
  );
4454
- }
4455
4997
  const context = await this.findAgent(state, chainId, asset);
4456
4998
  if (!context) throw new OwneyError("NOT_CONNECTED", "Yieldseeker agent is unavailable.");
4457
4999
  try {
@@ -4461,14 +5003,15 @@ var YieldseekerAgent = class {
4461
5003
  apiKey: this.owneyApiKey,
4462
5004
  routingApiBaseUrl: this.routingApiBaseUrl,
4463
5005
  yieldseekerSignature: await this.auth.getToken(state, chainId),
5006
+ refreshYieldseekerSignature: (rejected) => this.auth.refreshToken(state, chainId, rejected),
4464
5007
  userId: context.user.userId,
4465
5008
  agentId: context.agent.agentId,
4466
5009
  agentWallet: before.agentWallet,
4467
5010
  asset: YIELDSEEKER_ASSET_METADATA[asset].address,
4468
5011
  callData: data,
4469
5012
  onPending: (pending) => {
4470
- const current = storage5.getItem(pendingKey);
4471
- storage5.setItem(pendingKey, JSON.stringify({
5013
+ const current = storage5.getItem(pendingKey2);
5014
+ storage5.setItem(pendingKey2, JSON.stringify({
4472
5015
  startedAt: Date.now(),
4473
5016
  ...current ? JSON.parse(current) : {},
4474
5017
  ...pending
@@ -4476,8 +5019,9 @@ var YieldseekerAgent = class {
4476
5019
  }
4477
5020
  });
4478
5021
  txHash = upgraded.txHash;
5022
+ receipt = upgraded.receipt;
4479
5023
  } catch (error) {
4480
- if (error?.code === 4001 || error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && (error.details?.safeToFallback === true || error.details?.executionFailed === true)) storage5.removeItem(pendingKey);
5024
+ if (error?.code === 4001 || error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && (error.details?.safeToFallback === true || error.details?.executionFailed === true)) storage5.removeItem(pendingKey2);
4481
5025
  throw error;
4482
5026
  }
4483
5027
  } else {
@@ -4487,20 +5031,13 @@ var YieldseekerAgent = class {
4487
5031
  data,
4488
5032
  value: "0",
4489
5033
  chainId
5034
+ }, void 0, (value) => {
5035
+ receipt = value;
4490
5036
  });
4491
5037
  }
4492
- const status = await this.getWalletStatus(state, chainId, asset);
4493
- if (!status?.verified || status.version !== "v2") {
4494
- throw new OwneyError(
4495
- "WITHDRAW_FAILED",
4496
- "Yieldseeker wallet upgrade was not confirmed as V2.",
4497
- { txHash, version: status?.version ?? "missing" },
4498
- this.id
4499
- );
4500
- }
4501
- if (gasMode === "sponsored" && typeof window !== "undefined")
4502
- window.localStorage.removeItem(`owney.yieldseeker.upgrade:${state.walletAddress.toLowerCase()}:${before.agentWallet.toLowerCase()}`);
4503
- return { txHash, status };
5038
+ const confirmed = await confirmUpgrade(txHash, receipt ?? await this.waitForReceipt(state, chainId, txHash));
5039
+ if (gasMode === "sponsored") clearConfirmedUpgrade();
5040
+ return confirmed;
4504
5041
  }
4505
5042
  async disconnect() {
4506
5043
  this.withdrawalVersionCache.clear();
@@ -4573,30 +5110,48 @@ var YieldseekerAgent = class {
4573
5110
  submitted(txHash);
4574
5111
  receipt = await this.waitForReceipt(state, chainId, txHash);
4575
5112
  } else {
4576
- txHash = await this.submitTransaction(state, chainId, {
4577
- from: getAddress4(state.walletAddress),
4578
- to: YIELDSEEKER_ASSET_METADATA[asset].address,
4579
- data: encodeFunctionData({
4580
- abi: erc20Abi2,
4581
- functionName: "transfer",
4582
- args: [getAddress4(context.wallet.walletAddress), BigInt(amount)]
4583
- }),
4584
- value: "0",
4585
- chainId
4586
- }, submitted, (value) => {
4587
- receipt = value;
4588
- });
4589
- }
4590
- if (this.readGeneration === generation) {
4591
- void this.refreshSnapshotAfterMovement(state, chainId, context, "deposit", movement);
4592
- }
4593
- const base6 = { agentId: this.id, chainId, token: asset, walletAddress: state.walletAddress, agentWallet: context.wallet.walletAddress, intendedAmount: amount, confirmedAmount: amount, txHash, transfer: "confirmed", registration: "not-applicable", delivery: "not-applicable" };
4594
- const evidence = receipt ? inspectFundingReceipt(receipt, base6, YIELDSEEKER_ASSET_METADATA[asset].address, "deposit") : base6;
4595
- notifyFunding(onProgress, evidence);
4596
- return { txHash, smartWallet: context.wallet.walletAddress, amount, evidence };
4597
- }
4598
- async withdraw(state, chainId, asset, amount, onProgress, gasMode = "auto") {
4599
- let ownerTransferAttempted = false;
5113
+ try {
5114
+ txHash = await this.submitTransaction(state, chainId, {
5115
+ from: getAddress4(state.walletAddress),
5116
+ to: YIELDSEEKER_ASSET_METADATA[asset].address,
5117
+ data: encodeFunctionData({
5118
+ abi: erc20Abi3,
5119
+ functionName: "transfer",
5120
+ args: [getAddress4(context.wallet.walletAddress), BigInt(amount)]
5121
+ }),
5122
+ value: "0",
5123
+ chainId
5124
+ }, submitted, (value) => {
5125
+ receipt = value;
5126
+ });
5127
+ } catch (error) {
5128
+ if (error instanceof OwneyError && error.details?.ownerTransferNotExecuted === true && error.details.reason === "TRANSACTION_REPLACED_WITH_DIFFERENT_CALL") {
5129
+ notifyFunding(onProgress, {
5130
+ agentId: this.id,
5131
+ chainId,
5132
+ token: asset,
5133
+ walletAddress: state.walletAddress,
5134
+ agentWallet: context.wallet.walletAddress,
5135
+ intendedAmount: amount,
5136
+ txHash: String(error.details.originalTransactionHash),
5137
+ transfer: "failed",
5138
+ registration: "not-applicable",
5139
+ delivery: "not-applicable"
5140
+ });
5141
+ }
5142
+ throw error;
5143
+ }
5144
+ }
5145
+ if (this.readGeneration === generation) {
5146
+ void this.refreshSnapshotAfterMovement(state, chainId, context, "deposit", movement);
5147
+ }
5148
+ const base6 = { agentId: this.id, chainId, token: asset, walletAddress: state.walletAddress, agentWallet: context.wallet.walletAddress, intendedAmount: amount, confirmedAmount: amount, txHash, transfer: "confirmed", registration: "not-applicable", delivery: "not-applicable" };
5149
+ const evidence = receipt ? inspectFundingReceipt(receipt, base6, YIELDSEEKER_ASSET_METADATA[asset].address, "deposit") : base6;
5150
+ notifyFunding(onProgress, evidence);
5151
+ return { txHash, smartWallet: context.wallet.walletAddress, amount, evidence };
5152
+ }
5153
+ async withdraw(state, chainId, asset, amount, onProgress, gasMode = "auto", onBeforeSubmission) {
5154
+ let ownerTransferAttempted = false;
4600
5155
  let evidence = {
4601
5156
  agentId: this.id,
4602
5157
  chainId,
@@ -4613,10 +5168,10 @@ var YieldseekerAgent = class {
4613
5168
  notifyFunding(onProgress, evidence);
4614
5169
  }, () => {
4615
5170
  ownerTransferAttempted = true;
4616
- }, gasMode);
5171
+ }, gasMode, onBeforeSubmission);
4617
5172
  } catch (error) {
4618
5173
  if (error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED") this.withdrawalVersionCache.clear();
4619
- if (!ownerTransferAttempted || error?.code === 4001 || error instanceof OwneyError && (error.code === "AGENT_TRANSACTION_REVERTED" || error.code === "SPONSOR_REQUEST_FAILED" && (error.details?.safeToFallback === true || error.details?.executionFailed === true)))
5174
+ if (!ownerTransferAttempted || error?.code === 4001 || error instanceof OwneyError && (error.code === "AGENT_TRANSACTION_REVERTED" || error.details?.ownerTransferNotExecuted === true || error.code === "SPONSOR_REQUEST_FAILED" && (error.details?.safeToFallback === true || error.details?.executionFailed === true)))
4620
5175
  notifyFunding(onProgress, {
4621
5176
  ...evidence,
4622
5177
  ...error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true ? { userOpHash: void 0, callsId: void 0 } : {},
@@ -4626,7 +5181,7 @@ var YieldseekerAgent = class {
4626
5181
  throw error;
4627
5182
  }
4628
5183
  }
4629
- async withdrawInternal(state, chainId, asset, amount, onProgress, onOwnerTransferAttempted, gasMode = "auto") {
5184
+ async withdrawInternal(state, chainId, asset, amount, onProgress, onOwnerTransferAttempted, gasMode = "auto", onBeforeSubmission) {
4630
5185
  this.assertChain(chainId);
4631
5186
  this.assertAsset(asset);
4632
5187
  if (amount !== void 0 && BigInt(amount) <= 0n) {
@@ -4671,37 +5226,140 @@ var YieldseekerAgent = class {
4671
5226
  let confirmedMovement = false;
4672
5227
  let settlement = null;
4673
5228
  try {
4674
- if (amount === void 0) this.advancePortfolioVersion(this.contextKey(state, chainId, asset));
4675
- const portfolio = await this.loadPortfolioContext(
4676
- state,
4677
- chainId,
4678
- context
5229
+ const maxSettlementBasis = amount === void 0 ? this.snapshotMovement(state, chainId, context, 0n) : null;
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)
4679
5251
  );
5252
+ const positionsRead = this.readWithdrawalPositions(state, chainId, context);
4680
5253
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
4681
- const readIdle = async () => {
4682
- const balance = await withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
4683
- address: metadata.address,
4684
- abi: erc20Abi2,
4685
- functionName: "balanceOf",
4686
- args: [getAddress4(context.wallet.walletAddress)]
5254
+ const agentWallet = getAddress4(context.wallet.walletAddress);
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
4687
5261
  }), chainId, "eth_call", this.id);
4688
5262
  this.assertSession(generation, chainId, asset);
4689
- return balance;
5263
+ if (!isAddress3(feeTracker) || /^0x0{40}$/i.test(feeTracker))
5264
+ throw this.invalidResponse("wallet fee tracker");
5265
+ const [idle2, fees] = await Promise.all([
5266
+ withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5267
+ address: metadata.address,
5268
+ abi: erc20Abi3,
5269
+ functionName: "balanceOf",
5270
+ args: [agentWallet],
5271
+ blockNumber
5272
+ }), chainId, "eth_call", this.id),
5273
+ withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5274
+ address: getAddress4(feeTracker),
5275
+ abi: YIELDSEEKER_FEE_TRACKER_ABI,
5276
+ functionName: "getFeesOwed",
5277
+ args: [agentWallet],
5278
+ blockNumber
5279
+ }), chainId, "eth_call", this.id)
5280
+ ]);
5281
+ this.assertSession(generation, chainId, asset);
5282
+ if (typeof idle2 !== "bigint" || idle2 < 0n || typeof fees !== "bigint" || fees < 0n)
5283
+ throw this.invalidResponse("wallet withdrawal balance");
5284
+ return { blockNumber, idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
4690
5285
  };
4691
- const idle = await readIdle();
4692
- let positions = portfolio.positions;
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;
5298
+ const idle = initialFunds.idle;
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()))];
4693
5301
  let positionsStatus = "empty";
4694
- const validatePositions = (values) => {
4695
- const seen = /* @__PURE__ */ new Set();
4696
- for (const value of values) {
4697
- const address = value?.yieldOption?.address;
4698
- if (typeof address !== "string" || !isAddress2(address) || seen.has(address.toLowerCase()) || typeof value.assetsRaw !== "string" || !/^\d+$/.test(value.assetsRaw) || typeof value.withdrawableAssetsRaw !== "string" || !/^\d+$/.test(value.withdrawableAssetsRaw))
4699
- throw this.invalidResponse("yield positions");
4700
- seen.add(address.toLowerCase());
4701
- }
4702
- return values;
5302
+ const validatePositions = (values) => this.validateWithdrawalPositions(values, chainId, asset);
5303
+ validatePositions(positions);
5304
+ const zeroValuePositions = /* @__PURE__ */ new WeakSet();
5305
+ const hasPositions = (values) => values.some((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n));
5306
+ const knownPositions = /* @__PURE__ */ new Map();
5307
+ const rememberPositions = (values) => {
5308
+ for (const position2 of values) knownPositions.set(position2.yieldOption.address.toLowerCase(), position2);
5309
+ };
5310
+ const verifyKnownPositions = async (blockNumber, values = [...knownPositions.values()], timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
5311
+ const verifiedPositions = [];
5312
+ if (timeoutMs <= 0) return { verified: false, positions: verifiedPositions };
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) => {
5316
+ const address = getAddress4(position2.yieldOption.address);
5317
+ try {
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);
5322
+ if (typeof underlying !== "string" || !isAddress3(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n) return null;
5323
+ const verified = { ...position2, assetsRaw: shares.toString(), withdrawableAssetsRaw: withdrawable.toString() };
5324
+ if (shares > 0n && withdrawable === 0n) {
5325
+ let positiveValue = false;
5326
+ const valuations = await Promise.all(["convertToAssets", "previewRedeem"].map(async (functionName) => {
5327
+ try {
5328
+ const value = await this.getReceiptClient().readContract({
5329
+ address,
5330
+ abi: erc4626Abi,
5331
+ functionName,
5332
+ args: [shares],
5333
+ blockNumber
5334
+ });
5335
+ if (typeof value !== "bigint" || value < 0n) return null;
5336
+ if (value > 0n && !positiveValue) {
5337
+ positiveValue = true;
5338
+ verifiedPositions.push(verified);
5339
+ }
5340
+ return value;
5341
+ } catch {
5342
+ return null;
5343
+ }
5344
+ }));
5345
+ if (positiveValue) return verified;
5346
+ if (blockNumber === void 0 || valuations.some((value) => value !== 0n)) return null;
5347
+ zeroValuePositions.add(verified);
5348
+ }
5349
+ verifiedPositions.push(verified);
5350
+ return verified;
5351
+ } catch {
5352
+ return null;
5353
+ }
5354
+ });
5355
+ const result = await boundedVerification(Promise.all(reads), timeoutMs);
5356
+ active = false;
5357
+ this.assertSession(generation, chainId, asset);
5358
+ return {
5359
+ verified: result !== void 0 && result.every((position2) => position2 !== null),
5360
+ positions: [...verifiedPositions]
5361
+ };
4703
5362
  };
4704
- const hasPositions = (values) => values.some((position2) => BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n);
4705
5363
  const refreshPositions = async () => {
4706
5364
  this.assertSession(generation, chainId, asset);
4707
5365
  const response = await this.walletRequest(
@@ -4714,22 +5372,41 @@ var YieldseekerAgent = class {
4714
5372
  return validatePositions(response.yieldPositions);
4715
5373
  };
4716
5374
  if (amount === void 0) {
4717
- validatePositions(positions);
5375
+ rememberPositions(funds.indexedPositions);
5376
+ rememberPositions(positions);
4718
5377
  positionsStatus = hasPositions(positions) ? "remaining" : "empty";
4719
5378
  }
4720
5379
  const deployed = positions.reduce(
4721
5380
  (total, position2) => total + BigInt(position2.withdrawableAssetsRaw),
4722
5381
  0n
4723
5382
  );
4724
- const totalAvailable = idle + deployed;
5383
+ const grossAvailable = idle + deployed;
5384
+ const totalAvailable = grossAvailable > initialFunds.fees ? grossAvailable - initialFunds.fees : 0n;
4725
5385
  const requested = amount === void 0 ? totalAvailable : BigInt(amount);
4726
5386
  if (requested === 0n) {
4727
5387
  this.invalidateFundingRead(state, chainId, asset);
4728
- if (amount === void 0 && positionsStatus === "remaining")
5388
+ const zeroPayoutCandidates = positions.filter((position2) => BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n);
5389
+ if (amount === void 0 && zeroPayoutCandidates.length) {
5390
+ const deadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
5391
+ const blockNumber = initialFunds.blockNumber ?? await boundedVerification(
5392
+ Promise.resolve().then(() => this.getReceiptClient().getBlockNumber({ cacheTime: 0 })).catch(() => void 0)
5393
+ );
5394
+ const proof = typeof blockNumber === "bigint" ? await verifyKnownPositions(blockNumber, zeroPayoutCandidates, deadline - Date.now()) : { verified: false, positions: [] };
5395
+ positions = proof.positions;
5396
+ positionsStatus = hasPositions(positions) ? "remaining" : proof.verified ? "empty" : "unavailable";
5397
+ if (positionsStatus !== "empty")
5398
+ throw new OwneyError(
5399
+ "WITHDRAW_FAILED",
5400
+ "Yieldseeker vault funds are not currently withdrawable.",
5401
+ { asset, positionsStatus, remainingPositions: positions.map((position2) => position2.yieldOption.address) },
5402
+ this.id
5403
+ );
5404
+ }
5405
+ if (grossAvailable > 0n && totalAvailable === 0n)
4729
5406
  throw new OwneyError(
4730
- "WITHDRAW_FAILED",
4731
- "Yieldseeker vault funds are not currently withdrawable.",
4732
- { asset, remainingPositions: positions.map((position2) => position2.yieldOption.address) },
5407
+ "WITHDRAW_INSUFFICIENT_BALANCE",
5408
+ "No owner-withdrawable Yieldseeker funds remain after reserved fees.",
5409
+ { asset, available: "0", reason: "YIELDSEEKER_FEES_ONLY", feesReserved: initialFunds.fees.toString() },
4733
5410
  this.id
4734
5411
  );
4735
5412
  throw new OwneyError(
@@ -4751,28 +5428,50 @@ var YieldseekerAgent = class {
4751
5428
  this.id
4752
5429
  );
4753
5430
  }
4754
- let remaining = requested > idle ? requested - idle : 0n;
5431
+ const requiredIdle = requested + initialFunds.fees;
5432
+ let remaining = requiredIdle > idle ? requiredIdle - idle : 0n;
4755
5433
  let grossUnwound = 0n;
5434
+ let latestUnwindBlock;
4756
5435
  const unwind = async (position2, assetsRaw) => {
4757
5436
  this.assertSession(generation, chainId, asset);
4758
- const response = await this.walletRequest(
4759
- state,
4760
- chainId,
4761
- this.agentPath(context, "withdraw-from-position"),
4762
- {
4763
- method: "POST",
4764
- body: { chainId, vaultAddress: position2.yieldOption.address, assetsRaw: assetsRaw.toString() }
4765
- }
4766
- );
4767
- if (!this.isTransactionHash(response?.transactionHash)) throw this.invalidResponse("position withdrawal");
4768
- await this.waitForReceipt(state, chainId, response.transactionHash);
5437
+ let unwindTransactionHash;
5438
+ try {
5439
+ const response = await this.walletRequest(
5440
+ state,
5441
+ chainId,
5442
+ this.agentPath(context, "withdraw-from-position"),
5443
+ {
5444
+ method: "POST",
5445
+ body: { chainId, vaultAddress: position2.yieldOption.address, assetsRaw: assetsRaw.toString() }
5446
+ }
5447
+ );
5448
+ if (!this.isTransactionHash(response?.transactionHash)) throw this.invalidResponse("position withdrawal");
5449
+ unwindTransactionHash = response.transactionHash;
5450
+ const unwindReceipt = await this.waitForReceipt(state, chainId, unwindTransactionHash);
5451
+ if (unwindReceipt) latestUnwindBlock = unwindReceipt.blockNumber;
5452
+ } catch (error) {
5453
+ this.invalidateFundingRead(state, chainId, asset);
5454
+ throw new OwneyError(
5455
+ error instanceof OwneyError ? error.code : "WITHDRAW_FAILED",
5456
+ "Could not confirm the Yieldseeker vault release. No owner transfer was submitted.",
5457
+ {
5458
+ ...error instanceof OwneyError ? error.details : {},
5459
+ withdrawalPhase: "unwind",
5460
+ reason: "YIELDSEEKER_UNWIND_UNRESOLVED",
5461
+ vaultAddress: position2.yieldOption.address,
5462
+ unwindTransactionHash,
5463
+ ownerTransferSubmitted: false
5464
+ },
5465
+ this.id
5466
+ );
5467
+ }
4769
5468
  confirmedMovement = true;
4770
5469
  this.assertSession(generation, chainId, asset);
4771
5470
  grossUnwound += assetsRaw;
4772
5471
  };
4773
5472
  if (amount === void 0) {
4774
5473
  for (let sweep = 0; sweep < YIELDSEEKER_MAX_WITHDRAW_SWEEPS && hasPositions(positions); sweep++) {
4775
- const before = new Map(positions.map((position2) => [position2.yieldOption.address.toLowerCase(), BigInt(position2.assetsRaw)]));
5474
+ const before = new Map(positions.map((position2) => [position2.yieldOption.address.toLowerCase(), BigInt(position2.withdrawableAssetsRaw)]));
4776
5475
  let unwound = false;
4777
5476
  for (const position2 of positions) {
4778
5477
  const available = BigInt(position2.withdrawableAssetsRaw);
@@ -4781,15 +5480,22 @@ var YieldseekerAgent = class {
4781
5480
  unwound = true;
4782
5481
  }
4783
5482
  if (!unwound) break;
4784
- try {
4785
- positions = await refreshPositions();
4786
- positionsStatus = hasPositions(positions) ? "remaining" : "empty";
4787
- } catch {
4788
- this.assertSession(generation, chainId, asset);
5483
+ const knownBeforeRefresh = new Set(knownPositions.keys());
5484
+ const [catalog, proof] = await Promise.all([
5485
+ boundedVerification(refreshPositions().catch(() => void 0)),
5486
+ verifyKnownPositions(latestUnwindBlock)
5487
+ ]);
5488
+ if (catalog) rememberPositions(catalog);
5489
+ this.assertSession(generation, chainId, asset);
5490
+ if (proof.verified) {
5491
+ positions = [...proof.positions, ...(catalog ?? []).filter((position2) => !knownBeforeRefresh.has(position2.yieldOption.address.toLowerCase()))];
5492
+ } else if (catalog) positions = catalog;
5493
+ else {
4789
5494
  positionsStatus = "unavailable";
4790
5495
  break;
4791
5496
  }
4792
- if (positions.some((position2) => BigInt(position2.assetsRaw) > 0n && before.has(position2.yieldOption.address.toLowerCase()) && BigInt(position2.assetsRaw) >= before.get(position2.yieldOption.address.toLowerCase()))) break;
5497
+ positionsStatus = hasPositions(positions) ? "remaining" : "empty";
5498
+ if (positions.some((position2) => BigInt(position2.withdrawableAssetsRaw) > 0n && before.has(position2.yieldOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) >= before.get(position2.yieldOption.address.toLowerCase()))) break;
4793
5499
  }
4794
5500
  } else {
4795
5501
  for (const position2 of positions) {
@@ -4807,8 +5513,9 @@ var YieldseekerAgent = class {
4807
5513
  remaining: remaining.toString()
4808
5514
  });
4809
5515
  }
4810
- const availableForOwner = confirmedMovement ? await readIdle() : idle;
4811
- const feeRate = portfolio.snapshot.feeRateBps;
5516
+ const ownerFunds = confirmedMovement ? await readOwnerFunds(latestUnwindBlock) : initialFunds;
5517
+ const availableForOwner = ownerFunds.available;
5518
+ const feeRate = feeRateBps ?? 0;
4812
5519
  const maxFee = confirmedMovement && Number.isInteger(feeRate) && feeRate >= 0 && feeRate <= 1e3 ? (grossUnwound * BigInt(feeRate) + 9999n) / 10000n : 0n;
4813
5520
  const shortfall = availableForOwner < requested ? requested - availableForOwner : 0n;
4814
5521
  if (availableForOwner === 0n || amount !== void 0 && shortfall > maxFee) {
@@ -4828,7 +5535,11 @@ var YieldseekerAgent = class {
4828
5535
  );
4829
5536
  }
4830
5537
  const payout = amount === void 0 || availableForOwner < requested ? availableForOwner : requested;
4831
- const plannedMovement = this.snapshotMovement(state, chainId, context, -payout);
5538
+ const plannedMovement = amount === void 0 && maxSettlementBasis ? {
5539
+ ...maxSettlementBasis,
5540
+ delta: maxSettlementBasis.delta - payout,
5541
+ expectedNetDeposits: maxSettlementBasis.expectedNetDeposits - payout
5542
+ } : this.snapshotMovement(state, chainId, context, -payout);
4832
5543
  const account = getAddress4(state.walletAddress);
4833
5544
  let receipt;
4834
5545
  const base6 = { agentId: this.id, chainId, token: asset, walletAddress: state.walletAddress, agentWallet: context.wallet.walletAddress, intendedAmount: amount === void 0 ? "MAX" : payout.toString(), transfer: "pending", registration: "not-applicable", delivery: "pending" };
@@ -4841,7 +5552,6 @@ var YieldseekerAgent = class {
4841
5552
  functionName: "withdrawAssetToUser",
4842
5553
  args: [account, metadata.address, payout]
4843
5554
  });
4844
- onOwnerTransferAttempted?.();
4845
5555
  let txHash;
4846
5556
  if (sponsor) {
4847
5557
  const submitted = await submitYieldseekerSponsoredWithdrawal({
@@ -4850,16 +5560,22 @@ var YieldseekerAgent = class {
4850
5560
  apiKey: this.owneyApiKey,
4851
5561
  routingApiBaseUrl: this.routingApiBaseUrl,
4852
5562
  yieldseekerSignature: await this.auth.getToken(state, chainId),
5563
+ refreshYieldseekerSignature: (rejected) => this.auth.refreshToken(state, chainId, rejected),
4853
5564
  userId: context.user.userId,
4854
5565
  agentId: context.agent.agentId,
4855
5566
  agentWallet: getAddress4(context.wallet.walletAddress),
4856
5567
  asset: metadata.address,
4857
5568
  callData: data,
4858
- onPending: (pending) => notifyFunding(onProgress, { ...base6, ...pending })
5569
+ onPending: (pending) => notifyFunding(onProgress, { ...base6, ...pending }),
5570
+ onBeforeSubmission: async (pending) => {
5571
+ await onBeforeSubmission?.({ ...base6, ...pending });
5572
+ onOwnerTransferAttempted?.();
5573
+ }
4859
5574
  });
4860
5575
  txHash = submitted.txHash;
4861
5576
  receipt = submitted.receipt;
4862
5577
  } else {
5578
+ onOwnerTransferAttempted?.();
4863
5579
  txHash = await this.submitTransaction(state, chainId, {
4864
5580
  from: account,
4865
5581
  to: getAddress4(context.wallet.walletAddress),
@@ -4874,14 +5590,26 @@ var YieldseekerAgent = class {
4874
5590
  settlement = plannedMovement;
4875
5591
  const evidence = receipt ? inspectFundingReceipt(receipt, { ...base6, txHash }, metadata.address, "withdraw") : { ...base6, txHash, confirmedAmount: payout.toString(), transfer: "confirmed", delivery: "confirmed" };
4876
5592
  notifyFunding(onProgress, evidence);
5593
+ let completion = maxLiquidity && !maxLiquidity.complete ? "unverified" : void 0;
4877
5594
  if (amount === void 0) {
4878
- try {
4879
- positions = await refreshPositions();
4880
- positionsStatus = hasPositions(positions) ? "remaining" : "empty";
4881
- } catch {
4882
- positionsStatus = "unavailable";
4883
- }
4884
- if (positionsStatus !== "empty") {
5595
+ const verificationDeadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
5596
+ const knownAtPayout = new Set(knownPositions.keys());
5597
+ const [catalogVerified, knownProof] = await Promise.all([
5598
+ boundedVerification(refreshPositions().then((values) => {
5599
+ rememberPositions(values);
5600
+ return true;
5601
+ }).catch(() => false)),
5602
+ verifyKnownPositions(receipt?.blockNumber)
5603
+ ]);
5604
+ const newPositions = [...knownPositions].filter(([address]) => !knownAtPayout.has(address)).map(([, position2]) => position2);
5605
+ const newProof = newPositions.length ? await verifyKnownPositions(receipt?.blockNumber, newPositions, verificationDeadline - Date.now()) : { verified: true, positions: [] };
5606
+ const proof = {
5607
+ verified: catalogVerified === true && knownProof.verified && newProof.verified,
5608
+ positions: [...knownProof.positions, ...newProof.positions]
5609
+ };
5610
+ positions = proof.positions;
5611
+ positionsStatus = hasPositions(positions) ? "remaining" : proof.verified ? "empty" : "unavailable";
5612
+ if (positionsStatus === "remaining") {
4885
5613
  const result = { txHash, type: "partial", amount: evidence.confirmedAmount ?? payout.toString(), evidence };
4886
5614
  throw new OwneyError(
4887
5615
  "WITHDRAW_PARTIAL_FAILURE",
@@ -4890,7 +5618,7 @@ var YieldseekerAgent = class {
4890
5618
  reason: "YIELDSEEKER_MAX_INCOMPLETE",
4891
5619
  asset,
4892
5620
  positionsStatus,
4893
- remainingPositions: positionsStatus === "remaining" ? positions.map((position2) => ({
5621
+ remainingPositions: positionsStatus === "remaining" ? positions.filter((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n)).map((position2) => ({
4894
5622
  vaultAddress: position2.yieldOption.address,
4895
5623
  sharesRaw: position2.assetsRaw,
4896
5624
  withdrawableAmountRaw: position2.withdrawableAssetsRaw
@@ -4900,6 +5628,7 @@ var YieldseekerAgent = class {
4900
5628
  this.id
4901
5629
  );
4902
5630
  }
5631
+ if (!proof.verified) completion = "unverified";
4903
5632
  }
4904
5633
  if (evidence.transfer === "failed" && evidence.confirmedAmount === "0")
4905
5634
  throw new OwneyError(
@@ -4908,7 +5637,17 @@ var YieldseekerAgent = class {
4908
5637
  { asset, available: "0", txHash },
4909
5638
  this.id
4910
5639
  );
4911
- return { txHash, type: amount === void 0 ? "full" : "partial", amount: evidence.confirmedAmount ?? payout.toString(), evidence };
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;
5643
+ return {
5644
+ txHash,
5645
+ type: amount === void 0 && !completion ? "full" : "partial",
5646
+ ...completion ? { completion } : {},
5647
+ ...feeAdjustment ? { feeAdjustment } : {},
5648
+ amount: evidence.confirmedAmount ?? payout.toString(),
5649
+ evidence
5650
+ };
4912
5651
  } finally {
4913
5652
  if (confirmedMovement && this.readGeneration === generation) {
4914
5653
  void this.refreshSnapshotAfterMovement(
@@ -4921,6 +5660,236 @@ var YieldseekerAgent = class {
4921
5660
  }
4922
5661
  }
4923
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
+ }
4924
5893
  async getBalances(state, chainId, asset) {
4925
5894
  this.assertChain(chainId);
4926
5895
  if (asset !== void 0) this.assertAsset(asset);
@@ -4955,6 +5924,23 @@ var YieldseekerAgent = class {
4955
5924
  if (!context) throw new OwneyError("NOT_CONNECTED", "Reconnect the agent before recovering funding.");
4956
5925
  return context.wallet.walletAddress;
4957
5926
  }
5927
+ async hasExistingDepositFunds(state, chainId, asset) {
5928
+ this.assertChain(chainId);
5929
+ this.assertAsset(asset);
5930
+ const generation = this.readGeneration;
5931
+ const context = await this.findAgent(state, chainId, asset);
5932
+ this.assertSession(generation, chainId, asset);
5933
+ if (!context) return void 0;
5934
+ const idle = await withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
5935
+ address: YIELDSEEKER_ASSET_METADATA[asset].address,
5936
+ abi: erc20Abi3,
5937
+ functionName: "balanceOf",
5938
+ args: [getAddress4(context.wallet.walletAddress)]
5939
+ }), chainId, "eth_call", this.id);
5940
+ this.assertSession(generation, chainId, asset);
5941
+ if (typeof idle !== "bigint" || idle < 0n) throw this.invalidResponse("wallet deposit balance");
5942
+ return idle > 0n ? true : void 0;
5943
+ }
4958
5944
  async getEarnings(state, chainId) {
4959
5945
  this.assertChain(chainId);
4960
5946
  return mapYieldseekerEarnings(await this.loadPortfolio(state, chainId, {}));
@@ -4985,7 +5971,7 @@ var YieldseekerAgent = class {
4985
5971
  );
4986
5972
  const vaultAddresses = new Set(
4987
5973
  catalog.flat().filter(
4988
- (yieldOption) => yieldOption.chainId === chainId && isAddress2(yieldOption.address)
5974
+ (yieldOption) => yieldOption.chainId === chainId && isAddress3(yieldOption.address)
4989
5975
  ).map((yieldOption) => yieldOption.address.toLowerCase())
4990
5976
  );
4991
5977
  return mapYieldseekerHistory(contexts, {
@@ -5112,7 +6098,7 @@ var YieldseekerAgent = class {
5112
6098
  `/users/${user.userId}/agents/${agent.agentId}/wallet`
5113
6099
  ).then((walletResponse) => {
5114
6100
  this.assertSession(generation, chainId, asset);
5115
- if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
6101
+ if (!walletResponse?.agentWallet || !isAddress3(walletResponse.agentWallet.walletAddress)) {
5116
6102
  throw this.invalidResponse("agent wallet");
5117
6103
  }
5118
6104
  const context = {
@@ -5525,7 +6511,7 @@ var YieldseekerAgent = class {
5525
6511
  this.agentPath(context, "deploy"),
5526
6512
  { method: "POST", body: {} }
5527
6513
  );
5528
- if (!deployed?.agentWallet || !isAddress2(deployed.agentWallet.walletAddress) || deployed.agentWallet.walletAddress.toLowerCase() !== walletAddress) {
6514
+ if (!deployed?.agentWallet || !isAddress3(deployed.agentWallet.walletAddress) || deployed.agentWallet.walletAddress.toLowerCase() !== walletAddress) {
5529
6515
  throw this.invalidResponse("agent deployment", {
5530
6516
  reason: "Deploy did not return the expected Agent Wallet."
5531
6517
  });
@@ -5656,25 +6642,79 @@ var YieldseekerAgent = class {
5656
6642
  value: BigInt(transaction.value)
5657
6643
  });
5658
6644
  onSubmitted?.(hash);
6645
+ let replacement;
5659
6646
  const receipt = await withPaidRpcDiagnostics(
5660
6647
  () => this.getReceiptClient().waitForTransactionReceipt({
5661
6648
  hash,
5662
- confirmations: 1
6649
+ confirmations: 1,
6650
+ onReplaced: (value) => {
6651
+ replacement = value;
6652
+ }
5663
6653
  }),
5664
6654
  8453,
5665
6655
  "eth_getTransactionReceipt",
5666
6656
  this.id
5667
6657
  );
6658
+ const actualHash = receipt.transactionHash;
6659
+ if (!this.isTransactionHash(actualHash))
6660
+ throw new OwneyError(
6661
+ "WITHDRAW_FAILED",
6662
+ "The confirmed transaction hash could not be verified.",
6663
+ { originalTransactionHash: hash, ownerTransferNotExecuted: false },
6664
+ this.id
6665
+ );
6666
+ if (actualHash.toLowerCase() !== hash.toLowerCase()) {
6667
+ if (!replacement || replacement.transaction.hash.toLowerCase() !== actualHash.toLowerCase()) {
6668
+ try {
6669
+ const [original2, actual2] = await Promise.all([
6670
+ this.getReceiptClient().getTransaction({ hash }),
6671
+ this.getReceiptClient().getTransaction({ hash: actualHash })
6672
+ ]);
6673
+ replacement = { replacedTransaction: original2, transaction: actual2 };
6674
+ } catch {
6675
+ replacement = void 0;
6676
+ }
6677
+ }
6678
+ const original = replacement?.replacedTransaction;
6679
+ const actual = replacement?.transaction;
6680
+ const sameNonce = !!original && !!actual && original.hash.toLowerCase() === hash.toLowerCase() && actual.hash.toLowerCase() === actualHash.toLowerCase() && original.from.toLowerCase() === account.toLowerCase() && actual.from.toLowerCase() === account.toLowerCase() && original.nonce === actual.nonce && (actual.chainId === void 0 || actual.chainId === chainId) && receipt.blockNumber !== null && receipt.blockNumber !== void 0;
6681
+ if (!sameNonce)
6682
+ throw new OwneyError(
6683
+ "WITHDRAW_FAILED",
6684
+ "The replacement transaction could not be verified.",
6685
+ {
6686
+ originalTransactionHash: hash,
6687
+ replacementTransactionHash: actualHash,
6688
+ reason: "TRANSACTION_REPLACEMENT_UNRESOLVED",
6689
+ ownerTransferNotExecuted: false
6690
+ },
6691
+ this.id
6692
+ );
6693
+ const sameCall = actual.to?.toLowerCase() === transaction.to.toLowerCase() && actual.input?.toLowerCase() === transaction.data.toLowerCase() && actual.value === BigInt(transaction.value);
6694
+ if (!sameCall)
6695
+ throw new OwneyError(
6696
+ "WITHDRAW_FAILED",
6697
+ "The transaction was replaced without executing this withdrawal.",
6698
+ {
6699
+ originalTransactionHash: hash,
6700
+ replacementTransactionHash: actualHash,
6701
+ reason: "TRANSACTION_REPLACED_WITH_DIFFERENT_CALL",
6702
+ ownerTransferNotExecuted: true
6703
+ },
6704
+ this.id
6705
+ );
6706
+ onSubmitted?.(actualHash);
6707
+ }
5668
6708
  if (receipt.status !== "success") {
5669
6709
  throw new OwneyError(
5670
6710
  "AGENT_TRANSACTION_REVERTED",
5671
- `Yieldseeker transaction reverted (${hash}).`,
5672
- { transactionHash: hash },
6711
+ `Yieldseeker transaction reverted (${actualHash}).`,
6712
+ { transactionHash: actualHash },
5673
6713
  this.id
5674
6714
  );
5675
6715
  }
5676
6716
  onReceipt?.(receipt);
5677
- return hash;
6717
+ return actualHash;
5678
6718
  }
5679
6719
  async waitForReceipt(state, chainId, transactionHash) {
5680
6720
  if (this.unwindReceiptWaiter) {
@@ -5701,12 +6741,12 @@ var YieldseekerAgent = class {
5701
6741
  return receipt;
5702
6742
  }
5703
6743
  assertTransaction(transaction, state, chainId) {
5704
- if (!transaction || typeof transaction.from !== "string" || !isAddress2(transaction.from) || typeof transaction.to !== "string" || !isAddress2(transaction.to) || typeof transaction.data !== "string" || !/^0x[a-fA-F0-9]*$/.test(transaction.data) || typeof transaction.value !== "string" || !/^[0-9]+$/.test(transaction.value) || transaction.chainId !== chainId || getAddress4(transaction.from) !== getAddress4(state.walletAddress)) {
6744
+ if (!transaction || typeof transaction.from !== "string" || !isAddress3(transaction.from) || typeof transaction.to !== "string" || !isAddress3(transaction.to) || typeof transaction.data !== "string" || !/^0x[a-fA-F0-9]*$/.test(transaction.data) || typeof transaction.value !== "string" || !/^[0-9]+$/.test(transaction.value) || transaction.chainId !== chainId || getAddress4(transaction.from) !== getAddress4(state.walletAddress)) {
5705
6745
  throw this.invalidResponse("transaction");
5706
6746
  }
5707
6747
  }
5708
6748
  assertAgent(agent) {
5709
- if (typeof agent.agentId !== "string" || typeof agent.assetAddress !== "string" || !isAddress2(agent.assetAddress) || agent.chainId !== 8453) {
6749
+ if (typeof agent.agentId !== "string" || typeof agent.assetAddress !== "string" || !isAddress3(agent.assetAddress) || agent.chainId !== 8453) {
5710
6750
  throw this.invalidResponse("agent");
5711
6751
  }
5712
6752
  }
@@ -6047,6 +7087,7 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
6047
7087
 
6048
7088
  // src/client.ts
6049
7089
  import {
7090
+ parseUnits as parseUnits2,
6050
7091
  createPublicClient as createPublicClient4,
6051
7092
  createWalletClient as createWalletClient4,
6052
7093
  custom as custom4
@@ -6154,6 +7195,18 @@ var planOf = (transfers) => JSON.stringify(
6154
7195
  amount: BigInt(t.amount).toString()
6155
7196
  }))
6156
7197
  );
7198
+ var pendingKey = (i) => `owney.token-batch.v1:${keccak256(toBytes(i.apiKey))}:${i.baseUrl ?? "default"}:${i.chainId}:${i.owner.toLowerCase()}:${i.token.toLowerCase()}`;
7199
+ function pendingTokenBatch(i) {
7200
+ try {
7201
+ const saved = read(pendingKey(i));
7202
+ if (!saved) return;
7203
+ const body = JSON.parse(saved);
7204
+ if (!body.serializedTransaction || body.chainId !== i.chainId || !isAddressEqual2(body.from, i.owner) || !isAddressEqual2(body.token, i.token) || !body.transfers?.length) return;
7205
+ return { txHash: keccak256(body.serializedTransaction), transfers: body.transfers.map(({ to, amount }) => ({ to, amount })) };
7206
+ } catch {
7207
+ return;
7208
+ }
7209
+ }
6157
7210
  function read(key2) {
6158
7211
  return typeof window === "undefined" ? memory.get(key2) : window.localStorage.getItem(key2);
6159
7212
  }
@@ -6170,11 +7223,11 @@ function clear(key2) {
6170
7223
  else window.localStorage.removeItem(key2);
6171
7224
  }
6172
7225
  function sponsorTokenBatch(i) {
6173
- const key2 = `owney.token-batch.v1:${keccak256(toBytes(i.apiKey))}:${i.baseUrl ?? "default"}:${i.chainId}:${i.owner.toLowerCase()}:${i.token.toLowerCase()}`;
7226
+ const key2 = pendingKey(i);
6174
7227
  const plan = planOf(i.transfers);
6175
7228
  const active = inflight.get(key2);
6176
7229
  if (active) {
6177
- if (active.plan !== plan)
7230
+ if (active.plan !== plan || i.resumeTxHash && pendingTokenBatch(i)?.txHash !== i.resumeTxHash)
6178
7231
  return Promise.reject(
6179
7232
  new Error(
6180
7233
  "A token deposit is already in progress. Wait for its result before depositing again."
@@ -6249,6 +7302,8 @@ async function execute(i, key2, plan) {
6249
7302
  }
6250
7303
  };
6251
7304
  const saved = read(key2);
7305
+ if (i.resumeTxHash && pendingTokenBatch(i)?.txHash !== i.resumeTxHash)
7306
+ throw new OwneyError("DEPOSIT_IN_PROGRESS", "The saved deposit changed. Check its status before depositing again.");
6252
7307
  if (saved) {
6253
7308
  const previous = JSON.parse(saved);
6254
7309
  if (previous.chainId !== i.chainId || !isAddressEqual2(previous.from, i.owner) || !isAddressEqual2(previous.token, i.token) || planOf(previous.transfers) !== plan)
@@ -6360,7 +7415,8 @@ function makeSponsoredTokenCallback(deps) {
6360
7415
  pub,
6361
7416
  wallet,
6362
7417
  onApproved: deps.onApproved,
6363
- onSubmitted: (hash) => deps.onSubmitted?.(hash, transfers)
7418
+ onSubmitted: (hash) => deps.onSubmitted?.(hash, transfers),
7419
+ resumeTxHash: deps.resumeTxHash
6364
7420
  });
6365
7421
  };
6366
7422
  const callback = makeVerificationAwareDepositCallback(
@@ -6446,7 +7502,7 @@ async function runAgentDepositBatch(chainId, legs, transfer) {
6446
7502
  }
6447
7503
 
6448
7504
  // src/lib/sponsored-calls-deposit.ts
6449
- import { encodeFunctionData as encodeFunctionData2, erc20Abi as erc20Abi3, toHex as toHex2 } from "viem";
7505
+ import { encodeFunctionData as encodeFunctionData2, erc20Abi as erc20Abi4, toHex as toHex2 } from "viem";
6450
7506
  var DEFAULT_POLL_INTERVAL_MS = 1500;
6451
7507
  var DEFAULT_MAX_POLLS = 30;
6452
7508
  async function paymasterSupported(provider, owner, chainId) {
@@ -6492,7 +7548,7 @@ function makeSponsoredCallsCallback(deps) {
6492
7548
  to: token,
6493
7549
  value: "0x0",
6494
7550
  data: encodeFunctionData2({
6495
- abi: erc20Abi3,
7551
+ abi: erc20Abi4,
6496
7552
  functionName: "transfer",
6497
7553
  args: [transfer.to, BigInt(transfer.amount)]
6498
7554
  })
@@ -6524,50 +7580,73 @@ function makeSponsoredCallsCallback(deps) {
6524
7580
  url.searchParams.append("owneyIntent", intent);
6525
7581
  paymasterUrl = url.toString();
6526
7582
  }
6527
- const sendResult = await deps.provider.request({
6528
- method: "wallet_sendCalls",
6529
- params: [
6530
- {
6531
- version: "2.0.0",
6532
- from: deps.ownerAddress,
6533
- chainId: toHex2(chainId),
6534
- atomicRequired: transfers.length > 1,
6535
- calls,
6536
- capabilities: {
6537
- paymasterService: { url: paymasterUrl }
7583
+ const requestedId = toHex2(globalThis.crypto.getRandomValues(new Uint8Array(32)));
7584
+ const progress = (id, transfer) => {
7585
+ try {
7586
+ deps.onCallsProgress?.(id, transfers, transfer);
7587
+ } catch {
7588
+ }
7589
+ };
7590
+ progress(requestedId, "pending");
7591
+ let sendResult;
7592
+ try {
7593
+ sendResult = await deps.provider.request({
7594
+ method: "wallet_sendCalls",
7595
+ params: [
7596
+ {
7597
+ version: "2.0.0",
7598
+ id: requestedId,
7599
+ from: deps.ownerAddress,
7600
+ chainId: toHex2(chainId),
7601
+ atomicRequired: transfers.length > 1,
7602
+ calls,
7603
+ capabilities: {
7604
+ paymasterService: { url: paymasterUrl }
7605
+ }
6538
7606
  }
6539
- }
6540
- ]
6541
- });
6542
- deps.onApproved?.();
7607
+ ]
7608
+ });
7609
+ } catch (error) {
7610
+ if (error?.code === 4001 || error?.code === "ACTION_REJECTED")
7611
+ progress(requestedId, "failed");
7612
+ throw error;
7613
+ }
7614
+ try {
7615
+ deps.onApproved?.();
7616
+ } catch {
7617
+ }
6543
7618
  const callsId = typeof sendResult === "string" ? sendResult : sendResult?.id;
6544
7619
  if (!callsId) {
6545
7620
  throw new OwneyError(
6546
7621
  "SPONSORED_CALLS_NO_ID",
6547
7622
  "Wallet accepted wallet_sendCalls but returned no calls id.",
6548
- { chainId }
7623
+ { chainId, callsId: requestedId }
6549
7624
  );
6550
7625
  }
7626
+ progress(callsId, "pending");
6551
7627
  for (let attempt = 0; attempt < maxPolls; attempt += 1) {
6552
7628
  const status = await deps.provider.request({
6553
7629
  method: "wallet_getCallsStatus",
6554
7630
  params: [callsId]
6555
7631
  });
7632
+ if (status?.id && status.id !== callsId || status?.chainId && Number(status.chainId) !== chainId)
7633
+ throw new OwneyError("SPONSORED_CALLS_NO_RECEIPT", "Wallet returned status for a different deposit batch.", { chainId, callsId });
6556
7634
  const txHash = status?.receipts?.[0]?.transactionHash;
6557
- if (txHash && !status?.receipts?.some((receipt) => receipt.transactionHash !== txHash)) {
7635
+ if (txHash && /^0x[\da-f]{64}$/i.test(txHash) && !status?.receipts?.some((receipt) => receipt.transactionHash !== txHash)) {
6558
7636
  try {
6559
7637
  deps.onSubmitted?.(txHash, transfers);
6560
7638
  } catch {
6561
7639
  }
6562
7640
  }
6563
- if (status?.receipts?.some((receipt) => receipt.status === "0x0") || typeof status?.status === "number" && status.status >= 400) {
7641
+ if (status?.status === 400 || status?.status === 500) {
7642
+ progress(callsId, "failed");
6564
7643
  throw new OwneyError(
6565
7644
  "SPONSOR_REQUEST_FAILED",
6566
7645
  "The sponsored deposit did not complete successfully.",
6567
7646
  { chainId, callsId, safeToFallback: false }
6568
7647
  );
6569
7648
  }
6570
- if (txHash && (transfers.length === 1 || status?.status === 200 || status?.status === "CONFIRMED")) {
7649
+ if (txHash && /^0x[\da-f]{64}$/i.test(txHash) && (transfers.length === 1 || status?.status === 200 || status?.status === "CONFIRMED")) {
6571
7650
  if (status?.receipts?.some(
6572
7651
  (receipt) => receipt.transactionHash !== txHash
6573
7652
  ))
@@ -6783,8 +7862,8 @@ var OwneySDK = class {
6783
7862
  return createPaidRpcClient(VIEM_CHAIN2[chainId], this.rpcUrls);
6784
7863
  }
6785
7864
  /** Builds the default USDC batch callback for the connected wallet. */
6786
- getDefaultSponsoredCallback(onApproved, onSubmitted) {
6787
- if (!onApproved && !onSubmitted && this.cachedSponsoredCallback)
7865
+ getDefaultSponsoredCallback(onApproved, onSubmitted, resumeTxHash) {
7866
+ if (!onApproved && !onSubmitted && !resumeTxHash && this.cachedSponsoredCallback)
6788
7867
  return this.cachedSponsoredCallback;
6789
7868
  const provider = this.requireConnectedProvider();
6790
7869
  const owner = this.state.walletAddress;
@@ -6794,6 +7873,7 @@ var OwneySDK = class {
6794
7873
  ownerAddress: owner,
6795
7874
  onApproved,
6796
7875
  onSubmitted,
7876
+ resumeTxHash,
6797
7877
  tokenAddressByChain: SPONSORED_USDC_BY_CHAIN,
6798
7878
  // Casts work around viem's chain-narrowed Client vs the generic
6799
7879
  // PublicClient/WalletClient param types — structurally identical at
@@ -6809,13 +7889,13 @@ var OwneySDK = class {
6809
7889
  transport: custom4(provider)
6810
7890
  })
6811
7891
  });
6812
- if (!onApproved && !onSubmitted) this.cachedSponsoredCallback = callback;
7892
+ if (!onApproved && !onSubmitted && !resumeTxHash) this.cachedSponsoredCallback = callback;
6813
7893
  return callback;
6814
7894
  }
6815
7895
  /** Builds the wallet-native sponsored calls callback for compatible paymasters. */
6816
- getDefaultSponsoredCallsCallback(asset, onApproved, onSubmitted) {
7896
+ getDefaultSponsoredCallsCallback(asset, onApproved, onSubmitted, onCallsProgress) {
6817
7897
  const cached = this.cachedSponsoredCallsCallbacks.get(asset);
6818
- if (!onApproved && !onSubmitted && cached) return cached;
7898
+ if (!onApproved && !onSubmitted && !onCallsProgress && cached) return cached;
6819
7899
  const provider = this.requireConnectedProvider();
6820
7900
  const callback = makeSponsoredCallsCallback({
6821
7901
  apiKey: this.apiKey,
@@ -6825,9 +7905,10 @@ var OwneySDK = class {
6825
7905
  paymasterServiceUrl: this.paymasterServiceUrl,
6826
7906
  onApproved,
6827
7907
  onSubmitted,
7908
+ onCallsProgress,
6828
7909
  tokenAddressByChain: sponsoredTokensFor(asset)
6829
7910
  });
6830
- if (!onApproved && !onSubmitted) this.cachedSponsoredCallsCallbacks.set(asset, callback);
7911
+ if (!onApproved && !onSubmitted && !onCallsProgress) this.cachedSponsoredCallsCallbacks.set(asset, callback);
6831
7912
  return callback;
6832
7913
  }
6833
7914
  /**
@@ -6836,8 +7917,8 @@ var OwneySDK = class {
6836
7917
  * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
6837
7918
  * single-use batch authorization instead of an EIP-3009 authorization.
6838
7919
  */
6839
- getDefaultWethSponsoredCallback(onApproved, onSubmitted) {
6840
- if (!onApproved && !onSubmitted && this.cachedWethSponsoredCallback)
7920
+ getDefaultWethSponsoredCallback(onApproved, onSubmitted, resumeTxHash) {
7921
+ if (!onApproved && !onSubmitted && !resumeTxHash && this.cachedWethSponsoredCallback)
6841
7922
  return this.cachedWethSponsoredCallback;
6842
7923
  const provider = this.requireConnectedProvider();
6843
7924
  const owner = this.state.walletAddress;
@@ -6847,6 +7928,7 @@ var OwneySDK = class {
6847
7928
  ownerAddress: owner,
6848
7929
  onApproved,
6849
7930
  onSubmitted,
7931
+ resumeTxHash,
6850
7932
  tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
6851
7933
  // Casts work around viem's chain-narrowed Client vs the generic
6852
7934
  // PublicClient/WalletClient param types — structurally identical at
@@ -6862,7 +7944,7 @@ var OwneySDK = class {
6862
7944
  transport: custom4(provider)
6863
7945
  })
6864
7946
  });
6865
- if (!onApproved && !onSubmitted) this.cachedWethSponsoredCallback = callback;
7947
+ if (!onApproved && !onSubmitted && !resumeTxHash) this.cachedWethSponsoredCallback = callback;
6866
7948
  return callback;
6867
7949
  }
6868
7950
  getAgent(agentId) {
@@ -7171,6 +8253,7 @@ var OwneySDK = class {
7171
8253
  async deposit(options) {
7172
8254
  const { amount, asset, depositCallback, agentId, onApproved } = options;
7173
8255
  const prepared = /* @__PURE__ */ new Map();
8256
+ const paidFallbackAgents = /* @__PURE__ */ new Set();
7174
8257
  const onProgress = options.onProgress ? (evidence) => {
7175
8258
  if (evidence.agentWallet) prepared.set(evidence.agentWallet.toLowerCase(), evidence);
7176
8259
  notifyFunding(options.onProgress, evidence);
@@ -7181,152 +8264,184 @@ var OwneySDK = class {
7181
8264
  if (evidence) notifyFunding(onProgress, { ...evidence, txHash: hash, transfer: "pending" });
7182
8265
  }
7183
8266
  } : void 0;
8267
+ const onCallsProgress = onProgress ? (callsId, destinations, transfer) => {
8268
+ for (const destination of destinations) {
8269
+ const evidence = prepared.get(destination.to.toLowerCase());
8270
+ if (evidence) notifyFunding(onProgress, { ...evidence, callsId, transfer });
8271
+ }
8272
+ } : void 0;
7184
8273
  if (BigInt(amount) === 0n) {
7185
8274
  return { agentResults: {} };
7186
8275
  }
7187
8276
  const state = this.requireState();
7188
8277
  const chainId = this.requireChainId();
7189
- const effectiveCallback = depositCallback ?? (this.paymasterServiceUrl ? this.getDefaultSponsoredCallsCallback(asset, onApproved, onSubmitted) : asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved, onSubmitted) : this.getDefaultSponsoredCallback(onApproved, onSubmitted));
7190
- if (agentId) {
7191
- if (this.isAgentDisabled(agentId)) {
7192
- throw new OwneyError(
7193
- "AGENT_DISABLED",
7194
- `Cannot deposit into disabled agent "${agentId}". Existing balances remain visible and withdrawable.`,
7195
- { agentId }
7196
- );
7197
- }
7198
- const agent = this.getAgent(agentId);
7199
- this.validateAssetSupport(agent, chainId, asset);
7200
- await this.validateMinDepositAmount(agent, state, chainId, asset, amount);
7201
- return this.depositWithFallback(
7202
- agent,
7203
- state,
7204
- chainId,
7205
- amount,
7206
- asset,
7207
- effectiveCallback,
7208
- depositCallback,
7209
- onProgress
7210
- );
7211
- }
7212
- const totalAmount = BigInt(amount);
7213
- const eligibleAgents = this.getEligibleAgents(chainId, asset, {
7214
- excludeDisabled: true
7215
- });
7216
- const exemptFlags = await Promise.all(
7217
- eligibleAgents.map(
7218
- (a) => this.hasExistingBalance(a, state, chainId, asset, true)
7219
- )
7220
- );
7221
- const exempt = new Set(
7222
- eligibleAgents.filter((_, i) => exemptFlags[i]).map((a) => a.id)
7223
- );
7224
- const agentAmounts = this.splitDepositAmount(
7225
- totalAmount,
7226
- eligibleAgents,
7227
- chainId,
7228
- asset,
7229
- exempt
7230
- );
7231
- if (agentAmounts.length === 0) {
7232
- const agentsRequiringActivation = eligibleAgents.filter(
7233
- (agent) => !exempt.has(agent.id)
7234
- );
7235
- const perAgentMinimum = agentsRequiringActivation.reduce(
7236
- (highest, agent) => {
7237
- const minimum = this.getMinDepositAmount(agent, chainId, asset);
7238
- return minimum > highest ? minimum : highest;
7239
- },
7240
- 0n
7241
- );
7242
- const minimumRequired = agentsRequiringActivation.length > 0 ? (perAgentMinimum > 0n ? perAgentMinimum : 1n) * BigInt(agentsRequiringActivation.length) : BigInt(eligibleAgents.length);
7243
- throw new OwneyError(
7244
- "DEPOSIT_AMOUNT_BELOW_MINIMUM",
7245
- `Amount "${amount}" cannot activate every eligible agent. The combined minimum is "${minimumRequired.toString()}" for ${asset}.`,
7246
- {
7247
- amount,
7248
- asset,
7249
- chainId,
7250
- minDepositAmount: minimumRequired.toString(),
7251
- perAgentDepositAmount: perAgentMinimum.toString(),
7252
- agentMinimums: agentsRequiringActivation.map((agent) => ({
7253
- agentId: agent.id,
7254
- minDepositAmount: this.getMinDepositAmount(
7255
- agent,
7256
- chainId,
7257
- asset
7258
- ).toString()
7259
- }))
7260
- }
7261
- );
7262
- }
7263
- const batchTransfer = getDepositBatchTransfer(effectiveCallback);
7264
- if (!depositCallback && batchTransfer) {
7265
- return runAgentDepositBatch(
7266
- chainId,
7267
- agentAmounts.map(({ agent, amount: amount2 }) => ({
7268
- id: agent.id,
7269
- amount: amount2,
7270
- run: (callback) => withFailureReporting(
7271
- this.apiKey,
7272
- agent.id,
7273
- () => onProgress ? agent.deposit(state, chainId, amount2, asset, callback, onProgress) : agent.deposit(state, chainId, amount2, asset, callback),
7274
- this.routingApiBaseUrl
7275
- )
7276
- })),
7277
- async (cid, transfers) => {
7278
- try {
7279
- return await batchTransfer(cid, transfers);
7280
- } catch (error) {
7281
- if (!(error instanceof OwneyError) || error.code !== "PERMIT2_APPROVAL_REQUIRED")
7282
- throw error;
7283
- const requiredAmount = transfers.reduce(
7284
- (sum, transfer) => sum + BigInt(transfer.amount),
7285
- 0n
7286
- );
7287
- await this.approvePermit2(
7288
- asset,
7289
- requiredAmount,
7290
- cid
7291
- );
7292
- return batchTransfer(cid, transfers);
7293
- }
8278
+ if (options.resumeTxHash && (depositCallback || this.paymasterServiceUrl || !/^0x[\da-f]{64}$/i.test(options.resumeTxHash)))
8279
+ throw new OwneyError("DEPOSIT_IN_PROGRESS", "This deposit cannot resume the saved relayer transaction.");
8280
+ const effectiveCallback = depositCallback ?? (this.paymasterServiceUrl ? this.getDefaultSponsoredCallsCallback(asset, onApproved, onSubmitted, onCallsProgress) : asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved, onSubmitted, options.resumeTxHash) : this.getDefaultSponsoredCallback(onApproved, onSubmitted, options.resumeTxHash));
8281
+ try {
8282
+ if (agentId) {
8283
+ if (this.isAgentDisabled(agentId)) {
8284
+ throw new OwneyError(
8285
+ "AGENT_DISABLED",
8286
+ `Cannot deposit into disabled agent "${agentId}". Existing balances remain visible and withdrawable.`,
8287
+ { agentId }
8288
+ );
7294
8289
  }
7295
- );
7296
- }
7297
- const agentResults = {};
7298
- for (const [
7299
- index,
7300
- { agent, amount: agentAmount }
7301
- ] of agentAmounts.entries()) {
7302
- try {
7303
- agentResults[agent.id] = await this.depositWithFallback(
8290
+ const agent = this.getAgent(agentId);
8291
+ this.validateAssetSupport(agent, chainId, asset);
8292
+ await this.validateMinDepositAmount(agent, state, chainId, asset, amount);
8293
+ return await this.depositWithFallback(
7304
8294
  agent,
7305
8295
  state,
7306
8296
  chainId,
7307
- agentAmount,
8297
+ amount,
7308
8298
  asset,
7309
8299
  effectiveCallback,
7310
8300
  depositCallback,
7311
- onProgress
8301
+ onProgress,
8302
+ () => paidFallbackAgents.add(agent.id),
8303
+ !!options.resumeTxHash
7312
8304
  );
7313
- } catch (error) {
7314
- if (Object.keys(agentResults).length === 0) throw error;
8305
+ }
8306
+ const totalAmount = BigInt(amount);
8307
+ const eligibleAgents = this.getEligibleAgents(chainId, asset, {
8308
+ excludeDisabled: true
8309
+ });
8310
+ const prospectiveShare = totalAmount / BigInt(eligibleAgents.length);
8311
+ const meetsEveryMinimum = eligibleAgents.every((agent) => prospectiveShare >= this.getMinDepositAmount(agent, chainId, asset));
8312
+ const exemptFlags = meetsEveryMinimum ? eligibleAgents.map(() => false) : await Promise.all(eligibleAgents.map((agent) => this.hasExistingBalance(agent, state, chainId, asset, true)));
8313
+ const exempt = new Set(
8314
+ eligibleAgents.filter((_, i) => exemptFlags[i]).map((a) => a.id)
8315
+ );
8316
+ const agentAmounts = this.splitDepositAmount(
8317
+ totalAmount,
8318
+ eligibleAgents,
8319
+ chainId,
8320
+ asset,
8321
+ exempt
8322
+ );
8323
+ if (agentAmounts.length === 0) {
8324
+ const agentsRequiringActivation = eligibleAgents.filter(
8325
+ (agent) => !exempt.has(agent.id)
8326
+ );
8327
+ const perAgentMinimum = agentsRequiringActivation.reduce(
8328
+ (highest, agent) => {
8329
+ const minimum = this.getMinDepositAmount(agent, chainId, asset);
8330
+ return minimum > highest ? minimum : highest;
8331
+ },
8332
+ 0n
8333
+ );
8334
+ const minimumRequired = agentsRequiringActivation.length > 0 ? (perAgentMinimum > 0n ? perAgentMinimum : 1n) * BigInt(agentsRequiringActivation.length) : BigInt(eligibleAgents.length);
7315
8335
  throw new OwneyError(
7316
- "DEPOSIT_PARTIAL_FAILURE",
7317
- "Some deposits completed before another agent failed. Check activity and balances before depositing again; the failed transfer may still settle.",
8336
+ "DEPOSIT_AMOUNT_BELOW_MINIMUM",
8337
+ `Amount "${amount}" cannot activate every eligible agent. The combined minimum is "${minimumRequired.toString()}" for ${asset}.`,
7318
8338
  {
7319
- agentResults,
7320
- failedAgentId: agent.id,
7321
- failedAmount: agentAmount,
7322
- unattemptedAgentIds: agentAmounts.slice(index + 1).map(({ agent: agent2 }) => agent2.id),
7323
- cause: error
7324
- },
7325
- agent.id
8339
+ amount,
8340
+ asset,
8341
+ chainId,
8342
+ minDepositAmount: minimumRequired.toString(),
8343
+ perAgentDepositAmount: perAgentMinimum.toString(),
8344
+ agentMinimums: agentsRequiringActivation.map((agent) => ({
8345
+ agentId: agent.id,
8346
+ minDepositAmount: this.getMinDepositAmount(
8347
+ agent,
8348
+ chainId,
8349
+ asset
8350
+ ).toString()
8351
+ }))
8352
+ }
7326
8353
  );
7327
8354
  }
8355
+ const batchTransfer = getDepositBatchTransfer(effectiveCallback);
8356
+ if (!depositCallback && batchTransfer) {
8357
+ return await runAgentDepositBatch(
8358
+ chainId,
8359
+ agentAmounts.map(({ agent, amount: amount2 }) => ({
8360
+ id: agent.id,
8361
+ amount: amount2,
8362
+ run: (callback) => withFailureReporting(
8363
+ this.apiKey,
8364
+ agent.id,
8365
+ () => onProgress ? agent.deposit(state, chainId, amount2, asset, callback, onProgress) : agent.deposit(state, chainId, amount2, asset, callback),
8366
+ this.routingApiBaseUrl
8367
+ )
8368
+ })),
8369
+ async (cid, transfers) => {
8370
+ try {
8371
+ return await batchTransfer(cid, transfers);
8372
+ } catch (error) {
8373
+ if (!(error instanceof OwneyError) || error.code !== "PERMIT2_APPROVAL_REQUIRED")
8374
+ throw error;
8375
+ const requiredAmount = transfers.reduce(
8376
+ (sum, transfer) => sum + BigInt(transfer.amount),
8377
+ 0n
8378
+ );
8379
+ await this.approvePermit2(
8380
+ asset,
8381
+ requiredAmount,
8382
+ cid
8383
+ );
8384
+ return batchTransfer(cid, transfers);
8385
+ }
8386
+ }
8387
+ );
8388
+ }
8389
+ const agentResults = {};
8390
+ for (const [
8391
+ index,
8392
+ { agent, amount: agentAmount }
8393
+ ] of agentAmounts.entries()) {
8394
+ try {
8395
+ agentResults[agent.id] = await this.depositWithFallback(
8396
+ agent,
8397
+ state,
8398
+ chainId,
8399
+ agentAmount,
8400
+ asset,
8401
+ effectiveCallback,
8402
+ depositCallback,
8403
+ onProgress,
8404
+ () => paidFallbackAgents.add(agent.id),
8405
+ !!options.resumeTxHash
8406
+ );
8407
+ } catch (error) {
8408
+ if (Object.keys(agentResults).length === 0) throw error;
8409
+ throw new OwneyError(
8410
+ "DEPOSIT_PARTIAL_FAILURE",
8411
+ "Some deposits completed before another agent failed. Check activity and balances before depositing again; the failed transfer may still settle.",
8412
+ {
8413
+ agentResults,
8414
+ failedAgentId: agent.id,
8415
+ failedAmount: agentAmount,
8416
+ unattemptedAgentIds: agentAmounts.slice(index + 1).map(({ agent: agent2 }) => agent2.id),
8417
+ cause: error
8418
+ },
8419
+ agent.id
8420
+ );
8421
+ }
8422
+ }
8423
+ return { agentResults };
8424
+ } catch (error) {
8425
+ if (!depositCallback && onProgress && !options.resumeTxHash) {
8426
+ const ids = agentId ? [agentId] : this.getEligibleAgents(chainId, asset).map((agent) => agent.id);
8427
+ for (const id of ids) {
8428
+ const evidence = [...prepared.values()].find((value) => value.agentId === id);
8429
+ if (paidFallbackAgents.has(id) || evidence?.transfer === "confirmed" || evidence?.transfer === "failed") continue;
8430
+ const rejected = error instanceof OwneyError && error.details?.notSubmitted === true;
8431
+ if (!rejected && (evidence?.txHash || evidence?.userOpHash || evidence?.callsId)) continue;
8432
+ notifyFunding(onProgress, { ...evidence ?? {
8433
+ agentId: id,
8434
+ chainId,
8435
+ token: asset,
8436
+ walletAddress: state.walletAddress,
8437
+ intendedAmount: amount,
8438
+ registration: "unknown",
8439
+ delivery: "not-applicable"
8440
+ }, transfer: "failed" });
8441
+ }
8442
+ }
8443
+ throw error;
7328
8444
  }
7329
- return { agentResults };
7330
8445
  }
7331
8446
  /**
7332
8447
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
@@ -7355,7 +8470,7 @@ var OwneySDK = class {
7355
8470
  * propagates unchanged. Scoped per agent invocation so one agent's failure
7356
8471
  * doesn't force a retry of sibling agents in a multi-agent split.
7357
8472
  */
7358
- async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback, onProgress) {
8473
+ async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback, onProgress, onPaidFallback, resumeOnly = false) {
7359
8474
  const attempt = (callback) => withFailureReporting(
7360
8475
  this.apiKey,
7361
8476
  agent.id,
@@ -7367,6 +8482,7 @@ var OwneySDK = class {
7367
8482
  try {
7368
8483
  return await attempt(effectiveCallback);
7369
8484
  } catch (error) {
8485
+ if (resumeOnly) throw error;
7370
8486
  if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
7371
8487
  approvalAttempted = true;
7372
8488
  console.warn(
@@ -7384,6 +8500,7 @@ var OwneySDK = class {
7384
8500
  `[owney-sdk] ${asset} sponsorship unavailable, falling back to user-paid deposit:`,
7385
8501
  error
7386
8502
  );
8503
+ onPaidFallback?.();
7387
8504
  return attempt(void 0);
7388
8505
  }
7389
8506
  }
@@ -7466,7 +8583,12 @@ var OwneySDK = class {
7466
8583
  */
7467
8584
  async hasExistingBalance(agent, state, chainId, asset, requireReliableRead = false) {
7468
8585
  try {
7469
- const balance = await agent.getBalances(state, chainId);
8586
+ if (agent.hasExistingDepositFunds && await agent.hasExistingDepositFunds(
8587
+ state,
8588
+ chainId,
8589
+ asset
8590
+ ).catch(() => void 0) === true) return true;
8591
+ const balance = await agent.getBalances(state, chainId, asset);
7470
8592
  const target = asset.toLowerCase();
7471
8593
  const token = balance.tokens.find(
7472
8594
  (t) => t.chainId === chainId && t.asset.toLowerCase() === target
@@ -7592,7 +8714,8 @@ var OwneySDK = class {
7592
8714
  return verified;
7593
8715
  }
7594
8716
  /** Resolve a saved sponsored-withdrawal ID without opening a wallet prompt. */
7595
- async resolveSponsoredWithdrawalEvidence(evidence) {
8717
+ async resolveSponsoredWithdrawalEvidence(evidence, options = {}) {
8718
+ if (evidence.callsId && !evidence.userOpHash) return this.resolveWalletCallsFundingEvidence(evidence, "withdraw");
7596
8719
  const { state, chainId, assertCurrent } = this.captureReadScope(evidence.chainId);
7597
8720
  if (evidence.agentId !== "yieldseeker" || chainId !== 8453 || evidence.txHash || state.walletAddress.toLowerCase() !== evidence.walletAddress.toLowerCase()) return evidence;
7598
8721
  const agent = this.getAgent("yieldseeker");
@@ -7608,18 +8731,26 @@ var OwneySDK = class {
7608
8731
  owner: state.walletAddress,
7609
8732
  agentWallet: wallet,
7610
8733
  asset: SupportedAssets.find((t) => t.chainId === chainId && t.symbol === evidence.token)?.address,
7611
- userOpHash: evidence.userOpHash
8734
+ userOpHash: evidence.userOpHash,
8735
+ ...evidence.userOperation ? { userOperation: Object.fromEntries([
8736
+ "sender",
8737
+ "nonce",
8738
+ "initCode",
8739
+ "callData",
8740
+ "callGasLimit",
8741
+ "verificationGasLimit",
8742
+ "preVerificationGas",
8743
+ "maxFeePerGas",
8744
+ "maxPriorityFeePerGas",
8745
+ "paymasterAndData"
8746
+ ].map((key2) => [key2, evidence.userOperation[key2]])) } : {},
8747
+ ...Number.isFinite(options.submittedAt) && options.submittedAt > 0 ? { submittedAt: options.submittedAt } : {}
7612
8748
  })
7613
8749
  });
7614
8750
  if (!response.ok) throw new OwneyError("AGENT_API_ERROR", "Could not resolve sponsored withdrawal status.");
7615
8751
  const payload = await response.json();
7616
8752
  txHash = payload.data?.txHash ?? void 0;
7617
8753
  executionFailed = payload.data?.executionFailed === true;
7618
- } else if (evidence.callsId) {
7619
- const status = await state.provider.request({ method: "wallet_getCallsStatus", params: [evidence.callsId] });
7620
- if (status?.id && status.id !== evidence.callsId || status?.chainId && Number(status.chainId) !== chainId) return evidence;
7621
- executionFailed = status?.status === 400 || status?.status === 500;
7622
- txHash = status?.receipts?.[0]?.transactionHash;
7623
8754
  }
7624
8755
  assertCurrent();
7625
8756
  if (executionFailed) return {
@@ -7630,6 +8761,60 @@ var OwneySDK = class {
7630
8761
  };
7631
8762
  return txHash && /^0x[0-9a-fA-F]{64}$/.test(txHash) ? { ...evidence, txHash } : evidence;
7632
8763
  }
8764
+ /** Recover an EIP-5792 deposit or payout using this account's exact batch ID. */
8765
+ async resolveWalletCallsFundingEvidence(evidence, direction) {
8766
+ const { state, chainId, assertCurrent } = this.captureReadScope(evidence.chainId);
8767
+ if (!evidence.callsId || evidence.txHash || state.walletAddress.toLowerCase() !== evidence.walletAddress.toLowerCase()) return evidence;
8768
+ const agent = this.getAgent(evidence.agentId);
8769
+ const wallet = agent.getFundingWallet ? await agent.getFundingWallet(state, chainId, evidence.token) : (await agent.getUserProfile(state, chainId)).smartWallet;
8770
+ if (!wallet || wallet.toLowerCase() !== evidence.agentWallet?.toLowerCase()) return evidence;
8771
+ const status = await state.provider.request({ method: "wallet_getCallsStatus", params: [evidence.callsId] });
8772
+ assertCurrent();
8773
+ if (status?.id !== void 0 && status.id !== evidence.callsId || status?.chainId !== void 0 && Number(status.chainId) !== chainId) return evidence;
8774
+ const statusCode2 = Number(status?.status);
8775
+ if (statusCode2 === 400 || statusCode2 === 500) return {
8776
+ ...evidence,
8777
+ transfer: "failed",
8778
+ delivery: direction === "withdraw" ? "failed" : "not-applicable"
8779
+ };
8780
+ const hashes = [...new Set((status?.receipts ?? []).map((receipt) => receipt.transactionHash?.toLowerCase()).filter((hash) => !!hash && /^0x[\da-f]{64}$/i.test(hash)))];
8781
+ if (hashes.length === 1) return { ...evidence, txHash: hashes[0] };
8782
+ if (hashes.length > 16) return evidence;
8783
+ const matches = [];
8784
+ for (const txHash of hashes) {
8785
+ const inspected = await this.inspectFunding({ ...evidence, txHash }, direction);
8786
+ assertCurrent();
8787
+ if (inspected.transfer === "confirmed") matches.push(inspected);
8788
+ }
8789
+ return matches.length === 1 ? matches[0] : evidence;
8790
+ }
8791
+ /** Whether a foreground deposit can resume the same signed batch, never a
8792
+ * fresh authorization. The batch rail independently rechecks the exact plan. */
8793
+ async canResumePendingDeposit(evidences, amount) {
8794
+ if (this.paymasterServiceUrl || !evidences.length || !/^\d+$/.test(amount)) return false;
8795
+ const first = evidences[0];
8796
+ const { state, chainId, assertCurrent } = this.captureReadScope(first.chainId);
8797
+ const token = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === first.token);
8798
+ if (!token) return false;
8799
+ const pending = pendingTokenBatch({
8800
+ apiKey: this.apiKey,
8801
+ baseUrl: this.routingApiBaseUrl,
8802
+ owner: state.walletAddress,
8803
+ chainId,
8804
+ token: token.address
8805
+ });
8806
+ if (!pending || pending.transfers.length !== evidences.length || pending.transfers.reduce((sum, transfer) => sum + BigInt(transfer.amount), 0n).toString() !== amount) return false;
8807
+ const destinations = /* @__PURE__ */ new Set();
8808
+ for (const evidence of evidences) {
8809
+ if (evidence.chainId !== chainId || evidence.token !== first.token || evidence.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || evidence.callsId || evidence.userOpHash || evidence.txHash?.toLowerCase() !== pending.txHash.toLowerCase()) return false;
8810
+ const agent = this.getAgent(evidence.agentId);
8811
+ const wallet = agent.getFundingWallet ? await agent.getFundingWallet(state, chainId, evidence.token) : (await agent.getUserProfile(state, chainId)).smartWallet;
8812
+ if (!wallet || destinations.has(wallet.toLowerCase()) || !pending.transfers.some((transfer) => transfer.to.toLowerCase() === wallet.toLowerCase() && transfer.amount === evidence.intendedAmount)) return false;
8813
+ destinations.add(wallet.toLowerCase());
8814
+ }
8815
+ assertCurrent();
8816
+ return true;
8817
+ }
7633
8818
  /** A display acknowledgement controls polling only; it never confirms a financial outcome. */
7634
8819
  endFundingBalanceRefresh(evidence, read2) {
7635
8820
  const { state, chainId } = this.captureReadScope(evidence.chainId);
@@ -7730,6 +8915,30 @@ var OwneySDK = class {
7730
8915
  }
7731
8916
  };
7732
8917
  const token = asset;
8918
+ const agentFailures = {};
8919
+ const recordAgentFailure = (id, error) => {
8920
+ if (!(error instanceof OwneyError)) return;
8921
+ const details = error.details ?? {};
8922
+ const codeValue = (value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(value) ? value : void 0;
8923
+ const revertNames = ["InsufficientBalance", "InvalidAsset", "Unauthorized", "Error", "Panic", "FailedOp", "FailedOpWithRevert"];
8924
+ const panicCodes = [0, 1, 17, 18, 33, 34, 49, 50, 65, 81];
8925
+ agentFailures[id] = {
8926
+ code: error.code,
8927
+ phase: ["prepare", "send", "observe"].includes(String(details.phase)) ? details.phase : void 0,
8928
+ failureCode: codeValue(details.failureCode),
8929
+ reason: codeValue(details.reason),
8930
+ withdrawalPhase: details.withdrawalPhase === "unwind" ? "unwind" : void 0,
8931
+ statusCode: typeof details.statusCode === "number" ? details.statusCode : void 0,
8932
+ rpcCode: typeof details.rpcCode === "number" ? details.rpcCode : void 0,
8933
+ rpcMethod: typeof details.rpcMethod === "string" && /^[A-Za-z_]{1,80}$/.test(details.rpcMethod) ? details.rpcMethod : void 0,
8934
+ revertSelector: typeof details.revertSelector === "string" && /^0x[0-9a-f]{8}$/i.test(details.revertSelector) ? details.revertSelector : void 0,
8935
+ revertName: typeof details.revertName === "string" && revertNames.includes(details.revertName) ? details.revertName : void 0,
8936
+ aaCode: typeof details.aaCode === "string" && /^AA[0-9]{2}$/.test(details.aaCode) ? details.aaCode : void 0,
8937
+ panicCode: typeof details.panicCode === "string" && /^0x[0-9a-f]{1,2}$/i.test(details.panicCode) && panicCodes.includes(Number.parseInt(details.panicCode.slice(2), 16)) ? details.panicCode : void 0,
8938
+ safeToFallback: typeof details.safeToFallback === "boolean" ? details.safeToFallback : void 0,
8939
+ executionFailed: typeof details.executionFailed === "boolean" ? details.executionFailed : void 0
8940
+ };
8941
+ };
7733
8942
  const submissionEvidence = /* @__PURE__ */ new Map();
7734
8943
  const submit = async (agent, requested2) => {
7735
8944
  attempted.add(agent.id);
@@ -7750,9 +8959,19 @@ var OwneySDK = class {
7750
8959
  };
7751
8960
  progress(evidence);
7752
8961
  try {
8962
+ if (options.onBeforeSubmission)
8963
+ return await agent.withdraw(
8964
+ state,
8965
+ chainId,
8966
+ token,
8967
+ requested2,
8968
+ progress,
8969
+ options.gasMode ?? "auto",
8970
+ (next) => options.onBeforeSubmission({ ...evidence, ...next })
8971
+ );
7753
8972
  return await (options.gasMode === void 0 ? agent.withdraw(state, chainId, token, requested2, progress) : agent.withdraw(state, chainId, token, requested2, progress, options.gasMode));
7754
8973
  } catch (error) {
7755
- if (!evidence.txHash && (this.isUserRejectedWithdrawal(error) || /insufficient (balance|funds)/i.test(error instanceof Error ? error.message : "")))
8974
+ if (!evidence.txHash && !evidence.userOpHash && !evidence.callsId && (this.isUserRejectedWithdrawal(error) || /insufficient (balance|funds)/i.test(error instanceof Error ? error.message : "")))
7756
8975
  progress({ ...evidence, transfer: "failed", delivery: "failed" });
7757
8976
  throw error;
7758
8977
  }
@@ -7802,6 +9021,7 @@ var OwneySDK = class {
7802
9021
  }
7803
9022
  console.error(`withdraw failed for agent "${agent.id}":`, err);
7804
9023
  agentErrors2[agent.id] = err instanceof Error ? err.message : String(err);
9024
+ recordAgentFailure(agent.id, err);
7805
9025
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
7806
9026
  void reportAgentFailure(
7807
9027
  this.apiKey,
@@ -7812,17 +9032,23 @@ var OwneySDK = class {
7812
9032
  }
7813
9033
  }
7814
9034
  if (Object.keys(results2).length === 0) {
9035
+ if (Object.keys(agentErrors2).length === 0)
9036
+ throw new OwneyError(
9037
+ "WITHDRAW_INSUFFICIENT_BALANCE",
9038
+ "No funds are currently available to withdraw for this asset.",
9039
+ { asset, available: "0" }
9040
+ );
7815
9041
  throw new OwneyError(
7816
9042
  "WITHDRAW_ALL_FAILED",
7817
9043
  `Failed to withdraw "${asset}" from all eligible agents.`,
7818
- { asset, agentErrors: agentErrors2 }
9044
+ { asset, agentErrors: agentErrors2, agentFailures }
7819
9045
  );
7820
9046
  }
7821
9047
  if (Object.keys(agentErrors2).length > 0) {
7822
9048
  throw new OwneyError(
7823
9049
  "WITHDRAW_PARTIAL_FAILURE",
7824
9050
  "Some agents could not complete the withdrawal.",
7825
- { asset, partialResults: results2, agentErrors: agentErrors2, ...Object.keys(incompleteWithdrawals).length ? { incompleteWithdrawals } : {} }
9051
+ { asset, partialResults: results2, agentErrors: agentErrors2, agentFailures, ...Object.keys(incompleteWithdrawals).length ? { incompleteWithdrawals } : {} }
7826
9052
  );
7827
9053
  }
7828
9054
  const amountsKnown = Object.values(results2).every(
@@ -7834,11 +9060,10 @@ var OwneySDK = class {
7834
9060
  };
7835
9061
  }
7836
9062
  const requested = BigInt(amount);
7837
- const aggregated = await this.getBalances(void 0, { chainId, asset: token });
9063
+ const { balances: aggregated, failures: balanceFailures } = await this.readAggregatedBalances({ chainId, asset: token });
7838
9064
  const unavailableAgents = eligibleAgents.filter(
7839
9065
  (agent) => !(agent.id in aggregated.agentBalances)
7840
9066
  );
7841
- const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
7842
9067
  const balances = projectAgentBalancesForAsset(
7843
9068
  eligibleAgents,
7844
9069
  aggregated.agentBalances,
@@ -7846,6 +9071,44 @@ var OwneySDK = class {
7846
9071
  asset,
7847
9072
  assetInfo.decimals
7848
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);
7849
9112
  const totalAvailable = balances.reduce((sum, x) => sum + x.balance, 0n);
7850
9113
  if (totalAvailable === 0n && unavailableAgents.length > 0) {
7851
9114
  throw new OwneyError(
@@ -7903,6 +9166,7 @@ var OwneySDK = class {
7903
9166
  if (this.isUserRejectedWithdrawal(err)) throw err;
7904
9167
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);
7905
9168
  agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
9169
+ recordAgentFailure(p.agent.id, err);
7906
9170
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
7907
9171
  void reportAgentFailure(
7908
9172
  this.apiKey,
@@ -7923,12 +9187,24 @@ var OwneySDK = class {
7923
9187
  {
7924
9188
  asset,
7925
9189
  amount,
7926
- agentErrors
9190
+ agentErrors,
9191
+ agentFailures
7927
9192
  }
7928
9193
  );
7929
9194
  }
7930
9195
  const withdrawn = sumWithdrawnAmount(results);
7931
- 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) {
7932
9208
  throw new OwneyError(
7933
9209
  "WITHDRAW_PARTIAL_FAILURE",
7934
9210
  `Withdrew "${withdrawn.toString()}" of requested "${amount}" of "${asset}". One or more agents failed; remaining balance could not cover the shortfall.`,
@@ -7938,6 +9214,7 @@ var OwneySDK = class {
7938
9214
  withdrawn: withdrawn.toString(),
7939
9215
  partialResults: results,
7940
9216
  agentErrors,
9217
+ agentFailures,
7941
9218
  ...unavailableAgentIds.length > 0 ? { unavailableAgents: unavailableAgentIds } : {}
7942
9219
  }
7943
9220
  );
@@ -7951,23 +9228,42 @@ var OwneySDK = class {
7951
9228
  * @returns {AgentBalance} for a single agent, or {OwneyBalances} with totalBalance and per-agent breakdown
7952
9229
  */
7953
9230
  async getBalances(agentId, options) {
7954
- const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
7955
- const asset = options?.asset;
7956
- const readBalance = (agent) => this.readAgent(
7957
- agent,
7958
- "balances",
7959
- () => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
7960
- asset ? { asset } : null
7961
- );
7962
9231
  if (agentId) {
9232
+ const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
9233
+ const asset = options?.asset;
7963
9234
  const agent = this.getAgent(agentId);
7964
- 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
+ );
7965
9241
  assertCurrent();
7966
9242
  return {
7967
9243
  ...result,
7968
9244
  balanceComposition: agent.balanceComposition ?? "tokens-include-positions"
7969
9245
  };
7970
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
+ );
7971
9267
  let totalBalance = 0;
7972
9268
  const results = {};
7973
9269
  const entries = [...this.getActiveAgents().entries()];
@@ -7984,39 +9280,33 @@ var OwneySDK = class {
7984
9280
  })
7985
9281
  );
7986
9282
  assertCurrent();
7987
- let successCount = 0;
7988
9283
  const agentErrors = {};
7989
9284
  const agentRetryAt = {};
7990
- const agentFailures = [];
9285
+ const agentFailures = /* @__PURE__ */ new Map();
7991
9286
  for (let i = 0; i < balanceResults.length; i++) {
7992
9287
  const settledResult = balanceResults[i];
7993
- const [agentId2] = entries[i];
9288
+ const [agentId] = entries[i];
7994
9289
  if (settledResult.status === "fulfilled") {
7995
9290
  const [id, b] = settledResult.value;
7996
9291
  results[id] = b;
7997
9292
  totalBalance += Number(b.totalBalance);
7998
- successCount += 1;
7999
9293
  continue;
8000
9294
  }
8001
9295
  const reason = settledResult.reason;
8002
- agentFailures.push(reason);
9296
+ agentFailures.set(agentId, reason);
8003
9297
  const retryDelay = rateLimitDelay(reason);
8004
- if (retryDelay !== void 0) agentRetryAt[agentId2] = Date.now() + retryDelay;
8005
- agentErrors[agentId2] = reason instanceof Error ? reason.message : String(reason);
8006
- console.error(`[owney-sdk] Balance fetch failed for agent "${agentId2}":`, reason);
8007
- }
8008
- if (successCount === 0) {
8009
- throw new OwneyError(
8010
- "BALANCE_ALL_FAILED",
8011
- "Failed to fetch balances for all active agents.",
8012
- { agentErrors, failures: agentFailures }
8013
- );
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);
8014
9301
  }
8015
9302
  return {
8016
- totalBalance: String(totalBalance),
8017
- totalBalanceAsset: "usdc",
8018
- agentBalances: results,
8019
- ...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
8020
9310
  };
8021
9311
  }
8022
9312
  /**