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

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
@@ -2887,10 +2887,11 @@ var YieldseekerApiClient = class {
2887
2887
  const payload = await response.json().catch(() => null);
2888
2888
  if (!response.ok) {
2889
2889
  const error = providerError(payload, `HTTP_${response.status}`);
2890
+ const retryAfter = response.headers.get("Retry-After");
2890
2891
  throw new YieldseekerApiError(
2891
2892
  response.status,
2892
2893
  error.code,
2893
- error.fields
2894
+ retryAfter ? { ...error.fields, headers: { "Retry-After": retryAfter } } : error.fields
2894
2895
  );
2895
2896
  }
2896
2897
  if (payload && typeof payload === "object" && payload.success === true && "data" in payload) {
@@ -3021,13 +3022,14 @@ function position(value, asset, baseAssetDecimals) {
3021
3022
  // differ from the underlying asset. Yieldseeker already converts it to
3022
3023
  // underlying base-asset units in `assetsBase`; pair that value with the
3023
3024
  // snapshot's corresponding `baseAssetDecimals` for display. Keep the raw
3024
- // share quantity separately because withdraw-from-position expects it.
3025
+ // share quantity separate from the withdrawable underlying asset amount.
3025
3026
  amount: decimal(
3026
3027
  value.assetsBase,
3027
3028
  baseAssetDecimals,
3028
3029
  "yield positions"
3029
3030
  ),
3030
3031
  amountRaw: String(value.assetsRaw),
3032
+ withdrawableAmountRaw: String(value.withdrawableAssetsRaw),
3031
3033
  apy: percent(option.riskAdjustedApy),
3032
3034
  tvl: Number(option.totalDepositsUsd),
3033
3035
  liquidity: Number(option.withdrawableDepositsUsd)
@@ -3356,8 +3358,12 @@ var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3356
3358
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3357
3359
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3358
3360
  var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3359
- var YIELDSEEKER_AGENT_LIST_CACHE_MS = 6e4;
3360
- var YIELDSEEKER_PORTFOLIO_CACHE_MS = 3e4;
3361
+ var YIELDSEEKER_AGENT_LIST_CACHE_MS = 12e4;
3362
+ var YIELDSEEKER_PORTFOLIO_CACHE_MS = 12e4;
3363
+ var YIELDSEEKER_SETTLEMENT_CACHE_MS = 5e3;
3364
+ var YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS = 15e3;
3365
+ var YIELDSEEKER_READ_FAILURE_COOLDOWN_MS = 6e4;
3366
+ var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
3361
3367
  var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
3362
3368
  function generateYieldseekerUsername() {
3363
3369
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
@@ -3427,6 +3433,15 @@ var YieldseekerAgent = class {
3427
3433
  readCache = /* @__PURE__ */ new Map();
3428
3434
  pendingReads = /* @__PURE__ */ new Map();
3429
3435
  readGeneration = 0;
3436
+ portfolioVersions = /* @__PURE__ */ new Map();
3437
+ snapshotFailures = /* @__PURE__ */ new Map();
3438
+ standardReadFailures = /* @__PURE__ */ new Map();
3439
+ reconcileUntil = /* @__PURE__ */ new Map();
3440
+ activityRefreshUntil = /* @__PURE__ */ new Map();
3441
+ // null means a confirmed movement has no reliable baseline. Do not guess
3442
+ // settlement; keep slow reconciliation after the fast window expires.
3443
+ snapshotMovements = /* @__PURE__ */ new Map();
3444
+ movementEpochs = /* @__PURE__ */ new Map();
3430
3445
  yieldOptions = /* @__PURE__ */ new Map();
3431
3446
  pendingYieldOptions = /* @__PURE__ */ new Map();
3432
3447
  constructor(owneyApiKey, options = {}) {
@@ -3457,6 +3472,13 @@ var YieldseekerAgent = class {
3457
3472
  this.pendingWalletContexts.clear();
3458
3473
  this.readCache.clear();
3459
3474
  this.pendingReads.clear();
3475
+ this.portfolioVersions.clear();
3476
+ this.snapshotFailures.clear();
3477
+ this.standardReadFailures.clear();
3478
+ this.reconcileUntil.clear();
3479
+ this.activityRefreshUntil.clear();
3480
+ this.snapshotMovements.clear();
3481
+ this.movementEpochs.clear();
3460
3482
  }
3461
3483
  async activateAgent(state, chainId, asset) {
3462
3484
  this.assertChain(chainId);
@@ -3476,41 +3498,37 @@ var YieldseekerAgent = class {
3476
3498
  );
3477
3499
  }
3478
3500
  const context = await this.ensureAgent(state, chainId, asset);
3501
+ const generation = this.readGeneration;
3502
+ const movement = this.snapshotMovement(state, chainId, context, BigInt(amount));
3479
3503
  let txHash;
3480
- try {
3481
- if (depositCallback) {
3482
- provideDepositVerificationContext(depositCallback, {
3483
- agentId: "yieldseeker",
3484
- signature: await this.auth.getToken(state, chainId),
3485
- userId: context.user.userId,
3486
- yieldseekerAgentId: context.agent.agentId
3487
- });
3488
- txHash = await depositCallback(
3489
- context.wallet.walletAddress,
3490
- chainId,
3491
- amount
3492
- );
3493
- await this.waitForReceipt(state, chainId, txHash);
3494
- } else {
3495
- txHash = await this.submitTransaction(state, chainId, {
3496
- from: getAddress2(state.walletAddress),
3497
- to: YIELDSEEKER_ASSET_METADATA[asset].address,
3498
- data: encodeFunctionData({
3499
- abi: erc20Abi,
3500
- functionName: "transfer",
3501
- args: [getAddress2(context.wallet.walletAddress), BigInt(amount)]
3502
- }),
3503
- value: "0",
3504
- chainId
3505
- });
3506
- }
3507
- } finally {
3508
- await this.refreshSnapshotAfterMovement(
3509
- state,
3504
+ if (depositCallback) {
3505
+ provideDepositVerificationContext(depositCallback, {
3506
+ agentId: "yieldseeker",
3507
+ signature: await this.auth.getToken(state, chainId),
3508
+ userId: context.user.userId,
3509
+ yieldseekerAgentId: context.agent.agentId
3510
+ });
3511
+ txHash = await depositCallback(
3512
+ context.wallet.walletAddress,
3510
3513
  chainId,
3511
- context,
3512
- "deposit"
3514
+ amount
3513
3515
  );
3516
+ await this.waitForReceipt(state, chainId, txHash);
3517
+ } else {
3518
+ txHash = await this.submitTransaction(state, chainId, {
3519
+ from: getAddress2(state.walletAddress),
3520
+ to: YIELDSEEKER_ASSET_METADATA[asset].address,
3521
+ data: encodeFunctionData({
3522
+ abi: erc20Abi,
3523
+ functionName: "transfer",
3524
+ args: [getAddress2(context.wallet.walletAddress), BigInt(amount)]
3525
+ }),
3526
+ value: "0",
3527
+ chainId
3528
+ });
3529
+ }
3530
+ if (this.readGeneration === generation) {
3531
+ await this.refreshSnapshotAfterMovement(state, chainId, context, "deposit", movement);
3514
3532
  }
3515
3533
  return {
3516
3534
  txHash,
@@ -3538,6 +3556,9 @@ var YieldseekerAgent = class {
3538
3556
  this.id
3539
3557
  );
3540
3558
  }
3559
+ const generation = this.readGeneration;
3560
+ let confirmedMovement = false;
3561
+ let settlement = null;
3541
3562
  try {
3542
3563
  const portfolio = await this.loadPortfolioContext(
3543
3564
  state,
@@ -3555,6 +3576,7 @@ var YieldseekerAgent = class {
3555
3576
  );
3556
3577
  const totalAvailable = idle + deployed;
3557
3578
  const requested = amount === void 0 ? totalAvailable : BigInt(amount);
3579
+ const plannedMovement = this.snapshotMovement(state, chainId, context, -requested);
3558
3580
  if (requested > totalAvailable) {
3559
3581
  throw new OwneyError(
3560
3582
  "WITHDRAW_INSUFFICIENT_BALANCE",
@@ -3590,6 +3612,7 @@ var YieldseekerAgent = class {
3590
3612
  throw this.invalidResponse("position withdrawal");
3591
3613
  }
3592
3614
  await this.waitForReceipt(state, chainId, response.transactionHash);
3615
+ confirmedMovement = true;
3593
3616
  remaining -= assetsRaw;
3594
3617
  }
3595
3618
  if (remaining > 0n) {
@@ -3614,18 +3637,23 @@ var YieldseekerAgent = class {
3614
3637
  value: "0",
3615
3638
  chainId
3616
3639
  });
3640
+ confirmedMovement = true;
3641
+ settlement = plannedMovement;
3617
3642
  return {
3618
3643
  txHash,
3619
3644
  type: amount === void 0 ? "full" : "partial",
3620
3645
  amount: requested.toString()
3621
3646
  };
3622
3647
  } finally {
3623
- await this.refreshSnapshotAfterMovement(
3624
- state,
3625
- chainId,
3626
- context,
3627
- "withdrawal"
3628
- );
3648
+ if (confirmedMovement && this.readGeneration === generation) {
3649
+ await this.refreshSnapshotAfterMovement(
3650
+ state,
3651
+ chainId,
3652
+ context,
3653
+ "withdrawal",
3654
+ settlement
3655
+ );
3656
+ }
3629
3657
  }
3630
3658
  }
3631
3659
  async getBalances(state, chainId) {
@@ -3719,6 +3747,16 @@ var YieldseekerAgent = class {
3719
3747
  contextKey(state, chainId, asset) {
3720
3748
  return `${this.userKey(state, chainId)}:${asset}`;
3721
3749
  }
3750
+ assertSession(generation, chainId, asset) {
3751
+ if (this.readGeneration !== generation) {
3752
+ throw new OwneyError(
3753
+ "NOT_CONNECTED",
3754
+ "Wallet session changed during the Yieldseeker request.",
3755
+ { rpcSource: "agent-api", chainId, ...asset ? { asset } : {} },
3756
+ this.id
3757
+ );
3758
+ }
3759
+ }
3722
3760
  cachedRead(key2, ttlMs, read2) {
3723
3761
  const cached = this.readCache.get(key2);
3724
3762
  if (cached && cached.expiresAt > Date.now()) {
@@ -3728,7 +3766,7 @@ var YieldseekerAgent = class {
3728
3766
  if (pending) return pending;
3729
3767
  const generation = this.readGeneration;
3730
3768
  const request = Promise.resolve().then(read2).then((value) => {
3731
- if (this.readGeneration === generation) {
3769
+ if (this.readGeneration === generation && this.pendingReads.get(key2) === request) {
3732
3770
  this.readCache.set(key2, {
3733
3771
  expiresAt: Date.now() + ttlMs,
3734
3772
  value
@@ -3776,6 +3814,7 @@ var YieldseekerAgent = class {
3776
3814
  chainId,
3777
3815
  `/users/${user.userId}/agents/${agent.agentId}/wallet`
3778
3816
  ).then((walletResponse) => {
3817
+ this.assertSession(generation, chainId, asset);
3779
3818
  if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3780
3819
  throw this.invalidResponse("agent wallet");
3781
3820
  }
@@ -3785,9 +3824,7 @@ var YieldseekerAgent = class {
3785
3824
  wallet: walletResponse.agentWallet,
3786
3825
  asset
3787
3826
  };
3788
- if (this.readGeneration === generation) {
3789
- this.agentContexts.set(key2, context);
3790
- }
3827
+ this.agentContexts.set(key2, context);
3791
3828
  return context;
3792
3829
  });
3793
3830
  this.pendingWalletContexts.set(key2, request);
@@ -3809,6 +3846,7 @@ var YieldseekerAgent = class {
3809
3846
  return persisted;
3810
3847
  }
3811
3848
  const walletAddress = getAddress2(state.walletAddress);
3849
+ const generation = this.readGeneration;
3812
3850
  let user = null;
3813
3851
  try {
3814
3852
  const login = await this.providerRequest(
@@ -3855,6 +3893,7 @@ var YieldseekerAgent = class {
3855
3893
  if (!user || !/^[a-zA-Z0-9_-]{1,128}$/.test(user.userId)) {
3856
3894
  throw this.invalidResponse("wallet identity");
3857
3895
  }
3896
+ this.assertSession(generation, chainId);
3858
3897
  const resolved = { userId: user.userId };
3859
3898
  this.users.set(key2, resolved);
3860
3899
  writeYieldseekerIdentity(state.walletAddress, chainId, resolved.userId);
@@ -3870,10 +3909,13 @@ var YieldseekerAgent = class {
3870
3909
  if (cached) return cached;
3871
3910
  const pending = this.pendingAgents.get(key2);
3872
3911
  if (pending) return pending;
3912
+ const generation = this.readGeneration;
3873
3913
  const request = this.resolveAgent(state, chainId, asset, true).then(
3874
3914
  async (context) => {
3915
+ this.assertSession(generation, chainId, asset);
3875
3916
  if (!context) throw this.invalidResponse("agent creation");
3876
3917
  await this.deployAgent(state, chainId, context);
3918
+ this.assertSession(generation, chainId, asset);
3877
3919
  this.agentContexts.set(key2, context);
3878
3920
  return context;
3879
3921
  }
@@ -3882,20 +3924,25 @@ var YieldseekerAgent = class {
3882
3924
  try {
3883
3925
  return await request;
3884
3926
  } finally {
3885
- this.pendingAgents.delete(key2);
3927
+ if (this.pendingAgents.get(key2) === request) this.pendingAgents.delete(key2);
3886
3928
  }
3887
3929
  }
3888
3930
  async findAgent(state, chainId, asset) {
3889
3931
  const key2 = this.contextKey(state, chainId, asset);
3890
3932
  const cached = this.agentContexts.get(key2);
3891
3933
  if (cached) return cached;
3934
+ const generation = this.readGeneration;
3892
3935
  const context = await this.resolveAgent(state, chainId, asset, false);
3936
+ this.assertSession(generation, chainId, asset);
3893
3937
  if (context) this.agentContexts.set(key2, context);
3894
3938
  return context;
3895
3939
  }
3896
3940
  async resolveAgent(state, chainId, asset, createIfMissing) {
3941
+ const generation = this.readGeneration;
3897
3942
  const user = await this.resolveUser(state, chainId);
3943
+ this.assertSession(generation, chainId, asset);
3898
3944
  const agents = await this.listAgents(state, chainId, user);
3945
+ this.assertSession(generation, chainId, asset);
3899
3946
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3900
3947
  let agent = agents.find(
3901
3948
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
@@ -3917,6 +3964,7 @@ var YieldseekerAgent = class {
3917
3964
  }
3918
3965
  }
3919
3966
  );
3967
+ this.assertSession(generation, chainId, asset);
3920
3968
  agent = created?.agent;
3921
3969
  if (agent) {
3922
3970
  this.readCache.set(this.agentListKey(state, chainId), {
@@ -3932,8 +3980,11 @@ var YieldseekerAgent = class {
3932
3980
  return this.contextForAgent(state, chainId, user, agent, asset);
3933
3981
  }
3934
3982
  async loadPortfolio(state, chainId, options) {
3983
+ const generation = this.readGeneration;
3935
3984
  const user = await this.resolveUser(state, chainId);
3985
+ this.assertSession(generation, chainId);
3936
3986
  const agents = await this.listAgents(state, chainId, user);
3987
+ this.assertSession(generation, chainId);
3937
3988
  const contexts = [];
3938
3989
  for (const agent of agents) {
3939
3990
  const asset = this.assetForAgent(agent);
@@ -3944,81 +3995,188 @@ var YieldseekerAgent = class {
3944
3995
  contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
3945
3996
  }
3946
3997
  const resolvedContexts = await Promise.all(contexts);
3998
+ this.assertSession(generation, chainId);
3947
3999
  return Promise.all(
3948
4000
  resolvedContexts.map(
3949
4001
  (context) => this.loadPortfolioContext(state, chainId, context, options)
3950
4002
  )
3951
4003
  );
3952
4004
  }
3953
- async loadPortfolioContext(state, chainId, context, options = {}) {
3954
- const contextKey = this.contextKey(state, chainId, context.asset);
3955
- const [snapshot, positions, historic, actions] = await Promise.all([
3956
- this.cachedRead(
3957
- `snapshot:${contextKey}`,
3958
- YIELDSEEKER_PORTFOLIO_CACHE_MS,
3959
- async () => {
3960
- const response = await this.walletRequest(
3961
- state,
3962
- chainId,
3963
- `${this.agentPath(context, "snapshot")}${query({
3964
- shouldOnlyUseRecentValue: true,
3965
- shouldAllowStaleOnError: true
3966
- })}`
3967
- );
3968
- if (!response?.agentSnapshot) {
3969
- throw this.invalidResponse("agent snapshot");
3970
- }
3971
- return response;
3972
- }
3973
- ),
3974
- this.cachedRead(
3975
- `positions:${contextKey}`,
3976
- YIELDSEEKER_PORTFOLIO_CACHE_MS,
3977
- async () => {
3978
- const response = await this.walletRequest(
3979
- state,
3980
- chainId,
3981
- this.agentPath(context, "yield-positions")
3982
- );
3983
- if (!Array.isArray(response?.yieldPositions)) {
3984
- throw this.invalidResponse("yield positions");
4005
+ portfolioVersion(contextKey) {
4006
+ return this.portfolioVersions.get(contextKey) ?? 0;
4007
+ }
4008
+ portfolioCacheMs(contextKey) {
4009
+ return (this.reconcileUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_CACHE_MS : YIELDSEEKER_PORTFOLIO_CACHE_MS;
4010
+ }
4011
+ snapshotMovement(state, chainId, context, delta) {
4012
+ const key2 = this.contextKey(state, chainId, context.asset);
4013
+ const previous = this.snapshotMovements.get(key2);
4014
+ if (this.snapshotMovements.has(key2)) {
4015
+ if (!previous || previous.delta > 0n !== delta > 0n) return null;
4016
+ return {
4017
+ ...previous,
4018
+ epoch: this.movementEpochs.get(key2) ?? 0,
4019
+ delta: previous.delta + delta,
4020
+ expectedBalance: previous.expectedBalance + delta,
4021
+ expectedNetDeposits: previous.expectedNetDeposits + delta
4022
+ };
4023
+ }
4024
+ const cached = this.readCache.get(`snapshot:${key2}`);
4025
+ if (!cached || cached.expiresAt <= Date.now()) return null;
4026
+ const snapshot = cached.value.agentSnapshot;
4027
+ if (snapshot.baseAssetDecimals !== YIELDSEEKER_ASSET_METADATA[context.asset].decimals) return null;
4028
+ if (!/^-?\d+$/.test(snapshot.netDepositsBase) || !/^\d+$/.test(snapshot.totalValueBase)) return null;
4029
+ return {
4030
+ epoch: this.movementEpochs.get(key2) ?? 0,
4031
+ delta,
4032
+ decimals: snapshot.baseAssetDecimals,
4033
+ expectedBalance: BigInt(snapshot.totalValueBase) + delta,
4034
+ expectedNetDeposits: BigInt(snapshot.netDepositsBase) + delta
4035
+ };
4036
+ }
4037
+ advancePortfolioVersion(contextKey) {
4038
+ const version = this.portfolioVersion(contextKey) + 1;
4039
+ this.portfolioVersions.set(contextKey, version);
4040
+ this.snapshotFailures.delete(contextKey);
4041
+ for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4042
+ const key2 = `${kind}:${contextKey}`;
4043
+ this.readCache.delete(key2);
4044
+ this.pendingReads.delete(key2);
4045
+ }
4046
+ return version;
4047
+ }
4048
+ requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, forceRefresh = false) {
4049
+ return this.walletRequest(
4050
+ state,
4051
+ chainId,
4052
+ `${this.agentPath(context, "snapshot")}${query(forceRefresh ? { shouldForceRefresh: true } : { shouldOnlyUseRecentValue: true, shouldAllowStaleOnError: true })}`
4053
+ ).then((response) => {
4054
+ if (!response?.agentSnapshot) {
4055
+ throw this.invalidResponse("agent snapshot");
4056
+ }
4057
+ if (this.readGeneration === generation && this.portfolioVersion(contextKey) === version) {
4058
+ this.snapshotFailures.delete(contextKey);
4059
+ const movement = this.snapshotMovements.get(contextKey);
4060
+ const snapshot = response.agentSnapshot;
4061
+ if (movement && snapshot.baseAssetDecimals === movement.decimals && /^-?\d+$/.test(snapshot.netDepositsBase) && /^\d+$/.test(snapshot.totalValueBase)) {
4062
+ const balance = BigInt(snapshot.totalValueBase);
4063
+ const netDeposits = BigInt(snapshot.netDepositsBase);
4064
+ const reconciled = movement.delta > 0n ? balance >= movement.expectedBalance && netDeposits >= movement.expectedNetDeposits : balance <= movement.expectedBalance && netDeposits <= movement.expectedNetDeposits;
4065
+ if (reconciled) {
4066
+ this.snapshotMovements.delete(contextKey);
4067
+ this.reconcileUntil.delete(contextKey);
3985
4068
  }
3986
- return response;
3987
4069
  }
3988
- ),
3989
- options.historic ? this.cachedRead(
3990
- `historic:${contextKey}`,
3991
- YIELDSEEKER_ACTIVITY_CACHE_MS,
3992
- () => this.walletRequest(
3993
- state,
3994
- chainId,
3995
- this.agentPath(context, "wallet/historic-position")
3996
- )
3997
- ) : Promise.resolve(void 0),
3998
- options.actions ? this.cachedRead(
3999
- `actions:${contextKey}`,
4000
- YIELDSEEKER_ACTIVITY_CACHE_MS,
4001
- () => this.walletRequest(
4070
+ }
4071
+ return response;
4072
+ }).catch((error) => {
4073
+ if (this.readGeneration === generation && this.portfolioVersion(contextKey) === version) {
4074
+ this.snapshotFailures.set(contextKey, {
4075
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, rateLimitDelay(error) ?? 0),
4076
+ error
4077
+ });
4078
+ }
4079
+ throw error;
4080
+ });
4081
+ }
4082
+ portfolioSnapshot(state, chainId, context, contextKey) {
4083
+ const key2 = `snapshot:${contextKey}`;
4084
+ const cached = this.readCache.get(key2);
4085
+ if ((!cached || cached.expiresAt <= Date.now()) && !this.pendingReads.has(key2)) {
4086
+ const failure = this.snapshotFailures.get(contextKey);
4087
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4088
+ this.advancePortfolioVersion(contextKey);
4089
+ }
4090
+ const version = this.portfolioVersion(contextKey);
4091
+ const generation = this.readGeneration;
4092
+ return {
4093
+ version,
4094
+ read: this.cachedRead(key2, this.portfolioCacheMs(contextKey), () => (
4095
+ // Force only during the bounded post-transaction window. Keep unknown
4096
+ // or unreconciled movements for verification, but do not let them turn
4097
+ // routine polling into forced refreshes indefinitely.
4098
+ this.requestPortfolioSnapshot(
4002
4099
  state,
4003
4100
  chainId,
4004
- this.agentPath(context, "actions")
4101
+ context,
4102
+ contextKey,
4103
+ version,
4104
+ generation,
4105
+ this.snapshotMovements.has(contextKey) && (this.reconcileUntil.get(contextKey) ?? 0) > Date.now()
4005
4106
  )
4006
- ) : Promise.resolve(void 0)
4007
- ]);
4008
- if (!snapshot?.agentSnapshot) {
4009
- throw this.invalidResponse("agent snapshot");
4010
- }
4011
- if (!Array.isArray(positions?.yieldPositions)) {
4012
- throw this.invalidResponse("yield positions");
4013
- }
4014
- return {
4015
- ...context,
4016
- snapshot: snapshot.agentSnapshot,
4017
- positions: positions.yieldPositions,
4018
- ...historic?.position ? { historic: historic.position } : {},
4019
- ...actions?.actions ? { actions: actions.actions } : {}
4107
+ ))
4020
4108
  };
4021
4109
  }
4110
+ async loadPortfolioContext(state, chainId, context, options = {}) {
4111
+ const contextKey = this.contextKey(state, chainId, context.asset);
4112
+ const generation = this.readGeneration;
4113
+ const assertSession = () => this.assertSession(generation, chainId, context.asset);
4114
+ for (let attempt = 0; attempt < 2; attempt++) {
4115
+ const { version, read: read2 } = this.portfolioSnapshot(state, chainId, context, contextKey);
4116
+ try {
4117
+ const snapshot = await read2;
4118
+ assertSession();
4119
+ if (this.portfolioVersion(contextKey) !== version) continue;
4120
+ const [positions, historic, actions] = await Promise.all([
4121
+ this.cachedRead(
4122
+ `positions:${contextKey}`,
4123
+ this.portfolioCacheMs(contextKey),
4124
+ async () => {
4125
+ const response = await this.walletRequest(
4126
+ state,
4127
+ chainId,
4128
+ this.agentPath(context, "yield-positions")
4129
+ );
4130
+ if (!Array.isArray(response?.yieldPositions)) {
4131
+ throw this.invalidResponse("yield positions");
4132
+ }
4133
+ return response;
4134
+ }
4135
+ ),
4136
+ options.historic ? this.cachedRead(
4137
+ `historic:${contextKey}`,
4138
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4139
+ () => this.walletRequest(
4140
+ state,
4141
+ chainId,
4142
+ this.agentPath(context, "wallet/historic-position")
4143
+ )
4144
+ ) : Promise.resolve(void 0),
4145
+ options.actions ? this.cachedRead(
4146
+ `actions:${contextKey}`,
4147
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4148
+ () => this.walletRequest(
4149
+ state,
4150
+ chainId,
4151
+ this.agentPath(context, "actions")
4152
+ )
4153
+ ) : Promise.resolve(void 0)
4154
+ ]);
4155
+ assertSession();
4156
+ if (this.portfolioVersion(contextKey) !== version) continue;
4157
+ if (!Array.isArray(positions?.yieldPositions)) {
4158
+ throw this.invalidResponse("yield positions");
4159
+ }
4160
+ return {
4161
+ ...context,
4162
+ snapshot: snapshot.agentSnapshot,
4163
+ positions: positions.yieldPositions,
4164
+ ...historic?.position ? { historic: historic.position } : {},
4165
+ ...actions?.actions ? { actions: actions.actions } : {}
4166
+ };
4167
+ } catch (error) {
4168
+ assertSession();
4169
+ if (this.portfolioVersion(contextKey) !== version) continue;
4170
+ throw error;
4171
+ }
4172
+ }
4173
+ throw new OwneyError(
4174
+ "AGENT_API_ERROR",
4175
+ "Yieldseeker portfolio changed during the read. Please retry.",
4176
+ { rpcSource: "agent-api", chainId, asset: context.asset },
4177
+ this.id
4178
+ );
4179
+ }
4022
4180
  async deployAgent(state, chainId, context) {
4023
4181
  if (context.wallet.initializedDate != null) return;
4024
4182
  const walletAddress = context.wallet.walletAddress.toLowerCase();
@@ -4035,42 +4193,52 @@ var YieldseekerAgent = class {
4035
4193
  }
4036
4194
  context.wallet = deployed.agentWallet;
4037
4195
  }
4038
- async refreshSnapshotAfterMovement(state, chainId, context, movement) {
4196
+ async refreshSnapshotAfterMovement(state, chainId, context, movement, settlement) {
4039
4197
  const contextKey = this.contextKey(state, chainId, context.asset);
4040
- const invalidatePortfolio = () => {
4041
- for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4042
- this.readCache.delete(`${kind}:${contextKey}`);
4043
- }
4044
- };
4045
- invalidatePortfolio();
4198
+ this.reconcileUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4199
+ this.activityRefreshUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4200
+ const epoch = this.movementEpochs.get(contextKey) ?? 0;
4201
+ this.snapshotMovements.set(contextKey, settlement?.epoch === epoch ? settlement : null);
4202
+ this.movementEpochs.set(contextKey, epoch + 1);
4203
+ const version = this.advancePortfolioVersion(contextKey);
4204
+ const generation = this.readGeneration;
4046
4205
  try {
4047
- const response = await this.walletRequest(
4048
- state,
4049
- chainId,
4050
- `${this.agentPath(context, "snapshot")}${query({
4051
- shouldForceRefresh: true
4052
- })}`
4206
+ await this.cachedRead(
4207
+ `snapshot:${contextKey}`,
4208
+ YIELDSEEKER_SETTLEMENT_CACHE_MS,
4209
+ () => this.requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, true)
4053
4210
  );
4054
- if (!response?.agentSnapshot) {
4055
- throw this.invalidResponse("agent snapshot refresh");
4056
- }
4057
4211
  } catch (error) {
4058
4212
  console.warn(
4059
4213
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
4060
4214
  error
4061
4215
  );
4062
- } finally {
4063
- invalidatePortfolio();
4064
4216
  }
4065
4217
  }
4066
4218
  agentPath(context, suffix) {
4067
4219
  return `/users/${context.user.userId}/agents/${context.agent.agentId}/${suffix}`;
4068
4220
  }
4069
4221
  async walletRequest(state, chainId, path, options = {}) {
4222
+ const read2 = (options.method ?? "GET") === "GET";
4223
+ const userKey = this.userKey(state, chainId);
4224
+ if (read2) {
4225
+ const failure = this.standardReadFailures.get(userKey);
4226
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4227
+ if (failure) this.standardReadFailures.delete(userKey);
4228
+ }
4229
+ const generation = this.readGeneration;
4070
4230
  try {
4071
4231
  return await this.providerRequest(state, chainId, path, options);
4072
4232
  } catch (error) {
4073
- throw this.mapApiError(error);
4233
+ const mapped = this.mapApiError(error);
4234
+ const delay = read2 ? rateLimitDelay(mapped) : void 0;
4235
+ if (delay !== void 0 && this.readGeneration === generation) {
4236
+ this.standardReadFailures.set(userKey, {
4237
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, delay),
4238
+ error: mapped
4239
+ });
4240
+ }
4241
+ throw mapped;
4074
4242
  }
4075
4243
  }
4076
4244
  async providerRequest(state, chainId, path, options = {}) {
@@ -4367,16 +4535,16 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
4367
4535
  const positionChain = position2.chain.trim().toUpperCase();
4368
4536
  const matchesChain = positionChain === String(chainId) || targetChain !== void 0 && positionChain === targetChain;
4369
4537
  if (!matchesChain || position2.asset.toUpperCase() !== target) continue;
4370
- if (position2.amountRaw !== void 0) {
4371
- try {
4372
- balance += BigInt(position2.amountRaw);
4373
- continue;
4374
- } catch {
4375
- }
4376
- }
4377
- balance += parseUnits(position2.amount, decimals);
4538
+ balance += position2.withdrawableAmountRaw !== void 0 ? BigInt(position2.withdrawableAmountRaw) : parseUnits(position2.amount, decimals);
4378
4539
  }
4379
4540
  }
4541
+ const assetTotal = agentBalance?.assetBalances?.find(
4542
+ (t) => t.chainId === chainId && t.asset.toUpperCase() === target
4543
+ );
4544
+ if (assetTotal) {
4545
+ const total = parseUnits(assetTotal.amount, decimals);
4546
+ if (total < balance) balance = total > 0n ? total : 0n;
4547
+ }
4380
4548
  return { agent, balance };
4381
4549
  });
4382
4550
  }
@@ -6005,7 +6173,14 @@ var OwneySDK = class {
6005
6173
  * @returns {AgentWithdrawResult} for a single agent, or {OwneyWithdrawResult} with per-agent results
6006
6174
  */
6007
6175
  async withdraw(options) {
6008
- const { asset, amount, agentId } = options;
6176
+ const { asset, amount, agentId, onAgentResult } = options;
6177
+ const notifyAgentResult = (id, result) => {
6178
+ try {
6179
+ onAgentResult?.(id, result);
6180
+ } catch (error) {
6181
+ console.warn("Withdrawal result observer failed:", error);
6182
+ }
6183
+ };
6009
6184
  const state = this.requireState();
6010
6185
  const chainId = this.requireChainId();
6011
6186
  const token = asset;
@@ -6021,12 +6196,14 @@ var OwneySDK = class {
6021
6196
  if (agentId) {
6022
6197
  const agent = this.getAgent(agentId);
6023
6198
  this.validateAssetSupport(agent, chainId, asset);
6024
- return withFailureReporting(
6199
+ const result = await withFailureReporting(
6025
6200
  this.apiKey,
6026
6201
  agent.id,
6027
6202
  () => agent.withdraw(state, chainId, token, amount),
6028
6203
  this.routingApiBaseUrl
6029
6204
  );
6205
+ notifyAgentResult(agent.id, result);
6206
+ return result;
6030
6207
  }
6031
6208
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
6032
6209
  const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
@@ -6036,6 +6213,7 @@ var OwneySDK = class {
6036
6213
  for (const agent of withdrawalAgents) {
6037
6214
  try {
6038
6215
  results2[agent.id] = await agent.withdraw(state, chainId, token);
6216
+ notifyAgentResult(agent.id, results2[agent.id]);
6039
6217
  } catch (err) {
6040
6218
  if (this.isUserRejectedWithdrawal(err)) throw err;
6041
6219
  console.error(`withdraw failed for agent "${agent.id}":`, err);
@@ -6133,6 +6311,7 @@ var OwneySDK = class {
6133
6311
  token,
6134
6312
  p.planned.toString()
6135
6313
  );
6314
+ notifyAgentResult(p.agent.id, results[p.agent.id]);
6136
6315
  } catch (err) {
6137
6316
  if (this.isUserRejectedWithdrawal(err)) throw err;
6138
6317
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);