@owney/sdk 0.7.25-beta.7 → 0.7.25-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2905,10 +2905,11 @@ var YieldseekerApiClient = class {
2905
2905
  const payload = await response.json().catch(() => null);
2906
2906
  if (!response.ok) {
2907
2907
  const error = providerError(payload, `HTTP_${response.status}`);
2908
+ const retryAfter = response.headers.get("Retry-After");
2908
2909
  throw new YieldseekerApiError(
2909
2910
  response.status,
2910
2911
  error.code,
2911
- error.fields
2912
+ retryAfter ? { ...error.fields, headers: { "Retry-After": retryAfter } } : error.fields
2912
2913
  );
2913
2914
  }
2914
2915
  if (payload && typeof payload === "object" && payload.success === true && "data" in payload) {
@@ -3039,13 +3040,14 @@ function position(value, asset, baseAssetDecimals) {
3039
3040
  // differ from the underlying asset. Yieldseeker already converts it to
3040
3041
  // underlying base-asset units in `assetsBase`; pair that value with the
3041
3042
  // snapshot's corresponding `baseAssetDecimals` for display. Keep the raw
3042
- // share quantity separately because withdraw-from-position expects it.
3043
+ // share quantity separate from the withdrawable underlying asset amount.
3043
3044
  amount: decimal(
3044
3045
  value.assetsBase,
3045
3046
  baseAssetDecimals,
3046
3047
  "yield positions"
3047
3048
  ),
3048
3049
  amountRaw: String(value.assetsRaw),
3050
+ withdrawableAmountRaw: String(value.withdrawableAssetsRaw),
3049
3051
  apy: percent(option.riskAdjustedApy),
3050
3052
  tvl: Number(option.totalDepositsUsd),
3051
3053
  liquidity: Number(option.withdrawableDepositsUsd)
@@ -3374,8 +3376,12 @@ var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3374
3376
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3375
3377
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3376
3378
  var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3377
- var YIELDSEEKER_AGENT_LIST_CACHE_MS = 6e4;
3378
- var YIELDSEEKER_PORTFOLIO_CACHE_MS = 3e4;
3379
+ var YIELDSEEKER_AGENT_LIST_CACHE_MS = 12e4;
3380
+ var YIELDSEEKER_PORTFOLIO_CACHE_MS = 12e4;
3381
+ var YIELDSEEKER_SETTLEMENT_CACHE_MS = 5e3;
3382
+ var YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS = 15e3;
3383
+ var YIELDSEEKER_READ_FAILURE_COOLDOWN_MS = 6e4;
3384
+ var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
3379
3385
  var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
3380
3386
  function generateYieldseekerUsername() {
3381
3387
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
@@ -3445,6 +3451,15 @@ var YieldseekerAgent = class {
3445
3451
  readCache = /* @__PURE__ */ new Map();
3446
3452
  pendingReads = /* @__PURE__ */ new Map();
3447
3453
  readGeneration = 0;
3454
+ portfolioVersions = /* @__PURE__ */ new Map();
3455
+ snapshotFailures = /* @__PURE__ */ new Map();
3456
+ standardReadFailures = /* @__PURE__ */ new Map();
3457
+ reconcileUntil = /* @__PURE__ */ new Map();
3458
+ activityRefreshUntil = /* @__PURE__ */ new Map();
3459
+ // null means a confirmed movement has no reliable baseline. Do not guess
3460
+ // settlement; keep slow reconciliation after the fast window expires.
3461
+ snapshotMovements = /* @__PURE__ */ new Map();
3462
+ movementEpochs = /* @__PURE__ */ new Map();
3448
3463
  yieldOptions = /* @__PURE__ */ new Map();
3449
3464
  pendingYieldOptions = /* @__PURE__ */ new Map();
3450
3465
  constructor(owneyApiKey, options = {}) {
@@ -3475,6 +3490,13 @@ var YieldseekerAgent = class {
3475
3490
  this.pendingWalletContexts.clear();
3476
3491
  this.readCache.clear();
3477
3492
  this.pendingReads.clear();
3493
+ this.portfolioVersions.clear();
3494
+ this.snapshotFailures.clear();
3495
+ this.standardReadFailures.clear();
3496
+ this.reconcileUntil.clear();
3497
+ this.activityRefreshUntil.clear();
3498
+ this.snapshotMovements.clear();
3499
+ this.movementEpochs.clear();
3478
3500
  }
3479
3501
  async activateAgent(state, chainId, asset) {
3480
3502
  this.assertChain(chainId);
@@ -3494,41 +3516,37 @@ var YieldseekerAgent = class {
3494
3516
  );
3495
3517
  }
3496
3518
  const context = await this.ensureAgent(state, chainId, asset);
3519
+ const generation = this.readGeneration;
3520
+ const movement = this.snapshotMovement(state, chainId, context, BigInt(amount));
3497
3521
  let txHash;
3498
- try {
3499
- if (depositCallback) {
3500
- provideDepositVerificationContext(depositCallback, {
3501
- agentId: "yieldseeker",
3502
- signature: await this.auth.getToken(state, chainId),
3503
- userId: context.user.userId,
3504
- yieldseekerAgentId: context.agent.agentId
3505
- });
3506
- txHash = await depositCallback(
3507
- context.wallet.walletAddress,
3508
- chainId,
3509
- amount
3510
- );
3511
- await this.waitForReceipt(state, chainId, txHash);
3512
- } else {
3513
- txHash = await this.submitTransaction(state, chainId, {
3514
- from: (0, import_viem7.getAddress)(state.walletAddress),
3515
- to: YIELDSEEKER_ASSET_METADATA[asset].address,
3516
- data: (0, import_viem7.encodeFunctionData)({
3517
- abi: import_viem7.erc20Abi,
3518
- functionName: "transfer",
3519
- args: [(0, import_viem7.getAddress)(context.wallet.walletAddress), BigInt(amount)]
3520
- }),
3521
- value: "0",
3522
- chainId
3523
- });
3524
- }
3525
- } finally {
3526
- await this.refreshSnapshotAfterMovement(
3527
- state,
3522
+ if (depositCallback) {
3523
+ provideDepositVerificationContext(depositCallback, {
3524
+ agentId: "yieldseeker",
3525
+ signature: await this.auth.getToken(state, chainId),
3526
+ userId: context.user.userId,
3527
+ yieldseekerAgentId: context.agent.agentId
3528
+ });
3529
+ txHash = await depositCallback(
3530
+ context.wallet.walletAddress,
3528
3531
  chainId,
3529
- context,
3530
- "deposit"
3532
+ amount
3531
3533
  );
3534
+ await this.waitForReceipt(state, chainId, txHash);
3535
+ } else {
3536
+ txHash = await this.submitTransaction(state, chainId, {
3537
+ from: (0, import_viem7.getAddress)(state.walletAddress),
3538
+ to: YIELDSEEKER_ASSET_METADATA[asset].address,
3539
+ data: (0, import_viem7.encodeFunctionData)({
3540
+ abi: import_viem7.erc20Abi,
3541
+ functionName: "transfer",
3542
+ args: [(0, import_viem7.getAddress)(context.wallet.walletAddress), BigInt(amount)]
3543
+ }),
3544
+ value: "0",
3545
+ chainId
3546
+ });
3547
+ }
3548
+ if (this.readGeneration === generation) {
3549
+ await this.refreshSnapshotAfterMovement(state, chainId, context, "deposit", movement);
3532
3550
  }
3533
3551
  return {
3534
3552
  txHash,
@@ -3556,6 +3574,9 @@ var YieldseekerAgent = class {
3556
3574
  this.id
3557
3575
  );
3558
3576
  }
3577
+ const generation = this.readGeneration;
3578
+ let confirmedMovement = false;
3579
+ let settlement = null;
3559
3580
  try {
3560
3581
  const portfolio = await this.loadPortfolioContext(
3561
3582
  state,
@@ -3573,6 +3594,7 @@ var YieldseekerAgent = class {
3573
3594
  );
3574
3595
  const totalAvailable = idle + deployed;
3575
3596
  const requested = amount === void 0 ? totalAvailable : BigInt(amount);
3597
+ const plannedMovement = this.snapshotMovement(state, chainId, context, -requested);
3576
3598
  if (requested > totalAvailable) {
3577
3599
  throw new OwneyError(
3578
3600
  "WITHDRAW_INSUFFICIENT_BALANCE",
@@ -3608,6 +3630,7 @@ var YieldseekerAgent = class {
3608
3630
  throw this.invalidResponse("position withdrawal");
3609
3631
  }
3610
3632
  await this.waitForReceipt(state, chainId, response.transactionHash);
3633
+ confirmedMovement = true;
3611
3634
  remaining -= assetsRaw;
3612
3635
  }
3613
3636
  if (remaining > 0n) {
@@ -3632,18 +3655,23 @@ var YieldseekerAgent = class {
3632
3655
  value: "0",
3633
3656
  chainId
3634
3657
  });
3658
+ confirmedMovement = true;
3659
+ settlement = plannedMovement;
3635
3660
  return {
3636
3661
  txHash,
3637
3662
  type: amount === void 0 ? "full" : "partial",
3638
3663
  amount: requested.toString()
3639
3664
  };
3640
3665
  } finally {
3641
- await this.refreshSnapshotAfterMovement(
3642
- state,
3643
- chainId,
3644
- context,
3645
- "withdrawal"
3646
- );
3666
+ if (confirmedMovement && this.readGeneration === generation) {
3667
+ await this.refreshSnapshotAfterMovement(
3668
+ state,
3669
+ chainId,
3670
+ context,
3671
+ "withdrawal",
3672
+ settlement
3673
+ );
3674
+ }
3647
3675
  }
3648
3676
  }
3649
3677
  async getBalances(state, chainId) {
@@ -3737,6 +3765,16 @@ var YieldseekerAgent = class {
3737
3765
  contextKey(state, chainId, asset) {
3738
3766
  return `${this.userKey(state, chainId)}:${asset}`;
3739
3767
  }
3768
+ assertSession(generation, chainId, asset) {
3769
+ if (this.readGeneration !== generation) {
3770
+ throw new OwneyError(
3771
+ "NOT_CONNECTED",
3772
+ "Wallet session changed during the Yieldseeker request.",
3773
+ { rpcSource: "agent-api", chainId, ...asset ? { asset } : {} },
3774
+ this.id
3775
+ );
3776
+ }
3777
+ }
3740
3778
  cachedRead(key2, ttlMs, read2) {
3741
3779
  const cached = this.readCache.get(key2);
3742
3780
  if (cached && cached.expiresAt > Date.now()) {
@@ -3746,7 +3784,7 @@ var YieldseekerAgent = class {
3746
3784
  if (pending) return pending;
3747
3785
  const generation = this.readGeneration;
3748
3786
  const request = Promise.resolve().then(read2).then((value) => {
3749
- if (this.readGeneration === generation) {
3787
+ if (this.readGeneration === generation && this.pendingReads.get(key2) === request) {
3750
3788
  this.readCache.set(key2, {
3751
3789
  expiresAt: Date.now() + ttlMs,
3752
3790
  value
@@ -3794,6 +3832,7 @@ var YieldseekerAgent = class {
3794
3832
  chainId,
3795
3833
  `/users/${user.userId}/agents/${agent.agentId}/wallet`
3796
3834
  ).then((walletResponse) => {
3835
+ this.assertSession(generation, chainId, asset);
3797
3836
  if (!walletResponse?.agentWallet || !(0, import_viem7.isAddress)(walletResponse.agentWallet.walletAddress)) {
3798
3837
  throw this.invalidResponse("agent wallet");
3799
3838
  }
@@ -3803,9 +3842,7 @@ var YieldseekerAgent = class {
3803
3842
  wallet: walletResponse.agentWallet,
3804
3843
  asset
3805
3844
  };
3806
- if (this.readGeneration === generation) {
3807
- this.agentContexts.set(key2, context);
3808
- }
3845
+ this.agentContexts.set(key2, context);
3809
3846
  return context;
3810
3847
  });
3811
3848
  this.pendingWalletContexts.set(key2, request);
@@ -3827,6 +3864,7 @@ var YieldseekerAgent = class {
3827
3864
  return persisted;
3828
3865
  }
3829
3866
  const walletAddress = (0, import_viem7.getAddress)(state.walletAddress);
3867
+ const generation = this.readGeneration;
3830
3868
  let user = null;
3831
3869
  try {
3832
3870
  const login = await this.providerRequest(
@@ -3873,6 +3911,7 @@ var YieldseekerAgent = class {
3873
3911
  if (!user || !/^[a-zA-Z0-9_-]{1,128}$/.test(user.userId)) {
3874
3912
  throw this.invalidResponse("wallet identity");
3875
3913
  }
3914
+ this.assertSession(generation, chainId);
3876
3915
  const resolved = { userId: user.userId };
3877
3916
  this.users.set(key2, resolved);
3878
3917
  writeYieldseekerIdentity(state.walletAddress, chainId, resolved.userId);
@@ -3888,10 +3927,13 @@ var YieldseekerAgent = class {
3888
3927
  if (cached) return cached;
3889
3928
  const pending = this.pendingAgents.get(key2);
3890
3929
  if (pending) return pending;
3930
+ const generation = this.readGeneration;
3891
3931
  const request = this.resolveAgent(state, chainId, asset, true).then(
3892
3932
  async (context) => {
3933
+ this.assertSession(generation, chainId, asset);
3893
3934
  if (!context) throw this.invalidResponse("agent creation");
3894
3935
  await this.deployAgent(state, chainId, context);
3936
+ this.assertSession(generation, chainId, asset);
3895
3937
  this.agentContexts.set(key2, context);
3896
3938
  return context;
3897
3939
  }
@@ -3900,20 +3942,25 @@ var YieldseekerAgent = class {
3900
3942
  try {
3901
3943
  return await request;
3902
3944
  } finally {
3903
- this.pendingAgents.delete(key2);
3945
+ if (this.pendingAgents.get(key2) === request) this.pendingAgents.delete(key2);
3904
3946
  }
3905
3947
  }
3906
3948
  async findAgent(state, chainId, asset) {
3907
3949
  const key2 = this.contextKey(state, chainId, asset);
3908
3950
  const cached = this.agentContexts.get(key2);
3909
3951
  if (cached) return cached;
3952
+ const generation = this.readGeneration;
3910
3953
  const context = await this.resolveAgent(state, chainId, asset, false);
3954
+ this.assertSession(generation, chainId, asset);
3911
3955
  if (context) this.agentContexts.set(key2, context);
3912
3956
  return context;
3913
3957
  }
3914
3958
  async resolveAgent(state, chainId, asset, createIfMissing) {
3959
+ const generation = this.readGeneration;
3915
3960
  const user = await this.resolveUser(state, chainId);
3961
+ this.assertSession(generation, chainId, asset);
3916
3962
  const agents = await this.listAgents(state, chainId, user);
3963
+ this.assertSession(generation, chainId, asset);
3917
3964
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3918
3965
  let agent = agents.find(
3919
3966
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
@@ -3935,6 +3982,7 @@ var YieldseekerAgent = class {
3935
3982
  }
3936
3983
  }
3937
3984
  );
3985
+ this.assertSession(generation, chainId, asset);
3938
3986
  agent = created?.agent;
3939
3987
  if (agent) {
3940
3988
  this.readCache.set(this.agentListKey(state, chainId), {
@@ -3950,8 +3998,11 @@ var YieldseekerAgent = class {
3950
3998
  return this.contextForAgent(state, chainId, user, agent, asset);
3951
3999
  }
3952
4000
  async loadPortfolio(state, chainId, options) {
4001
+ const generation = this.readGeneration;
3953
4002
  const user = await this.resolveUser(state, chainId);
4003
+ this.assertSession(generation, chainId);
3954
4004
  const agents = await this.listAgents(state, chainId, user);
4005
+ this.assertSession(generation, chainId);
3955
4006
  const contexts = [];
3956
4007
  for (const agent of agents) {
3957
4008
  const asset = this.assetForAgent(agent);
@@ -3962,81 +4013,187 @@ var YieldseekerAgent = class {
3962
4013
  contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
3963
4014
  }
3964
4015
  const resolvedContexts = await Promise.all(contexts);
4016
+ this.assertSession(generation, chainId);
3965
4017
  return Promise.all(
3966
4018
  resolvedContexts.map(
3967
4019
  (context) => this.loadPortfolioContext(state, chainId, context, options)
3968
4020
  )
3969
4021
  );
3970
4022
  }
3971
- async loadPortfolioContext(state, chainId, context, options = {}) {
3972
- const contextKey = this.contextKey(state, chainId, context.asset);
3973
- const [snapshot, positions, historic, actions] = await Promise.all([
3974
- this.cachedRead(
3975
- `snapshot:${contextKey}`,
3976
- YIELDSEEKER_PORTFOLIO_CACHE_MS,
3977
- async () => {
3978
- const response = await this.walletRequest(
3979
- state,
3980
- chainId,
3981
- `${this.agentPath(context, "snapshot")}${query({
3982
- shouldOnlyUseRecentValue: true,
3983
- shouldAllowStaleOnError: true
3984
- })}`
3985
- );
3986
- if (!response?.agentSnapshot) {
3987
- throw this.invalidResponse("agent snapshot");
3988
- }
3989
- return response;
3990
- }
3991
- ),
3992
- this.cachedRead(
3993
- `positions:${contextKey}`,
3994
- YIELDSEEKER_PORTFOLIO_CACHE_MS,
3995
- async () => {
3996
- const response = await this.walletRequest(
3997
- state,
3998
- chainId,
3999
- this.agentPath(context, "yield-positions")
4000
- );
4001
- if (!Array.isArray(response?.yieldPositions)) {
4002
- throw this.invalidResponse("yield positions");
4023
+ portfolioVersion(contextKey) {
4024
+ return this.portfolioVersions.get(contextKey) ?? 0;
4025
+ }
4026
+ portfolioCacheMs(contextKey) {
4027
+ return (this.reconcileUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_CACHE_MS : YIELDSEEKER_PORTFOLIO_CACHE_MS;
4028
+ }
4029
+ snapshotMovement(state, chainId, context, delta) {
4030
+ const key2 = this.contextKey(state, chainId, context.asset);
4031
+ const previous = this.snapshotMovements.get(key2);
4032
+ if (this.snapshotMovements.has(key2)) {
4033
+ if (!previous || previous.delta > 0n !== delta > 0n) return null;
4034
+ return {
4035
+ ...previous,
4036
+ epoch: this.movementEpochs.get(key2) ?? 0,
4037
+ delta: previous.delta + delta,
4038
+ expectedBalance: previous.expectedBalance + delta,
4039
+ expectedNetDeposits: previous.expectedNetDeposits + delta
4040
+ };
4041
+ }
4042
+ const cached = this.readCache.get(`snapshot:${key2}`);
4043
+ if (!cached || cached.expiresAt <= Date.now()) return null;
4044
+ const snapshot = cached.value.agentSnapshot;
4045
+ if (snapshot.baseAssetDecimals !== YIELDSEEKER_ASSET_METADATA[context.asset].decimals) return null;
4046
+ if (!/^-?\d+$/.test(snapshot.netDepositsBase) || !/^\d+$/.test(snapshot.totalValueBase)) return null;
4047
+ return {
4048
+ epoch: this.movementEpochs.get(key2) ?? 0,
4049
+ delta,
4050
+ decimals: snapshot.baseAssetDecimals,
4051
+ expectedBalance: BigInt(snapshot.totalValueBase) + delta,
4052
+ expectedNetDeposits: BigInt(snapshot.netDepositsBase) + delta
4053
+ };
4054
+ }
4055
+ advancePortfolioVersion(contextKey) {
4056
+ const version = this.portfolioVersion(contextKey) + 1;
4057
+ this.portfolioVersions.set(contextKey, version);
4058
+ this.snapshotFailures.delete(contextKey);
4059
+ for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4060
+ const key2 = `${kind}:${contextKey}`;
4061
+ this.readCache.delete(key2);
4062
+ this.pendingReads.delete(key2);
4063
+ }
4064
+ return version;
4065
+ }
4066
+ requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, forceRefresh = false) {
4067
+ return this.walletRequest(
4068
+ state,
4069
+ chainId,
4070
+ `${this.agentPath(context, "snapshot")}${query(forceRefresh ? { shouldForceRefresh: true } : { shouldOnlyUseRecentValue: true, shouldAllowStaleOnError: true })}`
4071
+ ).then((response) => {
4072
+ if (!response?.agentSnapshot) {
4073
+ throw this.invalidResponse("agent snapshot");
4074
+ }
4075
+ if (this.readGeneration === generation && this.portfolioVersion(contextKey) === version) {
4076
+ this.snapshotFailures.delete(contextKey);
4077
+ const movement = this.snapshotMovements.get(contextKey);
4078
+ const snapshot = response.agentSnapshot;
4079
+ if (movement && snapshot.baseAssetDecimals === movement.decimals && /^-?\d+$/.test(snapshot.netDepositsBase) && /^\d+$/.test(snapshot.totalValueBase)) {
4080
+ const balance = BigInt(snapshot.totalValueBase);
4081
+ const netDeposits = BigInt(snapshot.netDepositsBase);
4082
+ const reconciled = movement.delta > 0n ? balance >= movement.expectedBalance && netDeposits >= movement.expectedNetDeposits : balance <= movement.expectedBalance && netDeposits <= movement.expectedNetDeposits;
4083
+ if (reconciled) {
4084
+ this.snapshotMovements.delete(contextKey);
4085
+ this.reconcileUntil.delete(contextKey);
4003
4086
  }
4004
- return response;
4005
4087
  }
4006
- ),
4007
- options.historic ? this.cachedRead(
4008
- `historic:${contextKey}`,
4009
- YIELDSEEKER_ACTIVITY_CACHE_MS,
4010
- () => this.walletRequest(
4011
- state,
4012
- chainId,
4013
- this.agentPath(context, "wallet/historic-position")
4014
- )
4015
- ) : Promise.resolve(void 0),
4016
- options.actions ? this.cachedRead(
4017
- `actions:${contextKey}`,
4018
- YIELDSEEKER_ACTIVITY_CACHE_MS,
4019
- () => this.walletRequest(
4088
+ }
4089
+ return response;
4090
+ }).catch((error) => {
4091
+ if (this.readGeneration === generation && this.portfolioVersion(contextKey) === version) {
4092
+ this.snapshotFailures.set(contextKey, {
4093
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, rateLimitDelay(error) ?? 0),
4094
+ error
4095
+ });
4096
+ }
4097
+ throw error;
4098
+ });
4099
+ }
4100
+ portfolioSnapshot(state, chainId, context, contextKey) {
4101
+ const key2 = `snapshot:${contextKey}`;
4102
+ const cached = this.readCache.get(key2);
4103
+ if ((!cached || cached.expiresAt <= Date.now()) && !this.pendingReads.has(key2)) {
4104
+ const failure = this.snapshotFailures.get(contextKey);
4105
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4106
+ this.advancePortfolioVersion(contextKey);
4107
+ }
4108
+ const version = this.portfolioVersion(contextKey);
4109
+ const generation = this.readGeneration;
4110
+ return {
4111
+ version,
4112
+ read: this.cachedRead(key2, this.portfolioCacheMs(contextKey), () => (
4113
+ // Only event-driven reconciliation forces refreshes. Routine reads
4114
+ // remain non-forced; the cache and shared cooldown still bound calls.
4115
+ this.requestPortfolioSnapshot(
4020
4116
  state,
4021
4117
  chainId,
4022
- this.agentPath(context, "actions")
4118
+ context,
4119
+ contextKey,
4120
+ version,
4121
+ generation,
4122
+ this.snapshotMovements.has(contextKey)
4023
4123
  )
4024
- ) : Promise.resolve(void 0)
4025
- ]);
4026
- if (!snapshot?.agentSnapshot) {
4027
- throw this.invalidResponse("agent snapshot");
4028
- }
4029
- if (!Array.isArray(positions?.yieldPositions)) {
4030
- throw this.invalidResponse("yield positions");
4031
- }
4032
- return {
4033
- ...context,
4034
- snapshot: snapshot.agentSnapshot,
4035
- positions: positions.yieldPositions,
4036
- ...historic?.position ? { historic: historic.position } : {},
4037
- ...actions?.actions ? { actions: actions.actions } : {}
4124
+ ))
4038
4125
  };
4039
4126
  }
4127
+ async loadPortfolioContext(state, chainId, context, options = {}) {
4128
+ const contextKey = this.contextKey(state, chainId, context.asset);
4129
+ const generation = this.readGeneration;
4130
+ const assertSession = () => this.assertSession(generation, chainId, context.asset);
4131
+ for (let attempt = 0; attempt < 2; attempt++) {
4132
+ const { version, read: read2 } = this.portfolioSnapshot(state, chainId, context, contextKey);
4133
+ try {
4134
+ const snapshot = await read2;
4135
+ assertSession();
4136
+ if (this.portfolioVersion(contextKey) !== version) continue;
4137
+ const [positions, historic, actions] = await Promise.all([
4138
+ this.cachedRead(
4139
+ `positions:${contextKey}`,
4140
+ this.portfolioCacheMs(contextKey),
4141
+ async () => {
4142
+ const response = await this.walletRequest(
4143
+ state,
4144
+ chainId,
4145
+ this.agentPath(context, "yield-positions")
4146
+ );
4147
+ if (!Array.isArray(response?.yieldPositions)) {
4148
+ throw this.invalidResponse("yield positions");
4149
+ }
4150
+ return response;
4151
+ }
4152
+ ),
4153
+ options.historic ? this.cachedRead(
4154
+ `historic:${contextKey}`,
4155
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4156
+ () => this.walletRequest(
4157
+ state,
4158
+ chainId,
4159
+ this.agentPath(context, "wallet/historic-position")
4160
+ )
4161
+ ) : Promise.resolve(void 0),
4162
+ options.actions ? this.cachedRead(
4163
+ `actions:${contextKey}`,
4164
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4165
+ () => this.walletRequest(
4166
+ state,
4167
+ chainId,
4168
+ this.agentPath(context, "actions")
4169
+ )
4170
+ ) : Promise.resolve(void 0)
4171
+ ]);
4172
+ assertSession();
4173
+ if (this.portfolioVersion(contextKey) !== version) continue;
4174
+ if (!Array.isArray(positions?.yieldPositions)) {
4175
+ throw this.invalidResponse("yield positions");
4176
+ }
4177
+ return {
4178
+ ...context,
4179
+ snapshot: snapshot.agentSnapshot,
4180
+ positions: positions.yieldPositions,
4181
+ ...historic?.position ? { historic: historic.position } : {},
4182
+ ...actions?.actions ? { actions: actions.actions } : {}
4183
+ };
4184
+ } catch (error) {
4185
+ assertSession();
4186
+ if (this.portfolioVersion(contextKey) !== version) continue;
4187
+ throw error;
4188
+ }
4189
+ }
4190
+ throw new OwneyError(
4191
+ "AGENT_API_ERROR",
4192
+ "Yieldseeker portfolio changed during the read. Please retry.",
4193
+ { rpcSource: "agent-api", chainId, asset: context.asset },
4194
+ this.id
4195
+ );
4196
+ }
4040
4197
  async deployAgent(state, chainId, context) {
4041
4198
  if (context.wallet.initializedDate != null) return;
4042
4199
  const walletAddress = context.wallet.walletAddress.toLowerCase();
@@ -4053,42 +4210,52 @@ var YieldseekerAgent = class {
4053
4210
  }
4054
4211
  context.wallet = deployed.agentWallet;
4055
4212
  }
4056
- async refreshSnapshotAfterMovement(state, chainId, context, movement) {
4213
+ async refreshSnapshotAfterMovement(state, chainId, context, movement, settlement) {
4057
4214
  const contextKey = this.contextKey(state, chainId, context.asset);
4058
- const invalidatePortfolio = () => {
4059
- for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4060
- this.readCache.delete(`${kind}:${contextKey}`);
4061
- }
4062
- };
4063
- invalidatePortfolio();
4215
+ this.reconcileUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4216
+ this.activityRefreshUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4217
+ const epoch = this.movementEpochs.get(contextKey) ?? 0;
4218
+ this.snapshotMovements.set(contextKey, settlement?.epoch === epoch ? settlement : null);
4219
+ this.movementEpochs.set(contextKey, epoch + 1);
4220
+ const version = this.advancePortfolioVersion(contextKey);
4221
+ const generation = this.readGeneration;
4064
4222
  try {
4065
- const response = await this.walletRequest(
4066
- state,
4067
- chainId,
4068
- `${this.agentPath(context, "snapshot")}${query({
4069
- shouldForceRefresh: true
4070
- })}`
4223
+ await this.cachedRead(
4224
+ `snapshot:${contextKey}`,
4225
+ YIELDSEEKER_SETTLEMENT_CACHE_MS,
4226
+ () => this.requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, true)
4071
4227
  );
4072
- if (!response?.agentSnapshot) {
4073
- throw this.invalidResponse("agent snapshot refresh");
4074
- }
4075
4228
  } catch (error) {
4076
4229
  console.warn(
4077
4230
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
4078
4231
  error
4079
4232
  );
4080
- } finally {
4081
- invalidatePortfolio();
4082
4233
  }
4083
4234
  }
4084
4235
  agentPath(context, suffix) {
4085
4236
  return `/users/${context.user.userId}/agents/${context.agent.agentId}/${suffix}`;
4086
4237
  }
4087
4238
  async walletRequest(state, chainId, path, options = {}) {
4239
+ const read2 = (options.method ?? "GET") === "GET";
4240
+ const userKey = this.userKey(state, chainId);
4241
+ if (read2) {
4242
+ const failure = this.standardReadFailures.get(userKey);
4243
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4244
+ if (failure) this.standardReadFailures.delete(userKey);
4245
+ }
4246
+ const generation = this.readGeneration;
4088
4247
  try {
4089
4248
  return await this.providerRequest(state, chainId, path, options);
4090
4249
  } catch (error) {
4091
- throw this.mapApiError(error);
4250
+ const mapped = this.mapApiError(error);
4251
+ const delay = read2 ? rateLimitDelay(mapped) : void 0;
4252
+ if (delay !== void 0 && this.readGeneration === generation) {
4253
+ this.standardReadFailures.set(userKey, {
4254
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, delay),
4255
+ error: mapped
4256
+ });
4257
+ }
4258
+ throw mapped;
4092
4259
  }
4093
4260
  }
4094
4261
  async providerRequest(state, chainId, path, options = {}) {
@@ -4385,16 +4552,16 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
4385
4552
  const positionChain = position2.chain.trim().toUpperCase();
4386
4553
  const matchesChain = positionChain === String(chainId) || targetChain !== void 0 && positionChain === targetChain;
4387
4554
  if (!matchesChain || position2.asset.toUpperCase() !== target) continue;
4388
- if (position2.amountRaw !== void 0) {
4389
- try {
4390
- balance += BigInt(position2.amountRaw);
4391
- continue;
4392
- } catch {
4393
- }
4394
- }
4395
- balance += (0, import_viem8.parseUnits)(position2.amount, decimals);
4555
+ balance += position2.withdrawableAmountRaw !== void 0 ? BigInt(position2.withdrawableAmountRaw) : (0, import_viem8.parseUnits)(position2.amount, decimals);
4396
4556
  }
4397
4557
  }
4558
+ const assetTotal = agentBalance?.assetBalances?.find(
4559
+ (t) => t.chainId === chainId && t.asset.toUpperCase() === target
4560
+ );
4561
+ if (assetTotal) {
4562
+ const total = (0, import_viem8.parseUnits)(assetTotal.amount, decimals);
4563
+ if (total < balance) balance = total > 0n ? total : 0n;
4564
+ }
4398
4565
  return { agent, balance };
4399
4566
  });
4400
4567
  }
@@ -6015,7 +6182,14 @@ var OwneySDK = class {
6015
6182
  * @returns {AgentWithdrawResult} for a single agent, or {OwneyWithdrawResult} with per-agent results
6016
6183
  */
6017
6184
  async withdraw(options) {
6018
- const { asset, amount, agentId } = options;
6185
+ const { asset, amount, agentId, onAgentResult } = options;
6186
+ const notifyAgentResult = (id, result) => {
6187
+ try {
6188
+ onAgentResult?.(id, result);
6189
+ } catch (error) {
6190
+ console.warn("Withdrawal result observer failed:", error);
6191
+ }
6192
+ };
6019
6193
  const state = this.requireState();
6020
6194
  const chainId = this.requireChainId();
6021
6195
  const token = asset;
@@ -6031,12 +6205,14 @@ var OwneySDK = class {
6031
6205
  if (agentId) {
6032
6206
  const agent = this.getAgent(agentId);
6033
6207
  this.validateAssetSupport(agent, chainId, asset);
6034
- return withFailureReporting(
6208
+ const result = await withFailureReporting(
6035
6209
  this.apiKey,
6036
6210
  agent.id,
6037
6211
  () => agent.withdraw(state, chainId, token, amount),
6038
6212
  this.routingApiBaseUrl
6039
6213
  );
6214
+ notifyAgentResult(agent.id, result);
6215
+ return result;
6040
6216
  }
6041
6217
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
6042
6218
  const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
@@ -6046,6 +6222,7 @@ var OwneySDK = class {
6046
6222
  for (const agent of withdrawalAgents) {
6047
6223
  try {
6048
6224
  results2[agent.id] = await agent.withdraw(state, chainId, token);
6225
+ notifyAgentResult(agent.id, results2[agent.id]);
6049
6226
  } catch (err) {
6050
6227
  if (this.isUserRejectedWithdrawal(err)) throw err;
6051
6228
  console.error(`withdraw failed for agent "${agent.id}":`, err);
@@ -6143,6 +6320,7 @@ var OwneySDK = class {
6143
6320
  token,
6144
6321
  p.planned.toString()
6145
6322
  );
6323
+ notifyAgentResult(p.agent.id, results[p.agent.id]);
6146
6324
  } catch (err) {
6147
6325
  if (this.isUserRejectedWithdrawal(err)) throw err;
6148
6326
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);