@owney/sdk 0.7.25-beta.1 → 0.7.25-beta.10

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
@@ -125,6 +125,34 @@ function rateLimitDelay(error, now = Date.now()) {
125
125
  }
126
126
 
127
127
  // src/lib/agent-reads.ts
128
+ function rateLimitDiagnostics(error) {
129
+ const seen = /* @__PURE__ */ new Set();
130
+ let details = {};
131
+ function visit(value, depth = 0) {
132
+ if (depth > 6 || !value || typeof value !== "object" || seen.has(value))
133
+ return;
134
+ seen.add(value);
135
+ const record = value;
136
+ for (const key2 of [
137
+ "rpcSource",
138
+ "chainId",
139
+ "rpcMethod",
140
+ "providerRequestId",
141
+ "statusCode"
142
+ ]) {
143
+ const candidate = record[key2];
144
+ if (candidate !== void 0 && details[key2] === void 0)
145
+ details = { ...details, [key2]: candidate };
146
+ }
147
+ for (const key2 of ["details", "cause", "response", "fields", "error"])
148
+ visit(record[key2], depth + 1);
149
+ }
150
+ visit(error);
151
+ return {
152
+ rpcSource: details.rpcSource ?? "agent-api",
153
+ ...details
154
+ };
155
+ }
128
156
  var AgentReads = class {
129
157
  inFlight = /* @__PURE__ */ new Map();
130
158
  cooldowns = /* @__PURE__ */ new Map();
@@ -132,14 +160,15 @@ var AgentReads = class {
132
160
  clearInFlight() {
133
161
  this.inFlight.clear();
134
162
  }
135
- limited(agentId, until) {
163
+ limited(agentId, until, diagnostics) {
136
164
  return new OwneyError(
137
165
  "AGENT_RATE_LIMITED",
138
166
  "Too many requests. Please wait before trying again.",
139
167
  {
140
168
  statusCode: 429,
141
169
  retryAt: until,
142
- retryAfterSeconds: Math.max(0, Math.ceil((until - Date.now()) / 1e3))
170
+ retryAfterSeconds: Math.max(0, Math.ceil((until - Date.now()) / 1e3)),
171
+ ...diagnostics
143
172
  },
144
173
  agentId
145
174
  );
@@ -147,7 +176,9 @@ var AgentReads = class {
147
176
  run(agentId, key2, fetch2) {
148
177
  const cooldown = this.cooldowns.get(agentId);
149
178
  if (cooldown && cooldown.until > Date.now()) {
150
- return Promise.reject(this.limited(agentId, cooldown.until));
179
+ return Promise.reject(
180
+ this.limited(agentId, cooldown.until, cooldown.diagnostics)
181
+ );
151
182
  }
152
183
  const requestKey = JSON.stringify([agentId, key2]);
153
184
  const existing = this.inFlight.get(requestKey);
@@ -168,8 +199,9 @@ var AgentReads = class {
168
199
  Math.min(3e4 * 2 ** (failures - 1), 3e5)
169
200
  );
170
201
  const until = Math.max(previous?.until ?? 0, Date.now() + delay);
171
- this.cooldowns.set(agentId, { until, failures });
172
- throw this.limited(agentId, until);
202
+ const diagnostics = rateLimitDiagnostics(error);
203
+ this.cooldowns.set(agentId, { until, failures, diagnostics });
204
+ throw this.limited(agentId, until, diagnostics);
173
205
  }
174
206
  ).finally(() => {
175
207
  if (this.inFlight.get(requestKey) === promise)
@@ -182,7 +214,7 @@ var AgentReads = class {
182
214
 
183
215
  // src/agents/zyfai/zyfai.agent.ts
184
216
  import { SMART_SESSIONS_VALIDATOR, ZyfaiSDK } from "@zyfai/sdk";
185
- import { createPublicClient, http, parseAbi } from "viem";
217
+ import { parseAbi } from "viem";
186
218
  import { base, arbitrum, mainnet } from "viem/chains";
187
219
 
188
220
  // src/types/config.ts
@@ -1063,15 +1095,148 @@ function protocolsPolicyNeedsUpdate(current, desiredProtocols, desiredAutoSelect
1063
1095
  return !protocolListsEqual(current.protocols, desiredProtocols);
1064
1096
  }
1065
1097
 
1098
+ // src/lib/paid-rpc.ts
1099
+ import {
1100
+ createPublicClient,
1101
+ http
1102
+ } from "viem";
1103
+ var PAID_RPC_RETRY_COUNT = 3;
1104
+ var PAID_RPC_RETRY_DELAY_MS = 1e3;
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) {
1122
+ const url = rpcUrls?.[chainId]?.trim();
1123
+ if (url) return url;
1124
+ return rpcProxyUrl(chainId, apiKey, baseUrl);
1125
+ }
1126
+ function resolveRpcUrls(rpcUrls, apiKey = "", baseUrl) {
1127
+ return {
1128
+ 1: resolveRpcUrl(rpcUrls, 1, apiKey, baseUrl),
1129
+ 8453: resolveRpcUrl(rpcUrls, 8453, apiKey, baseUrl),
1130
+ 42161: resolveRpcUrl(rpcUrls, 42161, apiKey, baseUrl)
1131
+ };
1132
+ }
1133
+ function createPaidRpcClient(chain, rpcUrls) {
1134
+ const url = resolveRpcUrl(
1135
+ rpcUrls,
1136
+ chain.id
1137
+ );
1138
+ return createPublicClient({
1139
+ chain,
1140
+ transport: http(url, {
1141
+ retryCount: PAID_RPC_RETRY_COUNT,
1142
+ retryDelay: PAID_RPC_RETRY_DELAY_MS
1143
+ })
1144
+ });
1145
+ }
1146
+ function headerValue(error, name) {
1147
+ const seen = /* @__PURE__ */ new Set();
1148
+ let value;
1149
+ function visit(candidate, depth = 0) {
1150
+ if (value || depth > 6 || !candidate || typeof candidate !== "object")
1151
+ return;
1152
+ if (seen.has(candidate)) return;
1153
+ seen.add(candidate);
1154
+ const record = candidate;
1155
+ const headers = record.headers;
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
+ }
1163
+ if (typeof found === "string" && found) {
1164
+ value = found;
1165
+ return;
1166
+ }
1167
+ for (const key2 of ["cause", "details", "response", "error"])
1168
+ visit(record[key2], depth + 1);
1169
+ }
1170
+ visit(error);
1171
+ return value;
1172
+ }
1173
+ function statusCode(error) {
1174
+ const seen = /* @__PURE__ */ new Set();
1175
+ let status;
1176
+ function visit(candidate, depth = 0) {
1177
+ if (status || depth > 6 || !candidate || typeof candidate !== "object")
1178
+ return;
1179
+ if (seen.has(candidate)) return;
1180
+ seen.add(candidate);
1181
+ const record = candidate;
1182
+ for (const key2 of ["status", "statusCode"]) {
1183
+ const parsed = Number(record[key2]);
1184
+ if (Number.isInteger(parsed) && parsed >= 100 && parsed <= 599) {
1185
+ status = parsed;
1186
+ return;
1187
+ }
1188
+ }
1189
+ for (const key2 of ["cause", "details", "response", "error"])
1190
+ visit(record[key2], depth + 1);
1191
+ }
1192
+ visit(error);
1193
+ return status;
1194
+ }
1195
+ function paidRpcError(error, chainId, rpcMethod, agentId) {
1196
+ const delay = rateLimitDelay(error);
1197
+ const providerRequestId = headerValue(error, "x-alchemy-request-id") ?? headerValue(error, "x-request-id");
1198
+ if (delay === void 0) {
1199
+ return new OwneyError(
1200
+ "AGENT_API_ERROR",
1201
+ "The blockchain RPC request failed.",
1202
+ {
1203
+ rpcSource: "paid-rpc",
1204
+ chainId,
1205
+ rpcMethod,
1206
+ ...statusCode(error) ? { statusCode: statusCode(error) } : {},
1207
+ ...providerRequestId ? { providerRequestId } : {}
1208
+ },
1209
+ agentId
1210
+ );
1211
+ }
1212
+ const retryAt = Date.now() + delay;
1213
+ return new OwneyError(
1214
+ "AGENT_RATE_LIMITED",
1215
+ "The blockchain RPC is rate limited. Please wait before trying again.",
1216
+ {
1217
+ rpcSource: "paid-rpc",
1218
+ chainId,
1219
+ rpcMethod,
1220
+ statusCode: 429,
1221
+ retryAt,
1222
+ retryAfterSeconds: Math.max(0, Math.ceil(delay / 1e3)),
1223
+ ...providerRequestId ? { providerRequestId } : {}
1224
+ },
1225
+ agentId
1226
+ );
1227
+ }
1228
+ async function withPaidRpcDiagnostics(operation, chainId, rpcMethod, agentId) {
1229
+ try {
1230
+ return await operation();
1231
+ } catch (error) {
1232
+ throw paidRpcError(error, chainId, rpcMethod, agentId);
1233
+ }
1234
+ }
1235
+
1066
1236
  // src/agents/zyfai/zyfai.agent.ts
1067
1237
  var ERC7579_IS_MODULE_INSTALLED_ABI = parseAbi([
1068
1238
  "function isModuleInstalled(uint256 moduleTypeId, address module, bytes additionalContext) view returns (bool)"
1069
1239
  ]);
1070
- var DEFAULT_ZYFAI_RPC_URLS = {
1071
- 8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1072
- 42161: "https://arb-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1073
- 1: "https://eth-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V"
1074
- };
1075
1240
  var WETH_ADDRESS_BY_CHAIN = {
1076
1241
  8453: "0x4200000000000000000000000000000000000006",
1077
1242
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
@@ -1137,7 +1302,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1137
1302
  earningsSnapshot = null;
1138
1303
  earningsGeneration = 0;
1139
1304
  constructor(apiKey, rpcUrls, referralSource) {
1140
- this.rpcUrls = rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS;
1305
+ this.rpcUrls = resolveRpcUrls(rpcUrls);
1141
1306
  this.sdk = new ZyfaiSDK({
1142
1307
  apiKey,
1143
1308
  rpcUrls: this.rpcUrls,
@@ -1151,10 +1316,10 @@ var ZyfaiAgent = class _ZyfaiAgent {
1151
1316
  getPublicClient(chainId) {
1152
1317
  const cached = this.publicClients.get(chainId);
1153
1318
  if (cached) return cached;
1154
- const client = createPublicClient({
1155
- chain: VIEM_CHAIN[chainId],
1156
- transport: http(this.rpcUrls[chainId])
1157
- });
1319
+ const client = createPaidRpcClient(
1320
+ VIEM_CHAIN[chainId],
1321
+ this.rpcUrls
1322
+ );
1158
1323
  this.publicClients.set(chainId, client);
1159
1324
  return client;
1160
1325
  }
@@ -2287,11 +2452,12 @@ async function postSponsorBatchTransfer(input) {
2287
2452
  // src/lib/permit2.ts
2288
2453
  import { bytesToHex as bytesToHex2 } from "viem";
2289
2454
  var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
2455
+ var MAX_UINT256 = 2n ** 256n - 1n;
2290
2456
  function permit2ApprovalAmount(requiredAmount) {
2291
2457
  if (requiredAmount <= 0n) {
2292
2458
  throw new Error("Permit2 approval requires a positive deposit amount");
2293
2459
  }
2294
- return requiredAmount;
2460
+ return MAX_UINT256;
2295
2461
  }
2296
2462
  var ERC20_ALLOWANCE_ABI = [
2297
2463
  {
@@ -2327,12 +2493,13 @@ function randomPermit2Nonce() {
2327
2493
  globalThis.crypto.getRandomValues(bytes);
2328
2494
  return BigInt(bytesToHex2(bytes));
2329
2495
  }
2330
- async function readPermit2Allowance(publicClient, token, owner) {
2496
+ async function readPermit2Allowance(publicClient, token, owner, blockNumber) {
2331
2497
  return publicClient.readContract({
2332
2498
  address: token,
2333
2499
  abi: ERC20_ALLOWANCE_ABI,
2334
2500
  functionName: "allowance",
2335
- args: [owner, PERMIT2_ADDRESS]
2501
+ args: [owner, PERMIT2_ADDRESS],
2502
+ ...blockNumber === void 0 ? {} : { blockNumber }
2336
2503
  });
2337
2504
  }
2338
2505
  async function readErc20Balance(publicClient, token, owner) {
@@ -2720,10 +2887,11 @@ var YieldseekerApiClient = class {
2720
2887
  const payload = await response.json().catch(() => null);
2721
2888
  if (!response.ok) {
2722
2889
  const error = providerError(payload, `HTTP_${response.status}`);
2890
+ const retryAfter = response.headers.get("Retry-After");
2723
2891
  throw new YieldseekerApiError(
2724
2892
  response.status,
2725
2893
  error.code,
2726
- error.fields
2894
+ retryAfter ? { ...error.fields, headers: { "Retry-After": retryAfter } } : error.fields
2727
2895
  );
2728
2896
  }
2729
2897
  if (payload && typeof payload === "object" && payload.success === true && "data" in payload) {
@@ -2854,13 +3022,14 @@ function position(value, asset, baseAssetDecimals) {
2854
3022
  // differ from the underlying asset. Yieldseeker already converts it to
2855
3023
  // underlying base-asset units in `assetsBase`; pair that value with the
2856
3024
  // snapshot's corresponding `baseAssetDecimals` for display. Keep the raw
2857
- // share quantity separately because withdraw-from-position expects it.
3025
+ // share quantity separate from the withdrawable underlying asset amount.
2858
3026
  amount: decimal(
2859
3027
  value.assetsBase,
2860
3028
  baseAssetDecimals,
2861
3029
  "yield positions"
2862
3030
  ),
2863
3031
  amountRaw: String(value.assetsRaw),
3032
+ withdrawableAmountRaw: String(value.withdrawableAssetsRaw),
2864
3033
  apy: percent(option.riskAdjustedApy),
2865
3034
  tvl: Number(option.totalDepositsUsd),
2866
3035
  liquidity: Number(option.withdrawableDepositsUsd)
@@ -3185,10 +3354,18 @@ function mapYieldseekerAgentApy(options, days) {
3185
3354
 
3186
3355
  // src/agents/yieldseeker/yieldseeker.agent.ts
3187
3356
  var OWNEY_AGENT_NAME = "owney";
3357
+ var OWNEY_AGENT_RULE_PRESET = "explorative";
3188
3358
  var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3189
3359
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3190
3360
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3191
3361
  var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3362
+ var YIELDSEEKER_AGENT_LIST_CACHE_MS = 12e4;
3363
+ var YIELDSEEKER_PORTFOLIO_CACHE_MS = 12e4;
3364
+ var YIELDSEEKER_SETTLEMENT_CACHE_MS = 5e3;
3365
+ var YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS = 15e3;
3366
+ var YIELDSEEKER_READ_FAILURE_COOLDOWN_MS = 6e4;
3367
+ var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
3368
+ var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
3192
3369
  function generateYieldseekerUsername() {
3193
3370
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
3194
3371
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -3232,6 +3409,7 @@ function query(params) {
3232
3409
  var YieldseekerAgent = class {
3233
3410
  id = "yieldseeker";
3234
3411
  balanceComposition = "tokens-plus-positions";
3412
+ withdrawalRequiresWalletApproval = true;
3235
3413
  supportedChainIds = [8453];
3236
3414
  supportedAssets = [
3237
3415
  {
@@ -3245,11 +3423,26 @@ var YieldseekerAgent = class {
3245
3423
  ];
3246
3424
  api;
3247
3425
  auth;
3426
+ rpcUrls;
3427
+ receiptClient;
3248
3428
  transactionExecutor;
3249
3429
  unwindReceiptWaiter;
3250
3430
  agentContexts = /* @__PURE__ */ new Map();
3251
3431
  users = /* @__PURE__ */ new Map();
3252
3432
  pendingAgents = /* @__PURE__ */ new Map();
3433
+ pendingWalletContexts = /* @__PURE__ */ new Map();
3434
+ readCache = /* @__PURE__ */ new Map();
3435
+ pendingReads = /* @__PURE__ */ new Map();
3436
+ readGeneration = 0;
3437
+ portfolioVersions = /* @__PURE__ */ new Map();
3438
+ snapshotFailures = /* @__PURE__ */ new Map();
3439
+ standardReadFailures = /* @__PURE__ */ new Map();
3440
+ reconcileUntil = /* @__PURE__ */ new Map();
3441
+ activityRefreshUntil = /* @__PURE__ */ new Map();
3442
+ // null means a confirmed movement has no reliable baseline. Do not guess
3443
+ // settlement; keep slow reconciliation after the fast window expires.
3444
+ snapshotMovements = /* @__PURE__ */ new Map();
3445
+ movementEpochs = /* @__PURE__ */ new Map();
3253
3446
  yieldOptions = /* @__PURE__ */ new Map();
3254
3447
  pendingYieldOptions = /* @__PURE__ */ new Map();
3255
3448
  constructor(owneyApiKey, options = {}) {
@@ -3259,10 +3452,16 @@ var YieldseekerAgent = class {
3259
3452
  options.fetchFn
3260
3453
  );
3261
3454
  this.auth = new YieldseekerAuth(options.auth);
3455
+ this.rpcUrls = options.rpcUrls;
3262
3456
  this.transactionExecutor = options.transactionExecutor;
3263
3457
  this.unwindReceiptWaiter = options.unwindReceiptWaiter;
3264
3458
  }
3459
+ getReceiptClient() {
3460
+ this.receiptClient ??= createPaidRpcClient(base3, this.rpcUrls);
3461
+ return this.receiptClient;
3462
+ }
3265
3463
  async disconnect() {
3464
+ this.readGeneration += 1;
3266
3465
  this.auth.clear();
3267
3466
  for (const key2 of this.users.keys()) {
3268
3467
  const [walletAddress, chainId] = key2.split(":");
@@ -3271,6 +3470,16 @@ var YieldseekerAgent = class {
3271
3470
  this.users.clear();
3272
3471
  this.agentContexts.clear();
3273
3472
  this.pendingAgents.clear();
3473
+ this.pendingWalletContexts.clear();
3474
+ this.readCache.clear();
3475
+ this.pendingReads.clear();
3476
+ this.portfolioVersions.clear();
3477
+ this.snapshotFailures.clear();
3478
+ this.standardReadFailures.clear();
3479
+ this.reconcileUntil.clear();
3480
+ this.activityRefreshUntil.clear();
3481
+ this.snapshotMovements.clear();
3482
+ this.movementEpochs.clear();
3274
3483
  }
3275
3484
  async activateAgent(state, chainId, asset) {
3276
3485
  this.assertChain(chainId);
@@ -3290,41 +3499,37 @@ var YieldseekerAgent = class {
3290
3499
  );
3291
3500
  }
3292
3501
  const context = await this.ensureAgent(state, chainId, asset);
3502
+ const generation = this.readGeneration;
3503
+ const movement = this.snapshotMovement(state, chainId, context, BigInt(amount));
3293
3504
  let txHash;
3294
- try {
3295
- if (depositCallback) {
3296
- provideDepositVerificationContext(depositCallback, {
3297
- agentId: "yieldseeker",
3298
- signature: await this.auth.getToken(state, chainId),
3299
- userId: context.user.userId,
3300
- yieldseekerAgentId: context.agent.agentId
3301
- });
3302
- txHash = await depositCallback(
3303
- context.wallet.walletAddress,
3304
- chainId,
3305
- amount
3306
- );
3307
- await this.waitForReceipt(state, chainId, txHash);
3308
- } else {
3309
- txHash = await this.submitTransaction(state, chainId, {
3310
- from: getAddress2(state.walletAddress),
3311
- to: YIELDSEEKER_ASSET_METADATA[asset].address,
3312
- data: encodeFunctionData({
3313
- abi: erc20Abi,
3314
- functionName: "transfer",
3315
- args: [getAddress2(context.wallet.walletAddress), BigInt(amount)]
3316
- }),
3317
- value: "0",
3318
- chainId
3319
- });
3320
- }
3321
- } finally {
3322
- await this.refreshSnapshotAfterMovement(
3323
- state,
3505
+ if (depositCallback) {
3506
+ provideDepositVerificationContext(depositCallback, {
3507
+ agentId: "yieldseeker",
3508
+ signature: await this.auth.getToken(state, chainId),
3509
+ userId: context.user.userId,
3510
+ yieldseekerAgentId: context.agent.agentId
3511
+ });
3512
+ txHash = await depositCallback(
3513
+ context.wallet.walletAddress,
3324
3514
  chainId,
3325
- context,
3326
- "deposit"
3515
+ amount
3327
3516
  );
3517
+ await this.waitForReceipt(state, chainId, txHash);
3518
+ } else {
3519
+ txHash = await this.submitTransaction(state, chainId, {
3520
+ from: getAddress2(state.walletAddress),
3521
+ to: YIELDSEEKER_ASSET_METADATA[asset].address,
3522
+ data: encodeFunctionData({
3523
+ abi: erc20Abi,
3524
+ functionName: "transfer",
3525
+ args: [getAddress2(context.wallet.walletAddress), BigInt(amount)]
3526
+ }),
3527
+ value: "0",
3528
+ chainId
3529
+ });
3530
+ }
3531
+ if (this.readGeneration === generation) {
3532
+ void this.refreshSnapshotAfterMovement(state, chainId, context, "deposit", movement);
3328
3533
  }
3329
3534
  return {
3330
3535
  txHash,
@@ -3352,6 +3557,9 @@ var YieldseekerAgent = class {
3352
3557
  this.id
3353
3558
  );
3354
3559
  }
3560
+ const generation = this.readGeneration;
3561
+ let confirmedMovement = false;
3562
+ let settlement = null;
3355
3563
  try {
3356
3564
  const portfolio = await this.loadPortfolioContext(
3357
3565
  state,
@@ -3369,6 +3577,7 @@ var YieldseekerAgent = class {
3369
3577
  );
3370
3578
  const totalAvailable = idle + deployed;
3371
3579
  const requested = amount === void 0 ? totalAvailable : BigInt(amount);
3580
+ const plannedMovement = this.snapshotMovement(state, chainId, context, -requested);
3372
3581
  if (requested > totalAvailable) {
3373
3582
  throw new OwneyError(
3374
3583
  "WITHDRAW_INSUFFICIENT_BALANCE",
@@ -3404,6 +3613,7 @@ var YieldseekerAgent = class {
3404
3613
  throw this.invalidResponse("position withdrawal");
3405
3614
  }
3406
3615
  await this.waitForReceipt(state, chainId, response.transactionHash);
3616
+ confirmedMovement = true;
3407
3617
  remaining -= assetsRaw;
3408
3618
  }
3409
3619
  if (remaining > 0n) {
@@ -3428,18 +3638,23 @@ var YieldseekerAgent = class {
3428
3638
  value: "0",
3429
3639
  chainId
3430
3640
  });
3641
+ confirmedMovement = true;
3642
+ settlement = plannedMovement;
3431
3643
  return {
3432
3644
  txHash,
3433
3645
  type: amount === void 0 ? "full" : "partial",
3434
3646
  amount: requested.toString()
3435
3647
  };
3436
3648
  } finally {
3437
- await this.refreshSnapshotAfterMovement(
3438
- state,
3439
- chainId,
3440
- context,
3441
- "withdrawal"
3442
- );
3649
+ if (confirmedMovement && this.readGeneration === generation) {
3650
+ void this.refreshSnapshotAfterMovement(
3651
+ state,
3652
+ chainId,
3653
+ context,
3654
+ "withdrawal",
3655
+ settlement
3656
+ );
3657
+ }
3443
3658
  }
3444
3659
  }
3445
3660
  async getBalances(state, chainId) {
@@ -3464,7 +3679,7 @@ var YieldseekerAgent = class {
3464
3679
  this.assertChain(chainId);
3465
3680
  const asset = options?.tokenSymbol?.toUpperCase();
3466
3681
  if (asset !== void 0) this.assertAsset(asset);
3467
- const contexts = await this.loadPortfolio(state, chainId, {
3682
+ const contexts = await this.loadActivityPortfolio(state, chainId, {
3468
3683
  ...asset ? { asset } : {},
3469
3684
  historic: true,
3470
3685
  actions: true
@@ -3491,7 +3706,7 @@ var YieldseekerAgent = class {
3491
3706
  this.assertChain(chainId);
3492
3707
  return mapYieldseekerProfile(
3493
3708
  state.walletAddress,
3494
- await this.loadPortfolio(state, chainId, {})
3709
+ await this.loadActivityPortfolio(state, chainId, {})
3495
3710
  );
3496
3711
  }
3497
3712
  async getAgentApy(days, options) {
@@ -3533,6 +3748,95 @@ var YieldseekerAgent = class {
3533
3748
  contextKey(state, chainId, asset) {
3534
3749
  return `${this.userKey(state, chainId)}:${asset}`;
3535
3750
  }
3751
+ assertSession(generation, chainId, asset) {
3752
+ if (this.readGeneration !== generation) {
3753
+ throw new OwneyError(
3754
+ "NOT_CONNECTED",
3755
+ "Wallet session changed during the Yieldseeker request.",
3756
+ { rpcSource: "agent-api", chainId, ...asset ? { asset } : {} },
3757
+ this.id
3758
+ );
3759
+ }
3760
+ }
3761
+ cachedRead(key2, ttlMs, read2) {
3762
+ const cached = this.readCache.get(key2);
3763
+ if (cached && cached.expiresAt > Date.now()) {
3764
+ return Promise.resolve(cached.value);
3765
+ }
3766
+ const pending = this.pendingReads.get(key2);
3767
+ if (pending) return pending;
3768
+ const generation = this.readGeneration;
3769
+ const request = Promise.resolve().then(read2).then((value) => {
3770
+ if (this.readGeneration === generation && this.pendingReads.get(key2) === request) {
3771
+ this.readCache.set(key2, {
3772
+ expiresAt: Date.now() + ttlMs,
3773
+ value
3774
+ });
3775
+ }
3776
+ return value;
3777
+ }).finally(() => {
3778
+ if (this.pendingReads.get(key2) === request) {
3779
+ this.pendingReads.delete(key2);
3780
+ }
3781
+ });
3782
+ this.pendingReads.set(key2, request);
3783
+ return request;
3784
+ }
3785
+ agentListKey(state, chainId) {
3786
+ return `agents:${this.userKey(state, chainId)}`;
3787
+ }
3788
+ async listAgents(state, chainId, user) {
3789
+ const response = await this.cachedRead(
3790
+ this.agentListKey(state, chainId),
3791
+ YIELDSEEKER_AGENT_LIST_CACHE_MS,
3792
+ async () => {
3793
+ const response2 = await this.walletRequest(
3794
+ state,
3795
+ chainId,
3796
+ `/users/${user.userId}/agents`
3797
+ );
3798
+ if (!Array.isArray(response2?.agents)) {
3799
+ throw this.invalidResponse("agent list");
3800
+ }
3801
+ return response2;
3802
+ }
3803
+ );
3804
+ return response.agents;
3805
+ }
3806
+ async contextForAgent(state, chainId, user, agent, asset) {
3807
+ const key2 = this.contextKey(state, chainId, asset);
3808
+ const cached = this.agentContexts.get(key2);
3809
+ if (cached?.agent.agentId === agent.agentId) return cached;
3810
+ const pending = this.pendingWalletContexts.get(key2);
3811
+ if (pending) return pending;
3812
+ const generation = this.readGeneration;
3813
+ const request = this.walletRequest(
3814
+ state,
3815
+ chainId,
3816
+ `/users/${user.userId}/agents/${agent.agentId}/wallet`
3817
+ ).then((walletResponse) => {
3818
+ this.assertSession(generation, chainId, asset);
3819
+ if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3820
+ throw this.invalidResponse("agent wallet");
3821
+ }
3822
+ const context = {
3823
+ user,
3824
+ agent,
3825
+ wallet: walletResponse.agentWallet,
3826
+ asset
3827
+ };
3828
+ this.agentContexts.set(key2, context);
3829
+ return context;
3830
+ });
3831
+ this.pendingWalletContexts.set(key2, request);
3832
+ try {
3833
+ return await request;
3834
+ } finally {
3835
+ if (this.pendingWalletContexts.get(key2) === request) {
3836
+ this.pendingWalletContexts.delete(key2);
3837
+ }
3838
+ }
3839
+ }
3536
3840
  async resolveUser(state, chainId) {
3537
3841
  const key2 = this.userKey(state, chainId);
3538
3842
  const inMemory = this.users.get(key2);
@@ -3543,6 +3847,7 @@ var YieldseekerAgent = class {
3543
3847
  return persisted;
3544
3848
  }
3545
3849
  const walletAddress = getAddress2(state.walletAddress);
3850
+ const generation = this.readGeneration;
3546
3851
  let user = null;
3547
3852
  try {
3548
3853
  const login = await this.providerRequest(
@@ -3589,6 +3894,7 @@ var YieldseekerAgent = class {
3589
3894
  if (!user || !/^[a-zA-Z0-9_-]{1,128}$/.test(user.userId)) {
3590
3895
  throw this.invalidResponse("wallet identity");
3591
3896
  }
3897
+ this.assertSession(generation, chainId);
3592
3898
  const resolved = { userId: user.userId };
3593
3899
  this.users.set(key2, resolved);
3594
3900
  writeYieldseekerIdentity(state.walletAddress, chainId, resolved.userId);
@@ -3604,10 +3910,13 @@ var YieldseekerAgent = class {
3604
3910
  if (cached) return cached;
3605
3911
  const pending = this.pendingAgents.get(key2);
3606
3912
  if (pending) return pending;
3913
+ const generation = this.readGeneration;
3607
3914
  const request = this.resolveAgent(state, chainId, asset, true).then(
3608
3915
  async (context) => {
3916
+ this.assertSession(generation, chainId, asset);
3609
3917
  if (!context) throw this.invalidResponse("agent creation");
3610
3918
  await this.deployAgent(state, chainId, context);
3919
+ this.assertSession(generation, chainId, asset);
3611
3920
  this.agentContexts.set(key2, context);
3612
3921
  return context;
3613
3922
  }
@@ -3616,29 +3925,27 @@ var YieldseekerAgent = class {
3616
3925
  try {
3617
3926
  return await request;
3618
3927
  } finally {
3619
- this.pendingAgents.delete(key2);
3928
+ if (this.pendingAgents.get(key2) === request) this.pendingAgents.delete(key2);
3620
3929
  }
3621
3930
  }
3622
3931
  async findAgent(state, chainId, asset) {
3623
3932
  const key2 = this.contextKey(state, chainId, asset);
3624
3933
  const cached = this.agentContexts.get(key2);
3625
3934
  if (cached) return cached;
3935
+ const generation = this.readGeneration;
3626
3936
  const context = await this.resolveAgent(state, chainId, asset, false);
3937
+ this.assertSession(generation, chainId, asset);
3627
3938
  if (context) this.agentContexts.set(key2, context);
3628
3939
  return context;
3629
3940
  }
3630
3941
  async resolveAgent(state, chainId, asset, createIfMissing) {
3942
+ const generation = this.readGeneration;
3631
3943
  const user = await this.resolveUser(state, chainId);
3632
- const response = await this.walletRequest(
3633
- state,
3634
- chainId,
3635
- `/users/${user.userId}/agents`
3636
- );
3637
- if (!Array.isArray(response?.agents)) {
3638
- throw this.invalidResponse("agent list");
3639
- }
3944
+ this.assertSession(generation, chainId, asset);
3945
+ const agents = await this.listAgents(state, chainId, user);
3946
+ this.assertSession(generation, chainId, asset);
3640
3947
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3641
- let agent = response.agents.find(
3948
+ let agent = agents.find(
3642
3949
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
3643
3950
  );
3644
3951
  if (!agent && createIfMissing) {
@@ -3654,99 +3961,254 @@ var YieldseekerAgent = class {
3654
3961
  chainId,
3655
3962
  assetAddress: metadata.address,
3656
3963
  type: "vault",
3657
- rulePreset: null
3964
+ rulePreset: OWNEY_AGENT_RULE_PRESET
3658
3965
  }
3659
3966
  }
3660
3967
  );
3968
+ this.assertSession(generation, chainId, asset);
3661
3969
  agent = created?.agent;
3970
+ if (agent) {
3971
+ this.readCache.set(this.agentListKey(state, chainId), {
3972
+ expiresAt: Date.now() + YIELDSEEKER_AGENT_LIST_CACHE_MS,
3973
+ value: {
3974
+ agents: [...agents, agent]
3975
+ }
3976
+ });
3977
+ }
3662
3978
  }
3663
3979
  if (!agent) return null;
3664
3980
  this.assertAgent(agent);
3665
- const walletResponse = await this.walletRequest(
3666
- state,
3667
- chainId,
3668
- `/users/${user.userId}/agents/${agent.agentId}/wallet`
3669
- );
3670
- if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3671
- throw this.invalidResponse("agent wallet");
3672
- }
3673
- return { user, agent, wallet: walletResponse.agentWallet, asset };
3981
+ return this.contextForAgent(state, chainId, user, agent, asset);
3674
3982
  }
3675
3983
  async loadPortfolio(state, chainId, options) {
3676
- const user = await this.resolveUser(state, chainId);
3677
- const response = await this.walletRequest(
3678
- state,
3679
- chainId,
3680
- `/users/${user.userId}/agents`
3984
+ const contexts = await this.resolvePortfolioContexts(state, chainId, options.asset);
3985
+ return Promise.all(
3986
+ contexts.map(
3987
+ (context) => this.loadPortfolioContext(state, chainId, context, options)
3988
+ )
3681
3989
  );
3682
- if (!Array.isArray(response?.agents)) {
3683
- throw this.invalidResponse("agent list");
3684
- }
3990
+ }
3991
+ async loadActivityPortfolio(state, chainId, options) {
3992
+ const contexts = await this.resolvePortfolioContexts(state, chainId, options.asset);
3993
+ return Promise.all(
3994
+ contexts.map(
3995
+ (context) => this.loadActivityContext(state, chainId, context, options)
3996
+ )
3997
+ );
3998
+ }
3999
+ async resolvePortfolioContexts(state, chainId, assetFilter) {
4000
+ const generation = this.readGeneration;
4001
+ const user = await this.resolveUser(state, chainId);
4002
+ this.assertSession(generation, chainId);
4003
+ const agents = await this.listAgents(state, chainId, user);
4004
+ this.assertSession(generation, chainId);
3685
4005
  const contexts = [];
3686
- for (const agent of response.agents) {
4006
+ for (const agent of agents) {
3687
4007
  const asset = this.assetForAgent(agent);
3688
- if (!this.isOwneyAgent(agent) || !asset || agent.chainId !== chainId || agent.type !== "vault" || options.asset && options.asset !== asset) {
4008
+ if (!this.isOwneyAgent(agent) || !asset || agent.chainId !== chainId || agent.type !== "vault" || assetFilter && assetFilter !== asset) {
3689
4009
  continue;
3690
4010
  }
3691
4011
  this.assertAgent(agent);
3692
- const walletResponse = await this.walletRequest(
3693
- state,
3694
- chainId,
3695
- `/users/${user.userId}/agents/${agent.agentId}/wallet`
3696
- );
3697
- if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3698
- throw this.invalidResponse("agent wallet");
3699
- }
3700
- const context = {
3701
- user,
3702
- agent,
3703
- wallet: walletResponse.agentWallet,
3704
- asset
4012
+ contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
4013
+ }
4014
+ const resolvedContexts = await Promise.all(contexts);
4015
+ this.assertSession(generation, chainId);
4016
+ return resolvedContexts;
4017
+ }
4018
+ portfolioVersion(contextKey) {
4019
+ return this.portfolioVersions.get(contextKey) ?? 0;
4020
+ }
4021
+ portfolioCacheMs(contextKey) {
4022
+ return (this.reconcileUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_CACHE_MS : YIELDSEEKER_PORTFOLIO_CACHE_MS;
4023
+ }
4024
+ snapshotMovement(state, chainId, context, delta) {
4025
+ const key2 = this.contextKey(state, chainId, context.asset);
4026
+ const previous = this.snapshotMovements.get(key2);
4027
+ if (this.snapshotMovements.has(key2)) {
4028
+ if (!previous || previous.delta > 0n !== delta > 0n) return null;
4029
+ return {
4030
+ ...previous,
4031
+ epoch: this.movementEpochs.get(key2) ?? 0,
4032
+ delta: previous.delta + delta,
4033
+ expectedBalance: previous.expectedBalance + delta,
4034
+ expectedNetDeposits: previous.expectedNetDeposits + delta
3705
4035
  };
3706
- this.agentContexts.set(this.contextKey(state, chainId, asset), context);
3707
- contexts.push(context);
3708
4036
  }
3709
- return Promise.all(
3710
- contexts.map(
3711
- (context) => this.loadPortfolioContext(state, chainId, context, options)
3712
- )
3713
- );
4037
+ const cached = this.readCache.get(`snapshot:${key2}`);
4038
+ if (!cached || cached.expiresAt <= Date.now()) return null;
4039
+ const snapshot = cached.value.agentSnapshot;
4040
+ if (snapshot.baseAssetDecimals !== YIELDSEEKER_ASSET_METADATA[context.asset].decimals) return null;
4041
+ if (!/^-?\d+$/.test(snapshot.netDepositsBase) || !/^\d+$/.test(snapshot.totalValueBase)) return null;
4042
+ return {
4043
+ epoch: this.movementEpochs.get(key2) ?? 0,
4044
+ delta,
4045
+ decimals: snapshot.baseAssetDecimals,
4046
+ expectedBalance: BigInt(snapshot.totalValueBase) + delta,
4047
+ expectedNetDeposits: BigInt(snapshot.netDepositsBase) + delta
4048
+ };
4049
+ }
4050
+ advancePortfolioVersion(contextKey) {
4051
+ const version = this.portfolioVersion(contextKey) + 1;
4052
+ this.portfolioVersions.set(contextKey, version);
4053
+ this.snapshotFailures.delete(contextKey);
4054
+ for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4055
+ const key2 = `${kind}:${contextKey}`;
4056
+ this.readCache.delete(key2);
4057
+ this.pendingReads.delete(key2);
4058
+ }
4059
+ return version;
4060
+ }
4061
+ requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, forceRefresh = false) {
4062
+ return this.walletRequest(
4063
+ state,
4064
+ chainId,
4065
+ `${this.agentPath(context, "snapshot")}${query(forceRefresh ? { shouldForceRefresh: true } : { shouldOnlyUseRecentValue: true, shouldAllowStaleOnError: true })}`
4066
+ ).then((response) => {
4067
+ if (!response?.agentSnapshot) {
4068
+ throw this.invalidResponse("agent snapshot");
4069
+ }
4070
+ if (this.readGeneration === generation && this.portfolioVersion(contextKey) === version) {
4071
+ this.snapshotFailures.delete(contextKey);
4072
+ const movement = this.snapshotMovements.get(contextKey);
4073
+ const snapshot = response.agentSnapshot;
4074
+ if (movement && snapshot.baseAssetDecimals === movement.decimals && /^-?\d+$/.test(snapshot.netDepositsBase) && /^\d+$/.test(snapshot.totalValueBase)) {
4075
+ const balance = BigInt(snapshot.totalValueBase);
4076
+ const netDeposits = BigInt(snapshot.netDepositsBase);
4077
+ const reconciled = movement.delta > 0n ? balance >= movement.expectedBalance && netDeposits >= movement.expectedNetDeposits : balance <= movement.expectedBalance && netDeposits <= movement.expectedNetDeposits;
4078
+ if (reconciled) {
4079
+ this.snapshotMovements.delete(contextKey);
4080
+ this.reconcileUntil.delete(contextKey);
4081
+ }
4082
+ }
4083
+ }
4084
+ return response;
4085
+ }).catch((error) => {
4086
+ if (this.readGeneration === generation && this.portfolioVersion(contextKey) === version) {
4087
+ this.snapshotFailures.set(contextKey, {
4088
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, rateLimitDelay(error) ?? 0),
4089
+ error
4090
+ });
4091
+ }
4092
+ throw error;
4093
+ });
4094
+ }
4095
+ portfolioSnapshot(state, chainId, context, contextKey) {
4096
+ const key2 = `snapshot:${contextKey}`;
4097
+ const cached = this.readCache.get(key2);
4098
+ if ((!cached || cached.expiresAt <= Date.now()) && !this.pendingReads.has(key2)) {
4099
+ const failure = this.snapshotFailures.get(contextKey);
4100
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4101
+ this.advancePortfolioVersion(contextKey);
4102
+ }
4103
+ const version = this.portfolioVersion(contextKey);
4104
+ const generation = this.readGeneration;
4105
+ return {
4106
+ version,
4107
+ read: this.cachedRead(key2, this.portfolioCacheMs(contextKey), () => (
4108
+ // Force only during the bounded post-transaction window. Keep unknown
4109
+ // or unreconciled movements for verification, but do not let them turn
4110
+ // routine polling into forced refreshes indefinitely.
4111
+ this.requestPortfolioSnapshot(
4112
+ state,
4113
+ chainId,
4114
+ context,
4115
+ contextKey,
4116
+ version,
4117
+ generation,
4118
+ this.snapshotMovements.has(contextKey) && (this.reconcileUntil.get(contextKey) ?? 0) > Date.now()
4119
+ )
4120
+ ))
4121
+ };
3714
4122
  }
3715
4123
  async loadPortfolioContext(state, chainId, context, options = {}) {
3716
- const [snapshot, positions, historic, actions] = await Promise.all([
3717
- this.walletRequest(
3718
- state,
3719
- chainId,
3720
- `${this.agentPath(context, "snapshot")}${query({
3721
- shouldOnlyUseRecentValue: true,
3722
- shouldAllowStaleOnError: true
3723
- })}`
3724
- ),
3725
- this.walletRequest(
3726
- state,
3727
- chainId,
3728
- this.agentPath(context, "yield-positions")
4124
+ const contextKey = this.contextKey(state, chainId, context.asset);
4125
+ const generation = this.readGeneration;
4126
+ const assertSession = () => this.assertSession(generation, chainId, context.asset);
4127
+ for (let attempt = 0; attempt < 2; attempt++) {
4128
+ const { version, read: read2 } = this.portfolioSnapshot(state, chainId, context, contextKey);
4129
+ const parts = this.loadPortfolioParts(state, chainId, context, options, contextKey);
4130
+ try {
4131
+ const [snapshot, portfolioParts] = await Promise.all([read2, parts]);
4132
+ assertSession();
4133
+ if (this.portfolioVersion(contextKey) !== version) continue;
4134
+ return {
4135
+ ...context,
4136
+ snapshot: snapshot.agentSnapshot,
4137
+ ...portfolioParts
4138
+ };
4139
+ } catch (error) {
4140
+ assertSession();
4141
+ if (this.portfolioVersion(contextKey) !== version) continue;
4142
+ throw error;
4143
+ }
4144
+ }
4145
+ throw new OwneyError(
4146
+ "AGENT_API_ERROR",
4147
+ "Yieldseeker portfolio changed during the read. Please retry.",
4148
+ { rpcSource: "agent-api", chainId, asset: context.asset },
4149
+ this.id
4150
+ );
4151
+ }
4152
+ async loadActivityContext(state, chainId, context, options) {
4153
+ const contextKey = this.contextKey(state, chainId, context.asset);
4154
+ const generation = this.readGeneration;
4155
+ for (let attempt = 0; attempt < 2; attempt++) {
4156
+ const version = this.portfolioVersion(contextKey);
4157
+ try {
4158
+ const parts = await this.loadPortfolioParts(state, chainId, context, options, contextKey);
4159
+ this.assertSession(generation, chainId, context.asset);
4160
+ if (this.portfolioVersion(contextKey) !== version) continue;
4161
+ return { ...context, ...parts };
4162
+ } catch (error) {
4163
+ this.assertSession(generation, chainId, context.asset);
4164
+ if (this.portfolioVersion(contextKey) !== version) continue;
4165
+ throw error;
4166
+ }
4167
+ }
4168
+ throw new OwneyError(
4169
+ "AGENT_API_ERROR",
4170
+ "Yieldseeker activity changed during the read. Please retry.",
4171
+ { rpcSource: "agent-api", chainId, asset: context.asset },
4172
+ this.id
4173
+ );
4174
+ }
4175
+ async loadPortfolioParts(state, chainId, context, options, contextKey) {
4176
+ const [positions, historic, actions] = await Promise.all([
4177
+ this.cachedRead(
4178
+ `positions:${contextKey}`,
4179
+ this.portfolioCacheMs(contextKey),
4180
+ async () => {
4181
+ const response = await this.walletRequest(
4182
+ state,
4183
+ chainId,
4184
+ this.agentPath(context, "yield-positions")
4185
+ );
4186
+ if (!Array.isArray(response?.yieldPositions)) {
4187
+ throw this.invalidResponse("yield positions");
4188
+ }
4189
+ return response;
4190
+ }
3729
4191
  ),
3730
- options.historic ? this.walletRequest(
3731
- state,
3732
- chainId,
3733
- this.agentPath(context, "wallet/historic-position")
4192
+ options.historic ? this.cachedRead(
4193
+ `historic:${contextKey}`,
4194
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4195
+ () => this.walletRequest(
4196
+ state,
4197
+ chainId,
4198
+ this.agentPath(context, "wallet/historic-position")
4199
+ )
3734
4200
  ) : Promise.resolve(void 0),
3735
- options.actions ? this.walletRequest(
3736
- state,
3737
- chainId,
3738
- this.agentPath(context, "actions")
4201
+ options.actions ? this.cachedRead(
4202
+ `actions:${contextKey}`,
4203
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4204
+ () => this.walletRequest(
4205
+ state,
4206
+ chainId,
4207
+ this.agentPath(context, "actions")
4208
+ )
3739
4209
  ) : Promise.resolve(void 0)
3740
4210
  ]);
3741
- if (!snapshot?.agentSnapshot) {
3742
- throw this.invalidResponse("agent snapshot");
3743
- }
3744
- if (!Array.isArray(positions?.yieldPositions)) {
3745
- throw this.invalidResponse("yield positions");
3746
- }
3747
4211
  return {
3748
- ...context,
3749
- snapshot: snapshot.agentSnapshot,
3750
4212
  positions: positions.yieldPositions,
3751
4213
  ...historic?.position ? { historic: historic.position } : {},
3752
4214
  ...actions?.actions ? { actions: actions.actions } : {}
@@ -3768,18 +4230,21 @@ var YieldseekerAgent = class {
3768
4230
  }
3769
4231
  context.wallet = deployed.agentWallet;
3770
4232
  }
3771
- async refreshSnapshotAfterMovement(state, chainId, context, movement) {
4233
+ async refreshSnapshotAfterMovement(state, chainId, context, movement, settlement) {
4234
+ const contextKey = this.contextKey(state, chainId, context.asset);
4235
+ this.reconcileUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4236
+ this.activityRefreshUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4237
+ const epoch = this.movementEpochs.get(contextKey) ?? 0;
4238
+ this.snapshotMovements.set(contextKey, settlement?.epoch === epoch ? settlement : null);
4239
+ this.movementEpochs.set(contextKey, epoch + 1);
4240
+ const version = this.advancePortfolioVersion(contextKey);
4241
+ const generation = this.readGeneration;
3772
4242
  try {
3773
- const response = await this.walletRequest(
3774
- state,
3775
- chainId,
3776
- `${this.agentPath(context, "snapshot")}${query({
3777
- shouldForceRefresh: true
3778
- })}`
4243
+ await this.cachedRead(
4244
+ `snapshot:${contextKey}`,
4245
+ YIELDSEEKER_SETTLEMENT_CACHE_MS,
4246
+ () => this.requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, true)
3779
4247
  );
3780
- if (!response?.agentSnapshot) {
3781
- throw this.invalidResponse("agent snapshot refresh");
3782
- }
3783
4248
  } catch (error) {
3784
4249
  console.warn(
3785
4250
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
@@ -3791,10 +4256,26 @@ var YieldseekerAgent = class {
3791
4256
  return `/users/${context.user.userId}/agents/${context.agent.agentId}/${suffix}`;
3792
4257
  }
3793
4258
  async walletRequest(state, chainId, path, options = {}) {
4259
+ const read2 = (options.method ?? "GET") === "GET";
4260
+ const userKey = this.userKey(state, chainId);
4261
+ if (read2) {
4262
+ const failure = this.standardReadFailures.get(userKey);
4263
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4264
+ if (failure) this.standardReadFailures.delete(userKey);
4265
+ }
4266
+ const generation = this.readGeneration;
3794
4267
  try {
3795
4268
  return await this.providerRequest(state, chainId, path, options);
3796
4269
  } catch (error) {
3797
- throw this.mapApiError(error);
4270
+ const mapped = this.mapApiError(error);
4271
+ const delay = read2 ? rateLimitDelay(mapped) : void 0;
4272
+ if (delay !== void 0 && this.readGeneration === generation) {
4273
+ this.standardReadFailures.set(userKey, {
4274
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, delay),
4275
+ error: mapped
4276
+ });
4277
+ }
4278
+ throw mapped;
3798
4279
  }
3799
4280
  }
3800
4281
  async providerRequest(state, chainId, path, options = {}) {
@@ -3835,6 +4316,7 @@ var YieldseekerAgent = class {
3835
4316
  code,
3836
4317
  `Yieldseeker request failed: ${error.providerCode}.`,
3837
4318
  {
4319
+ rpcSource: "agent-api",
3838
4320
  statusCode: error.status,
3839
4321
  providerCode: error.providerCode,
3840
4322
  ...error.responseFields ? { fields: error.responseFields } : {}
@@ -3853,12 +4335,12 @@ var YieldseekerAgent = class {
3853
4335
  chain: base3,
3854
4336
  transport: custom2(state.provider)
3855
4337
  });
3856
- const publicClient = createPublicClient3({
4338
+ const walletChainClient = createPublicClient3({
3857
4339
  chain: base3,
3858
4340
  transport: custom2(state.provider)
3859
4341
  });
3860
4342
  await ensureWalletOnChain(
3861
- publicClient,
4343
+ walletChainClient,
3862
4344
  walletClient,
3863
4345
  8453
3864
4346
  );
@@ -3869,10 +4351,15 @@ var YieldseekerAgent = class {
3869
4351
  data: transaction.data,
3870
4352
  value: BigInt(transaction.value)
3871
4353
  });
3872
- const receipt = await publicClient.waitForTransactionReceipt({
3873
- hash,
3874
- confirmations: 1
3875
- });
4354
+ const receipt = await withPaidRpcDiagnostics(
4355
+ () => this.getReceiptClient().waitForTransactionReceipt({
4356
+ hash,
4357
+ confirmations: 1
4358
+ }),
4359
+ 8453,
4360
+ "eth_getTransactionReceipt",
4361
+ this.id
4362
+ );
3876
4363
  if (receipt.status !== "success") {
3877
4364
  throw new OwneyError(
3878
4365
  "AGENT_TRANSACTION_REVERTED",
@@ -3888,14 +4375,15 @@ var YieldseekerAgent = class {
3888
4375
  await this.unwindReceiptWaiter(state, chainId, transactionHash);
3889
4376
  return;
3890
4377
  }
3891
- const publicClient = createPublicClient3({
3892
- chain: base3,
3893
- transport: custom2(state.provider)
3894
- });
3895
- const receipt = await publicClient.waitForTransactionReceipt({
3896
- hash: transactionHash,
3897
- confirmations: 1
3898
- });
4378
+ const receipt = await withPaidRpcDiagnostics(
4379
+ () => this.getReceiptClient().waitForTransactionReceipt({
4380
+ hash: transactionHash,
4381
+ confirmations: 1
4382
+ }),
4383
+ 8453,
4384
+ "eth_getTransactionReceipt",
4385
+ this.id
4386
+ );
3899
4387
  if (receipt.status !== "success") {
3900
4388
  throw new OwneyError(
3901
4389
  "AGENT_TRANSACTION_REVERTED",
@@ -4084,16 +4572,16 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
4084
4572
  const positionChain = position2.chain.trim().toUpperCase();
4085
4573
  const matchesChain = positionChain === String(chainId) || targetChain !== void 0 && positionChain === targetChain;
4086
4574
  if (!matchesChain || position2.asset.toUpperCase() !== target) continue;
4087
- if (position2.amountRaw !== void 0) {
4088
- try {
4089
- balance += BigInt(position2.amountRaw);
4090
- continue;
4091
- } catch {
4092
- }
4093
- }
4094
- balance += parseUnits(position2.amount, decimals);
4575
+ balance += position2.withdrawableAmountRaw !== void 0 ? BigInt(position2.withdrawableAmountRaw) : parseUnits(position2.amount, decimals);
4095
4576
  }
4096
4577
  }
4578
+ const assetTotal = agentBalance?.assetBalances?.find(
4579
+ (t) => t.chainId === chainId && t.asset.toUpperCase() === target
4580
+ );
4581
+ if (assetTotal) {
4582
+ const total = parseUnits(assetTotal.amount, decimals);
4583
+ if (total < balance) balance = total > 0n ? total : 0n;
4584
+ }
4097
4585
  return { agent, balance };
4098
4586
  });
4099
4587
  }
@@ -4267,6 +4755,41 @@ import {
4267
4755
  // src/lib/permit2-batch.ts
4268
4756
  import { parseAbi as parseAbi2, hashStruct } from "viem";
4269
4757
  var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
4758
+ var BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
4759
+ var MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
4760
+ var ERC2612_PERMIT_TYPES = {
4761
+ Permit: [
4762
+ { name: "owner", type: "address" },
4763
+ { name: "spender", type: "address" },
4764
+ { name: "value", type: "uint256" },
4765
+ { name: "nonce", type: "uint256" },
4766
+ { name: "deadline", type: "uint256" }
4767
+ ]
4768
+ };
4769
+ var ERC2612_READ_ABI = parseAbi2([
4770
+ "function name() view returns (string)",
4771
+ "function version() view returns (string)",
4772
+ "function nonces(address owner) view returns (uint256)"
4773
+ ]);
4774
+ function erc2612TypedData(input) {
4775
+ return {
4776
+ domain: {
4777
+ name: input.permit.tokenName,
4778
+ version: input.permit.tokenVersion,
4779
+ chainId: input.chainId,
4780
+ verifyingContract: input.token
4781
+ },
4782
+ types: ERC2612_PERMIT_TYPES,
4783
+ primaryType: "Permit",
4784
+ message: {
4785
+ owner: input.owner,
4786
+ spender: BATCH_PERMIT2_ADDRESS,
4787
+ value: BigInt(input.permit.value),
4788
+ nonce: BigInt(input.permit.nonce),
4789
+ deadline: BigInt(input.permit.deadline)
4790
+ }
4791
+ };
4792
+ }
4270
4793
  var PERMIT_BATCH_TYPES = {
4271
4794
  PermitBatchWitnessTransferFrom: [
4272
4795
  { name: "permitted", type: "TokenPermissions[]" },
@@ -4418,28 +4941,67 @@ async function execute(i, key2, plan) {
4418
4941
  "DEPOSIT_INSUFFICIENT_BALANCE",
4419
4942
  "Insufficient token balance for this deposit."
4420
4943
  );
4421
- if (allowance < total)
4944
+ if (allowance < total && (i.chainId !== 8453 || !isAddressEqual(i.token, BASE_USDC_ADDRESS)))
4422
4945
  throw new OwneyError(
4423
4946
  "PERMIT2_APPROVAL_REQUIRED",
4424
4947
  "token deposits need a one-time Permit2 approval."
4425
4948
  );
4426
- const relayer = await getSponsorRelayerAddress({
4949
+ const now = (await i.pub.getBlock()).timestamp;
4950
+ let erc2612Permit;
4951
+ if (allowance < total) {
4952
+ const [tokenName, tokenVersion, permitNonce] = await Promise.all([
4953
+ i.pub.readContract({
4954
+ address: i.token,
4955
+ abi: ERC2612_READ_ABI,
4956
+ functionName: "name"
4957
+ }),
4958
+ i.pub.readContract({
4959
+ address: i.token,
4960
+ abi: ERC2612_READ_ABI,
4961
+ functionName: "version"
4962
+ }),
4963
+ i.pub.readContract({
4964
+ address: i.token,
4965
+ abi: ERC2612_READ_ABI,
4966
+ functionName: "nonces",
4967
+ args: [i.owner]
4968
+ })
4969
+ ]);
4970
+ const unsignedPermit = {
4971
+ value: MAX_UINT256.toString(),
4972
+ nonce: permitNonce.toString(),
4973
+ deadline: (now + 900n).toString(),
4974
+ tokenName,
4975
+ tokenVersion
4976
+ };
4977
+ const signature2 = await i.wallet.signTypedData({
4978
+ account: i.owner,
4979
+ ...erc2612TypedData({
4980
+ chainId: 8453,
4981
+ token: i.token,
4982
+ owner: i.owner,
4983
+ permit: unsignedPermit
4984
+ })
4985
+ });
4986
+ erc2612Permit = { ...unsignedPermit, signature: signature2 };
4987
+ }
4988
+ const spender = erc2612Permit ? MULTICALL3_ADDRESS : await getSponsorRelayerAddress({
4427
4989
  apiKey: i.apiKey,
4428
4990
  baseUrl: i.baseUrl,
4429
4991
  chainId: i.chainId
4430
4992
  });
4431
- const now = (await i.pub.getBlock()).timestamp;
4432
4993
  const unsigned = {
4433
4994
  chainId: i.chainId,
4434
4995
  token: i.token,
4435
4996
  from: i.owner,
4436
4997
  transfers: i.transfers,
4437
4998
  nonce: randomPermit2Nonce().toString(),
4438
- deadline: (now + 900n).toString()
4999
+ deadline: (now + 900n).toString(),
5000
+ ...erc2612Permit ? { erc2612Permit } : {}
4439
5001
  };
4440
5002
  const signature = await i.wallet.signTypedData({
4441
5003
  account: i.owner,
4442
- ...batchTypedData(unsigned, relayer)
5004
+ ...batchTypedData(unsigned, spender)
4443
5005
  });
4444
5006
  i.onApproved?.();
4445
5007
  return send({ ...unsigned, signature });
@@ -4459,8 +5021,8 @@ function makeSponsoredTokenCallback(deps) {
4459
5021
  "CHAIN_UNSUPPORTED",
4460
5022
  `No sponsored token configured for chain ${chainId}`
4461
5023
  );
4462
- const pub = deps.getPublicClient(chainId), wallet = deps.getWalletClient(chainId);
4463
- await ensureWalletOnChain(pub, wallet, chainId);
5024
+ const pub = deps.getPublicClient(chainId), walletChain = deps.getWalletChainClient?.(chainId) ?? pub, wallet = deps.getWalletClient(chainId);
5025
+ await ensureWalletOnChain(walletChain, wallet, chainId);
4464
5026
  return sponsorTokenBatch({
4465
5027
  apiKey: deps.apiKey,
4466
5028
  baseUrl: deps.baseUrl,
@@ -4700,6 +5262,8 @@ function makeSponsoredCallsCallback(deps) {
4700
5262
  }
4701
5263
 
4702
5264
  // src/client.ts
5265
+ var PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS = 6;
5266
+ var PERMIT2_ALLOWANCE_VERIFY_DELAY_MS = 250;
4703
5267
  function encodeMultiAgentCursor(map) {
4704
5268
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
4705
5269
  }
@@ -4760,6 +5324,7 @@ var OwneySDK = class {
4760
5324
  // leave every user's agent profile alone.
4761
5325
  orgAgentConfig;
4762
5326
  orgAgentConfigPromise = null;
5327
+ rpcUrls;
4763
5328
  zyfaiRpcUrls;
4764
5329
  yieldseekerApiBaseUrl;
4765
5330
  yieldseekerSiweOrigin;
@@ -4784,7 +5349,12 @@ var OwneySDK = class {
4784
5349
  constructor(config) {
4785
5350
  this.apiKey = config.apiKey;
4786
5351
  if (config.debug) setOwneyDebug(true);
4787
- this.zyfaiRpcUrls = config.zyfaiRpcUrls;
5352
+ this.rpcUrls = resolveRpcUrls(
5353
+ config.rpcUrls,
5354
+ config.apiKey,
5355
+ config.routingApiBaseUrl
5356
+ );
5357
+ this.zyfaiRpcUrls = config.rpcUrls ? void 0 : config.zyfaiRpcUrls;
4788
5358
  this.yieldseekerApiBaseUrl = config.yieldseekerApiBaseUrl;
4789
5359
  this.yieldseekerSiweOrigin = config.yieldseekerSiweOrigin;
4790
5360
  this.routingApiBaseUrl = config.routingApiBaseUrl;
@@ -4874,6 +5444,9 @@ var OwneySDK = class {
4874
5444
  }
4875
5445
  return this.state.provider;
4876
5446
  }
5447
+ getPaidRpcClient(chainId) {
5448
+ return createPaidRpcClient(VIEM_CHAIN2[chainId], this.rpcUrls);
5449
+ }
4877
5450
  /** Builds the default USDC batch callback for the connected wallet. */
4878
5451
  getDefaultSponsoredCallback(onApproved) {
4879
5452
  if (!onApproved && this.cachedSponsoredCallback)
@@ -4889,7 +5462,8 @@ var OwneySDK = class {
4889
5462
  // Casts work around viem's chain-narrowed Client vs the generic
4890
5463
  // PublicClient/WalletClient param types — structurally identical at
4891
5464
  // runtime, but the two share a name TS treats as unrelated.
4892
- getPublicClient: (cid) => createPublicClient4({
5465
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5466
+ getWalletChainClient: (cid) => createPublicClient4({
4893
5467
  chain: VIEM_CHAIN2[cid],
4894
5468
  transport: custom3(provider)
4895
5469
  }),
@@ -4939,7 +5513,8 @@ var OwneySDK = class {
4939
5513
  // Casts work around viem's chain-narrowed Client vs the generic
4940
5514
  // PublicClient/WalletClient param types — structurally identical at
4941
5515
  // runtime, but the two share a name TS treats as unrelated.
4942
- getPublicClient: (cid) => createPublicClient4({
5516
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5517
+ getWalletChainClient: (cid) => createPublicClient4({
4943
5518
  chain: VIEM_CHAIN2[cid],
4944
5519
  transport: custom3(provider)
4945
5520
  }),
@@ -5052,12 +5627,21 @@ var OwneySDK = class {
5052
5627
  createAgent(agentId, key2) {
5053
5628
  if (agentId === "zyfai") {
5054
5629
  if (!key2) return null;
5055
- return new ZyfaiAgent(key2, this.zyfaiRpcUrls, this.referralSource);
5630
+ return new ZyfaiAgent(
5631
+ key2,
5632
+ this.zyfaiRpcUrls ? resolveRpcUrls(
5633
+ this.zyfaiRpcUrls,
5634
+ this.apiKey,
5635
+ this.routingApiBaseUrl
5636
+ ) : this.rpcUrls,
5637
+ this.referralSource
5638
+ );
5056
5639
  }
5057
5640
  if (agentId === "yieldseeker") {
5058
5641
  return new YieldseekerAgent(this.apiKey, {
5059
5642
  auth: { origin: this.yieldseekerSiweOrigin },
5060
- baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl)
5643
+ baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl),
5644
+ rpcUrls: this.rpcUrls
5061
5645
  });
5062
5646
  }
5063
5647
  return null;
@@ -5348,7 +5932,8 @@ var OwneySDK = class {
5348
5932
  );
5349
5933
  await this.approvePermit2(
5350
5934
  asset,
5351
- requiredAmount
5935
+ requiredAmount,
5936
+ cid
5352
5937
  );
5353
5938
  return batchTransfer(cid, transfers);
5354
5939
  }
@@ -5392,10 +5977,12 @@ var OwneySDK = class {
5392
5977
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
5393
5978
  * two independent auto-recovery mechanisms:
5394
5979
  *
5395
- * 1. Missing Permit2 allowance: when the app did not supply its own
5396
- * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
5397
- * token deposit, this is the wallet's first Permit2 deposit for that token. We send
5398
- * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
5980
+ * 1. Missing Permit2 allowance outside the atomic Base-USDC path: when the
5981
+ * app did not supply its own callback and the attempt fails with
5982
+ * `PERMIT2_APPROVAL_REQUIRED`, this is the wallet's first Permit2 deposit
5983
+ * for that token. Base USDC bundles a gasless ERC-2612 approval inside
5984
+ * its sponsored deposit and never reaches this branch. Other tokens send
5985
+ * the one-time user-paid Permit2 approval via `approvePermit2()` and
5399
5986
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
5400
5987
  * per call so a wallet/agent that keeps reporting the allowance as
5401
5988
  * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
@@ -5438,7 +6025,8 @@ var OwneySDK = class {
5438
6025
  );
5439
6026
  await this.approvePermit2(
5440
6027
  asset,
5441
- BigInt(amount)
6028
+ BigInt(amount),
6029
+ chainId
5442
6030
  );
5443
6031
  continue;
5444
6032
  }
@@ -5585,6 +6173,32 @@ var OwneySDK = class {
5585
6173
  }
5586
6174
  return eligible;
5587
6175
  }
6176
+ /**
6177
+ * Run owner-approved withdrawals before relayer-only withdrawals. Wallet
6178
+ * approval is the only point at which the user can cancel the aggregate
6179
+ * operation, so no relayer leg should commit before it has completed.
6180
+ */
6181
+ orderAgentsForWithdrawal(agents) {
6182
+ return agents.map((agent, index) => ({ agent, index })).sort((left, right) => {
6183
+ const approvalOrder = Number(Boolean(right.agent.withdrawalRequiresWalletApproval)) - Number(Boolean(left.agent.withdrawalRequiresWalletApproval));
6184
+ return approvalOrder || left.index - right.index;
6185
+ }).map(({ agent }) => agent);
6186
+ }
6187
+ isUserRejectedWithdrawal(error) {
6188
+ let current = error;
6189
+ const seen = /* @__PURE__ */ new Set();
6190
+ while (current && typeof current === "object" && !seen.has(current)) {
6191
+ seen.add(current);
6192
+ const candidate = current;
6193
+ if (candidate.code === 4001 || candidate.code === "4001") return true;
6194
+ const message = [candidate.message, candidate.shortMessage].filter((value) => typeof value === "string").join(" ");
6195
+ if (/user (?:rejected|denied)|rejected by user/i.test(message)) {
6196
+ return true;
6197
+ }
6198
+ current = candidate.cause;
6199
+ }
6200
+ return false;
6201
+ }
5588
6202
  // --- Fund operations ---
5589
6203
  /**
5590
6204
  * Withdraw funds from a specific agent, or all agents that support the active chain+asset if agentId is omitted.
@@ -5596,7 +6210,14 @@ var OwneySDK = class {
5596
6210
  * @returns {AgentWithdrawResult} for a single agent, or {OwneyWithdrawResult} with per-agent results
5597
6211
  */
5598
6212
  async withdraw(options) {
5599
- const { asset, amount, agentId } = options;
6213
+ const { asset, amount, agentId, onAgentResult } = options;
6214
+ const notifyAgentResult = (id, result) => {
6215
+ try {
6216
+ onAgentResult?.(id, result);
6217
+ } catch (error) {
6218
+ console.warn("Withdrawal result observer failed:", error);
6219
+ }
6220
+ };
5600
6221
  const state = this.requireState();
5601
6222
  const chainId = this.requireChainId();
5602
6223
  const token = asset;
@@ -5612,21 +6233,26 @@ var OwneySDK = class {
5612
6233
  if (agentId) {
5613
6234
  const agent = this.getAgent(agentId);
5614
6235
  this.validateAssetSupport(agent, chainId, asset);
5615
- return withFailureReporting(
6236
+ const result = await withFailureReporting(
5616
6237
  this.apiKey,
5617
6238
  agent.id,
5618
6239
  () => agent.withdraw(state, chainId, token, amount),
5619
6240
  this.routingApiBaseUrl
5620
6241
  );
6242
+ notifyAgentResult(agent.id, result);
6243
+ return result;
5621
6244
  }
5622
6245
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
6246
+ const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
5623
6247
  if (!amount) {
5624
6248
  const results2 = {};
5625
6249
  const agentErrors2 = {};
5626
- for (const agent of eligibleAgents) {
6250
+ for (const agent of withdrawalAgents) {
5627
6251
  try {
5628
6252
  results2[agent.id] = await agent.withdraw(state, chainId, token);
6253
+ notifyAgentResult(agent.id, results2[agent.id]);
5629
6254
  } catch (err) {
6255
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5630
6256
  console.error(`withdraw failed for agent "${agent.id}":`, err);
5631
6257
  agentErrors2[agent.id] = err instanceof Error ? err.message : String(err);
5632
6258
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5707,12 +6333,13 @@ var OwneySDK = class {
5707
6333
  planned: 0n
5708
6334
  }));
5709
6335
  const plans = [...disabledPlans, ...enabledPlans];
6336
+ const orderedPlans = this.orderAgentsForWithdrawal(plans.map((p) => p.agent)).map((agent) => plans.find((plan) => plan.agent === agent));
5710
6337
  const results = {};
5711
6338
  const agentErrors = {
5712
6339
  ...aggregated.agentErrors ?? {}
5713
6340
  };
5714
- for (let i = 0; i < plans.length; i++) {
5715
- const p = plans[i];
6341
+ for (let i = 0; i < orderedPlans.length; i++) {
6342
+ const p = orderedPlans[i];
5716
6343
  if (p.planned === 0n) continue;
5717
6344
  try {
5718
6345
  results[p.agent.id] = await p.agent.withdraw(
@@ -5721,7 +6348,9 @@ var OwneySDK = class {
5721
6348
  token,
5722
6349
  p.planned.toString()
5723
6350
  );
6351
+ notifyAgentResult(p.agent.id, results[p.agent.id]);
5724
6352
  } catch (err) {
6353
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5725
6354
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);
5726
6355
  agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
5727
6356
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5733,7 +6362,7 @@ var OwneySDK = class {
5733
6362
  );
5734
6363
  const failedAmount = p.planned;
5735
6364
  p.planned = 0n;
5736
- redistributeShare(plans, i, failedAmount);
6365
+ redistributeShare(orderedPlans, i, failedAmount);
5737
6366
  }
5738
6367
  }
5739
6368
  if (Object.keys(results).length === 0) {
@@ -5843,23 +6472,11 @@ var OwneySDK = class {
5843
6472
  const agent = this.getAgent(agentId);
5844
6473
  return this.readAgent(agent, "earnings", () => agent.getEarnings(state, chainId));
5845
6474
  }
5846
- let totalEarnings = 0;
5847
- const results = {};
5848
6475
  const entries = [...this.getActiveAgents().entries()];
5849
- const earningsResults = await Promise.all(
5850
- entries.map(async ([id, agent]) => {
5851
- const e = await this.readAgent(agent, "earnings", () => agent.getEarnings(state, chainId));
5852
- return [id, e];
5853
- })
6476
+ return this.aggregateEarnings(
6477
+ entries,
6478
+ (agent) => this.readAgent(agent, "earnings", () => agent.getEarnings(state, chainId))
5854
6479
  );
5855
- for (const [id, e] of earningsResults) {
5856
- results[id] = e;
5857
- totalEarnings += Number(e.lifetimeEarnings);
5858
- }
5859
- return {
5860
- totalEarnings: String(totalEarnings),
5861
- agentEarnings: results
5862
- };
5863
6480
  }
5864
6481
  /**
5865
6482
  * Return refreshed earnings while letting each agent own its refresh policy.
@@ -5874,18 +6491,42 @@ var OwneySDK = class {
5874
6491
  () => agent.refreshEarnings?.(state, chainId) ?? agent.getEarnings(state, chainId)
5875
6492
  );
5876
6493
  if (agentId) return refreshAgent(this.getAgent(agentId));
5877
- let totalEarnings = 0;
6494
+ return this.aggregateEarnings([...this.getActiveAgents().entries()], refreshAgent);
6495
+ }
6496
+ async aggregateEarnings(entries, read2) {
6497
+ const settled = await Promise.allSettled(entries.map(([, agent]) => read2(agent)));
5878
6498
  const agentEarnings = {};
5879
- const refreshed = await Promise.all(
5880
- [...this.getActiveAgents().entries()].map(
5881
- async ([id, agent]) => [id, await refreshAgent(agent)]
5882
- )
5883
- );
5884
- for (const [id, earnings] of refreshed) {
5885
- agentEarnings[id] = earnings;
5886
- totalEarnings += Number(earnings.lifetimeEarnings);
6499
+ const agentErrors = {};
6500
+ const agentRetryAt = {};
6501
+ const failures = [];
6502
+ let totalEarnings = 0;
6503
+ for (let i = 0; i < settled.length; i++) {
6504
+ const [id] = entries[i];
6505
+ const result = settled[i];
6506
+ if (result.status === "fulfilled") {
6507
+ agentEarnings[id] = result.value;
6508
+ totalEarnings += Number(result.value.lifetimeEarnings);
6509
+ continue;
6510
+ }
6511
+ const reason = result.reason;
6512
+ failures.push(reason);
6513
+ const retryDelay = rateLimitDelay(reason);
6514
+ if (retryDelay !== void 0) agentRetryAt[id] = Date.now() + retryDelay;
6515
+ agentErrors[id] = reason instanceof Error ? reason.message : String(reason);
6516
+ console.error(`[owney-sdk] Earnings fetch failed for agent "${id}":`, reason);
5887
6517
  }
5888
- return { totalEarnings: String(totalEarnings), agentEarnings };
6518
+ if (entries.length > 0 && Object.keys(agentEarnings).length === 0) {
6519
+ throw new OwneyError(
6520
+ "EARNINGS_ALL_FAILED",
6521
+ "Failed to fetch earnings for all active agents.",
6522
+ { agentErrors, failures }
6523
+ );
6524
+ }
6525
+ return {
6526
+ totalEarnings: String(totalEarnings),
6527
+ agentEarnings,
6528
+ ...Object.keys(agentErrors).length > 0 ? { agentErrors, agentRetryAt } : {}
6529
+ };
5889
6530
  }
5890
6531
  /**
5891
6532
  * Get the weighted APY for the user's account over a time period.
@@ -6159,16 +6800,18 @@ var OwneySDK = class {
6159
6800
  }
6160
6801
  /**
6161
6802
  * User-paid approval of Permit2 on the selected token for the active chain.
6162
- * Approves exactly the pending deposit amount. Another approval is required
6163
- * for a later deposit once this allowance has been consumed. Resolves after
6164
- * one confirmation so the subsequent deposit attempt sees the new allowance.
6803
+ * Grants the maximum ERC20 allowance so later deposits do not require another
6804
+ * approval. Resolves after one confirmation so the subsequent deposit attempt
6805
+ * sees the new allowance. Deposit retries pass their captured chain id so a
6806
+ * concurrent activation cannot redirect the approval to another network.
6165
6807
  *
6166
6808
  * @param requiredAmount Raw base-unit amount the pending deposit must cover.
6809
+ * @param expectedChainId Chain captured by the deposit that requested approval.
6167
6810
  * @returns the approval transaction hash.
6168
6811
  */
6169
- async approvePermit2(asset = "WETH", requiredAmount = 0n) {
6812
+ async approvePermit2(asset = "WETH", requiredAmount = 0n, expectedChainId) {
6170
6813
  const state = this.requireState();
6171
- const chainId = this.requireChainId();
6814
+ const chainId = expectedChainId ?? this.requireChainId();
6172
6815
  this.getEligibleAgents(chainId, asset, { excludeDisabled: true });
6173
6816
  const token = sponsoredTokensFor(asset)[chainId];
6174
6817
  if (!token) {
@@ -6178,16 +6821,18 @@ var OwneySDK = class {
6178
6821
  );
6179
6822
  }
6180
6823
  const provider = this.requireConnectedProvider();
6181
- const publicClient = createPublicClient4({
6824
+ const walletChainClient = createPublicClient4({
6182
6825
  chain: VIEM_CHAIN2[chainId],
6183
6826
  transport: custom3(provider)
6184
6827
  });
6828
+ const publicClient = this.getPaidRpcClient(chainId);
6185
6829
  const approvalAmount = permit2ApprovalAmount(requiredAmount);
6186
6830
  const wallet = createWalletClient3({
6187
6831
  account: state.walletAddress,
6188
6832
  chain: VIEM_CHAIN2[chainId],
6189
6833
  transport: custom3(provider)
6190
6834
  });
6835
+ await ensureWalletOnChain(walletChainClient, wallet, chainId);
6191
6836
  const hash = await wallet.writeContract({
6192
6837
  address: token,
6193
6838
  abi: ERC20_ALLOWANCE_ABI,
@@ -6196,14 +6841,53 @@ var OwneySDK = class {
6196
6841
  account: state.walletAddress,
6197
6842
  chain: VIEM_CHAIN2[chainId]
6198
6843
  });
6199
- const receipt = await publicClient.waitForTransactionReceipt({
6200
- hash,
6201
- confirmations: 1
6202
- });
6844
+ const receipt = await withPaidRpcDiagnostics(
6845
+ () => publicClient.waitForTransactionReceipt({
6846
+ hash,
6847
+ confirmations: 1
6848
+ }),
6849
+ chainId,
6850
+ "eth_getTransactionReceipt"
6851
+ );
6203
6852
  if (receipt.status !== "success") {
6204
6853
  throw new Error(`Permit2 approval reverted (tx ${hash})`);
6205
6854
  }
6206
- return hash;
6855
+ let observedAllowance = 0n;
6856
+ let verificationError;
6857
+ for (let attempt = 0; attempt < PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS; attempt += 1) {
6858
+ try {
6859
+ observedAllowance = await readPermit2Allowance(
6860
+ publicClient,
6861
+ token,
6862
+ state.walletAddress,
6863
+ attempt === 0 ? receipt.blockNumber : void 0
6864
+ );
6865
+ verificationError = void 0;
6866
+ if (observedAllowance >= requiredAmount) return hash;
6867
+ } catch (error) {
6868
+ verificationError = error;
6869
+ }
6870
+ if (attempt + 1 < PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS) {
6871
+ await new Promise(
6872
+ (resolve) => setTimeout(resolve, PERMIT2_ALLOWANCE_VERIFY_DELAY_MS)
6873
+ );
6874
+ }
6875
+ }
6876
+ throw new OwneyError(
6877
+ "PERMIT2_APPROVAL_REQUIRED",
6878
+ "Permit2 approval was confirmed, but the required token allowance was not observable.",
6879
+ {
6880
+ approvalConfirmed: true,
6881
+ approvalTxHash: hash,
6882
+ owner: state.walletAddress,
6883
+ token,
6884
+ spender: PERMIT2_ADDRESS,
6885
+ chainId,
6886
+ requiredAmount: requiredAmount.toString(),
6887
+ observedAllowance: observedAllowance.toString(),
6888
+ ...verificationError instanceof Error ? { verificationError: verificationError.message } : {}
6889
+ }
6890
+ );
6207
6891
  }
6208
6892
  // --- Discovery (no wallet required) ---
6209
6893
  /**