@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.js CHANGED
@@ -1102,21 +1102,32 @@ import {
1102
1102
  } from "viem";
1103
1103
  var PAID_RPC_RETRY_COUNT = 3;
1104
1104
  var PAID_RPC_RETRY_DELAY_MS = 1e3;
1105
- var DEFAULT_RPC_URLS = {
1106
- 1: "https://eth-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1107
- 8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1108
- 42161: "https://arb-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V"
1109
- };
1110
- function resolveRpcUrl(rpcUrls, chainId) {
1105
+ function configuredRpcProxyBaseUrl(baseUrl) {
1106
+ const configured = (baseUrl ?? "")?.trim();
1107
+ if (!configured) {
1108
+ throw new Error(
1109
+ "OWNEY_ROUTING_API_BASE_URL is required for RPC proxy requests unless routingApiBaseUrl or all rpcUrls are configured."
1110
+ );
1111
+ }
1112
+ return configured;
1113
+ }
1114
+ function rpcProxyUrl(chainId, apiKey, baseUrl) {
1115
+ const url = new URL(
1116
+ `${configuredRpcProxyBaseUrl(baseUrl).replace(/\/$/, "")}/api/v1/rpc/${chainId}`
1117
+ );
1118
+ if (apiKey) url.searchParams.set("apiKey", apiKey);
1119
+ return url.toString();
1120
+ }
1121
+ function resolveRpcUrl(rpcUrls, chainId, apiKey = "", baseUrl) {
1111
1122
  const url = rpcUrls?.[chainId]?.trim();
1112
1123
  if (url) return url;
1113
- return DEFAULT_RPC_URLS[chainId];
1124
+ return rpcProxyUrl(chainId, apiKey, baseUrl);
1114
1125
  }
1115
- function resolveRpcUrls(rpcUrls) {
1126
+ function resolveRpcUrls(rpcUrls, apiKey = "", baseUrl) {
1116
1127
  return {
1117
- 1: resolveRpcUrl(rpcUrls, 1),
1118
- 8453: resolveRpcUrl(rpcUrls, 8453),
1119
- 42161: resolveRpcUrl(rpcUrls, 42161)
1128
+ 1: resolveRpcUrl(rpcUrls, 1, apiKey, baseUrl),
1129
+ 8453: resolveRpcUrl(rpcUrls, 8453, apiKey, baseUrl),
1130
+ 42161: resolveRpcUrl(rpcUrls, 42161, apiKey, baseUrl)
1120
1131
  };
1121
1132
  }
1122
1133
  function createPaidRpcClient(chain, rpcUrls) {
@@ -1142,7 +1153,13 @@ function headerValue(error, name) {
1142
1153
  seen.add(candidate);
1143
1154
  const record = candidate;
1144
1155
  const headers = record.headers;
1145
- const found = typeof headers?.get === "function" ? headers.get(name) : headers?.[name] ?? headers?.[name.toLowerCase()];
1156
+ let found;
1157
+ if (typeof headers?.get === "function") {
1158
+ found = headers.get(name);
1159
+ } else {
1160
+ const headerRecord = headers;
1161
+ found = headerRecord?.[name] ?? headerRecord?.[name.toLowerCase()];
1162
+ }
1146
1163
  if (typeof found === "string" && found) {
1147
1164
  value = found;
1148
1165
  return;
@@ -2870,10 +2887,11 @@ var YieldseekerApiClient = class {
2870
2887
  const payload = await response.json().catch(() => null);
2871
2888
  if (!response.ok) {
2872
2889
  const error = providerError(payload, `HTTP_${response.status}`);
2890
+ const retryAfter = response.headers.get("Retry-After");
2873
2891
  throw new YieldseekerApiError(
2874
2892
  response.status,
2875
2893
  error.code,
2876
- error.fields
2894
+ retryAfter ? { ...error.fields, headers: { "Retry-After": retryAfter } } : error.fields
2877
2895
  );
2878
2896
  }
2879
2897
  if (payload && typeof payload === "object" && payload.success === true && "data" in payload) {
@@ -3004,13 +3022,14 @@ function position(value, asset, baseAssetDecimals) {
3004
3022
  // differ from the underlying asset. Yieldseeker already converts it to
3005
3023
  // underlying base-asset units in `assetsBase`; pair that value with the
3006
3024
  // snapshot's corresponding `baseAssetDecimals` for display. Keep the raw
3007
- // share quantity separately because withdraw-from-position expects it.
3025
+ // share quantity separate from the withdrawable underlying asset amount.
3008
3026
  amount: decimal(
3009
3027
  value.assetsBase,
3010
3028
  baseAssetDecimals,
3011
3029
  "yield positions"
3012
3030
  ),
3013
3031
  amountRaw: String(value.assetsRaw),
3032
+ withdrawableAmountRaw: String(value.withdrawableAssetsRaw),
3014
3033
  apy: percent(option.riskAdjustedApy),
3015
3034
  tvl: Number(option.totalDepositsUsd),
3016
3035
  liquidity: Number(option.withdrawableDepositsUsd)
@@ -3339,8 +3358,12 @@ var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3339
3358
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3340
3359
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3341
3360
  var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3342
- var YIELDSEEKER_AGENT_LIST_CACHE_MS = 6e4;
3343
- 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;
3344
3367
  var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
3345
3368
  function generateYieldseekerUsername() {
3346
3369
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
@@ -3410,6 +3433,15 @@ var YieldseekerAgent = class {
3410
3433
  readCache = /* @__PURE__ */ new Map();
3411
3434
  pendingReads = /* @__PURE__ */ new Map();
3412
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();
3413
3445
  yieldOptions = /* @__PURE__ */ new Map();
3414
3446
  pendingYieldOptions = /* @__PURE__ */ new Map();
3415
3447
  constructor(owneyApiKey, options = {}) {
@@ -3440,6 +3472,13 @@ var YieldseekerAgent = class {
3440
3472
  this.pendingWalletContexts.clear();
3441
3473
  this.readCache.clear();
3442
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();
3443
3482
  }
3444
3483
  async activateAgent(state, chainId, asset) {
3445
3484
  this.assertChain(chainId);
@@ -3459,41 +3498,37 @@ var YieldseekerAgent = class {
3459
3498
  );
3460
3499
  }
3461
3500
  const context = await this.ensureAgent(state, chainId, asset);
3501
+ const generation = this.readGeneration;
3502
+ const movement = this.snapshotMovement(state, chainId, context, BigInt(amount));
3462
3503
  let txHash;
3463
- try {
3464
- if (depositCallback) {
3465
- provideDepositVerificationContext(depositCallback, {
3466
- agentId: "yieldseeker",
3467
- signature: await this.auth.getToken(state, chainId),
3468
- userId: context.user.userId,
3469
- yieldseekerAgentId: context.agent.agentId
3470
- });
3471
- txHash = await depositCallback(
3472
- context.wallet.walletAddress,
3473
- chainId,
3474
- amount
3475
- );
3476
- await this.waitForReceipt(state, chainId, txHash);
3477
- } else {
3478
- txHash = await this.submitTransaction(state, chainId, {
3479
- from: getAddress2(state.walletAddress),
3480
- to: YIELDSEEKER_ASSET_METADATA[asset].address,
3481
- data: encodeFunctionData({
3482
- abi: erc20Abi,
3483
- functionName: "transfer",
3484
- args: [getAddress2(context.wallet.walletAddress), BigInt(amount)]
3485
- }),
3486
- value: "0",
3487
- chainId
3488
- });
3489
- }
3490
- } finally {
3491
- await this.refreshSnapshotAfterMovement(
3492
- 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,
3493
3513
  chainId,
3494
- context,
3495
- "deposit"
3514
+ amount
3496
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);
3497
3532
  }
3498
3533
  return {
3499
3534
  txHash,
@@ -3521,6 +3556,9 @@ var YieldseekerAgent = class {
3521
3556
  this.id
3522
3557
  );
3523
3558
  }
3559
+ const generation = this.readGeneration;
3560
+ let confirmedMovement = false;
3561
+ let settlement = null;
3524
3562
  try {
3525
3563
  const portfolio = await this.loadPortfolioContext(
3526
3564
  state,
@@ -3538,6 +3576,7 @@ var YieldseekerAgent = class {
3538
3576
  );
3539
3577
  const totalAvailable = idle + deployed;
3540
3578
  const requested = amount === void 0 ? totalAvailable : BigInt(amount);
3579
+ const plannedMovement = this.snapshotMovement(state, chainId, context, -requested);
3541
3580
  if (requested > totalAvailable) {
3542
3581
  throw new OwneyError(
3543
3582
  "WITHDRAW_INSUFFICIENT_BALANCE",
@@ -3573,6 +3612,7 @@ var YieldseekerAgent = class {
3573
3612
  throw this.invalidResponse("position withdrawal");
3574
3613
  }
3575
3614
  await this.waitForReceipt(state, chainId, response.transactionHash);
3615
+ confirmedMovement = true;
3576
3616
  remaining -= assetsRaw;
3577
3617
  }
3578
3618
  if (remaining > 0n) {
@@ -3597,18 +3637,23 @@ var YieldseekerAgent = class {
3597
3637
  value: "0",
3598
3638
  chainId
3599
3639
  });
3640
+ confirmedMovement = true;
3641
+ settlement = plannedMovement;
3600
3642
  return {
3601
3643
  txHash,
3602
3644
  type: amount === void 0 ? "full" : "partial",
3603
3645
  amount: requested.toString()
3604
3646
  };
3605
3647
  } finally {
3606
- await this.refreshSnapshotAfterMovement(
3607
- state,
3608
- chainId,
3609
- context,
3610
- "withdrawal"
3611
- );
3648
+ if (confirmedMovement && this.readGeneration === generation) {
3649
+ await this.refreshSnapshotAfterMovement(
3650
+ state,
3651
+ chainId,
3652
+ context,
3653
+ "withdrawal",
3654
+ settlement
3655
+ );
3656
+ }
3612
3657
  }
3613
3658
  }
3614
3659
  async getBalances(state, chainId) {
@@ -3702,6 +3747,16 @@ var YieldseekerAgent = class {
3702
3747
  contextKey(state, chainId, asset) {
3703
3748
  return `${this.userKey(state, chainId)}:${asset}`;
3704
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
+ }
3705
3760
  cachedRead(key2, ttlMs, read2) {
3706
3761
  const cached = this.readCache.get(key2);
3707
3762
  if (cached && cached.expiresAt > Date.now()) {
@@ -3711,7 +3766,7 @@ var YieldseekerAgent = class {
3711
3766
  if (pending) return pending;
3712
3767
  const generation = this.readGeneration;
3713
3768
  const request = Promise.resolve().then(read2).then((value) => {
3714
- if (this.readGeneration === generation) {
3769
+ if (this.readGeneration === generation && this.pendingReads.get(key2) === request) {
3715
3770
  this.readCache.set(key2, {
3716
3771
  expiresAt: Date.now() + ttlMs,
3717
3772
  value
@@ -3759,6 +3814,7 @@ var YieldseekerAgent = class {
3759
3814
  chainId,
3760
3815
  `/users/${user.userId}/agents/${agent.agentId}/wallet`
3761
3816
  ).then((walletResponse) => {
3817
+ this.assertSession(generation, chainId, asset);
3762
3818
  if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3763
3819
  throw this.invalidResponse("agent wallet");
3764
3820
  }
@@ -3768,9 +3824,7 @@ var YieldseekerAgent = class {
3768
3824
  wallet: walletResponse.agentWallet,
3769
3825
  asset
3770
3826
  };
3771
- if (this.readGeneration === generation) {
3772
- this.agentContexts.set(key2, context);
3773
- }
3827
+ this.agentContexts.set(key2, context);
3774
3828
  return context;
3775
3829
  });
3776
3830
  this.pendingWalletContexts.set(key2, request);
@@ -3792,6 +3846,7 @@ var YieldseekerAgent = class {
3792
3846
  return persisted;
3793
3847
  }
3794
3848
  const walletAddress = getAddress2(state.walletAddress);
3849
+ const generation = this.readGeneration;
3795
3850
  let user = null;
3796
3851
  try {
3797
3852
  const login = await this.providerRequest(
@@ -3838,6 +3893,7 @@ var YieldseekerAgent = class {
3838
3893
  if (!user || !/^[a-zA-Z0-9_-]{1,128}$/.test(user.userId)) {
3839
3894
  throw this.invalidResponse("wallet identity");
3840
3895
  }
3896
+ this.assertSession(generation, chainId);
3841
3897
  const resolved = { userId: user.userId };
3842
3898
  this.users.set(key2, resolved);
3843
3899
  writeYieldseekerIdentity(state.walletAddress, chainId, resolved.userId);
@@ -3853,10 +3909,13 @@ var YieldseekerAgent = class {
3853
3909
  if (cached) return cached;
3854
3910
  const pending = this.pendingAgents.get(key2);
3855
3911
  if (pending) return pending;
3912
+ const generation = this.readGeneration;
3856
3913
  const request = this.resolveAgent(state, chainId, asset, true).then(
3857
3914
  async (context) => {
3915
+ this.assertSession(generation, chainId, asset);
3858
3916
  if (!context) throw this.invalidResponse("agent creation");
3859
3917
  await this.deployAgent(state, chainId, context);
3918
+ this.assertSession(generation, chainId, asset);
3860
3919
  this.agentContexts.set(key2, context);
3861
3920
  return context;
3862
3921
  }
@@ -3865,20 +3924,25 @@ var YieldseekerAgent = class {
3865
3924
  try {
3866
3925
  return await request;
3867
3926
  } finally {
3868
- this.pendingAgents.delete(key2);
3927
+ if (this.pendingAgents.get(key2) === request) this.pendingAgents.delete(key2);
3869
3928
  }
3870
3929
  }
3871
3930
  async findAgent(state, chainId, asset) {
3872
3931
  const key2 = this.contextKey(state, chainId, asset);
3873
3932
  const cached = this.agentContexts.get(key2);
3874
3933
  if (cached) return cached;
3934
+ const generation = this.readGeneration;
3875
3935
  const context = await this.resolveAgent(state, chainId, asset, false);
3936
+ this.assertSession(generation, chainId, asset);
3876
3937
  if (context) this.agentContexts.set(key2, context);
3877
3938
  return context;
3878
3939
  }
3879
3940
  async resolveAgent(state, chainId, asset, createIfMissing) {
3941
+ const generation = this.readGeneration;
3880
3942
  const user = await this.resolveUser(state, chainId);
3943
+ this.assertSession(generation, chainId, asset);
3881
3944
  const agents = await this.listAgents(state, chainId, user);
3945
+ this.assertSession(generation, chainId, asset);
3882
3946
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3883
3947
  let agent = agents.find(
3884
3948
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
@@ -3900,6 +3964,7 @@ var YieldseekerAgent = class {
3900
3964
  }
3901
3965
  }
3902
3966
  );
3967
+ this.assertSession(generation, chainId, asset);
3903
3968
  agent = created?.agent;
3904
3969
  if (agent) {
3905
3970
  this.readCache.set(this.agentListKey(state, chainId), {
@@ -3915,8 +3980,11 @@ var YieldseekerAgent = class {
3915
3980
  return this.contextForAgent(state, chainId, user, agent, asset);
3916
3981
  }
3917
3982
  async loadPortfolio(state, chainId, options) {
3983
+ const generation = this.readGeneration;
3918
3984
  const user = await this.resolveUser(state, chainId);
3985
+ this.assertSession(generation, chainId);
3919
3986
  const agents = await this.listAgents(state, chainId, user);
3987
+ this.assertSession(generation, chainId);
3920
3988
  const contexts = [];
3921
3989
  for (const agent of agents) {
3922
3990
  const asset = this.assetForAgent(agent);
@@ -3927,81 +3995,187 @@ var YieldseekerAgent = class {
3927
3995
  contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
3928
3996
  }
3929
3997
  const resolvedContexts = await Promise.all(contexts);
3998
+ this.assertSession(generation, chainId);
3930
3999
  return Promise.all(
3931
4000
  resolvedContexts.map(
3932
4001
  (context) => this.loadPortfolioContext(state, chainId, context, options)
3933
4002
  )
3934
4003
  );
3935
4004
  }
3936
- async loadPortfolioContext(state, chainId, context, options = {}) {
3937
- const contextKey = this.contextKey(state, chainId, context.asset);
3938
- const [snapshot, positions, historic, actions] = await Promise.all([
3939
- this.cachedRead(
3940
- `snapshot:${contextKey}`,
3941
- YIELDSEEKER_PORTFOLIO_CACHE_MS,
3942
- async () => {
3943
- const response = await this.walletRequest(
3944
- state,
3945
- chainId,
3946
- `${this.agentPath(context, "snapshot")}${query({
3947
- shouldOnlyUseRecentValue: true,
3948
- shouldAllowStaleOnError: true
3949
- })}`
3950
- );
3951
- if (!response?.agentSnapshot) {
3952
- throw this.invalidResponse("agent snapshot");
3953
- }
3954
- return response;
3955
- }
3956
- ),
3957
- this.cachedRead(
3958
- `positions:${contextKey}`,
3959
- YIELDSEEKER_PORTFOLIO_CACHE_MS,
3960
- async () => {
3961
- const response = await this.walletRequest(
3962
- state,
3963
- chainId,
3964
- this.agentPath(context, "yield-positions")
3965
- );
3966
- if (!Array.isArray(response?.yieldPositions)) {
3967
- 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);
3968
4068
  }
3969
- return response;
3970
4069
  }
3971
- ),
3972
- options.historic ? this.cachedRead(
3973
- `historic:${contextKey}`,
3974
- YIELDSEEKER_ACTIVITY_CACHE_MS,
3975
- () => this.walletRequest(
3976
- state,
3977
- chainId,
3978
- this.agentPath(context, "wallet/historic-position")
3979
- )
3980
- ) : Promise.resolve(void 0),
3981
- options.actions ? this.cachedRead(
3982
- `actions:${contextKey}`,
3983
- YIELDSEEKER_ACTIVITY_CACHE_MS,
3984
- () => 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
+ // Only event-driven reconciliation forces refreshes. Routine reads
4096
+ // remain non-forced; the cache and shared cooldown still bound calls.
4097
+ this.requestPortfolioSnapshot(
3985
4098
  state,
3986
4099
  chainId,
3987
- this.agentPath(context, "actions")
4100
+ context,
4101
+ contextKey,
4102
+ version,
4103
+ generation,
4104
+ this.snapshotMovements.has(contextKey)
3988
4105
  )
3989
- ) : Promise.resolve(void 0)
3990
- ]);
3991
- if (!snapshot?.agentSnapshot) {
3992
- throw this.invalidResponse("agent snapshot");
3993
- }
3994
- if (!Array.isArray(positions?.yieldPositions)) {
3995
- throw this.invalidResponse("yield positions");
3996
- }
3997
- return {
3998
- ...context,
3999
- snapshot: snapshot.agentSnapshot,
4000
- positions: positions.yieldPositions,
4001
- ...historic?.position ? { historic: historic.position } : {},
4002
- ...actions?.actions ? { actions: actions.actions } : {}
4106
+ ))
4003
4107
  };
4004
4108
  }
4109
+ async loadPortfolioContext(state, chainId, context, options = {}) {
4110
+ const contextKey = this.contextKey(state, chainId, context.asset);
4111
+ const generation = this.readGeneration;
4112
+ const assertSession = () => this.assertSession(generation, chainId, context.asset);
4113
+ for (let attempt = 0; attempt < 2; attempt++) {
4114
+ const { version, read: read2 } = this.portfolioSnapshot(state, chainId, context, contextKey);
4115
+ try {
4116
+ const snapshot = await read2;
4117
+ assertSession();
4118
+ if (this.portfolioVersion(contextKey) !== version) continue;
4119
+ const [positions, historic, actions] = await Promise.all([
4120
+ this.cachedRead(
4121
+ `positions:${contextKey}`,
4122
+ this.portfolioCacheMs(contextKey),
4123
+ async () => {
4124
+ const response = await this.walletRequest(
4125
+ state,
4126
+ chainId,
4127
+ this.agentPath(context, "yield-positions")
4128
+ );
4129
+ if (!Array.isArray(response?.yieldPositions)) {
4130
+ throw this.invalidResponse("yield positions");
4131
+ }
4132
+ return response;
4133
+ }
4134
+ ),
4135
+ options.historic ? this.cachedRead(
4136
+ `historic:${contextKey}`,
4137
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4138
+ () => this.walletRequest(
4139
+ state,
4140
+ chainId,
4141
+ this.agentPath(context, "wallet/historic-position")
4142
+ )
4143
+ ) : Promise.resolve(void 0),
4144
+ options.actions ? this.cachedRead(
4145
+ `actions:${contextKey}`,
4146
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4147
+ () => this.walletRequest(
4148
+ state,
4149
+ chainId,
4150
+ this.agentPath(context, "actions")
4151
+ )
4152
+ ) : Promise.resolve(void 0)
4153
+ ]);
4154
+ assertSession();
4155
+ if (this.portfolioVersion(contextKey) !== version) continue;
4156
+ if (!Array.isArray(positions?.yieldPositions)) {
4157
+ throw this.invalidResponse("yield positions");
4158
+ }
4159
+ return {
4160
+ ...context,
4161
+ snapshot: snapshot.agentSnapshot,
4162
+ positions: positions.yieldPositions,
4163
+ ...historic?.position ? { historic: historic.position } : {},
4164
+ ...actions?.actions ? { actions: actions.actions } : {}
4165
+ };
4166
+ } catch (error) {
4167
+ assertSession();
4168
+ if (this.portfolioVersion(contextKey) !== version) continue;
4169
+ throw error;
4170
+ }
4171
+ }
4172
+ throw new OwneyError(
4173
+ "AGENT_API_ERROR",
4174
+ "Yieldseeker portfolio changed during the read. Please retry.",
4175
+ { rpcSource: "agent-api", chainId, asset: context.asset },
4176
+ this.id
4177
+ );
4178
+ }
4005
4179
  async deployAgent(state, chainId, context) {
4006
4180
  if (context.wallet.initializedDate != null) return;
4007
4181
  const walletAddress = context.wallet.walletAddress.toLowerCase();
@@ -4018,42 +4192,52 @@ var YieldseekerAgent = class {
4018
4192
  }
4019
4193
  context.wallet = deployed.agentWallet;
4020
4194
  }
4021
- async refreshSnapshotAfterMovement(state, chainId, context, movement) {
4195
+ async refreshSnapshotAfterMovement(state, chainId, context, movement, settlement) {
4022
4196
  const contextKey = this.contextKey(state, chainId, context.asset);
4023
- const invalidatePortfolio = () => {
4024
- for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4025
- this.readCache.delete(`${kind}:${contextKey}`);
4026
- }
4027
- };
4028
- invalidatePortfolio();
4197
+ this.reconcileUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4198
+ this.activityRefreshUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4199
+ const epoch = this.movementEpochs.get(contextKey) ?? 0;
4200
+ this.snapshotMovements.set(contextKey, settlement?.epoch === epoch ? settlement : null);
4201
+ this.movementEpochs.set(contextKey, epoch + 1);
4202
+ const version = this.advancePortfolioVersion(contextKey);
4203
+ const generation = this.readGeneration;
4029
4204
  try {
4030
- const response = await this.walletRequest(
4031
- state,
4032
- chainId,
4033
- `${this.agentPath(context, "snapshot")}${query({
4034
- shouldForceRefresh: true
4035
- })}`
4205
+ await this.cachedRead(
4206
+ `snapshot:${contextKey}`,
4207
+ YIELDSEEKER_SETTLEMENT_CACHE_MS,
4208
+ () => this.requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, true)
4036
4209
  );
4037
- if (!response?.agentSnapshot) {
4038
- throw this.invalidResponse("agent snapshot refresh");
4039
- }
4040
4210
  } catch (error) {
4041
4211
  console.warn(
4042
4212
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
4043
4213
  error
4044
4214
  );
4045
- } finally {
4046
- invalidatePortfolio();
4047
4215
  }
4048
4216
  }
4049
4217
  agentPath(context, suffix) {
4050
4218
  return `/users/${context.user.userId}/agents/${context.agent.agentId}/${suffix}`;
4051
4219
  }
4052
4220
  async walletRequest(state, chainId, path, options = {}) {
4221
+ const read2 = (options.method ?? "GET") === "GET";
4222
+ const userKey = this.userKey(state, chainId);
4223
+ if (read2) {
4224
+ const failure = this.standardReadFailures.get(userKey);
4225
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4226
+ if (failure) this.standardReadFailures.delete(userKey);
4227
+ }
4228
+ const generation = this.readGeneration;
4053
4229
  try {
4054
4230
  return await this.providerRequest(state, chainId, path, options);
4055
4231
  } catch (error) {
4056
- throw this.mapApiError(error);
4232
+ const mapped = this.mapApiError(error);
4233
+ const delay = read2 ? rateLimitDelay(mapped) : void 0;
4234
+ if (delay !== void 0 && this.readGeneration === generation) {
4235
+ this.standardReadFailures.set(userKey, {
4236
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, delay),
4237
+ error: mapped
4238
+ });
4239
+ }
4240
+ throw mapped;
4057
4241
  }
4058
4242
  }
4059
4243
  async providerRequest(state, chainId, path, options = {}) {
@@ -4350,16 +4534,16 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
4350
4534
  const positionChain = position2.chain.trim().toUpperCase();
4351
4535
  const matchesChain = positionChain === String(chainId) || targetChain !== void 0 && positionChain === targetChain;
4352
4536
  if (!matchesChain || position2.asset.toUpperCase() !== target) continue;
4353
- if (position2.amountRaw !== void 0) {
4354
- try {
4355
- balance += BigInt(position2.amountRaw);
4356
- continue;
4357
- } catch {
4358
- }
4359
- }
4360
- balance += parseUnits(position2.amount, decimals);
4537
+ balance += position2.withdrawableAmountRaw !== void 0 ? BigInt(position2.withdrawableAmountRaw) : parseUnits(position2.amount, decimals);
4361
4538
  }
4362
4539
  }
4540
+ const assetTotal = agentBalance?.assetBalances?.find(
4541
+ (t) => t.chainId === chainId && t.asset.toUpperCase() === target
4542
+ );
4543
+ if (assetTotal) {
4544
+ const total = parseUnits(assetTotal.amount, decimals);
4545
+ if (total < balance) balance = total > 0n ? total : 0n;
4546
+ }
4363
4547
  return { agent, balance };
4364
4548
  });
4365
4549
  }
@@ -5127,8 +5311,12 @@ var OwneySDK = class {
5127
5311
  constructor(config) {
5128
5312
  this.apiKey = config.apiKey;
5129
5313
  if (config.debug) setOwneyDebug(true);
5130
- this.rpcUrls = config.rpcUrls;
5131
- this.zyfaiRpcUrls = config.zyfaiRpcUrls;
5314
+ this.rpcUrls = resolveRpcUrls(
5315
+ config.rpcUrls,
5316
+ config.apiKey,
5317
+ config.routingApiBaseUrl
5318
+ );
5319
+ this.zyfaiRpcUrls = config.rpcUrls ? void 0 : config.zyfaiRpcUrls;
5132
5320
  this.yieldseekerApiBaseUrl = config.yieldseekerApiBaseUrl;
5133
5321
  this.yieldseekerSiweOrigin = config.yieldseekerSiweOrigin;
5134
5322
  this.routingApiBaseUrl = config.routingApiBaseUrl;
@@ -5403,7 +5591,11 @@ var OwneySDK = class {
5403
5591
  if (!key2) return null;
5404
5592
  return new ZyfaiAgent(
5405
5593
  key2,
5406
- this.rpcUrls ?? this.zyfaiRpcUrls,
5594
+ this.zyfaiRpcUrls ? resolveRpcUrls(
5595
+ this.zyfaiRpcUrls,
5596
+ this.apiKey,
5597
+ this.routingApiBaseUrl
5598
+ ) : this.rpcUrls,
5407
5599
  this.referralSource
5408
5600
  );
5409
5601
  }
@@ -5980,7 +6172,14 @@ var OwneySDK = class {
5980
6172
  * @returns {AgentWithdrawResult} for a single agent, or {OwneyWithdrawResult} with per-agent results
5981
6173
  */
5982
6174
  async withdraw(options) {
5983
- const { asset, amount, agentId } = options;
6175
+ const { asset, amount, agentId, onAgentResult } = options;
6176
+ const notifyAgentResult = (id, result) => {
6177
+ try {
6178
+ onAgentResult?.(id, result);
6179
+ } catch (error) {
6180
+ console.warn("Withdrawal result observer failed:", error);
6181
+ }
6182
+ };
5984
6183
  const state = this.requireState();
5985
6184
  const chainId = this.requireChainId();
5986
6185
  const token = asset;
@@ -5996,12 +6195,14 @@ var OwneySDK = class {
5996
6195
  if (agentId) {
5997
6196
  const agent = this.getAgent(agentId);
5998
6197
  this.validateAssetSupport(agent, chainId, asset);
5999
- return withFailureReporting(
6198
+ const result = await withFailureReporting(
6000
6199
  this.apiKey,
6001
6200
  agent.id,
6002
6201
  () => agent.withdraw(state, chainId, token, amount),
6003
6202
  this.routingApiBaseUrl
6004
6203
  );
6204
+ notifyAgentResult(agent.id, result);
6205
+ return result;
6005
6206
  }
6006
6207
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
6007
6208
  const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
@@ -6011,6 +6212,7 @@ var OwneySDK = class {
6011
6212
  for (const agent of withdrawalAgents) {
6012
6213
  try {
6013
6214
  results2[agent.id] = await agent.withdraw(state, chainId, token);
6215
+ notifyAgentResult(agent.id, results2[agent.id]);
6014
6216
  } catch (err) {
6015
6217
  if (this.isUserRejectedWithdrawal(err)) throw err;
6016
6218
  console.error(`withdraw failed for agent "${agent.id}":`, err);
@@ -6108,6 +6310,7 @@ var OwneySDK = class {
6108
6310
  token,
6109
6311
  p.planned.toString()
6110
6312
  );
6313
+ notifyAgentResult(p.agent.id, results[p.agent.id]);
6111
6314
  } catch (err) {
6112
6315
  if (this.isUserRejectedWithdrawal(err)) throw err;
6113
6316
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);