@owney/sdk 0.7.25-beta.6 → 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
@@ -1133,21 +1133,32 @@ function protocolsPolicyNeedsUpdate(current, desiredProtocols, desiredAutoSelect
1133
1133
  var import_viem = require("viem");
1134
1134
  var PAID_RPC_RETRY_COUNT = 3;
1135
1135
  var PAID_RPC_RETRY_DELAY_MS = 1e3;
1136
- var DEFAULT_RPC_URLS = {
1137
- 1: "https://eth-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1138
- 8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1139
- 42161: "https://arb-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V"
1140
- };
1141
- function resolveRpcUrl(rpcUrls, chainId) {
1136
+ function configuredRpcProxyBaseUrl(baseUrl) {
1137
+ const configured = (baseUrl ?? "")?.trim();
1138
+ if (!configured) {
1139
+ throw new Error(
1140
+ "OWNEY_ROUTING_API_BASE_URL is required for RPC proxy requests unless routingApiBaseUrl or all rpcUrls are configured."
1141
+ );
1142
+ }
1143
+ return configured;
1144
+ }
1145
+ function rpcProxyUrl(chainId, apiKey, baseUrl) {
1146
+ const url = new URL(
1147
+ `${configuredRpcProxyBaseUrl(baseUrl).replace(/\/$/, "")}/api/v1/rpc/${chainId}`
1148
+ );
1149
+ if (apiKey) url.searchParams.set("apiKey", apiKey);
1150
+ return url.toString();
1151
+ }
1152
+ function resolveRpcUrl(rpcUrls, chainId, apiKey = "", baseUrl) {
1142
1153
  const url = rpcUrls?.[chainId]?.trim();
1143
1154
  if (url) return url;
1144
- return DEFAULT_RPC_URLS[chainId];
1155
+ return rpcProxyUrl(chainId, apiKey, baseUrl);
1145
1156
  }
1146
- function resolveRpcUrls(rpcUrls) {
1157
+ function resolveRpcUrls(rpcUrls, apiKey = "", baseUrl) {
1147
1158
  return {
1148
- 1: resolveRpcUrl(rpcUrls, 1),
1149
- 8453: resolveRpcUrl(rpcUrls, 8453),
1150
- 42161: resolveRpcUrl(rpcUrls, 42161)
1159
+ 1: resolveRpcUrl(rpcUrls, 1, apiKey, baseUrl),
1160
+ 8453: resolveRpcUrl(rpcUrls, 8453, apiKey, baseUrl),
1161
+ 42161: resolveRpcUrl(rpcUrls, 42161, apiKey, baseUrl)
1151
1162
  };
1152
1163
  }
1153
1164
  function createPaidRpcClient(chain, rpcUrls) {
@@ -1173,7 +1184,13 @@ function headerValue(error, name) {
1173
1184
  seen.add(candidate);
1174
1185
  const record = candidate;
1175
1186
  const headers = record.headers;
1176
- const found = typeof headers?.get === "function" ? headers.get(name) : headers?.[name] ?? headers?.[name.toLowerCase()];
1187
+ let found;
1188
+ if (typeof headers?.get === "function") {
1189
+ found = headers.get(name);
1190
+ } else {
1191
+ const headerRecord = headers;
1192
+ found = headerRecord?.[name] ?? headerRecord?.[name.toLowerCase()];
1193
+ }
1177
1194
  if (typeof found === "string" && found) {
1178
1195
  value = found;
1179
1196
  return;
@@ -2888,10 +2905,11 @@ var YieldseekerApiClient = class {
2888
2905
  const payload = await response.json().catch(() => null);
2889
2906
  if (!response.ok) {
2890
2907
  const error = providerError(payload, `HTTP_${response.status}`);
2908
+ const retryAfter = response.headers.get("Retry-After");
2891
2909
  throw new YieldseekerApiError(
2892
2910
  response.status,
2893
2911
  error.code,
2894
- error.fields
2912
+ retryAfter ? { ...error.fields, headers: { "Retry-After": retryAfter } } : error.fields
2895
2913
  );
2896
2914
  }
2897
2915
  if (payload && typeof payload === "object" && payload.success === true && "data" in payload) {
@@ -3022,13 +3040,14 @@ function position(value, asset, baseAssetDecimals) {
3022
3040
  // differ from the underlying asset. Yieldseeker already converts it to
3023
3041
  // underlying base-asset units in `assetsBase`; pair that value with the
3024
3042
  // snapshot's corresponding `baseAssetDecimals` for display. Keep the raw
3025
- // share quantity separately because withdraw-from-position expects it.
3043
+ // share quantity separate from the withdrawable underlying asset amount.
3026
3044
  amount: decimal(
3027
3045
  value.assetsBase,
3028
3046
  baseAssetDecimals,
3029
3047
  "yield positions"
3030
3048
  ),
3031
3049
  amountRaw: String(value.assetsRaw),
3050
+ withdrawableAmountRaw: String(value.withdrawableAssetsRaw),
3032
3051
  apy: percent(option.riskAdjustedApy),
3033
3052
  tvl: Number(option.totalDepositsUsd),
3034
3053
  liquidity: Number(option.withdrawableDepositsUsd)
@@ -3357,8 +3376,12 @@ var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3357
3376
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3358
3377
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3359
3378
  var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3360
- var YIELDSEEKER_AGENT_LIST_CACHE_MS = 6e4;
3361
- 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;
3362
3385
  var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
3363
3386
  function generateYieldseekerUsername() {
3364
3387
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
@@ -3428,6 +3451,15 @@ var YieldseekerAgent = class {
3428
3451
  readCache = /* @__PURE__ */ new Map();
3429
3452
  pendingReads = /* @__PURE__ */ new Map();
3430
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();
3431
3463
  yieldOptions = /* @__PURE__ */ new Map();
3432
3464
  pendingYieldOptions = /* @__PURE__ */ new Map();
3433
3465
  constructor(owneyApiKey, options = {}) {
@@ -3458,6 +3490,13 @@ var YieldseekerAgent = class {
3458
3490
  this.pendingWalletContexts.clear();
3459
3491
  this.readCache.clear();
3460
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();
3461
3500
  }
3462
3501
  async activateAgent(state, chainId, asset) {
3463
3502
  this.assertChain(chainId);
@@ -3477,41 +3516,37 @@ var YieldseekerAgent = class {
3477
3516
  );
3478
3517
  }
3479
3518
  const context = await this.ensureAgent(state, chainId, asset);
3519
+ const generation = this.readGeneration;
3520
+ const movement = this.snapshotMovement(state, chainId, context, BigInt(amount));
3480
3521
  let txHash;
3481
- try {
3482
- if (depositCallback) {
3483
- provideDepositVerificationContext(depositCallback, {
3484
- agentId: "yieldseeker",
3485
- signature: await this.auth.getToken(state, chainId),
3486
- userId: context.user.userId,
3487
- yieldseekerAgentId: context.agent.agentId
3488
- });
3489
- txHash = await depositCallback(
3490
- context.wallet.walletAddress,
3491
- chainId,
3492
- amount
3493
- );
3494
- await this.waitForReceipt(state, chainId, txHash);
3495
- } else {
3496
- txHash = await this.submitTransaction(state, chainId, {
3497
- from: (0, import_viem7.getAddress)(state.walletAddress),
3498
- to: YIELDSEEKER_ASSET_METADATA[asset].address,
3499
- data: (0, import_viem7.encodeFunctionData)({
3500
- abi: import_viem7.erc20Abi,
3501
- functionName: "transfer",
3502
- args: [(0, import_viem7.getAddress)(context.wallet.walletAddress), BigInt(amount)]
3503
- }),
3504
- value: "0",
3505
- chainId
3506
- });
3507
- }
3508
- } finally {
3509
- await this.refreshSnapshotAfterMovement(
3510
- 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,
3511
3531
  chainId,
3512
- context,
3513
- "deposit"
3532
+ amount
3514
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);
3515
3550
  }
3516
3551
  return {
3517
3552
  txHash,
@@ -3539,6 +3574,9 @@ var YieldseekerAgent = class {
3539
3574
  this.id
3540
3575
  );
3541
3576
  }
3577
+ const generation = this.readGeneration;
3578
+ let confirmedMovement = false;
3579
+ let settlement = null;
3542
3580
  try {
3543
3581
  const portfolio = await this.loadPortfolioContext(
3544
3582
  state,
@@ -3556,6 +3594,7 @@ var YieldseekerAgent = class {
3556
3594
  );
3557
3595
  const totalAvailable = idle + deployed;
3558
3596
  const requested = amount === void 0 ? totalAvailable : BigInt(amount);
3597
+ const plannedMovement = this.snapshotMovement(state, chainId, context, -requested);
3559
3598
  if (requested > totalAvailable) {
3560
3599
  throw new OwneyError(
3561
3600
  "WITHDRAW_INSUFFICIENT_BALANCE",
@@ -3591,6 +3630,7 @@ var YieldseekerAgent = class {
3591
3630
  throw this.invalidResponse("position withdrawal");
3592
3631
  }
3593
3632
  await this.waitForReceipt(state, chainId, response.transactionHash);
3633
+ confirmedMovement = true;
3594
3634
  remaining -= assetsRaw;
3595
3635
  }
3596
3636
  if (remaining > 0n) {
@@ -3615,18 +3655,23 @@ var YieldseekerAgent = class {
3615
3655
  value: "0",
3616
3656
  chainId
3617
3657
  });
3658
+ confirmedMovement = true;
3659
+ settlement = plannedMovement;
3618
3660
  return {
3619
3661
  txHash,
3620
3662
  type: amount === void 0 ? "full" : "partial",
3621
3663
  amount: requested.toString()
3622
3664
  };
3623
3665
  } finally {
3624
- await this.refreshSnapshotAfterMovement(
3625
- state,
3626
- chainId,
3627
- context,
3628
- "withdrawal"
3629
- );
3666
+ if (confirmedMovement && this.readGeneration === generation) {
3667
+ await this.refreshSnapshotAfterMovement(
3668
+ state,
3669
+ chainId,
3670
+ context,
3671
+ "withdrawal",
3672
+ settlement
3673
+ );
3674
+ }
3630
3675
  }
3631
3676
  }
3632
3677
  async getBalances(state, chainId) {
@@ -3720,6 +3765,16 @@ var YieldseekerAgent = class {
3720
3765
  contextKey(state, chainId, asset) {
3721
3766
  return `${this.userKey(state, chainId)}:${asset}`;
3722
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
+ }
3723
3778
  cachedRead(key2, ttlMs, read2) {
3724
3779
  const cached = this.readCache.get(key2);
3725
3780
  if (cached && cached.expiresAt > Date.now()) {
@@ -3729,7 +3784,7 @@ var YieldseekerAgent = class {
3729
3784
  if (pending) return pending;
3730
3785
  const generation = this.readGeneration;
3731
3786
  const request = Promise.resolve().then(read2).then((value) => {
3732
- if (this.readGeneration === generation) {
3787
+ if (this.readGeneration === generation && this.pendingReads.get(key2) === request) {
3733
3788
  this.readCache.set(key2, {
3734
3789
  expiresAt: Date.now() + ttlMs,
3735
3790
  value
@@ -3777,6 +3832,7 @@ var YieldseekerAgent = class {
3777
3832
  chainId,
3778
3833
  `/users/${user.userId}/agents/${agent.agentId}/wallet`
3779
3834
  ).then((walletResponse) => {
3835
+ this.assertSession(generation, chainId, asset);
3780
3836
  if (!walletResponse?.agentWallet || !(0, import_viem7.isAddress)(walletResponse.agentWallet.walletAddress)) {
3781
3837
  throw this.invalidResponse("agent wallet");
3782
3838
  }
@@ -3786,9 +3842,7 @@ var YieldseekerAgent = class {
3786
3842
  wallet: walletResponse.agentWallet,
3787
3843
  asset
3788
3844
  };
3789
- if (this.readGeneration === generation) {
3790
- this.agentContexts.set(key2, context);
3791
- }
3845
+ this.agentContexts.set(key2, context);
3792
3846
  return context;
3793
3847
  });
3794
3848
  this.pendingWalletContexts.set(key2, request);
@@ -3810,6 +3864,7 @@ var YieldseekerAgent = class {
3810
3864
  return persisted;
3811
3865
  }
3812
3866
  const walletAddress = (0, import_viem7.getAddress)(state.walletAddress);
3867
+ const generation = this.readGeneration;
3813
3868
  let user = null;
3814
3869
  try {
3815
3870
  const login = await this.providerRequest(
@@ -3856,6 +3911,7 @@ var YieldseekerAgent = class {
3856
3911
  if (!user || !/^[a-zA-Z0-9_-]{1,128}$/.test(user.userId)) {
3857
3912
  throw this.invalidResponse("wallet identity");
3858
3913
  }
3914
+ this.assertSession(generation, chainId);
3859
3915
  const resolved = { userId: user.userId };
3860
3916
  this.users.set(key2, resolved);
3861
3917
  writeYieldseekerIdentity(state.walletAddress, chainId, resolved.userId);
@@ -3871,10 +3927,13 @@ var YieldseekerAgent = class {
3871
3927
  if (cached) return cached;
3872
3928
  const pending = this.pendingAgents.get(key2);
3873
3929
  if (pending) return pending;
3930
+ const generation = this.readGeneration;
3874
3931
  const request = this.resolveAgent(state, chainId, asset, true).then(
3875
3932
  async (context) => {
3933
+ this.assertSession(generation, chainId, asset);
3876
3934
  if (!context) throw this.invalidResponse("agent creation");
3877
3935
  await this.deployAgent(state, chainId, context);
3936
+ this.assertSession(generation, chainId, asset);
3878
3937
  this.agentContexts.set(key2, context);
3879
3938
  return context;
3880
3939
  }
@@ -3883,20 +3942,25 @@ var YieldseekerAgent = class {
3883
3942
  try {
3884
3943
  return await request;
3885
3944
  } finally {
3886
- this.pendingAgents.delete(key2);
3945
+ if (this.pendingAgents.get(key2) === request) this.pendingAgents.delete(key2);
3887
3946
  }
3888
3947
  }
3889
3948
  async findAgent(state, chainId, asset) {
3890
3949
  const key2 = this.contextKey(state, chainId, asset);
3891
3950
  const cached = this.agentContexts.get(key2);
3892
3951
  if (cached) return cached;
3952
+ const generation = this.readGeneration;
3893
3953
  const context = await this.resolveAgent(state, chainId, asset, false);
3954
+ this.assertSession(generation, chainId, asset);
3894
3955
  if (context) this.agentContexts.set(key2, context);
3895
3956
  return context;
3896
3957
  }
3897
3958
  async resolveAgent(state, chainId, asset, createIfMissing) {
3959
+ const generation = this.readGeneration;
3898
3960
  const user = await this.resolveUser(state, chainId);
3961
+ this.assertSession(generation, chainId, asset);
3899
3962
  const agents = await this.listAgents(state, chainId, user);
3963
+ this.assertSession(generation, chainId, asset);
3900
3964
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3901
3965
  let agent = agents.find(
3902
3966
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
@@ -3918,6 +3982,7 @@ var YieldseekerAgent = class {
3918
3982
  }
3919
3983
  }
3920
3984
  );
3985
+ this.assertSession(generation, chainId, asset);
3921
3986
  agent = created?.agent;
3922
3987
  if (agent) {
3923
3988
  this.readCache.set(this.agentListKey(state, chainId), {
@@ -3933,8 +3998,11 @@ var YieldseekerAgent = class {
3933
3998
  return this.contextForAgent(state, chainId, user, agent, asset);
3934
3999
  }
3935
4000
  async loadPortfolio(state, chainId, options) {
4001
+ const generation = this.readGeneration;
3936
4002
  const user = await this.resolveUser(state, chainId);
4003
+ this.assertSession(generation, chainId);
3937
4004
  const agents = await this.listAgents(state, chainId, user);
4005
+ this.assertSession(generation, chainId);
3938
4006
  const contexts = [];
3939
4007
  for (const agent of agents) {
3940
4008
  const asset = this.assetForAgent(agent);
@@ -3945,81 +4013,187 @@ var YieldseekerAgent = class {
3945
4013
  contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
3946
4014
  }
3947
4015
  const resolvedContexts = await Promise.all(contexts);
4016
+ this.assertSession(generation, chainId);
3948
4017
  return Promise.all(
3949
4018
  resolvedContexts.map(
3950
4019
  (context) => this.loadPortfolioContext(state, chainId, context, options)
3951
4020
  )
3952
4021
  );
3953
4022
  }
3954
- async loadPortfolioContext(state, chainId, context, options = {}) {
3955
- const contextKey = this.contextKey(state, chainId, context.asset);
3956
- const [snapshot, positions, historic, actions] = await Promise.all([
3957
- this.cachedRead(
3958
- `snapshot:${contextKey}`,
3959
- YIELDSEEKER_PORTFOLIO_CACHE_MS,
3960
- async () => {
3961
- const response = await this.walletRequest(
3962
- state,
3963
- chainId,
3964
- `${this.agentPath(context, "snapshot")}${query({
3965
- shouldOnlyUseRecentValue: true,
3966
- shouldAllowStaleOnError: true
3967
- })}`
3968
- );
3969
- if (!response?.agentSnapshot) {
3970
- throw this.invalidResponse("agent snapshot");
3971
- }
3972
- return response;
3973
- }
3974
- ),
3975
- this.cachedRead(
3976
- `positions:${contextKey}`,
3977
- YIELDSEEKER_PORTFOLIO_CACHE_MS,
3978
- async () => {
3979
- const response = await this.walletRequest(
3980
- state,
3981
- chainId,
3982
- this.agentPath(context, "yield-positions")
3983
- );
3984
- if (!Array.isArray(response?.yieldPositions)) {
3985
- 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);
3986
4086
  }
3987
- return response;
3988
4087
  }
3989
- ),
3990
- options.historic ? this.cachedRead(
3991
- `historic:${contextKey}`,
3992
- YIELDSEEKER_ACTIVITY_CACHE_MS,
3993
- () => this.walletRequest(
3994
- state,
3995
- chainId,
3996
- this.agentPath(context, "wallet/historic-position")
3997
- )
3998
- ) : Promise.resolve(void 0),
3999
- options.actions ? this.cachedRead(
4000
- `actions:${contextKey}`,
4001
- YIELDSEEKER_ACTIVITY_CACHE_MS,
4002
- () => 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(
4003
4116
  state,
4004
4117
  chainId,
4005
- this.agentPath(context, "actions")
4118
+ context,
4119
+ contextKey,
4120
+ version,
4121
+ generation,
4122
+ this.snapshotMovements.has(contextKey)
4006
4123
  )
4007
- ) : Promise.resolve(void 0)
4008
- ]);
4009
- if (!snapshot?.agentSnapshot) {
4010
- throw this.invalidResponse("agent snapshot");
4011
- }
4012
- if (!Array.isArray(positions?.yieldPositions)) {
4013
- throw this.invalidResponse("yield positions");
4014
- }
4015
- return {
4016
- ...context,
4017
- snapshot: snapshot.agentSnapshot,
4018
- positions: positions.yieldPositions,
4019
- ...historic?.position ? { historic: historic.position } : {},
4020
- ...actions?.actions ? { actions: actions.actions } : {}
4124
+ ))
4021
4125
  };
4022
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
+ }
4023
4197
  async deployAgent(state, chainId, context) {
4024
4198
  if (context.wallet.initializedDate != null) return;
4025
4199
  const walletAddress = context.wallet.walletAddress.toLowerCase();
@@ -4036,42 +4210,52 @@ var YieldseekerAgent = class {
4036
4210
  }
4037
4211
  context.wallet = deployed.agentWallet;
4038
4212
  }
4039
- async refreshSnapshotAfterMovement(state, chainId, context, movement) {
4213
+ async refreshSnapshotAfterMovement(state, chainId, context, movement, settlement) {
4040
4214
  const contextKey = this.contextKey(state, chainId, context.asset);
4041
- const invalidatePortfolio = () => {
4042
- for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4043
- this.readCache.delete(`${kind}:${contextKey}`);
4044
- }
4045
- };
4046
- 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;
4047
4222
  try {
4048
- const response = await this.walletRequest(
4049
- state,
4050
- chainId,
4051
- `${this.agentPath(context, "snapshot")}${query({
4052
- shouldForceRefresh: true
4053
- })}`
4223
+ await this.cachedRead(
4224
+ `snapshot:${contextKey}`,
4225
+ YIELDSEEKER_SETTLEMENT_CACHE_MS,
4226
+ () => this.requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, true)
4054
4227
  );
4055
- if (!response?.agentSnapshot) {
4056
- throw this.invalidResponse("agent snapshot refresh");
4057
- }
4058
4228
  } catch (error) {
4059
4229
  console.warn(
4060
4230
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
4061
4231
  error
4062
4232
  );
4063
- } finally {
4064
- invalidatePortfolio();
4065
4233
  }
4066
4234
  }
4067
4235
  agentPath(context, suffix) {
4068
4236
  return `/users/${context.user.userId}/agents/${context.agent.agentId}/${suffix}`;
4069
4237
  }
4070
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;
4071
4247
  try {
4072
4248
  return await this.providerRequest(state, chainId, path, options);
4073
4249
  } catch (error) {
4074
- 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;
4075
4259
  }
4076
4260
  }
4077
4261
  async providerRequest(state, chainId, path, options = {}) {
@@ -4368,16 +4552,16 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
4368
4552
  const positionChain = position2.chain.trim().toUpperCase();
4369
4553
  const matchesChain = positionChain === String(chainId) || targetChain !== void 0 && positionChain === targetChain;
4370
4554
  if (!matchesChain || position2.asset.toUpperCase() !== target) continue;
4371
- if (position2.amountRaw !== void 0) {
4372
- try {
4373
- balance += BigInt(position2.amountRaw);
4374
- continue;
4375
- } catch {
4376
- }
4377
- }
4378
- balance += (0, import_viem8.parseUnits)(position2.amount, decimals);
4555
+ balance += position2.withdrawableAmountRaw !== void 0 ? BigInt(position2.withdrawableAmountRaw) : (0, import_viem8.parseUnits)(position2.amount, decimals);
4379
4556
  }
4380
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
+ }
4381
4565
  return { agent, balance };
4382
4566
  });
4383
4567
  }
@@ -5137,8 +5321,12 @@ var OwneySDK = class {
5137
5321
  constructor(config) {
5138
5322
  this.apiKey = config.apiKey;
5139
5323
  if (config.debug) setOwneyDebug(true);
5140
- this.rpcUrls = config.rpcUrls;
5141
- this.zyfaiRpcUrls = config.zyfaiRpcUrls;
5324
+ this.rpcUrls = resolveRpcUrls(
5325
+ config.rpcUrls,
5326
+ config.apiKey,
5327
+ config.routingApiBaseUrl
5328
+ );
5329
+ this.zyfaiRpcUrls = config.rpcUrls ? void 0 : config.zyfaiRpcUrls;
5142
5330
  this.yieldseekerApiBaseUrl = config.yieldseekerApiBaseUrl;
5143
5331
  this.yieldseekerSiweOrigin = config.yieldseekerSiweOrigin;
5144
5332
  this.routingApiBaseUrl = config.routingApiBaseUrl;
@@ -5413,7 +5601,11 @@ var OwneySDK = class {
5413
5601
  if (!key2) return null;
5414
5602
  return new ZyfaiAgent(
5415
5603
  key2,
5416
- this.rpcUrls ?? this.zyfaiRpcUrls,
5604
+ this.zyfaiRpcUrls ? resolveRpcUrls(
5605
+ this.zyfaiRpcUrls,
5606
+ this.apiKey,
5607
+ this.routingApiBaseUrl
5608
+ ) : this.rpcUrls,
5417
5609
  this.referralSource
5418
5610
  );
5419
5611
  }
@@ -5990,7 +6182,14 @@ var OwneySDK = class {
5990
6182
  * @returns {AgentWithdrawResult} for a single agent, or {OwneyWithdrawResult} with per-agent results
5991
6183
  */
5992
6184
  async withdraw(options) {
5993
- 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
+ };
5994
6193
  const state = this.requireState();
5995
6194
  const chainId = this.requireChainId();
5996
6195
  const token = asset;
@@ -6006,12 +6205,14 @@ var OwneySDK = class {
6006
6205
  if (agentId) {
6007
6206
  const agent = this.getAgent(agentId);
6008
6207
  this.validateAssetSupport(agent, chainId, asset);
6009
- return withFailureReporting(
6208
+ const result = await withFailureReporting(
6010
6209
  this.apiKey,
6011
6210
  agent.id,
6012
6211
  () => agent.withdraw(state, chainId, token, amount),
6013
6212
  this.routingApiBaseUrl
6014
6213
  );
6214
+ notifyAgentResult(agent.id, result);
6215
+ return result;
6015
6216
  }
6016
6217
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
6017
6218
  const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
@@ -6021,6 +6222,7 @@ var OwneySDK = class {
6021
6222
  for (const agent of withdrawalAgents) {
6022
6223
  try {
6023
6224
  results2[agent.id] = await agent.withdraw(state, chainId, token);
6225
+ notifyAgentResult(agent.id, results2[agent.id]);
6024
6226
  } catch (err) {
6025
6227
  if (this.isUserRejectedWithdrawal(err)) throw err;
6026
6228
  console.error(`withdraw failed for agent "${agent.id}":`, err);
@@ -6118,6 +6320,7 @@ var OwneySDK = class {
6118
6320
  token,
6119
6321
  p.planned.toString()
6120
6322
  );
6323
+ notifyAgentResult(p.agent.id, results[p.agent.id]);
6121
6324
  } catch (err) {
6122
6325
  if (this.isUserRejectedWithdrawal(err)) throw err;
6123
6326
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);