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

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
@@ -1,3 +1,93 @@
1
+ // src/lib/funding-evidence.ts
2
+ import { decodeEventLog, decodeFunctionData, erc20Abi, parseAbi } from "viem";
3
+ function notifyFunding(callback, evidence) {
4
+ try {
5
+ void Promise.resolve(callback?.({ ...evidence })).catch(() => {
6
+ });
7
+ } catch {
8
+ }
9
+ }
10
+ var fullWithdrawalAbi = parseAbi(["function withdrawAllAssetToUser(address recipient, address asset)"]);
11
+ var withdrawalEventAbi = parseAbi(["event WithdrewTokenToUser(address indexed owner, address indexed recipient, address indexed token, uint256 amount)"]);
12
+ function isFullYieldseekerWithdrawal(transaction, evidence, tokenAddress) {
13
+ if (evidence.agentId !== "yieldseeker" || transaction.chainId !== evidence.chainId || transaction.hash.toLowerCase() !== evidence.txHash?.toLowerCase() || transaction.to?.toLowerCase() !== evidence.agentWallet?.toLowerCase() || transaction.from.toLowerCase() !== evidence.walletAddress.toLowerCase()) return false;
14
+ try {
15
+ const decoded = decodeFunctionData({ abi: fullWithdrawalAbi, data: transaction.input });
16
+ return decoded.args[0].toLowerCase() === evidence.walletAddress.toLowerCase() && decoded.args[1].toLowerCase() === tokenAddress.toLowerCase();
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ function inspectFundingReceipt(receipt, evidence, tokenAddress, direction) {
22
+ if (receipt.status !== "success") return { ...evidence, transfer: "failed", delivery: "failed" };
23
+ let amount = 0n;
24
+ const seen = /* @__PURE__ */ new Set();
25
+ const attributed = /* @__PURE__ */ new Map();
26
+ const agent = evidence.agentWallet?.toLowerCase();
27
+ const owner = evidence.walletAddress.toLowerCase();
28
+ const logIndexes = [];
29
+ const transferLogs = [];
30
+ const zeroTransferLogs = [];
31
+ for (const log of [...receipt.logs].sort((a, b) => a.logIndex - b.logIndex)) {
32
+ if (log.address.toLowerCase() !== tokenAddress.toLowerCase()) continue;
33
+ if (seen.has(log.logIndex)) continue;
34
+ seen.add(log.logIndex);
35
+ try {
36
+ const event = decodeEventLog({ abi: erc20Abi, data: log.data, topics: log.topics });
37
+ if (event.eventName !== "Transfer") continue;
38
+ const { from, to, value } = event.args;
39
+ if (direction === "withdraw" && value === 0n && from.toLowerCase() === agent && to.toLowerCase() === owner)
40
+ zeroTransferLogs.push(log.logIndex);
41
+ let matched = 0n;
42
+ if (direction === "deposit") {
43
+ if (to.toLowerCase() === agent) matched = value;
44
+ } else {
45
+ const sender = from.toLowerCase();
46
+ const recipient = to.toLowerCase();
47
+ const available = sender === agent ? value : attributed.get(sender) ?? 0n;
48
+ const carried = available < value ? available : value;
49
+ if (sender !== agent) attributed.set(sender, available - carried);
50
+ if (recipient === owner) matched = carried;
51
+ else if (recipient !== agent) attributed.set(recipient, (attributed.get(recipient) ?? 0n) + carried);
52
+ }
53
+ if (matched === 0n) continue;
54
+ amount += matched;
55
+ transferLogs.push({ logIndex: log.logIndex, amountRaw: matched.toString() });
56
+ logIndexes.push(log.logIndex);
57
+ } catch {
58
+ }
59
+ }
60
+ const expected = /^\d+$/.test(evidence.intendedAmount) ? BigInt(evidence.intendedAmount) : void 0;
61
+ const emptyPayout = direction === "withdraw" && evidence.agentId === "yieldseeker" && amount === 0n && zeroTransferLogs.length > 0 && receipt.logs.some((log) => {
62
+ if (log.address.toLowerCase() !== agent) return false;
63
+ try {
64
+ const { args } = decodeEventLog({ abi: withdrawalEventAbi, data: log.data, topics: log.topics });
65
+ return args.owner.toLowerCase() === owner && args.recipient.toLowerCase() === owner && args.token.toLowerCase() === tokenAddress.toLowerCase() && args.amount === 0n;
66
+ } catch {
67
+ return false;
68
+ }
69
+ });
70
+ if (emptyPayout) return {
71
+ ...evidence,
72
+ blockNumber: receipt.blockNumber.toString(),
73
+ confirmedAmount: "0",
74
+ logIndexes: zeroTransferLogs,
75
+ transferLogs: zeroTransferLogs.map((logIndex) => ({ logIndex, amountRaw: "0" })),
76
+ transfer: "failed",
77
+ delivery: "failed"
78
+ };
79
+ const delivered = amount > 0n && (expected === void 0 || amount >= expected);
80
+ return {
81
+ ...evidence,
82
+ blockNumber: receipt.blockNumber.toString(),
83
+ logIndexes,
84
+ transferLogs,
85
+ ...amount > 0n ? { confirmedAmount: amount.toString() } : {},
86
+ transfer: delivered ? "confirmed" : "unknown",
87
+ delivery: direction === "withdraw" ? delivered ? "confirmed" : "unknown" : "not-applicable"
88
+ };
89
+ }
90
+
1
91
  // src/lib/deposit-batch-callback.ts
2
92
  var batchCallbacks = /* @__PURE__ */ new WeakMap();
3
93
  var getDepositBatchTransfer = (callback) => callback ? batchCallbacks.get(callback) : void 0;
@@ -125,6 +215,34 @@ function rateLimitDelay(error, now = Date.now()) {
125
215
  }
126
216
 
127
217
  // src/lib/agent-reads.ts
218
+ function rateLimitDiagnostics(error) {
219
+ const seen = /* @__PURE__ */ new Set();
220
+ let details = {};
221
+ function visit(value, depth = 0) {
222
+ if (depth > 6 || !value || typeof value !== "object" || seen.has(value))
223
+ return;
224
+ seen.add(value);
225
+ const record = value;
226
+ for (const key2 of [
227
+ "rpcSource",
228
+ "chainId",
229
+ "rpcMethod",
230
+ "providerRequestId",
231
+ "statusCode"
232
+ ]) {
233
+ const candidate = record[key2];
234
+ if (candidate !== void 0 && details[key2] === void 0)
235
+ details = { ...details, [key2]: candidate };
236
+ }
237
+ for (const key2 of ["details", "cause", "response", "fields", "error"])
238
+ visit(record[key2], depth + 1);
239
+ }
240
+ visit(error);
241
+ return {
242
+ rpcSource: details.rpcSource ?? "agent-api",
243
+ ...details
244
+ };
245
+ }
128
246
  var AgentReads = class {
129
247
  inFlight = /* @__PURE__ */ new Map();
130
248
  cooldowns = /* @__PURE__ */ new Map();
@@ -132,14 +250,15 @@ var AgentReads = class {
132
250
  clearInFlight() {
133
251
  this.inFlight.clear();
134
252
  }
135
- limited(agentId, until) {
253
+ limited(agentId, until, diagnostics) {
136
254
  return new OwneyError(
137
255
  "AGENT_RATE_LIMITED",
138
256
  "Too many requests. Please wait before trying again.",
139
257
  {
140
258
  statusCode: 429,
141
259
  retryAt: until,
142
- retryAfterSeconds: Math.max(0, Math.ceil((until - Date.now()) / 1e3))
260
+ retryAfterSeconds: Math.max(0, Math.ceil((until - Date.now()) / 1e3)),
261
+ ...diagnostics
143
262
  },
144
263
  agentId
145
264
  );
@@ -147,7 +266,9 @@ var AgentReads = class {
147
266
  run(agentId, key2, fetch2) {
148
267
  const cooldown = this.cooldowns.get(agentId);
149
268
  if (cooldown && cooldown.until > Date.now()) {
150
- return Promise.reject(this.limited(agentId, cooldown.until));
269
+ return Promise.reject(
270
+ this.limited(agentId, cooldown.until, cooldown.diagnostics)
271
+ );
151
272
  }
152
273
  const requestKey = JSON.stringify([agentId, key2]);
153
274
  const existing = this.inFlight.get(requestKey);
@@ -168,8 +289,9 @@ var AgentReads = class {
168
289
  Math.min(3e4 * 2 ** (failures - 1), 3e5)
169
290
  );
170
291
  const until = Math.max(previous?.until ?? 0, Date.now() + delay);
171
- this.cooldowns.set(agentId, { until, failures });
172
- throw this.limited(agentId, until);
292
+ const diagnostics = rateLimitDiagnostics(error);
293
+ this.cooldowns.set(agentId, { until, failures, diagnostics });
294
+ throw this.limited(agentId, until, diagnostics);
173
295
  }
174
296
  ).finally(() => {
175
297
  if (this.inFlight.get(requestKey) === promise)
@@ -180,9 +302,72 @@ var AgentReads = class {
180
302
  }
181
303
  };
182
304
 
305
+ // src/agents/zyfai/zyfai.withdraw-submission.ts
306
+ var listenersByClient = /* @__PURE__ */ new WeakMap();
307
+ function observeWithdrawalSubmission(sdk, chainId, token) {
308
+ const client = sdk.httpClient;
309
+ if (!client || typeof client.get !== "function" || typeof client.post !== "function")
310
+ return { attempted: () => void 0, rejected: () => false, request: () => void 0, stop: () => {
311
+ } };
312
+ let listeners = listenersByClient.get(client);
313
+ if (!listeners) {
314
+ listeners = /* @__PURE__ */ new Set();
315
+ listenersByClient.set(client, listeners);
316
+ const observers = listeners;
317
+ for (const method of ["get", "post"]) {
318
+ const original = client[method];
319
+ client[method] = async function(...args) {
320
+ const [url, options] = args;
321
+ const withdrawal = method === "get" && url === "/users/withdraw" || method === "post" && url === "/users/partial-withdraw";
322
+ const payload = method === "get" ? options?.params : options;
323
+ const matching = withdrawal ? [...observers].filter((observer) => observer.chainId === payload?.chainId && observer.token === payload?.tokenSymbol) : [];
324
+ const startedAt = Date.now();
325
+ const request2 = {
326
+ attempted: true,
327
+ method: method === "get" ? "GET" : "POST",
328
+ path: method === "get" ? "/users/withdraw" : "/users/partial-withdraw"
329
+ };
330
+ const notify = (outcome) => {
331
+ if (outcome === "rejected" && matching.length !== 1) return;
332
+ for (const observer of matching) {
333
+ try {
334
+ observer.notify(outcome, { ...request2 });
335
+ } catch {
336
+ }
337
+ }
338
+ };
339
+ notify("attempted");
340
+ try {
341
+ return await original.apply(this, args);
342
+ } catch (error) {
343
+ const message = error instanceof Error ? error.message : "";
344
+ request2.failure = message === "Unauthorized: Invalid API key" ? "unauthorized" : message === "Forbidden: Access denied" ? "forbidden" : message === "Rate limit exceeded. Please try again later." ? "rate-limited" : message === "Network error: Unable to reach the server" ? "network" : "other";
345
+ if (["unauthorized", "forbidden", "rate-limited"].includes(request2.failure)) notify("rejected");
346
+ throw error;
347
+ } finally {
348
+ request2.elapsedMs = Math.max(0, Date.now() - startedAt);
349
+ notify("finished");
350
+ }
351
+ };
352
+ }
353
+ }
354
+ let submitted = false;
355
+ let rejected = false;
356
+ let request = { attempted: false };
357
+ const listener = { chainId, token, notify: (outcome, evidence) => {
358
+ request = evidence;
359
+ if (outcome === "attempted") submitted = true;
360
+ else if (outcome === "rejected") rejected = true;
361
+ } };
362
+ listeners.add(listener);
363
+ return { attempted: () => submitted, rejected: () => rejected, request: () => ({ ...request }), stop: () => {
364
+ listeners.delete(listener);
365
+ } };
366
+ }
367
+
183
368
  // src/agents/zyfai/zyfai.agent.ts
184
369
  import { SMART_SESSIONS_VALIDATOR, ZyfaiSDK } from "@zyfai/sdk";
185
- import { createPublicClient, http, parseAbi } from "viem";
370
+ import { formatUnits, parseAbi as parseAbi2 } from "viem";
186
371
  import { base, arbitrum, mainnet } from "viem/chains";
187
372
 
188
373
  // src/types/config.ts
@@ -355,7 +540,11 @@ function mapPendingAllocations(raw2) {
355
540
  }
356
541
  function mapBalances(raw2, _chainId, smartWallet) {
357
542
  const portfolio = raw2.portfolio;
358
- const portfolioByChain = portfolio.portfolioByChain ?? {};
543
+ if (!portfolio?.portfolioByChain || typeof portfolio.portfolioByChain !== "object")
544
+ throw new Error("Zyfai balance snapshot is unavailable.");
545
+ const portfolioByChain = portfolio.portfolioByChain;
546
+ const fetchedAt = Date.now();
547
+ const provenance = portfolio;
359
548
  let totalBalance = 0;
360
549
  const tokens = [];
361
550
  for (const [chainIdKey, tokensBySymbol] of Object.entries(portfolioByChain)) {
@@ -364,7 +553,8 @@ function mapBalances(raw2, _chainId, smartWallet) {
364
553
  symbol,
365
554
  { balance, balanceWithFee, decimals }
366
555
  ] of Object.entries(tokensBySymbol ?? {})) {
367
- const amount = hexToDecimal(balanceWithFee ?? balance ?? "0", decimals);
556
+ if (balanceWithFee === void 0 && balance === void 0) throw new Error("Zyfai asset balance is unavailable.");
557
+ const amount = hexToDecimal(balanceWithFee ?? balance, decimals);
368
558
  totalBalance += amount;
369
559
  tokens.push({
370
560
  chain: CHAIN_ID_TO_NAME[positionChainId] ?? "BASE",
@@ -414,6 +604,15 @@ function mapBalances(raw2, _chainId, smartWallet) {
414
604
  totalBalanceAsset: "usdc",
415
605
  tokens,
416
606
  positions,
607
+ readMetadata: tokens.map((token) => ({
608
+ chainId: token.chainId,
609
+ asset: token.asset,
610
+ fetchedAt,
611
+ providerUpdatedAt: typeof provenance.updatedAt === "string" ? provenance.updatedAt : void 0,
612
+ snapshotId: provenance.snapshotId === void 0 ? void 0 : String(provenance.snapshotId),
613
+ blockNumber: provenance.blockNumber === void 0 ? void 0 : String(provenance.blockNumber),
614
+ positionsStatus: Array.isArray(portfolio.positions) ? "available" : "unavailable"
615
+ })),
417
616
  ...pendingAllocations ? { pendingAllocations } : {}
418
617
  };
419
618
  }
@@ -754,28 +953,6 @@ function mapDailyEarnings(raw2, chainId, tokenSymbol) {
754
953
  return { walletAddress: raw2.walletAddress, chainId, assets };
755
954
  }
756
955
 
757
- // src/agents/zyfai/zyfai.withdraw-amount.ts
758
- var TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
759
- function topicToAddress(topic) {
760
- return `0x${(topic ?? "").slice(-40)}`.toLowerCase();
761
- }
762
- function extractWithdrawnAmount(logs, recipient, token, decimals) {
763
- const wantRecipient = recipient.toLowerCase();
764
- const wantToken = token.toLowerCase();
765
- let total = 0n;
766
- let matched = false;
767
- for (const log of logs) {
768
- if (log.address.toLowerCase() !== wantToken) continue;
769
- if ((log.topics[0] ?? "").toLowerCase() !== TRANSFER_TOPIC) continue;
770
- if (topicToAddress(log.topics[2]) !== wantRecipient) continue;
771
- if (!log.data || log.data === "0x") continue;
772
- total += BigInt(log.data);
773
- matched = true;
774
- }
775
- if (!matched) return void 0;
776
- return String(hexToDecimal(`0x${total.toString(16)}`, decimals));
777
- }
778
-
779
956
  // src/agents/zyfai/zyfai.history-cursor.ts
780
957
  var InvalidHistoryCursorError = class extends Error {
781
958
  code = "invalid_cursor";
@@ -1063,15 +1240,148 @@ function protocolsPolicyNeedsUpdate(current, desiredProtocols, desiredAutoSelect
1063
1240
  return !protocolListsEqual(current.protocols, desiredProtocols);
1064
1241
  }
1065
1242
 
1243
+ // src/lib/paid-rpc.ts
1244
+ import {
1245
+ createPublicClient,
1246
+ http
1247
+ } from "viem";
1248
+ var PAID_RPC_RETRY_COUNT = 3;
1249
+ var PAID_RPC_RETRY_DELAY_MS = 1e3;
1250
+ function configuredRpcProxyBaseUrl(baseUrl) {
1251
+ const configured = (baseUrl ?? "")?.trim();
1252
+ if (!configured) {
1253
+ throw new Error(
1254
+ "OWNEY_ROUTING_API_BASE_URL is required for RPC proxy requests unless routingApiBaseUrl or all rpcUrls are configured."
1255
+ );
1256
+ }
1257
+ return configured;
1258
+ }
1259
+ function rpcProxyUrl(chainId, apiKey, baseUrl) {
1260
+ const url = new URL(
1261
+ `${configuredRpcProxyBaseUrl(baseUrl).replace(/\/$/, "")}/api/v1/rpc/${chainId}`
1262
+ );
1263
+ if (apiKey) url.searchParams.set("apiKey", apiKey);
1264
+ return url.toString();
1265
+ }
1266
+ function resolveRpcUrl(rpcUrls, chainId, apiKey = "", baseUrl) {
1267
+ const url = rpcUrls?.[chainId]?.trim();
1268
+ if (url) return url;
1269
+ return rpcProxyUrl(chainId, apiKey, baseUrl);
1270
+ }
1271
+ function resolveRpcUrls(rpcUrls, apiKey = "", baseUrl) {
1272
+ return {
1273
+ 1: resolveRpcUrl(rpcUrls, 1, apiKey, baseUrl),
1274
+ 8453: resolveRpcUrl(rpcUrls, 8453, apiKey, baseUrl),
1275
+ 42161: resolveRpcUrl(rpcUrls, 42161, apiKey, baseUrl)
1276
+ };
1277
+ }
1278
+ function createPaidRpcClient(chain, rpcUrls) {
1279
+ const url = resolveRpcUrl(
1280
+ rpcUrls,
1281
+ chain.id
1282
+ );
1283
+ return createPublicClient({
1284
+ chain,
1285
+ transport: http(url, {
1286
+ retryCount: PAID_RPC_RETRY_COUNT,
1287
+ retryDelay: PAID_RPC_RETRY_DELAY_MS
1288
+ })
1289
+ });
1290
+ }
1291
+ function headerValue(error, name) {
1292
+ const seen = /* @__PURE__ */ new Set();
1293
+ let value;
1294
+ function visit(candidate, depth = 0) {
1295
+ if (value || depth > 6 || !candidate || typeof candidate !== "object")
1296
+ return;
1297
+ if (seen.has(candidate)) return;
1298
+ seen.add(candidate);
1299
+ const record = candidate;
1300
+ const headers = record.headers;
1301
+ let found;
1302
+ if (typeof headers?.get === "function") {
1303
+ found = headers.get(name);
1304
+ } else {
1305
+ const headerRecord = headers;
1306
+ found = headerRecord?.[name] ?? headerRecord?.[name.toLowerCase()];
1307
+ }
1308
+ if (typeof found === "string" && found) {
1309
+ value = found;
1310
+ return;
1311
+ }
1312
+ for (const key2 of ["cause", "details", "response", "error"])
1313
+ visit(record[key2], depth + 1);
1314
+ }
1315
+ visit(error);
1316
+ return value;
1317
+ }
1318
+ function statusCode(error) {
1319
+ const seen = /* @__PURE__ */ new Set();
1320
+ let status;
1321
+ function visit(candidate, depth = 0) {
1322
+ if (status || depth > 6 || !candidate || typeof candidate !== "object")
1323
+ return;
1324
+ if (seen.has(candidate)) return;
1325
+ seen.add(candidate);
1326
+ const record = candidate;
1327
+ for (const key2 of ["status", "statusCode"]) {
1328
+ const parsed = Number(record[key2]);
1329
+ if (Number.isInteger(parsed) && parsed >= 100 && parsed <= 599) {
1330
+ status = parsed;
1331
+ return;
1332
+ }
1333
+ }
1334
+ for (const key2 of ["cause", "details", "response", "error"])
1335
+ visit(record[key2], depth + 1);
1336
+ }
1337
+ visit(error);
1338
+ return status;
1339
+ }
1340
+ function paidRpcError(error, chainId, rpcMethod, agentId) {
1341
+ const delay = rateLimitDelay(error);
1342
+ const providerRequestId = headerValue(error, "x-alchemy-request-id") ?? headerValue(error, "x-request-id");
1343
+ if (delay === void 0) {
1344
+ return new OwneyError(
1345
+ "AGENT_API_ERROR",
1346
+ "The blockchain RPC request failed.",
1347
+ {
1348
+ rpcSource: "paid-rpc",
1349
+ chainId,
1350
+ rpcMethod,
1351
+ ...statusCode(error) ? { statusCode: statusCode(error) } : {},
1352
+ ...providerRequestId ? { providerRequestId } : {}
1353
+ },
1354
+ agentId
1355
+ );
1356
+ }
1357
+ const retryAt = Date.now() + delay;
1358
+ return new OwneyError(
1359
+ "AGENT_RATE_LIMITED",
1360
+ "The blockchain RPC is rate limited. Please wait before trying again.",
1361
+ {
1362
+ rpcSource: "paid-rpc",
1363
+ chainId,
1364
+ rpcMethod,
1365
+ statusCode: 429,
1366
+ retryAt,
1367
+ retryAfterSeconds: Math.max(0, Math.ceil(delay / 1e3)),
1368
+ ...providerRequestId ? { providerRequestId } : {}
1369
+ },
1370
+ agentId
1371
+ );
1372
+ }
1373
+ async function withPaidRpcDiagnostics(operation, chainId, rpcMethod, agentId) {
1374
+ try {
1375
+ return await operation();
1376
+ } catch (error) {
1377
+ throw paidRpcError(error, chainId, rpcMethod, agentId);
1378
+ }
1379
+ }
1380
+
1066
1381
  // src/agents/zyfai/zyfai.agent.ts
1067
- var ERC7579_IS_MODULE_INSTALLED_ABI = parseAbi([
1382
+ var ERC7579_IS_MODULE_INSTALLED_ABI = parseAbi2([
1068
1383
  "function isModuleInstalled(uint256 moduleTypeId, address module, bytes additionalContext) view returns (bool)"
1069
1384
  ]);
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
1385
  var WETH_ADDRESS_BY_CHAIN = {
1076
1386
  8453: "0x4200000000000000000000000000000000000006",
1077
1387
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
@@ -1128,16 +1438,15 @@ var ZyfaiAgent = class _ZyfaiAgent {
1128
1438
  connectAuthPromise = null;
1129
1439
  rpcUrls;
1130
1440
  publicClients = /* @__PURE__ */ new Map();
1131
- // txHash -> resolved withdrawn amount. Receipts are immutable, so a
1132
- // successful decode is cached forever; failures are NOT cached so a transient
1133
- // RPC error retries on the next fetch.
1134
- withdrawAmountCache = /* @__PURE__ */ new Map();
1441
+ // Receipt-derived amounts and log identities, scoped to the owner and asset.
1442
+ // Failures remain retryable; successful immutable evidence has a bounded cache.
1443
+ fundingHistoryCache = /* @__PURE__ */ new Map();
1135
1444
  earningsReads = /* @__PURE__ */ new Map();
1136
1445
  earningsRefreshes = /* @__PURE__ */ new Map();
1137
1446
  earningsSnapshot = null;
1138
1447
  earningsGeneration = 0;
1139
1448
  constructor(apiKey, rpcUrls, referralSource) {
1140
- this.rpcUrls = rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS;
1449
+ this.rpcUrls = resolveRpcUrls(rpcUrls);
1141
1450
  this.sdk = new ZyfaiSDK({
1142
1451
  apiKey,
1143
1452
  rpcUrls: this.rpcUrls,
@@ -1151,16 +1460,17 @@ var ZyfaiAgent = class _ZyfaiAgent {
1151
1460
  getPublicClient(chainId) {
1152
1461
  const cached = this.publicClients.get(chainId);
1153
1462
  if (cached) return cached;
1154
- const client = createPublicClient({
1155
- chain: VIEM_CHAIN[chainId],
1156
- transport: http(this.rpcUrls[chainId])
1157
- });
1463
+ const client = createPaidRpcClient(
1464
+ VIEM_CHAIN[chainId],
1465
+ this.rpcUrls
1466
+ );
1158
1467
  this.publicClients.set(chainId, client);
1159
1468
  return client;
1160
1469
  }
1161
1470
  /**
1162
- * Fills in the true withdrawn amount for `Withdraw` entries by decoding the
1163
- * on-chain `Transfer` that lands in the user's wallet. The Zyfai history
1471
+ * Resolves funding amounts and log identities from token receipts. Withdrawals
1472
+ * must travel from this agent wallet to its owner; top-ups must reach the agent.
1473
+ * The Zyfai history
1164
1474
  * payload only reports the *remaining* position for a partial withdraw (see
1165
1475
  * the note in `mapEntries`), so without this the UI shows the leftover
1166
1476
  * balance instead of the amount withdrawn.
@@ -1172,48 +1482,62 @@ var ZyfaiAgent = class _ZyfaiAgent {
1172
1482
  * Entries with no symbol (older payload shapes) fall back to USDC to
1173
1483
  * preserve prior behavior.
1174
1484
  *
1175
- * Mutates `entries` in place. Runs all lookups concurrently and is tolerant
1176
- * of RPC failures: an entry whose amount can't be resolved keeps
1177
- * `amount: undefined`, which consumers render as a hidden/retryable row
1178
- * rather than a wrong number.
1485
+ * Mutates `entries` in place, sharing receipt requests within the page. RPC
1486
+ * failures leave provider top-ups unchanged and withdrawal amounts unknown.
1179
1487
  */
1180
- async resolveWithdrawnAmounts(entries, chainId, recipient) {
1181
- const withdrawals = entries.filter(
1182
- (e) => e.action === "Withdraw" && e.transactions[0]?.txHashes[0]
1488
+ async resolveFundingTransfers(entries, chainId, recipient, smartWallet, tokenSymbol) {
1489
+ const movements = entries.filter(
1490
+ (e) => (e.action === "Withdraw" || e.action === "Top up") && e.transactions[0]?.txHashes[0]
1183
1491
  );
1184
- if (withdrawals.length === 0) return;
1492
+ if (movements.length === 0) return;
1185
1493
  const client = this.getPublicClient(chainId);
1494
+ const receipts = /* @__PURE__ */ new Map();
1186
1495
  await Promise.all(
1187
- withdrawals.map(async (entry) => {
1496
+ movements.map(async (entry) => {
1188
1497
  const tx = entry.transactions[0];
1189
1498
  const txHash = tx.txHashes[0];
1190
- const rawSymbol = tx.tokenSymbol;
1499
+ const rawSymbol = tx.tokenSymbol ?? tokenSymbol;
1191
1500
  const normalizedSymbol = rawSymbol ? rawSymbol.toLowerCase() === "eth" ? "WETH" : rawSymbol.toUpperCase() : "USDC";
1192
1501
  const asset = SupportedAssets.find(
1193
1502
  (a) => a.chainId === chainId && a.symbol === normalizedSymbol
1194
- ) ?? SupportedAssets.find(
1195
- (a) => a.chainId === chainId && a.symbol === "USDC"
1196
1503
  );
1197
1504
  if (!asset) return;
1198
- const cacheKey = `${txHash}:${asset.address}`;
1199
- const cached = this.withdrawAmountCache.get(cacheKey);
1505
+ tx.tokenSymbol = asset.symbol;
1506
+ tx.tokenAddress = asset.address;
1507
+ tx.agentWallet = smartWallet;
1508
+ entry.id = `${chainId}:${txHash.toLowerCase()}:${entry.action}:${asset.address.toLowerCase()}:${smartWallet.toLowerCase()}`;
1509
+ if (!/^0x[0-9a-f]{64}$/i.test(txHash)) return;
1510
+ const cacheKey = `${entry.id}:${recipient.toLowerCase()}`;
1511
+ const cached = this.fundingHistoryCache.get(cacheKey);
1200
1512
  if (cached !== void 0) {
1201
- tx.amount = cached;
1513
+ Object.assign(tx, cached);
1202
1514
  return;
1203
1515
  }
1204
1516
  try {
1205
- const receipt = await client.getTransactionReceipt({
1206
- hash: txHash
1207
- });
1208
- const amount = extractWithdrawnAmount(
1209
- receipt.logs,
1210
- recipient,
1211
- asset.address,
1212
- asset.decimals
1213
- );
1214
- if (amount !== void 0) {
1215
- this.withdrawAmountCache.set(cacheKey, amount);
1216
- tx.amount = amount;
1517
+ if (!receipts.has(txHash)) receipts.set(txHash, client.getTransactionReceipt({ hash: txHash }));
1518
+ const receipt = await receipts.get(txHash);
1519
+ const evidence = inspectFundingReceipt(receipt, {
1520
+ agentId: "zyfai",
1521
+ walletAddress: recipient,
1522
+ chainId,
1523
+ token: asset.symbol,
1524
+ agentWallet: smartWallet,
1525
+ intendedAmount: "MAX",
1526
+ txHash,
1527
+ transfer: "unknown",
1528
+ registration: "not-applicable",
1529
+ delivery: "unknown"
1530
+ }, asset.address, entry.action === "Withdraw" ? "withdraw" : "deposit");
1531
+ if (evidence.transfer === "confirmed" && evidence.confirmedAmount) {
1532
+ Object.assign(tx, {
1533
+ amount: formatUnits(BigInt(evidence.confirmedAmount), asset.decimals),
1534
+ amountRaw: evidence.confirmedAmount,
1535
+ blockNumber: evidence.blockNumber,
1536
+ logIndexes: evidence.logIndexes,
1537
+ transferLogs: evidence.transferLogs
1538
+ });
1539
+ if (this.fundingHistoryCache.size >= 256) this.fundingHistoryCache.delete(this.fundingHistoryCache.keys().next().value);
1540
+ this.fundingHistoryCache.set(cacheKey, { ...tx });
1217
1541
  }
1218
1542
  } catch {
1219
1543
  }
@@ -1302,7 +1626,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1302
1626
  * Wraps `sdk.connectAccount` with a one-shot retry on auth errors. If the
1303
1627
  * cached JWT is rejected by the backend (revoked, signing-key rotated, etc.)
1304
1628
  * we drop the cache and try again — the second attempt has no cache entry,
1305
- * so it falls through to a real SIWE prompt. Zyfai collapses every 4xx into
1629
+ * so it falls through to a real SIWE prompt. Zyfai labels every HTTP 401 with
1306
1630
  * an `"Unauthorized: Invalid API key"` error so we can't distinguish revoked
1307
1631
  * JWT from a misconfigured API key; we retry once either way, and if the
1308
1632
  * second attempt still fails the original error propagates.
@@ -1335,6 +1659,17 @@ var ZyfaiAgent = class _ZyfaiAgent {
1335
1659
  await this.connectAuth(state, chainId);
1336
1660
  }
1337
1661
  }
1662
+ /** A rejected execution session must not be reused by the next foreground attempt. */
1663
+ invalidateRejectedWithdrawalSession(error, state, chainId, authToken) {
1664
+ const message = error instanceof Error ? error.message : "";
1665
+ if (!/(?:^|: )Unauthorized: Invalid API key$/.test(message)) return;
1666
+ const internals = this.sdk;
1667
+ if (typeof authToken !== "string" || internals.httpClient.authToken !== authToken || this.connectedAddress?.toLowerCase() !== state.walletAddress.toLowerCase() || this.connectedChainId !== chainId) return;
1668
+ clearSession(state.walletAddress, chainId);
1669
+ internals.httpClient.clearAuthToken();
1670
+ internals.authenticatedUserId = null;
1671
+ this.connectedChainId = null;
1672
+ }
1338
1673
  /**
1339
1674
  * Sync newly-available protocols onto an existing user's profile at wallet
1340
1675
  * connection. Zyfai's own protocol sync (`updateUserProtocols`) runs only when
@@ -1880,7 +2215,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1880
2215
  await this.ensureSessionKey(ownerAddress, chainId, redeployed.address);
1881
2216
  return redeployed.address;
1882
2217
  }
1883
- async deposit(state, chainId, amount, asset, depositCallback) {
2218
+ async deposit(state, chainId, amount, asset, depositCallback, onProgress) {
1884
2219
  try {
1885
2220
  const validChainId = isValidChainId(chainId);
1886
2221
  await this.ensureConnected(state, validChainId);
@@ -1889,17 +2224,48 @@ var ZyfaiAgent = class _ZyfaiAgent {
1889
2224
  this.getAddress(),
1890
2225
  validChainId
1891
2226
  );
2227
+ notifyFunding(onProgress, {
2228
+ agentId: this.id,
2229
+ chainId,
2230
+ token: asset,
2231
+ walletAddress: state.walletAddress,
2232
+ agentWallet: smartWallet,
2233
+ intendedAmount: amount,
2234
+ transfer: "pending",
2235
+ registration: "pending",
2236
+ delivery: "not-applicable"
2237
+ });
1892
2238
  const txHash = await depositCallback(smartWallet, validChainId, amount);
1893
- const tokenAddress = asset === "WETH" ? WETH_ADDRESS_BY_CHAIN[validChainId] : void 0;
2239
+ let evidence2 = {
2240
+ agentId: this.id,
2241
+ chainId,
2242
+ token: asset,
2243
+ walletAddress: state.walletAddress,
2244
+ agentWallet: smartWallet,
2245
+ intendedAmount: amount,
2246
+ txHash,
2247
+ transfer: "pending",
2248
+ registration: "pending",
2249
+ delivery: "not-applicable"
2250
+ };
2251
+ notifyFunding(onProgress, evidence2);
1894
2252
  try {
2253
+ const receipt = await this.getPublicClient(validChainId).getTransactionReceipt({ hash: txHash });
2254
+ const token = SupportedAssets.find((token2) => token2.chainId === chainId && token2.symbol === asset);
2255
+ evidence2 = inspectFundingReceipt(receipt, evidence2, token.address, "deposit");
2256
+ } catch {
2257
+ evidence2 = { ...evidence2, transfer: "unknown" };
2258
+ }
2259
+ notifyFunding(onProgress, evidence2);
2260
+ try {
2261
+ const tokenAddress = asset === "WETH" ? WETH_ADDRESS_BY_CHAIN[validChainId] : void 0;
1895
2262
  await (tokenAddress ? this.sdk.logDeposit(validChainId, txHash, amount, tokenAddress) : this.sdk.logDeposit(validChainId, txHash, amount));
1896
- } catch (logError) {
1897
- console.warn(
1898
- "[owney-sdk] Deposit landed on-chain but logDeposit failed (non-fatal):",
1899
- logError
1900
- );
2263
+ evidence2 = { ...evidence2, registration: "confirmed" };
2264
+ } catch {
2265
+ evidence2 = { ...evidence2, registration: "unknown" };
1901
2266
  }
1902
- return { txHash, smartWallet, amount };
2267
+ notifyFunding(onProgress, evidence2);
2268
+ return { txHash, smartWallet, amount, evidence: evidence2 };
1903
2269
  }
1904
2270
  await this.ensureWalletDeployed(this.getAddress(), validChainId);
1905
2271
  const raw2 = await this.sdk.depositFunds(
@@ -1909,22 +2275,88 @@ var ZyfaiAgent = class _ZyfaiAgent {
1909
2275
  asset,
1910
2276
  "aggressive"
1911
2277
  );
1912
- return mapDeposit(raw2);
2278
+ let evidence = {
2279
+ agentId: this.id,
2280
+ chainId,
2281
+ token: asset,
2282
+ walletAddress: state.walletAddress,
2283
+ agentWallet: raw2.smartWallet,
2284
+ intendedAmount: amount,
2285
+ txHash: raw2.txHash,
2286
+ transfer: "unknown",
2287
+ registration: "unknown",
2288
+ delivery: "not-applicable"
2289
+ };
2290
+ notifyFunding(onProgress, evidence);
2291
+ try {
2292
+ const receipt = await this.getPublicClient(validChainId).getTransactionReceipt({ hash: raw2.txHash });
2293
+ const token = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === asset);
2294
+ evidence = inspectFundingReceipt(receipt, evidence, token.address, "deposit");
2295
+ } catch {
2296
+ }
2297
+ notifyFunding(onProgress, evidence);
2298
+ return { ...mapDeposit(raw2), evidence };
1913
2299
  } catch (error) {
1914
2300
  throw error;
1915
2301
  }
1916
2302
  }
1917
2303
  // --- IAgent: Fund operations ---
1918
- async withdraw(state, chainId, token, amount) {
2304
+ async withdraw(state, chainId, token, amount, onProgress) {
1919
2305
  const validChainId = isValidChainId(chainId);
1920
- await this.ensureConnected(state, validChainId);
1921
- const raw2 = await this.sdk.withdrawFunds(
1922
- this.getAddress(),
1923
- validChainId,
1924
- amount,
1925
- token
1926
- );
2306
+ try {
2307
+ await this.ensureConnected(state, validChainId);
2308
+ } catch (error) {
2309
+ notifyFunding(onProgress, {
2310
+ agentId: this.id,
2311
+ chainId,
2312
+ token,
2313
+ walletAddress: state.walletAddress,
2314
+ intendedAmount: amount ?? "MAX",
2315
+ transfer: "failed",
2316
+ registration: "not-applicable",
2317
+ delivery: "failed"
2318
+ });
2319
+ throw error;
2320
+ }
2321
+ const observation = observeWithdrawalSubmission(this.sdk, validChainId, token);
2322
+ const authToken = this.sdk.httpClient?.authToken;
2323
+ let raw2;
2324
+ try {
2325
+ raw2 = await this.sdk.withdrawFunds(this.getAddress(), validChainId, amount, token);
2326
+ } catch (error) {
2327
+ this.invalidateRejectedWithdrawalSession(error, state, validChainId, authToken);
2328
+ if (observation.attempted() !== void 0) {
2329
+ const failed = observation.attempted() === false || observation.rejected();
2330
+ notifyFunding(onProgress, {
2331
+ agentId: this.id,
2332
+ chainId,
2333
+ token,
2334
+ walletAddress: state.walletAddress,
2335
+ intendedAmount: amount ?? "MAX",
2336
+ transfer: failed ? "failed" : "unknown",
2337
+ registration: "not-applicable",
2338
+ delivery: failed ? "failed" : "unknown",
2339
+ withdrawalRequest: observation.request()
2340
+ });
2341
+ }
2342
+ throw error;
2343
+ } finally {
2344
+ observation.stop();
2345
+ }
1927
2346
  if (!raw2.success) {
2347
+ const unresolved = !!raw2.txHash || /already|pending|queued|in.progress/i.test(raw2.message ?? "");
2348
+ notifyFunding(onProgress, {
2349
+ agentId: this.id,
2350
+ chainId,
2351
+ token,
2352
+ walletAddress: state.walletAddress,
2353
+ intendedAmount: amount ?? "MAX",
2354
+ txHash: raw2.txHash,
2355
+ transfer: unresolved ? "unknown" : "failed",
2356
+ registration: "not-applicable",
2357
+ delivery: unresolved ? "unknown" : "failed",
2358
+ withdrawalRequest: observation.request()
2359
+ });
1928
2360
  throw new OwneyError(
1929
2361
  "WITHDRAW_FAILED",
1930
2362
  raw2.message || "Zyfai withdraw failed.",
@@ -1932,13 +2364,45 @@ var ZyfaiAgent = class _ZyfaiAgent {
1932
2364
  this.id
1933
2365
  );
1934
2366
  }
1935
- return mapWithdraw(raw2);
2367
+ const evidence = {
2368
+ agentId: this.id,
2369
+ chainId,
2370
+ token,
2371
+ walletAddress: state.walletAddress,
2372
+ intendedAmount: amount ?? "MAX",
2373
+ txHash: raw2.txHash,
2374
+ transfer: "pending",
2375
+ registration: "not-applicable",
2376
+ delivery: "pending",
2377
+ withdrawalRequest: observation.request()
2378
+ };
2379
+ notifyFunding(onProgress, evidence);
2380
+ return { ...mapWithdraw(raw2), evidence };
2381
+ }
2382
+ /** Registration recovery never signs, transfers, activates, or refreshes authentication. */
2383
+ async retryDepositRegistration(state, evidence) {
2384
+ const internals = this.sdk;
2385
+ if (!internals.httpClient.authToken || internals.authenticatedUserId === null || this.connectedAddress?.toLowerCase() !== state.walletAddress.toLowerCase() || this.connectedChainId !== evidence.chainId) {
2386
+ throw new OwneyError("NOT_CONNECTED", "Reconnect the wallet before recovering deposit registration.");
2387
+ }
2388
+ if (evidence.transfer !== "confirmed" || !evidence.txHash) {
2389
+ throw new OwneyError("AGENT_API_ERROR", "Deposit transfer must be verified before registration recovery.");
2390
+ }
2391
+ const cid = isValidChainId(evidence.chainId);
2392
+ const tokenAddress = evidence.token === "WETH" ? WETH_ADDRESS_BY_CHAIN[cid] : void 0;
2393
+ await this.sdk.logDeposit(cid, evidence.txHash, evidence.intendedAmount, tokenAddress);
2394
+ return { ...evidence, registration: "confirmed" };
1936
2395
  }
1937
2396
  // --- IAgent: Portfolio reads ---
2397
+ async getFundingWallet(state, chainId) {
2398
+ return (await this.sdk.getSmartWalletAddress(state.walletAddress, isValidChainId(chainId))).address;
2399
+ }
1938
2400
  async getBalances(state, chainId) {
2401
+ const requestedAt = Date.now();
1939
2402
  const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
1940
2403
  const raw2 = await this.sdk.getPortfolio(this.getAddress());
1941
- return mapBalances(raw2, validChainId, smartWallet);
2404
+ const balance = mapBalances(raw2, validChainId, smartWallet);
2405
+ return { ...balance, readMetadata: balance.readMetadata?.map((read2) => ({ ...read2, requestedAt })) };
1942
2406
  }
1943
2407
  earningsKey(state, chainId, smartWallet) {
1944
2408
  return JSON.stringify([
@@ -2011,7 +2475,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
2011
2475
  /**
2012
2476
  * Owney speaks asset symbols ("USDC" / "WETH"); Zyfai's history endpoint
2013
2477
  * takes lowercase `assetType` and denominates WETH as "eth" (the same
2014
- * spelling `resolveWithdrawnAmounts` normalizes on the way in). Anything
2478
+ * spelling `resolveFundingTransfers` normalizes on the way in). Anything
2015
2479
  * unrecognised returns undefined so the request stays unfiltered rather
2016
2480
  * than silently narrowing to nothing.
2017
2481
  */
@@ -2030,6 +2494,14 @@ var ZyfaiAgent = class _ZyfaiAgent {
2030
2494
  }
2031
2495
  async getHistory(state, chainId, options) {
2032
2496
  const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
2497
+ return this.readHistory(state, smartWallet, validChainId, options);
2498
+ }
2499
+ /** Recovery reads never enter connectAuth or wallet activation. */
2500
+ async getFundingHistory(state, chainId, options) {
2501
+ const smartWallet = await this.getFundingWallet(state, chainId);
2502
+ return this.readHistory(state, smartWallet, isValidChainId(chainId), options);
2503
+ }
2504
+ async readHistory(state, smartWallet, validChainId, options) {
2033
2505
  const limit = options?.limit ?? 10;
2034
2506
  const assetType = _ZyfaiAgent.toZyfaiAssetType(options?.tokenSymbol);
2035
2507
  const cursor = options?.cursor ? decodeHistoryCursor(options.cursor) : null;
@@ -2071,9 +2543,16 @@ var ZyfaiAgent = class _ZyfaiAgent {
2071
2543
  rawOffset: firstOvershoot ? firstOvershoot.rawIdx : offset
2072
2544
  }) : void 0;
2073
2545
  const data = mapEntries(pageData, validChainId);
2074
- await this.resolveWithdrawnAmounts(data, validChainId, state.walletAddress);
2546
+ await this.resolveFundingTransfers(data, validChainId, state.walletAddress, smartWallet, options?.tokenSymbol);
2547
+ const seen = /* @__PURE__ */ new Set();
2548
+ const unique = data.filter((entry) => {
2549
+ if (!entry.id || !entry.transactions[0]?.transferLogs?.length) return true;
2550
+ if (seen.has(entry.id)) return false;
2551
+ seen.add(entry.id);
2552
+ return true;
2553
+ });
2075
2554
  return {
2076
- data,
2555
+ data: unique,
2077
2556
  hasMore,
2078
2557
  nextCursor
2079
2558
  };
@@ -2127,7 +2606,7 @@ import {
2127
2606
  createWalletClient as createWalletClient2,
2128
2607
  custom as custom2,
2129
2608
  encodeFunctionData,
2130
- erc20Abi,
2609
+ erc20Abi as erc20Abi2,
2131
2610
  getAddress as getAddress2,
2132
2611
  isAddress as isAddress2
2133
2612
  } from "viem";
@@ -2287,11 +2766,12 @@ async function postSponsorBatchTransfer(input) {
2287
2766
  // src/lib/permit2.ts
2288
2767
  import { bytesToHex as bytesToHex2 } from "viem";
2289
2768
  var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
2769
+ var MAX_UINT256 = 2n ** 256n - 1n;
2290
2770
  function permit2ApprovalAmount(requiredAmount) {
2291
2771
  if (requiredAmount <= 0n) {
2292
2772
  throw new Error("Permit2 approval requires a positive deposit amount");
2293
2773
  }
2294
- return requiredAmount;
2774
+ return MAX_UINT256;
2295
2775
  }
2296
2776
  var ERC20_ALLOWANCE_ABI = [
2297
2777
  {
@@ -2327,12 +2807,13 @@ function randomPermit2Nonce() {
2327
2807
  globalThis.crypto.getRandomValues(bytes);
2328
2808
  return BigInt(bytesToHex2(bytes));
2329
2809
  }
2330
- async function readPermit2Allowance(publicClient, token, owner) {
2810
+ async function readPermit2Allowance(publicClient, token, owner, blockNumber) {
2331
2811
  return publicClient.readContract({
2332
2812
  address: token,
2333
2813
  abi: ERC20_ALLOWANCE_ABI,
2334
2814
  functionName: "allowance",
2335
- args: [owner, PERMIT2_ADDRESS]
2815
+ args: [owner, PERMIT2_ADDRESS],
2816
+ ...blockNumber === void 0 ? {} : { blockNumber }
2336
2817
  });
2337
2818
  }
2338
2819
  async function readErc20Balance(publicClient, token, owner) {
@@ -2720,10 +3201,11 @@ var YieldseekerApiClient = class {
2720
3201
  const payload = await response.json().catch(() => null);
2721
3202
  if (!response.ok) {
2722
3203
  const error = providerError(payload, `HTTP_${response.status}`);
3204
+ const retryAfter = response.headers.get("Retry-After");
2723
3205
  throw new YieldseekerApiError(
2724
3206
  response.status,
2725
3207
  error.code,
2726
- error.fields
3208
+ retryAfter ? { ...error.fields, headers: { "Retry-After": retryAfter } } : error.fields
2727
3209
  );
2728
3210
  }
2729
3211
  if (payload && typeof payload === "object" && payload.success === true && "data" in payload) {
@@ -2745,7 +3227,7 @@ var YieldseekerApiClient = class {
2745
3227
  };
2746
3228
 
2747
3229
  // src/agents/yieldseeker/yieldseeker.mapper.ts
2748
- import { formatUnits, isAddress } from "viem";
3230
+ import { formatUnits as formatUnits2, isAddress } from "viem";
2749
3231
 
2750
3232
  // src/lib/helpers/snapshot-apy.ts
2751
3233
  var DAY_MS = 864e5;
@@ -2812,10 +3294,10 @@ function raw(value, endpoint) {
2812
3294
  return BigInt(value);
2813
3295
  }
2814
3296
  function decimal(value, decimals, endpoint) {
2815
- return formatUnits(raw(value, endpoint), decimals);
3297
+ return formatUnits2(raw(value, endpoint), decimals);
2816
3298
  }
2817
3299
  function usd(rawAmount, decimals, price) {
2818
- return Number(formatUnits(rawAmount, decimals)) * price;
3300
+ return Number(formatUnits2(rawAmount, decimals)) * price;
2819
3301
  }
2820
3302
  function percent(value) {
2821
3303
  const result = Number(value);
@@ -2854,13 +3336,14 @@ function position(value, asset, baseAssetDecimals) {
2854
3336
  // differ from the underlying asset. Yieldseeker already converts it to
2855
3337
  // underlying base-asset units in `assetsBase`; pair that value with the
2856
3338
  // snapshot's corresponding `baseAssetDecimals` for display. Keep the raw
2857
- // share quantity separately because withdraw-from-position expects it.
3339
+ // share quantity separate from the withdrawable underlying asset amount.
2858
3340
  amount: decimal(
2859
3341
  value.assetsBase,
2860
3342
  baseAssetDecimals,
2861
3343
  "yield positions"
2862
3344
  ),
2863
3345
  amountRaw: String(value.assetsRaw),
3346
+ withdrawableAmountRaw: String(value.withdrawableAssetsRaw),
2864
3347
  apy: percent(option.riskAdjustedApy),
2865
3348
  tvl: Number(option.totalDepositsUsd),
2866
3349
  liquidity: Number(option.withdrawableDepositsUsd)
@@ -2910,7 +3393,17 @@ function mapYieldseekerBalances(contexts) {
2910
3393
  totalBalanceAsset: "usdc",
2911
3394
  assetBalances,
2912
3395
  tokens,
2913
- positions
3396
+ positions,
3397
+ readMetadata: contexts.map((context) => ({
3398
+ chainId: context.agent.chainId,
3399
+ asset: context.asset,
3400
+ fetchedAt: context.fetchedAt,
3401
+ readVersion: context.readVersion,
3402
+ providerUpdatedAt: context.snapshot.updatedDate,
3403
+ snapshotId: String(context.snapshot.agentWalletSnapshotId),
3404
+ netDepositsRaw: context.snapshot.netDepositsBase,
3405
+ positionsStatus: context.positionsUnavailable ? "unavailable" : "available"
3406
+ }))
2914
3407
  };
2915
3408
  }
2916
3409
  function mapYieldseekerEarnings(contexts) {
@@ -2922,7 +3415,7 @@ function mapYieldseekerEarnings(contexts) {
2922
3415
  chain: "BASE",
2923
3416
  chainId: 8453,
2924
3417
  asset: context.asset,
2925
- amount: formatUnits(amount, context.snapshot.baseAssetDecimals)
3418
+ amount: formatUnits2(amount, context.snapshot.baseAssetDecimals)
2926
3419
  });
2927
3420
  lifetimeEarnings += usd(
2928
3421
  amount,
@@ -3024,6 +3517,7 @@ function transactionHashes(details) {
3024
3517
  function actionEntry(action) {
3025
3518
  if (action.actionType.toLowerCase().includes("withdraw")) return void 0;
3026
3519
  return {
3520
+ id: `yieldseeker:action:${action.agentActionId}`,
3027
3521
  agent: "yieldseeker",
3028
3522
  action: actionType(action.actionType),
3029
3523
  date: action.createdDate,
@@ -3053,7 +3547,7 @@ function movementEntry(movement, wallet, agent, asset, ownerAddress, vaultAddres
3053
3547
  const owner = ownerAddress.toLowerCase();
3054
3548
  const agentWallet = wallet.walletAddress.toLowerCase();
3055
3549
  const baseAsset = agent.assetAddress.toLowerCase();
3056
- if (movement.chainId !== agent.chainId || movement.assetAddress.toLowerCase() !== baseAsset) {
3550
+ if (movement.chainId !== agent.chainId || movement.assetAddress.toLowerCase() !== baseAsset || raw(movement.assetAmount, "historic position") <= 0n) {
3057
3551
  return void 0;
3058
3552
  }
3059
3553
  let action;
@@ -3078,6 +3572,12 @@ function movementEntry(movement, wallet, agent, asset, ownerAddress, vaultAddres
3078
3572
  transactions: [
3079
3573
  {
3080
3574
  txHashes: [movement.transactionHash],
3575
+ tokenAddress: movement.assetAddress,
3576
+ agentWallet: wallet.walletAddress,
3577
+ logIndexes: [movement.logIndex],
3578
+ transferLogs: [{ logIndex: movement.logIndex, amountRaw: movement.assetAmount }],
3579
+ amountRaw: movement.assetAmount,
3580
+ blockNumber: String(movement.blockNumber),
3081
3581
  chainId: agent.chainId,
3082
3582
  tokenSymbol: asset,
3083
3583
  amount: decimal(
@@ -3123,7 +3623,8 @@ function mapYieldseekerHistory(contexts, options) {
3123
3623
  ungrouped.push(entry);
3124
3624
  continue;
3125
3625
  }
3126
- const key2 = `${entry.agent}:${entry.action}:${tx.chainId}:${hash.toLowerCase()}`;
3626
+ const key2 = `${entry.agent}:${entry.action}:${tx.chainId}:${hash.toLowerCase()}:${tx.tokenAddress?.toLowerCase() ?? ""}:${tx.agentWallet?.toLowerCase() ?? ""}`;
3627
+ entry.id = key2;
3127
3628
  const previous = grouped.get(key2);
3128
3629
  if (!previous) {
3129
3630
  grouped.set(key2, entry);
@@ -3136,14 +3637,52 @@ function mapYieldseekerHistory(contexts, options) {
3136
3637
  }
3137
3638
  previous.positions.push(...entry.positions);
3138
3639
  previous.transactions.push(...entry.transactions);
3640
+ } else if ((entry.action === "Top up" || entry.action === "Withdraw") && tx.amountRaw) {
3641
+ const target = previous.transactions[0];
3642
+ const known = new Set(target.logIndexes ?? []);
3643
+ if (tx.logIndexes?.some((index) => !known.has(index))) {
3644
+ target.amountRaw = String(BigInt(target.amountRaw ?? "0") + BigInt(tx.amountRaw));
3645
+ target.amount = formatUnits2(
3646
+ BigInt(target.amountRaw),
3647
+ YIELDSEEKER_ASSET_METADATA[target.tokenSymbol].decimals
3648
+ );
3649
+ target.logIndexes = [...known, ...tx.logIndexes.filter((index) => !known.has(index))];
3650
+ target.transferLogs = [...target.transferLogs ?? [], ...(tx.transferLogs ?? []).filter((log) => !known.has(log.logIndex))];
3651
+ }
3139
3652
  }
3140
3653
  }
3141
- const filtered = [...grouped.values(), ...ungrouped].filter((entry) => (!options.fromDate || entry.date >= options.fromDate) && (!options.toDate || entry.date <= options.toDate)).sort((left, right) => right.date.localeCompare(left.date));
3654
+ const values = [...grouped.values(), ...ungrouped].filter((entry) => {
3655
+ const tx = entry.transactions[0];
3656
+ return tx?.amountRaw !== void 0 || ![...grouped.values()].some((other) => other !== entry && other.action === entry.action && other.transactions[0]?.amountRaw !== void 0 && other.transactions[0]?.txHashes[0]?.toLowerCase() === tx?.txHashes[0]?.toLowerCase());
3657
+ });
3658
+ let boundary;
3659
+ const scope = JSON.stringify([
3660
+ options.ownerAddress.toLowerCase(),
3661
+ contexts.map((context) => `${context.agent.chainId}:${context.wallet.walletAddress.toLowerCase()}:${context.asset}`).sort(),
3662
+ options.fromDate ?? null
3663
+ ]);
3664
+ if (options.cursor) {
3665
+ try {
3666
+ boundary = JSON.parse(decodeURIComponent(atob(options.cursor)));
3667
+ if (!boundary || boundary.scope !== scope || typeof boundary.id !== "string" || !Number.isFinite(Date.parse(boundary.date)) || !Number.isFinite(Date.parse(boundary.toDate))) throw new Error();
3668
+ } catch {
3669
+ throw new InvalidHistoryCursorError("Invalid Yieldseeker history cursor.");
3670
+ }
3671
+ }
3672
+ const toDate = boundary?.toDate ?? options.toDate ?? (/* @__PURE__ */ new Date()).toISOString();
3673
+ const filtered = values.filter((entry) => (!options.fromDate || entry.date >= options.fromDate) && entry.date <= toDate && (!boundary || entry.date < boundary.date || entry.date === boundary.date && (entry.id ?? "") > boundary.id)).sort((left, right) => right.date.localeCompare(left.date) || (left.id ?? "").localeCompare(right.id ?? ""));
3674
+ const data = filtered.slice(0, Math.max(1, options.limit));
3675
+ const hasMore = filtered.length > data.length;
3676
+ const last = data.at(-1);
3142
3677
  return {
3143
- data: filtered.slice(0, options.limit),
3144
- // v1 returns the whole action/movement collection and defines no cursor.
3145
- // Report a terminal page so callers never loop over the same prefix.
3146
- hasMore: false
3678
+ data,
3679
+ hasMore,
3680
+ ...hasMore && last ? { nextCursor: btoa(encodeURIComponent(JSON.stringify({
3681
+ date: last.date,
3682
+ id: last.id ?? "",
3683
+ toDate,
3684
+ scope
3685
+ }))) } : {}
3147
3686
  };
3148
3687
  }
3149
3688
  function mapYieldseekerProfile(address, contexts) {
@@ -3185,10 +3724,18 @@ function mapYieldseekerAgentApy(options, days) {
3185
3724
 
3186
3725
  // src/agents/yieldseeker/yieldseeker.agent.ts
3187
3726
  var OWNEY_AGENT_NAME = "owney";
3727
+ var OWNEY_AGENT_RULE_PRESET = "explorative";
3188
3728
  var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3189
3729
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3190
3730
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3191
3731
  var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3732
+ var YIELDSEEKER_AGENT_LIST_CACHE_MS = 12e4;
3733
+ var YIELDSEEKER_PORTFOLIO_CACHE_MS = 12e4;
3734
+ var YIELDSEEKER_SETTLEMENT_CACHE_MS = 5e3;
3735
+ var YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS = 15e3;
3736
+ var YIELDSEEKER_READ_FAILURE_COOLDOWN_MS = 6e4;
3737
+ var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
3738
+ var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
3192
3739
  function generateYieldseekerUsername() {
3193
3740
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
3194
3741
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -3232,6 +3779,7 @@ function query(params) {
3232
3779
  var YieldseekerAgent = class {
3233
3780
  id = "yieldseeker";
3234
3781
  balanceComposition = "tokens-plus-positions";
3782
+ withdrawalRequiresWalletApproval = true;
3235
3783
  supportedChainIds = [8453];
3236
3784
  supportedAssets = [
3237
3785
  {
@@ -3245,11 +3793,26 @@ var YieldseekerAgent = class {
3245
3793
  ];
3246
3794
  api;
3247
3795
  auth;
3796
+ rpcUrls;
3797
+ receiptClient;
3248
3798
  transactionExecutor;
3249
3799
  unwindReceiptWaiter;
3250
3800
  agentContexts = /* @__PURE__ */ new Map();
3251
3801
  users = /* @__PURE__ */ new Map();
3252
3802
  pendingAgents = /* @__PURE__ */ new Map();
3803
+ pendingWalletContexts = /* @__PURE__ */ new Map();
3804
+ readCache = /* @__PURE__ */ new Map();
3805
+ pendingReads = /* @__PURE__ */ new Map();
3806
+ readGeneration = 0;
3807
+ portfolioVersions = /* @__PURE__ */ new Map();
3808
+ snapshotFailures = /* @__PURE__ */ new Map();
3809
+ standardReadFailures = /* @__PURE__ */ new Map();
3810
+ reconcileUntil = /* @__PURE__ */ new Map();
3811
+ activityRefreshUntil = /* @__PURE__ */ new Map();
3812
+ // null means a confirmed movement has no reliable baseline. Do not guess
3813
+ // settlement; keep slow reconciliation after the fast window expires.
3814
+ snapshotMovements = /* @__PURE__ */ new Map();
3815
+ movementEpochs = /* @__PURE__ */ new Map();
3253
3816
  yieldOptions = /* @__PURE__ */ new Map();
3254
3817
  pendingYieldOptions = /* @__PURE__ */ new Map();
3255
3818
  constructor(owneyApiKey, options = {}) {
@@ -3259,10 +3822,16 @@ var YieldseekerAgent = class {
3259
3822
  options.fetchFn
3260
3823
  );
3261
3824
  this.auth = new YieldseekerAuth(options.auth);
3825
+ this.rpcUrls = options.rpcUrls;
3262
3826
  this.transactionExecutor = options.transactionExecutor;
3263
3827
  this.unwindReceiptWaiter = options.unwindReceiptWaiter;
3264
3828
  }
3829
+ getReceiptClient() {
3830
+ this.receiptClient ??= createPaidRpcClient(base3, this.rpcUrls);
3831
+ return this.receiptClient;
3832
+ }
3265
3833
  async disconnect() {
3834
+ this.readGeneration += 1;
3266
3835
  this.auth.clear();
3267
3836
  for (const key2 of this.users.keys()) {
3268
3837
  const [walletAddress, chainId] = key2.split(":");
@@ -3271,6 +3840,16 @@ var YieldseekerAgent = class {
3271
3840
  this.users.clear();
3272
3841
  this.agentContexts.clear();
3273
3842
  this.pendingAgents.clear();
3843
+ this.pendingWalletContexts.clear();
3844
+ this.readCache.clear();
3845
+ this.pendingReads.clear();
3846
+ this.portfolioVersions.clear();
3847
+ this.snapshotFailures.clear();
3848
+ this.standardReadFailures.clear();
3849
+ this.reconcileUntil.clear();
3850
+ this.activityRefreshUntil.clear();
3851
+ this.snapshotMovements.clear();
3852
+ this.movementEpochs.clear();
3274
3853
  }
3275
3854
  async activateAgent(state, chainId, asset) {
3276
3855
  this.assertChain(chainId);
@@ -3278,7 +3857,7 @@ var YieldseekerAgent = class {
3278
3857
  this.assertAsset(targetAsset);
3279
3858
  await this.ensureAgent(state, chainId, targetAsset);
3280
3859
  }
3281
- async deposit(state, chainId, amount, asset, depositCallback) {
3860
+ async deposit(state, chainId, amount, asset, depositCallback, onProgress) {
3282
3861
  this.assertChain(chainId);
3283
3862
  this.assertAsset(asset);
3284
3863
  if (BigInt(amount) <= 0n) {
@@ -3290,49 +3869,85 @@ var YieldseekerAgent = class {
3290
3869
  );
3291
3870
  }
3292
3871
  const context = await this.ensureAgent(state, chainId, asset);
3872
+ const generation = this.readGeneration;
3873
+ const movement = this.snapshotMovement(state, chainId, context, BigInt(amount));
3874
+ notifyFunding(onProgress, {
3875
+ agentId: this.id,
3876
+ chainId,
3877
+ token: asset,
3878
+ walletAddress: state.walletAddress,
3879
+ agentWallet: context.wallet.walletAddress,
3880
+ intendedAmount: amount,
3881
+ transfer: "pending",
3882
+ registration: "not-applicable",
3883
+ delivery: "not-applicable"
3884
+ });
3293
3885
  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,
3886
+ let receipt;
3887
+ const submitted = (hash) => notifyFunding(onProgress, { agentId: this.id, chainId, token: asset, walletAddress: state.walletAddress, agentWallet: context.wallet.walletAddress, intendedAmount: amount, txHash: hash, transfer: "pending", registration: "not-applicable", delivery: "not-applicable" });
3888
+ if (depositCallback) {
3889
+ provideDepositVerificationContext(depositCallback, {
3890
+ agentId: "yieldseeker",
3891
+ signature: await this.auth.getToken(state, chainId),
3892
+ userId: context.user.userId,
3893
+ yieldseekerAgentId: context.agent.agentId
3894
+ });
3895
+ txHash = await depositCallback(
3896
+ context.wallet.walletAddress,
3324
3897
  chainId,
3325
- context,
3326
- "deposit"
3898
+ amount
3327
3899
  );
3900
+ submitted(txHash);
3901
+ receipt = await this.waitForReceipt(state, chainId, txHash);
3902
+ } else {
3903
+ txHash = await this.submitTransaction(state, chainId, {
3904
+ from: getAddress2(state.walletAddress),
3905
+ to: YIELDSEEKER_ASSET_METADATA[asset].address,
3906
+ data: encodeFunctionData({
3907
+ abi: erc20Abi2,
3908
+ functionName: "transfer",
3909
+ args: [getAddress2(context.wallet.walletAddress), BigInt(amount)]
3910
+ }),
3911
+ value: "0",
3912
+ chainId
3913
+ }, submitted, (value) => {
3914
+ receipt = value;
3915
+ });
3328
3916
  }
3329
- return {
3330
- txHash,
3331
- smartWallet: context.wallet.walletAddress,
3332
- amount
3917
+ if (this.readGeneration === generation) {
3918
+ void this.refreshSnapshotAfterMovement(state, chainId, context, "deposit", movement);
3919
+ }
3920
+ const base5 = { agentId: this.id, chainId, token: asset, walletAddress: state.walletAddress, agentWallet: context.wallet.walletAddress, intendedAmount: amount, confirmedAmount: amount, txHash, transfer: "confirmed", registration: "not-applicable", delivery: "not-applicable" };
3921
+ const evidence = receipt ? inspectFundingReceipt(receipt, base5, YIELDSEEKER_ASSET_METADATA[asset].address, "deposit") : base5;
3922
+ notifyFunding(onProgress, evidence);
3923
+ return { txHash, smartWallet: context.wallet.walletAddress, amount, evidence };
3924
+ }
3925
+ async withdraw(state, chainId, asset, amount, onProgress) {
3926
+ let ownerTransferAttempted = false;
3927
+ let evidence = {
3928
+ agentId: this.id,
3929
+ chainId,
3930
+ token: asset,
3931
+ walletAddress: state.walletAddress,
3932
+ intendedAmount: amount ?? "MAX",
3933
+ transfer: "pending",
3934
+ registration: "not-applicable",
3935
+ delivery: "pending"
3333
3936
  };
3937
+ try {
3938
+ return await this.withdrawInternal(state, chainId, asset, amount, (next) => {
3939
+ evidence = { ...evidence, ...next };
3940
+ notifyFunding(onProgress, evidence);
3941
+ }, () => {
3942
+ ownerTransferAttempted = true;
3943
+ });
3944
+ } catch (error) {
3945
+ if (!ownerTransferAttempted || error instanceof OwneyError && error.code === "AGENT_TRANSACTION_REVERTED")
3946
+ notifyFunding(onProgress, { ...evidence, transfer: "failed", delivery: "failed" });
3947
+ throw error;
3948
+ }
3334
3949
  }
3335
- async withdraw(state, chainId, asset, amount) {
3950
+ async withdrawInternal(state, chainId, asset, amount, onProgress, onOwnerTransferAttempted) {
3336
3951
  this.assertChain(chainId);
3337
3952
  this.assertAsset(asset);
3338
3953
  if (amount !== void 0 && BigInt(amount) <= 0n) {
@@ -3352,6 +3967,9 @@ var YieldseekerAgent = class {
3352
3967
  this.id
3353
3968
  );
3354
3969
  }
3970
+ const generation = this.readGeneration;
3971
+ let confirmedMovement = false;
3972
+ let settlement = null;
3355
3973
  try {
3356
3974
  const portfolio = await this.loadPortfolioContext(
3357
3975
  state,
@@ -3359,16 +3977,32 @@ var YieldseekerAgent = class {
3359
3977
  context
3360
3978
  );
3361
3979
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3362
- const idleEntry = Object.entries(portfolio.snapshot.tokenBalances).find(
3363
- ([address]) => address.toLowerCase() === metadata.address.toLowerCase()
3364
- );
3365
- const idle = BigInt(idleEntry?.[1] ?? "0");
3980
+ const readIdle = async () => {
3981
+ const balance = await withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
3982
+ address: metadata.address,
3983
+ abi: erc20Abi2,
3984
+ functionName: "balanceOf",
3985
+ args: [getAddress2(context.wallet.walletAddress)]
3986
+ }), chainId, "eth_call", this.id);
3987
+ this.assertSession(generation, chainId, asset);
3988
+ return balance;
3989
+ };
3990
+ const idle = await readIdle();
3366
3991
  const deployed = portfolio.positions.reduce(
3367
3992
  (total, position2) => total + BigInt(position2.withdrawableAssetsRaw),
3368
3993
  0n
3369
3994
  );
3370
3995
  const totalAvailable = idle + deployed;
3371
3996
  const requested = amount === void 0 ? totalAvailable : BigInt(amount);
3997
+ if (requested === 0n) {
3998
+ this.invalidateFundingRead(state, chainId, asset);
3999
+ throw new OwneyError(
4000
+ "WITHDRAW_INSUFFICIENT_BALANCE",
4001
+ "No Yieldseeker funds are available to withdraw.",
4002
+ { asset, available: "0" },
4003
+ this.id
4004
+ );
4005
+ }
3372
4006
  if (requested > totalAvailable) {
3373
4007
  throw new OwneyError(
3374
4008
  "WITHDRAW_INSUFFICIENT_BALANCE",
@@ -3404,6 +4038,7 @@ var YieldseekerAgent = class {
3404
4038
  throw this.invalidResponse("position withdrawal");
3405
4039
  }
3406
4040
  await this.waitForReceipt(state, chainId, response.transactionHash);
4041
+ confirmedMovement = true;
3407
4042
  remaining -= assetsRaw;
3408
4043
  }
3409
4044
  if (remaining > 0n) {
@@ -3412,7 +4047,22 @@ var YieldseekerAgent = class {
3412
4047
  remaining: remaining.toString()
3413
4048
  });
3414
4049
  }
4050
+ const availableForOwner = confirmedMovement ? await readIdle() : idle;
4051
+ if (availableForOwner === 0n || amount !== void 0 && availableForOwner < requested) {
4052
+ this.invalidateFundingRead(state, chainId, asset);
4053
+ throw new OwneyError(
4054
+ "WITHDRAW_INSUFFICIENT_BALANCE",
4055
+ "Yieldseeker funds are not available for the owner transfer.",
4056
+ { asset, requested: amount ?? "MAX", available: availableForOwner.toString() },
4057
+ this.id
4058
+ );
4059
+ }
4060
+ const payout = amount === void 0 ? availableForOwner : requested;
4061
+ const plannedMovement = this.snapshotMovement(state, chainId, context, -payout);
3415
4062
  const account = getAddress2(state.walletAddress);
4063
+ let receipt;
4064
+ const base5 = { agentId: this.id, chainId, token: asset, walletAddress: state.walletAddress, agentWallet: context.wallet.walletAddress, intendedAmount: amount === void 0 ? "MAX" : requested.toString(), transfer: "pending", registration: "not-applicable", delivery: "pending" };
4065
+ onOwnerTransferAttempted?.();
3416
4066
  const txHash = await this.submitTransaction(state, chainId, {
3417
4067
  from: account,
3418
4068
  to: getAddress2(context.wallet.walletAddress),
@@ -3427,24 +4077,66 @@ var YieldseekerAgent = class {
3427
4077
  }),
3428
4078
  value: "0",
3429
4079
  chainId
4080
+ }, (hash) => notifyFunding(onProgress, { ...base5, txHash: hash }), (value) => {
4081
+ receipt = value;
3430
4082
  });
3431
- return {
3432
- txHash,
3433
- type: amount === void 0 ? "full" : "partial",
3434
- amount: requested.toString()
3435
- };
4083
+ confirmedMovement = true;
4084
+ settlement = plannedMovement;
4085
+ const evidence = receipt ? inspectFundingReceipt(receipt, { ...base5, txHash }, metadata.address, "withdraw") : { ...base5, txHash, confirmedAmount: payout.toString(), transfer: "confirmed", delivery: "confirmed" };
4086
+ notifyFunding(onProgress, evidence);
4087
+ if (evidence.transfer === "failed" && evidence.confirmedAmount === "0")
4088
+ throw new OwneyError(
4089
+ "WITHDRAW_INSUFFICIENT_BALANCE",
4090
+ "The Yieldseeker withdrawal completed without transferring funds.",
4091
+ { asset, available: "0", txHash },
4092
+ this.id
4093
+ );
4094
+ return { txHash, type: amount === void 0 ? "full" : "partial", amount: evidence.confirmedAmount ?? payout.toString(), evidence };
3436
4095
  } finally {
3437
- await this.refreshSnapshotAfterMovement(
3438
- state,
3439
- chainId,
3440
- context,
3441
- "withdrawal"
3442
- );
4096
+ if (confirmedMovement && this.readGeneration === generation) {
4097
+ void this.refreshSnapshotAfterMovement(
4098
+ state,
4099
+ chainId,
4100
+ context,
4101
+ "withdrawal",
4102
+ settlement
4103
+ );
4104
+ }
3443
4105
  }
3444
4106
  }
3445
- async getBalances(state, chainId) {
4107
+ async getBalances(state, chainId, asset) {
4108
+ this.assertChain(chainId);
4109
+ if (asset !== void 0) this.assertAsset(asset);
4110
+ return mapYieldseekerBalances(await this.loadPortfolio(state, chainId, { asset, allowMissingPositions: true }));
4111
+ }
4112
+ invalidateFundingRead(state, chainId, asset, refreshUntil = Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS) {
4113
+ this.assertAsset(asset);
4114
+ const key2 = this.contextKey(state, chainId, asset);
4115
+ const failure = this.snapshotFailures.get(key2);
4116
+ this.advancePortfolioVersion(key2);
4117
+ if (failure && failure.retryAt > Date.now()) this.snapshotFailures.set(key2, failure);
4118
+ if (!this.snapshotMovements.has(key2)) this.snapshotMovements.set(key2, null);
4119
+ const until = Math.min(refreshUntil, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4120
+ this.reconcileUntil.set(key2, Math.max(this.reconcileUntil.get(key2) ?? 0, until));
4121
+ this.activityRefreshUntil.set(key2, Math.max(this.activityRefreshUntil.get(key2) ?? 0, until));
4122
+ }
4123
+ endFundingBalanceRefresh(state, chainId, asset, read2) {
3446
4124
  this.assertChain(chainId);
3447
- return mapYieldseekerBalances(await this.loadPortfolio(state, chainId, {}));
4125
+ this.assertAsset(asset);
4126
+ const key2 = this.contextKey(state, chainId, asset);
4127
+ const cached = this.readCache.get(`snapshot:${key2}`);
4128
+ const snapshot = cached?.value?.agentSnapshot;
4129
+ if (!cached || !snapshot || read2.chainId !== chainId || read2.asset.toUpperCase() !== asset || read2.readVersion !== this.portfolioVersion(key2) || read2.fetchedAt !== cached.fetchedAt || read2.snapshotId !== String(snapshot.agentWalletSnapshotId) || read2.providerUpdatedAt !== snapshot.updatedDate || read2.netDepositsRaw !== snapshot.netDepositsBase) return false;
4130
+ this.snapshotMovements.delete(key2);
4131
+ this.reconcileUntil.delete(key2);
4132
+ cached.expiresAt = cached.fetchedAt + YIELDSEEKER_PORTFOLIO_CACHE_MS;
4133
+ return true;
4134
+ }
4135
+ async getFundingWallet(state, chainId, asset) {
4136
+ this.assertAsset(asset);
4137
+ const context = this.agentContexts.get(this.contextKey(state, chainId, asset));
4138
+ if (!context) throw new OwneyError("NOT_CONNECTED", "Reconnect the agent before recovering funding.");
4139
+ return context.wallet.walletAddress;
3448
4140
  }
3449
4141
  async getEarnings(state, chainId) {
3450
4142
  this.assertChain(chainId);
@@ -3464,7 +4156,7 @@ var YieldseekerAgent = class {
3464
4156
  this.assertChain(chainId);
3465
4157
  const asset = options?.tokenSymbol?.toUpperCase();
3466
4158
  if (asset !== void 0) this.assertAsset(asset);
3467
- const contexts = await this.loadPortfolio(state, chainId, {
4159
+ const contexts = await this.loadActivityPortfolio(state, chainId, {
3468
4160
  ...asset ? { asset } : {},
3469
4161
  historic: true,
3470
4162
  actions: true
@@ -3481,6 +4173,7 @@ var YieldseekerAgent = class {
3481
4173
  );
3482
4174
  return mapYieldseekerHistory(contexts, {
3483
4175
  limit: options?.limit ?? 10,
4176
+ cursor: options?.cursor,
3484
4177
  ownerAddress: state.walletAddress,
3485
4178
  vaultAddresses,
3486
4179
  ...options?.fromDate ? { fromDate: options.fromDate } : {},
@@ -3491,7 +4184,7 @@ var YieldseekerAgent = class {
3491
4184
  this.assertChain(chainId);
3492
4185
  return mapYieldseekerProfile(
3493
4186
  state.walletAddress,
3494
- await this.loadPortfolio(state, chainId, {})
4187
+ await this.loadActivityPortfolio(state, chainId, {})
3495
4188
  );
3496
4189
  }
3497
4190
  async getAgentApy(days, options) {
@@ -3533,18 +4226,109 @@ var YieldseekerAgent = class {
3533
4226
  contextKey(state, chainId, asset) {
3534
4227
  return `${this.userKey(state, chainId)}:${asset}`;
3535
4228
  }
3536
- async resolveUser(state, chainId) {
3537
- const key2 = this.userKey(state, chainId);
3538
- const inMemory = this.users.get(key2);
3539
- if (inMemory) return inMemory;
3540
- const persisted = readYieldseekerIdentity(state.walletAddress, chainId);
3541
- if (persisted) {
3542
- this.users.set(key2, persisted);
3543
- return persisted;
4229
+ assertSession(generation, chainId, asset) {
4230
+ if (this.readGeneration !== generation) {
4231
+ throw new OwneyError(
4232
+ "NOT_CONNECTED",
4233
+ "Wallet session changed during the Yieldseeker request.",
4234
+ { rpcSource: "agent-api", chainId, ...asset ? { asset } : {} },
4235
+ this.id
4236
+ );
3544
4237
  }
3545
- const walletAddress = getAddress2(state.walletAddress);
3546
- let user = null;
3547
- try {
4238
+ }
4239
+ cachedRead(key2, ttlMs, read2) {
4240
+ const cached = this.readCache.get(key2);
4241
+ if (cached && cached.expiresAt > Date.now()) {
4242
+ return Promise.resolve(cached.value);
4243
+ }
4244
+ const pending = this.pendingReads.get(key2);
4245
+ if (pending) return pending;
4246
+ const generation = this.readGeneration;
4247
+ const request = Promise.resolve().then(read2).then((value) => {
4248
+ if (this.readGeneration === generation && this.pendingReads.get(key2) === request) {
4249
+ this.readCache.set(key2, {
4250
+ expiresAt: Date.now() + ttlMs,
4251
+ fetchedAt: Date.now(),
4252
+ value
4253
+ });
4254
+ }
4255
+ return value;
4256
+ }).finally(() => {
4257
+ if (this.pendingReads.get(key2) === request) {
4258
+ this.pendingReads.delete(key2);
4259
+ }
4260
+ });
4261
+ this.pendingReads.set(key2, request);
4262
+ return request;
4263
+ }
4264
+ agentListKey(state, chainId) {
4265
+ return `agents:${this.userKey(state, chainId)}`;
4266
+ }
4267
+ async listAgents(state, chainId, user) {
4268
+ const response = await this.cachedRead(
4269
+ this.agentListKey(state, chainId),
4270
+ YIELDSEEKER_AGENT_LIST_CACHE_MS,
4271
+ async () => {
4272
+ const response2 = await this.walletRequest(
4273
+ state,
4274
+ chainId,
4275
+ `/users/${user.userId}/agents`
4276
+ );
4277
+ if (!Array.isArray(response2?.agents)) {
4278
+ throw this.invalidResponse("agent list");
4279
+ }
4280
+ return response2;
4281
+ }
4282
+ );
4283
+ return response.agents;
4284
+ }
4285
+ async contextForAgent(state, chainId, user, agent, asset) {
4286
+ const key2 = this.contextKey(state, chainId, asset);
4287
+ const cached = this.agentContexts.get(key2);
4288
+ if (cached?.agent.agentId === agent.agentId) return cached;
4289
+ const pending = this.pendingWalletContexts.get(key2);
4290
+ if (pending) return pending;
4291
+ const generation = this.readGeneration;
4292
+ const request = this.walletRequest(
4293
+ state,
4294
+ chainId,
4295
+ `/users/${user.userId}/agents/${agent.agentId}/wallet`
4296
+ ).then((walletResponse) => {
4297
+ this.assertSession(generation, chainId, asset);
4298
+ if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
4299
+ throw this.invalidResponse("agent wallet");
4300
+ }
4301
+ const context = {
4302
+ user,
4303
+ agent,
4304
+ wallet: walletResponse.agentWallet,
4305
+ asset
4306
+ };
4307
+ this.agentContexts.set(key2, context);
4308
+ return context;
4309
+ });
4310
+ this.pendingWalletContexts.set(key2, request);
4311
+ try {
4312
+ return await request;
4313
+ } finally {
4314
+ if (this.pendingWalletContexts.get(key2) === request) {
4315
+ this.pendingWalletContexts.delete(key2);
4316
+ }
4317
+ }
4318
+ }
4319
+ async resolveUser(state, chainId) {
4320
+ const key2 = this.userKey(state, chainId);
4321
+ const inMemory = this.users.get(key2);
4322
+ if (inMemory) return inMemory;
4323
+ const persisted = readYieldseekerIdentity(state.walletAddress, chainId);
4324
+ if (persisted) {
4325
+ this.users.set(key2, persisted);
4326
+ return persisted;
4327
+ }
4328
+ const walletAddress = getAddress2(state.walletAddress);
4329
+ const generation = this.readGeneration;
4330
+ let user = null;
4331
+ try {
3548
4332
  const login = await this.providerRequest(
3549
4333
  state,
3550
4334
  chainId,
@@ -3589,6 +4373,7 @@ var YieldseekerAgent = class {
3589
4373
  if (!user || !/^[a-zA-Z0-9_-]{1,128}$/.test(user.userId)) {
3590
4374
  throw this.invalidResponse("wallet identity");
3591
4375
  }
4376
+ this.assertSession(generation, chainId);
3592
4377
  const resolved = { userId: user.userId };
3593
4378
  this.users.set(key2, resolved);
3594
4379
  writeYieldseekerIdentity(state.walletAddress, chainId, resolved.userId);
@@ -3604,10 +4389,13 @@ var YieldseekerAgent = class {
3604
4389
  if (cached) return cached;
3605
4390
  const pending = this.pendingAgents.get(key2);
3606
4391
  if (pending) return pending;
4392
+ const generation = this.readGeneration;
3607
4393
  const request = this.resolveAgent(state, chainId, asset, true).then(
3608
4394
  async (context) => {
4395
+ this.assertSession(generation, chainId, asset);
3609
4396
  if (!context) throw this.invalidResponse("agent creation");
3610
4397
  await this.deployAgent(state, chainId, context);
4398
+ this.assertSession(generation, chainId, asset);
3611
4399
  this.agentContexts.set(key2, context);
3612
4400
  return context;
3613
4401
  }
@@ -3616,29 +4404,27 @@ var YieldseekerAgent = class {
3616
4404
  try {
3617
4405
  return await request;
3618
4406
  } finally {
3619
- this.pendingAgents.delete(key2);
4407
+ if (this.pendingAgents.get(key2) === request) this.pendingAgents.delete(key2);
3620
4408
  }
3621
4409
  }
3622
4410
  async findAgent(state, chainId, asset) {
3623
4411
  const key2 = this.contextKey(state, chainId, asset);
3624
4412
  const cached = this.agentContexts.get(key2);
3625
4413
  if (cached) return cached;
4414
+ const generation = this.readGeneration;
3626
4415
  const context = await this.resolveAgent(state, chainId, asset, false);
4416
+ this.assertSession(generation, chainId, asset);
3627
4417
  if (context) this.agentContexts.set(key2, context);
3628
4418
  return context;
3629
4419
  }
3630
4420
  async resolveAgent(state, chainId, asset, createIfMissing) {
4421
+ const generation = this.readGeneration;
3631
4422
  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
- }
4423
+ this.assertSession(generation, chainId, asset);
4424
+ const agents = await this.listAgents(state, chainId, user);
4425
+ this.assertSession(generation, chainId, asset);
3640
4426
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3641
- let agent = response.agents.find(
4427
+ let agent = agents.find(
3642
4428
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
3643
4429
  );
3644
4430
  if (!agent && createIfMissing) {
@@ -3654,99 +4440,260 @@ var YieldseekerAgent = class {
3654
4440
  chainId,
3655
4441
  assetAddress: metadata.address,
3656
4442
  type: "vault",
3657
- rulePreset: null
4443
+ rulePreset: OWNEY_AGENT_RULE_PRESET
3658
4444
  }
3659
4445
  }
3660
4446
  );
4447
+ this.assertSession(generation, chainId, asset);
3661
4448
  agent = created?.agent;
4449
+ if (agent) {
4450
+ this.readCache.set(this.agentListKey(state, chainId), {
4451
+ expiresAt: Date.now() + YIELDSEEKER_AGENT_LIST_CACHE_MS,
4452
+ fetchedAt: Date.now(),
4453
+ value: {
4454
+ agents: [...agents, agent]
4455
+ }
4456
+ });
4457
+ }
3662
4458
  }
3663
4459
  if (!agent) return null;
3664
4460
  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 };
4461
+ return this.contextForAgent(state, chainId, user, agent, asset);
3674
4462
  }
3675
4463
  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`
4464
+ const contexts = await this.resolvePortfolioContexts(state, chainId, options.asset);
4465
+ return Promise.all(
4466
+ contexts.map(
4467
+ (context) => this.loadPortfolioContext(state, chainId, context, options)
4468
+ )
3681
4469
  );
3682
- if (!Array.isArray(response?.agents)) {
3683
- throw this.invalidResponse("agent list");
3684
- }
4470
+ }
4471
+ async loadActivityPortfolio(state, chainId, options) {
4472
+ const contexts = await this.resolvePortfolioContexts(state, chainId, options.asset);
4473
+ return Promise.all(
4474
+ contexts.map(
4475
+ (context) => this.loadActivityContext(state, chainId, context, options)
4476
+ )
4477
+ );
4478
+ }
4479
+ async resolvePortfolioContexts(state, chainId, assetFilter) {
4480
+ const generation = this.readGeneration;
4481
+ const user = await this.resolveUser(state, chainId);
4482
+ this.assertSession(generation, chainId);
4483
+ const agents = await this.listAgents(state, chainId, user);
4484
+ this.assertSession(generation, chainId);
3685
4485
  const contexts = [];
3686
- for (const agent of response.agents) {
4486
+ for (const agent of agents) {
3687
4487
  const asset = this.assetForAgent(agent);
3688
- if (!this.isOwneyAgent(agent) || !asset || agent.chainId !== chainId || agent.type !== "vault" || options.asset && options.asset !== asset) {
4488
+ if (!this.isOwneyAgent(agent) || !asset || agent.chainId !== chainId || agent.type !== "vault" || assetFilter && assetFilter !== asset) {
3689
4489
  continue;
3690
4490
  }
3691
4491
  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
4492
+ contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
4493
+ }
4494
+ const resolvedContexts = await Promise.all(contexts);
4495
+ this.assertSession(generation, chainId);
4496
+ return resolvedContexts;
4497
+ }
4498
+ portfolioVersion(contextKey) {
4499
+ return this.portfolioVersions.get(contextKey) ?? 0;
4500
+ }
4501
+ portfolioCacheMs(contextKey) {
4502
+ return (this.reconcileUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_CACHE_MS : YIELDSEEKER_PORTFOLIO_CACHE_MS;
4503
+ }
4504
+ snapshotMovement(state, chainId, context, delta) {
4505
+ const key2 = this.contextKey(state, chainId, context.asset);
4506
+ const previous = this.snapshotMovements.get(key2);
4507
+ if (this.snapshotMovements.has(key2)) {
4508
+ if (!previous || previous.delta > 0n !== delta > 0n) return null;
4509
+ return {
4510
+ ...previous,
4511
+ epoch: this.movementEpochs.get(key2) ?? 0,
4512
+ delta: previous.delta + delta,
4513
+ expectedNetDeposits: previous.expectedNetDeposits + delta
3705
4514
  };
3706
- this.agentContexts.set(this.contextKey(state, chainId, asset), context);
3707
- contexts.push(context);
3708
4515
  }
3709
- return Promise.all(
3710
- contexts.map(
3711
- (context) => this.loadPortfolioContext(state, chainId, context, options)
3712
- )
3713
- );
4516
+ const cached = this.readCache.get(`snapshot:${key2}`);
4517
+ if (!cached || cached.expiresAt <= Date.now()) return null;
4518
+ const snapshot = cached.value.agentSnapshot;
4519
+ if (snapshot.baseAssetDecimals !== YIELDSEEKER_ASSET_METADATA[context.asset].decimals) return null;
4520
+ if (!/^-?\d+$/.test(snapshot.netDepositsBase) || !/^\d+$/.test(snapshot.totalValueBase)) return null;
4521
+ return {
4522
+ epoch: this.movementEpochs.get(key2) ?? 0,
4523
+ delta,
4524
+ decimals: snapshot.baseAssetDecimals,
4525
+ expectedNetDeposits: BigInt(snapshot.netDepositsBase) + delta
4526
+ };
4527
+ }
4528
+ advancePortfolioVersion(contextKey) {
4529
+ const version = this.portfolioVersion(contextKey) + 1;
4530
+ this.portfolioVersions.set(contextKey, version);
4531
+ this.snapshotFailures.delete(contextKey);
4532
+ for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4533
+ const key2 = `${kind}:${contextKey}`;
4534
+ this.readCache.delete(key2);
4535
+ this.pendingReads.delete(key2);
4536
+ }
4537
+ return version;
4538
+ }
4539
+ requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, forceRefresh = false) {
4540
+ return this.walletRequest(
4541
+ state,
4542
+ chainId,
4543
+ `${this.agentPath(context, "snapshot")}${query(forceRefresh ? { shouldForceRefresh: true } : { shouldOnlyUseRecentValue: true, shouldAllowStaleOnError: true })}`
4544
+ ).then((response) => {
4545
+ if (!response?.agentSnapshot) {
4546
+ throw this.invalidResponse("agent snapshot");
4547
+ }
4548
+ if (this.readGeneration === generation && this.portfolioVersion(contextKey) === version) {
4549
+ this.snapshotFailures.delete(contextKey);
4550
+ const movement = this.snapshotMovements.get(contextKey);
4551
+ const snapshot = response.agentSnapshot;
4552
+ if (movement && snapshot.baseAssetDecimals === movement.decimals && /^-?\d+$/.test(snapshot.netDepositsBase) && /^\d+$/.test(snapshot.totalValueBase)) {
4553
+ const netDeposits = BigInt(snapshot.netDepositsBase);
4554
+ const reconciled = movement.delta > 0n ? netDeposits >= movement.expectedNetDeposits : netDeposits <= movement.expectedNetDeposits;
4555
+ if (reconciled) {
4556
+ this.snapshotMovements.delete(contextKey);
4557
+ this.reconcileUntil.delete(contextKey);
4558
+ }
4559
+ }
4560
+ }
4561
+ return response;
4562
+ }).catch((error) => {
4563
+ if (this.readGeneration === generation && this.portfolioVersion(contextKey) === version) {
4564
+ this.snapshotFailures.set(contextKey, {
4565
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, rateLimitDelay(error) ?? 0),
4566
+ error
4567
+ });
4568
+ }
4569
+ throw error;
4570
+ });
4571
+ }
4572
+ portfolioSnapshot(state, chainId, context, contextKey) {
4573
+ const key2 = `snapshot:${contextKey}`;
4574
+ const cached = this.readCache.get(key2);
4575
+ if ((!cached || cached.expiresAt <= Date.now()) && !this.pendingReads.has(key2)) {
4576
+ const failure = this.snapshotFailures.get(contextKey);
4577
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4578
+ this.advancePortfolioVersion(contextKey);
4579
+ }
4580
+ const version = this.portfolioVersion(contextKey);
4581
+ const generation = this.readGeneration;
4582
+ return {
4583
+ version,
4584
+ read: this.cachedRead(key2, this.portfolioCacheMs(contextKey), () => (
4585
+ // Force only during the bounded post-transaction window. Keep unknown
4586
+ // or unreconciled movements for verification, but do not let them turn
4587
+ // routine polling into forced refreshes indefinitely.
4588
+ this.requestPortfolioSnapshot(
4589
+ state,
4590
+ chainId,
4591
+ context,
4592
+ contextKey,
4593
+ version,
4594
+ generation,
4595
+ this.snapshotMovements.has(contextKey) && (this.reconcileUntil.get(contextKey) ?? 0) > Date.now()
4596
+ )
4597
+ ))
4598
+ };
3714
4599
  }
3715
4600
  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")
4601
+ const contextKey = this.contextKey(state, chainId, context.asset);
4602
+ const generation = this.readGeneration;
4603
+ const assertSession = () => this.assertSession(generation, chainId, context.asset);
4604
+ for (let attempt = 0; attempt < 2; attempt++) {
4605
+ const { version, read: read2 } = this.portfolioSnapshot(state, chainId, context, contextKey);
4606
+ const parts = this.loadPortfolioParts(state, chainId, context, options, contextKey);
4607
+ try {
4608
+ let positionsUnavailable = false;
4609
+ const safeParts = options.allowMissingPositions ? parts.catch(() => {
4610
+ positionsUnavailable = true;
4611
+ return { positions: [] };
4612
+ }) : parts;
4613
+ const [snapshot, portfolioParts] = await Promise.all([read2, safeParts]);
4614
+ assertSession();
4615
+ if (this.portfolioVersion(contextKey) !== version) continue;
4616
+ return {
4617
+ ...context,
4618
+ snapshot: snapshot.agentSnapshot,
4619
+ fetchedAt: this.readCache.get(`snapshot:${contextKey}`)?.fetchedAt,
4620
+ readVersion: version,
4621
+ positionsUnavailable,
4622
+ ...portfolioParts
4623
+ };
4624
+ } catch (error) {
4625
+ assertSession();
4626
+ if (this.portfolioVersion(contextKey) !== version) continue;
4627
+ throw error;
4628
+ }
4629
+ }
4630
+ throw new OwneyError(
4631
+ "AGENT_API_ERROR",
4632
+ "Yieldseeker portfolio changed during the read. Please retry.",
4633
+ { rpcSource: "agent-api", chainId, asset: context.asset },
4634
+ this.id
4635
+ );
4636
+ }
4637
+ async loadActivityContext(state, chainId, context, options) {
4638
+ const contextKey = this.contextKey(state, chainId, context.asset);
4639
+ const generation = this.readGeneration;
4640
+ for (let attempt = 0; attempt < 2; attempt++) {
4641
+ const version = this.portfolioVersion(contextKey);
4642
+ try {
4643
+ const parts = await this.loadPortfolioParts(state, chainId, context, options, contextKey);
4644
+ this.assertSession(generation, chainId, context.asset);
4645
+ if (this.portfolioVersion(contextKey) !== version) continue;
4646
+ return { ...context, ...parts };
4647
+ } catch (error) {
4648
+ this.assertSession(generation, chainId, context.asset);
4649
+ if (this.portfolioVersion(contextKey) !== version) continue;
4650
+ throw error;
4651
+ }
4652
+ }
4653
+ throw new OwneyError(
4654
+ "AGENT_API_ERROR",
4655
+ "Yieldseeker activity changed during the read. Please retry.",
4656
+ { rpcSource: "agent-api", chainId, asset: context.asset },
4657
+ this.id
4658
+ );
4659
+ }
4660
+ async loadPortfolioParts(state, chainId, context, options, contextKey) {
4661
+ const [positions, historic, actions] = await Promise.all([
4662
+ this.cachedRead(
4663
+ `positions:${contextKey}`,
4664
+ this.portfolioCacheMs(contextKey),
4665
+ async () => {
4666
+ const response = await this.walletRequest(
4667
+ state,
4668
+ chainId,
4669
+ this.agentPath(context, "yield-positions")
4670
+ );
4671
+ if (!Array.isArray(response?.yieldPositions)) {
4672
+ throw this.invalidResponse("yield positions");
4673
+ }
4674
+ return response;
4675
+ }
3729
4676
  ),
3730
- options.historic ? this.walletRequest(
3731
- state,
3732
- chainId,
3733
- this.agentPath(context, "wallet/historic-position")
4677
+ options.historic ? this.cachedRead(
4678
+ `historic:${contextKey}`,
4679
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4680
+ () => this.walletRequest(
4681
+ state,
4682
+ chainId,
4683
+ this.agentPath(context, "wallet/historic-position")
4684
+ )
3734
4685
  ) : Promise.resolve(void 0),
3735
- options.actions ? this.walletRequest(
3736
- state,
3737
- chainId,
3738
- this.agentPath(context, "actions")
4686
+ options.actions ? this.cachedRead(
4687
+ `actions:${contextKey}`,
4688
+ (this.activityRefreshUntil.get(contextKey) ?? 0) > Date.now() ? YIELDSEEKER_SETTLEMENT_ACTIVITY_CACHE_MS : YIELDSEEKER_ACTIVITY_CACHE_MS,
4689
+ () => this.walletRequest(
4690
+ state,
4691
+ chainId,
4692
+ this.agentPath(context, "actions")
4693
+ )
3739
4694
  ) : Promise.resolve(void 0)
3740
4695
  ]);
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
4696
  return {
3748
- ...context,
3749
- snapshot: snapshot.agentSnapshot,
3750
4697
  positions: positions.yieldPositions,
3751
4698
  ...historic?.position ? { historic: historic.position } : {},
3752
4699
  ...actions?.actions ? { actions: actions.actions } : {}
@@ -3768,18 +4715,21 @@ var YieldseekerAgent = class {
3768
4715
  }
3769
4716
  context.wallet = deployed.agentWallet;
3770
4717
  }
3771
- async refreshSnapshotAfterMovement(state, chainId, context, movement) {
4718
+ async refreshSnapshotAfterMovement(state, chainId, context, movement, settlement) {
4719
+ const contextKey = this.contextKey(state, chainId, context.asset);
4720
+ this.reconcileUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4721
+ this.activityRefreshUntil.set(contextKey, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
4722
+ const epoch = this.movementEpochs.get(contextKey) ?? 0;
4723
+ this.snapshotMovements.set(contextKey, settlement?.epoch === epoch ? settlement : null);
4724
+ this.movementEpochs.set(contextKey, epoch + 1);
4725
+ const version = this.advancePortfolioVersion(contextKey);
4726
+ const generation = this.readGeneration;
3772
4727
  try {
3773
- const response = await this.walletRequest(
3774
- state,
3775
- chainId,
3776
- `${this.agentPath(context, "snapshot")}${query({
3777
- shouldForceRefresh: true
3778
- })}`
4728
+ await this.cachedRead(
4729
+ `snapshot:${contextKey}`,
4730
+ YIELDSEEKER_SETTLEMENT_CACHE_MS,
4731
+ () => this.requestPortfolioSnapshot(state, chainId, context, contextKey, version, generation, true)
3779
4732
  );
3780
- if (!response?.agentSnapshot) {
3781
- throw this.invalidResponse("agent snapshot refresh");
3782
- }
3783
4733
  } catch (error) {
3784
4734
  console.warn(
3785
4735
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
@@ -3791,10 +4741,26 @@ var YieldseekerAgent = class {
3791
4741
  return `/users/${context.user.userId}/agents/${context.agent.agentId}/${suffix}`;
3792
4742
  }
3793
4743
  async walletRequest(state, chainId, path, options = {}) {
4744
+ const read2 = (options.method ?? "GET") === "GET";
4745
+ const userKey = this.userKey(state, chainId);
4746
+ if (read2) {
4747
+ const failure = this.standardReadFailures.get(userKey);
4748
+ if (failure && failure.retryAt > Date.now()) throw failure.error;
4749
+ if (failure) this.standardReadFailures.delete(userKey);
4750
+ }
4751
+ const generation = this.readGeneration;
3794
4752
  try {
3795
4753
  return await this.providerRequest(state, chainId, path, options);
3796
4754
  } catch (error) {
3797
- throw this.mapApiError(error);
4755
+ const mapped = this.mapApiError(error);
4756
+ const delay = read2 ? rateLimitDelay(mapped) : void 0;
4757
+ if (delay !== void 0 && this.readGeneration === generation) {
4758
+ this.standardReadFailures.set(userKey, {
4759
+ retryAt: Date.now() + Math.max(YIELDSEEKER_READ_FAILURE_COOLDOWN_MS, delay),
4760
+ error: mapped
4761
+ });
4762
+ }
4763
+ throw mapped;
3798
4764
  }
3799
4765
  }
3800
4766
  async providerRequest(state, chainId, path, options = {}) {
@@ -3835,6 +4801,7 @@ var YieldseekerAgent = class {
3835
4801
  code,
3836
4802
  `Yieldseeker request failed: ${error.providerCode}.`,
3837
4803
  {
4804
+ rpcSource: "agent-api",
3838
4805
  statusCode: error.status,
3839
4806
  providerCode: error.providerCode,
3840
4807
  ...error.responseFields ? { fields: error.responseFields } : {}
@@ -3842,9 +4809,11 @@ var YieldseekerAgent = class {
3842
4809
  this.id
3843
4810
  );
3844
4811
  }
3845
- async submitTransaction(state, chainId, transaction) {
4812
+ async submitTransaction(state, chainId, transaction, onSubmitted, onReceipt) {
3846
4813
  if (this.transactionExecutor) {
3847
- return this.transactionExecutor(state, chainId, transaction);
4814
+ const hash2 = await this.transactionExecutor(state, chainId, transaction);
4815
+ onSubmitted?.(hash2);
4816
+ return hash2;
3848
4817
  }
3849
4818
  this.assertTransaction(transaction, state, chainId);
3850
4819
  const account = getAddress2(state.walletAddress);
@@ -3853,12 +4822,12 @@ var YieldseekerAgent = class {
3853
4822
  chain: base3,
3854
4823
  transport: custom2(state.provider)
3855
4824
  });
3856
- const publicClient = createPublicClient3({
4825
+ const walletChainClient = createPublicClient3({
3857
4826
  chain: base3,
3858
4827
  transport: custom2(state.provider)
3859
4828
  });
3860
4829
  await ensureWalletOnChain(
3861
- publicClient,
4830
+ walletChainClient,
3862
4831
  walletClient,
3863
4832
  8453
3864
4833
  );
@@ -3869,10 +4838,16 @@ var YieldseekerAgent = class {
3869
4838
  data: transaction.data,
3870
4839
  value: BigInt(transaction.value)
3871
4840
  });
3872
- const receipt = await publicClient.waitForTransactionReceipt({
3873
- hash,
3874
- confirmations: 1
3875
- });
4841
+ onSubmitted?.(hash);
4842
+ const receipt = await withPaidRpcDiagnostics(
4843
+ () => this.getReceiptClient().waitForTransactionReceipt({
4844
+ hash,
4845
+ confirmations: 1
4846
+ }),
4847
+ 8453,
4848
+ "eth_getTransactionReceipt",
4849
+ this.id
4850
+ );
3876
4851
  if (receipt.status !== "success") {
3877
4852
  throw new OwneyError(
3878
4853
  "AGENT_TRANSACTION_REVERTED",
@@ -3881,6 +4856,7 @@ var YieldseekerAgent = class {
3881
4856
  this.id
3882
4857
  );
3883
4858
  }
4859
+ onReceipt?.(receipt);
3884
4860
  return hash;
3885
4861
  }
3886
4862
  async waitForReceipt(state, chainId, transactionHash) {
@@ -3888,14 +4864,15 @@ var YieldseekerAgent = class {
3888
4864
  await this.unwindReceiptWaiter(state, chainId, transactionHash);
3889
4865
  return;
3890
4866
  }
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
- });
4867
+ const receipt = await withPaidRpcDiagnostics(
4868
+ () => this.getReceiptClient().waitForTransactionReceipt({
4869
+ hash: transactionHash,
4870
+ confirmations: 1
4871
+ }),
4872
+ 8453,
4873
+ "eth_getTransactionReceipt",
4874
+ this.id
4875
+ );
3899
4876
  if (receipt.status !== "success") {
3900
4877
  throw new OwneyError(
3901
4878
  "AGENT_TRANSACTION_REVERTED",
@@ -3904,6 +4881,7 @@ var YieldseekerAgent = class {
3904
4881
  this.id
3905
4882
  );
3906
4883
  }
4884
+ return receipt;
3907
4885
  }
3908
4886
  assertTransaction(transaction, state, chainId) {
3909
4887
  if (!transaction || typeof transaction.from !== "string" || !isAddress2(transaction.from) || typeof transaction.to !== "string" || !isAddress2(transaction.to) || typeof transaction.data !== "string" || !/^0x[a-fA-F0-9]*$/.test(transaction.data) || typeof transaction.value !== "string" || !/^[0-9]+$/.test(transaction.value) || transaction.chainId !== chainId || getAddress2(transaction.from) !== getAddress2(state.walletAddress)) {
@@ -4084,16 +5062,16 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
4084
5062
  const positionChain = position2.chain.trim().toUpperCase();
4085
5063
  const matchesChain = positionChain === String(chainId) || targetChain !== void 0 && positionChain === targetChain;
4086
5064
  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);
5065
+ balance += position2.withdrawableAmountRaw !== void 0 ? BigInt(position2.withdrawableAmountRaw) : parseUnits(position2.amount, decimals);
4095
5066
  }
4096
5067
  }
5068
+ const assetTotal = agentBalance?.assetBalances?.find(
5069
+ (t) => t.chainId === chainId && t.asset.toUpperCase() === target
5070
+ );
5071
+ if (assetTotal) {
5072
+ const total = parseUnits(assetTotal.amount, decimals);
5073
+ if (total < balance) balance = total > 0n ? total : 0n;
5074
+ }
4097
5075
  return { agent, balance };
4098
5076
  });
4099
5077
  }
@@ -4265,8 +5243,43 @@ import {
4265
5243
  } from "viem";
4266
5244
 
4267
5245
  // src/lib/permit2-batch.ts
4268
- import { parseAbi as parseAbi2, hashStruct } from "viem";
5246
+ import { parseAbi as parseAbi3, hashStruct } from "viem";
4269
5247
  var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
5248
+ var BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
5249
+ var MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
5250
+ var ERC2612_PERMIT_TYPES = {
5251
+ Permit: [
5252
+ { name: "owner", type: "address" },
5253
+ { name: "spender", type: "address" },
5254
+ { name: "value", type: "uint256" },
5255
+ { name: "nonce", type: "uint256" },
5256
+ { name: "deadline", type: "uint256" }
5257
+ ]
5258
+ };
5259
+ var ERC2612_READ_ABI = parseAbi3([
5260
+ "function name() view returns (string)",
5261
+ "function version() view returns (string)",
5262
+ "function nonces(address owner) view returns (uint256)"
5263
+ ]);
5264
+ function erc2612TypedData(input) {
5265
+ return {
5266
+ domain: {
5267
+ name: input.permit.tokenName,
5268
+ version: input.permit.tokenVersion,
5269
+ chainId: input.chainId,
5270
+ verifyingContract: input.token
5271
+ },
5272
+ types: ERC2612_PERMIT_TYPES,
5273
+ primaryType: "Permit",
5274
+ message: {
5275
+ owner: input.owner,
5276
+ spender: BATCH_PERMIT2_ADDRESS,
5277
+ value: BigInt(input.permit.value),
5278
+ nonce: BigInt(input.permit.nonce),
5279
+ deadline: BigInt(input.permit.deadline)
5280
+ }
5281
+ };
5282
+ }
4270
5283
  var PERMIT_BATCH_TYPES = {
4271
5284
  PermitBatchWitnessTransferFrom: [
4272
5285
  { name: "permitted", type: "TokenPermissions[]" },
@@ -4281,7 +5294,7 @@ var PERMIT_BATCH_TYPES = {
4281
5294
  { name: "amount", type: "uint256" }
4282
5295
  ]
4283
5296
  };
4284
- var PERMIT2_BATCH_ABI = parseAbi2([
5297
+ var PERMIT2_BATCH_ABI = parseAbi3([
4285
5298
  "struct TokenPermissions { address token; uint256 amount; }",
4286
5299
  "struct PermitBatchTransferFrom { TokenPermissions[] permitted; uint256 nonce; uint256 deadline; }",
4287
5300
  "struct SignatureTransferDetails { address to; uint256 requestedAmount; }",
@@ -4381,6 +5394,10 @@ async function execute(i, key2, plan) {
4381
5394
  };
4382
5395
  save(key2, body);
4383
5396
  }
5397
+ try {
5398
+ i.onSubmitted?.(keccak256(body.serializedTransaction));
5399
+ } catch {
5400
+ }
4384
5401
  const result = await postSponsorBatchTransfer({
4385
5402
  apiKey: i.apiKey,
4386
5403
  baseUrl: i.baseUrl,
@@ -4418,28 +5435,67 @@ async function execute(i, key2, plan) {
4418
5435
  "DEPOSIT_INSUFFICIENT_BALANCE",
4419
5436
  "Insufficient token balance for this deposit."
4420
5437
  );
4421
- if (allowance < total)
5438
+ if (allowance < total && (i.chainId !== 8453 || !isAddressEqual(i.token, BASE_USDC_ADDRESS)))
4422
5439
  throw new OwneyError(
4423
5440
  "PERMIT2_APPROVAL_REQUIRED",
4424
5441
  "token deposits need a one-time Permit2 approval."
4425
5442
  );
4426
- const relayer = await getSponsorRelayerAddress({
5443
+ const now = (await i.pub.getBlock()).timestamp;
5444
+ let erc2612Permit;
5445
+ if (allowance < total) {
5446
+ const [tokenName, tokenVersion, permitNonce] = await Promise.all([
5447
+ i.pub.readContract({
5448
+ address: i.token,
5449
+ abi: ERC2612_READ_ABI,
5450
+ functionName: "name"
5451
+ }),
5452
+ i.pub.readContract({
5453
+ address: i.token,
5454
+ abi: ERC2612_READ_ABI,
5455
+ functionName: "version"
5456
+ }),
5457
+ i.pub.readContract({
5458
+ address: i.token,
5459
+ abi: ERC2612_READ_ABI,
5460
+ functionName: "nonces",
5461
+ args: [i.owner]
5462
+ })
5463
+ ]);
5464
+ const unsignedPermit = {
5465
+ value: MAX_UINT256.toString(),
5466
+ nonce: permitNonce.toString(),
5467
+ deadline: (now + 900n).toString(),
5468
+ tokenName,
5469
+ tokenVersion
5470
+ };
5471
+ const signature2 = await i.wallet.signTypedData({
5472
+ account: i.owner,
5473
+ ...erc2612TypedData({
5474
+ chainId: 8453,
5475
+ token: i.token,
5476
+ owner: i.owner,
5477
+ permit: unsignedPermit
5478
+ })
5479
+ });
5480
+ erc2612Permit = { ...unsignedPermit, signature: signature2 };
5481
+ }
5482
+ const spender = erc2612Permit ? MULTICALL3_ADDRESS : await getSponsorRelayerAddress({
4427
5483
  apiKey: i.apiKey,
4428
5484
  baseUrl: i.baseUrl,
4429
5485
  chainId: i.chainId
4430
5486
  });
4431
- const now = (await i.pub.getBlock()).timestamp;
4432
5487
  const unsigned = {
4433
5488
  chainId: i.chainId,
4434
5489
  token: i.token,
4435
5490
  from: i.owner,
4436
5491
  transfers: i.transfers,
4437
5492
  nonce: randomPermit2Nonce().toString(),
4438
- deadline: (now + 900n).toString()
5493
+ deadline: (now + 900n).toString(),
5494
+ ...erc2612Permit ? { erc2612Permit } : {}
4439
5495
  };
4440
5496
  const signature = await i.wallet.signTypedData({
4441
5497
  account: i.owner,
4442
- ...batchTypedData(unsigned, relayer)
5498
+ ...batchTypedData(unsigned, spender)
4443
5499
  });
4444
5500
  i.onApproved?.();
4445
5501
  return send({ ...unsigned, signature });
@@ -4459,8 +5515,8 @@ function makeSponsoredTokenCallback(deps) {
4459
5515
  "CHAIN_UNSUPPORTED",
4460
5516
  `No sponsored token configured for chain ${chainId}`
4461
5517
  );
4462
- const pub = deps.getPublicClient(chainId), wallet = deps.getWalletClient(chainId);
4463
- await ensureWalletOnChain(pub, wallet, chainId);
5518
+ const pub = deps.getPublicClient(chainId), walletChain = deps.getWalletChainClient?.(chainId) ?? pub, wallet = deps.getWalletClient(chainId);
5519
+ await ensureWalletOnChain(walletChain, wallet, chainId);
4464
5520
  return sponsorTokenBatch({
4465
5521
  apiKey: deps.apiKey,
4466
5522
  baseUrl: deps.baseUrl,
@@ -4470,7 +5526,8 @@ function makeSponsoredTokenCallback(deps) {
4470
5526
  transfers,
4471
5527
  pub,
4472
5528
  wallet,
4473
- onApproved: deps.onApproved
5529
+ onApproved: deps.onApproved,
5530
+ onSubmitted: (hash) => deps.onSubmitted?.(hash, transfers)
4474
5531
  });
4475
5532
  };
4476
5533
  const callback = makeVerificationAwareDepositCallback(
@@ -4556,7 +5613,7 @@ async function runAgentDepositBatch(chainId, legs, transfer) {
4556
5613
  }
4557
5614
 
4558
5615
  // src/lib/sponsored-calls-deposit.ts
4559
- import { encodeFunctionData as encodeFunctionData2, erc20Abi as erc20Abi2, toHex } from "viem";
5616
+ import { encodeFunctionData as encodeFunctionData2, erc20Abi as erc20Abi3, toHex } from "viem";
4560
5617
  var DEFAULT_POLL_INTERVAL_MS = 1500;
4561
5618
  var DEFAULT_MAX_POLLS = 30;
4562
5619
  async function paymasterSupported(provider, owner, chainId) {
@@ -4602,7 +5659,7 @@ function makeSponsoredCallsCallback(deps) {
4602
5659
  to: token,
4603
5660
  value: "0x0",
4604
5661
  data: encodeFunctionData2({
4605
- abi: erc20Abi2,
5662
+ abi: erc20Abi3,
4606
5663
  functionName: "transfer",
4607
5664
  args: [transfer.to, BigInt(transfer.amount)]
4608
5665
  })
@@ -4664,6 +5721,12 @@ function makeSponsoredCallsCallback(deps) {
4664
5721
  params: [callsId]
4665
5722
  });
4666
5723
  const txHash = status?.receipts?.[0]?.transactionHash;
5724
+ if (txHash && !status?.receipts?.some((receipt) => receipt.transactionHash !== txHash)) {
5725
+ try {
5726
+ deps.onSubmitted?.(txHash, transfers);
5727
+ } catch {
5728
+ }
5729
+ }
4667
5730
  if (status?.receipts?.some((receipt) => receipt.status === "0x0") || typeof status?.status === "number" && status.status >= 400) {
4668
5731
  throw new OwneyError(
4669
5732
  "SPONSOR_REQUEST_FAILED",
@@ -4700,6 +5763,8 @@ function makeSponsoredCallsCallback(deps) {
4700
5763
  }
4701
5764
 
4702
5765
  // src/client.ts
5766
+ var PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS = 6;
5767
+ var PERMIT2_ALLOWANCE_VERIFY_DELAY_MS = 250;
4703
5768
  function encodeMultiAgentCursor(map) {
4704
5769
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
4705
5770
  }
@@ -4760,6 +5825,7 @@ var OwneySDK = class {
4760
5825
  // leave every user's agent profile alone.
4761
5826
  orgAgentConfig;
4762
5827
  orgAgentConfigPromise = null;
5828
+ rpcUrls;
4763
5829
  zyfaiRpcUrls;
4764
5830
  yieldseekerApiBaseUrl;
4765
5831
  yieldseekerSiweOrigin;
@@ -4770,6 +5836,7 @@ var OwneySDK = class {
4770
5836
  paymasterServiceUrl;
4771
5837
  cachedSponsoredCallsCallbacks = /* @__PURE__ */ new Map();
4772
5838
  agentReads = new AgentReads();
5839
+ recoveredFundingReads = /* @__PURE__ */ new Set();
4773
5840
  activationChecks = /* @__PURE__ */ new Map();
4774
5841
  readAgent(agent, method, fetch2, params = null) {
4775
5842
  const key2 = JSON.stringify([
@@ -4784,7 +5851,12 @@ var OwneySDK = class {
4784
5851
  constructor(config) {
4785
5852
  this.apiKey = config.apiKey;
4786
5853
  if (config.debug) setOwneyDebug(true);
4787
- this.zyfaiRpcUrls = config.zyfaiRpcUrls;
5854
+ this.rpcUrls = resolveRpcUrls(
5855
+ config.rpcUrls,
5856
+ config.apiKey,
5857
+ config.routingApiBaseUrl
5858
+ );
5859
+ this.zyfaiRpcUrls = config.rpcUrls ? void 0 : config.zyfaiRpcUrls;
4788
5860
  this.yieldseekerApiBaseUrl = config.yieldseekerApiBaseUrl;
4789
5861
  this.yieldseekerSiweOrigin = config.yieldseekerSiweOrigin;
4790
5862
  this.routingApiBaseUrl = config.routingApiBaseUrl;
@@ -4874,9 +5946,12 @@ var OwneySDK = class {
4874
5946
  }
4875
5947
  return this.state.provider;
4876
5948
  }
5949
+ getPaidRpcClient(chainId) {
5950
+ return createPaidRpcClient(VIEM_CHAIN2[chainId], this.rpcUrls);
5951
+ }
4877
5952
  /** Builds the default USDC batch callback for the connected wallet. */
4878
- getDefaultSponsoredCallback(onApproved) {
4879
- if (!onApproved && this.cachedSponsoredCallback)
5953
+ getDefaultSponsoredCallback(onApproved, onSubmitted) {
5954
+ if (!onApproved && !onSubmitted && this.cachedSponsoredCallback)
4880
5955
  return this.cachedSponsoredCallback;
4881
5956
  const provider = this.requireConnectedProvider();
4882
5957
  const owner = this.state.walletAddress;
@@ -4885,11 +5960,13 @@ var OwneySDK = class {
4885
5960
  baseUrl: this.routingApiBaseUrl,
4886
5961
  ownerAddress: owner,
4887
5962
  onApproved,
5963
+ onSubmitted,
4888
5964
  tokenAddressByChain: SPONSORED_USDC_BY_CHAIN,
4889
5965
  // Casts work around viem's chain-narrowed Client vs the generic
4890
5966
  // PublicClient/WalletClient param types — structurally identical at
4891
5967
  // runtime, but the two share a name TS treats as unrelated.
4892
- getPublicClient: (cid) => createPublicClient4({
5968
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5969
+ getWalletChainClient: (cid) => createPublicClient4({
4893
5970
  chain: VIEM_CHAIN2[cid],
4894
5971
  transport: custom3(provider)
4895
5972
  }),
@@ -4899,13 +5976,13 @@ var OwneySDK = class {
4899
5976
  transport: custom3(provider)
4900
5977
  })
4901
5978
  });
4902
- if (!onApproved) this.cachedSponsoredCallback = callback;
5979
+ if (!onApproved && !onSubmitted) this.cachedSponsoredCallback = callback;
4903
5980
  return callback;
4904
5981
  }
4905
5982
  /** Builds the wallet-native sponsored calls callback for compatible paymasters. */
4906
- getDefaultSponsoredCallsCallback(asset, onApproved) {
5983
+ getDefaultSponsoredCallsCallback(asset, onApproved, onSubmitted) {
4907
5984
  const cached = this.cachedSponsoredCallsCallbacks.get(asset);
4908
- if (!onApproved && cached) return cached;
5985
+ if (!onApproved && !onSubmitted && cached) return cached;
4909
5986
  const provider = this.requireConnectedProvider();
4910
5987
  const callback = makeSponsoredCallsCallback({
4911
5988
  apiKey: this.apiKey,
@@ -4914,9 +5991,10 @@ var OwneySDK = class {
4914
5991
  ownerAddress: this.state.walletAddress,
4915
5992
  paymasterServiceUrl: this.paymasterServiceUrl,
4916
5993
  onApproved,
5994
+ onSubmitted,
4917
5995
  tokenAddressByChain: sponsoredTokensFor(asset)
4918
5996
  });
4919
- if (!onApproved) this.cachedSponsoredCallsCallbacks.set(asset, callback);
5997
+ if (!onApproved && !onSubmitted) this.cachedSponsoredCallsCallbacks.set(asset, callback);
4920
5998
  return callback;
4921
5999
  }
4922
6000
  /**
@@ -4925,8 +6003,8 @@ var OwneySDK = class {
4925
6003
  * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
4926
6004
  * single-use batch authorization instead of an EIP-3009 authorization.
4927
6005
  */
4928
- getDefaultWethSponsoredCallback(onApproved) {
4929
- if (!onApproved && this.cachedWethSponsoredCallback)
6006
+ getDefaultWethSponsoredCallback(onApproved, onSubmitted) {
6007
+ if (!onApproved && !onSubmitted && this.cachedWethSponsoredCallback)
4930
6008
  return this.cachedWethSponsoredCallback;
4931
6009
  const provider = this.requireConnectedProvider();
4932
6010
  const owner = this.state.walletAddress;
@@ -4935,11 +6013,13 @@ var OwneySDK = class {
4935
6013
  baseUrl: this.routingApiBaseUrl,
4936
6014
  ownerAddress: owner,
4937
6015
  onApproved,
6016
+ onSubmitted,
4938
6017
  tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
4939
6018
  // Casts work around viem's chain-narrowed Client vs the generic
4940
6019
  // PublicClient/WalletClient param types — structurally identical at
4941
6020
  // runtime, but the two share a name TS treats as unrelated.
4942
- getPublicClient: (cid) => createPublicClient4({
6021
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
6022
+ getWalletChainClient: (cid) => createPublicClient4({
4943
6023
  chain: VIEM_CHAIN2[cid],
4944
6024
  transport: custom3(provider)
4945
6025
  }),
@@ -4949,7 +6029,7 @@ var OwneySDK = class {
4949
6029
  transport: custom3(provider)
4950
6030
  })
4951
6031
  });
4952
- if (!onApproved) this.cachedWethSponsoredCallback = callback;
6032
+ if (!onApproved && !onSubmitted) this.cachedWethSponsoredCallback = callback;
4953
6033
  return callback;
4954
6034
  }
4955
6035
  getAgent(agentId) {
@@ -4959,6 +6039,9 @@ var OwneySDK = class {
4959
6039
  }
4960
6040
  return agent;
4961
6041
  }
6042
+ getActiveAgentIds() {
6043
+ return [...this.getActiveAgents().keys()].sort();
6044
+ }
4962
6045
  getActiveAgents() {
4963
6046
  const active = /* @__PURE__ */ new Map();
4964
6047
  for (const id of this.activeAgents) {
@@ -5052,12 +6135,21 @@ var OwneySDK = class {
5052
6135
  createAgent(agentId, key2) {
5053
6136
  if (agentId === "zyfai") {
5054
6137
  if (!key2) return null;
5055
- return new ZyfaiAgent(key2, this.zyfaiRpcUrls, this.referralSource);
6138
+ return new ZyfaiAgent(
6139
+ key2,
6140
+ this.zyfaiRpcUrls ? resolveRpcUrls(
6141
+ this.zyfaiRpcUrls,
6142
+ this.apiKey,
6143
+ this.routingApiBaseUrl
6144
+ ) : this.rpcUrls,
6145
+ this.referralSource
6146
+ );
5056
6147
  }
5057
6148
  if (agentId === "yieldseeker") {
5058
6149
  return new YieldseekerAgent(this.apiKey, {
5059
6150
  auth: { origin: this.yieldseekerSiweOrigin },
5060
- baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl)
6151
+ baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl),
6152
+ rpcUrls: this.rpcUrls
5061
6153
  });
5062
6154
  }
5063
6155
  return null;
@@ -5244,12 +6336,23 @@ var OwneySDK = class {
5244
6336
  */
5245
6337
  async deposit(options) {
5246
6338
  const { amount, asset, depositCallback, agentId, onApproved } = options;
6339
+ const prepared = /* @__PURE__ */ new Map();
6340
+ const onProgress = options.onProgress ? (evidence) => {
6341
+ if (evidence.agentWallet) prepared.set(evidence.agentWallet.toLowerCase(), evidence);
6342
+ notifyFunding(options.onProgress, evidence);
6343
+ } : void 0;
6344
+ const onSubmitted = onProgress ? (hash, destinations) => {
6345
+ for (const destination of destinations) {
6346
+ const evidence = prepared.get(destination.to.toLowerCase());
6347
+ if (evidence) notifyFunding(onProgress, { ...evidence, txHash: hash, transfer: "pending" });
6348
+ }
6349
+ } : void 0;
5247
6350
  if (BigInt(amount) === 0n) {
5248
6351
  return { agentResults: {} };
5249
6352
  }
5250
6353
  const state = this.requireState();
5251
6354
  const chainId = this.requireChainId();
5252
- const effectiveCallback = depositCallback ?? (this.paymasterServiceUrl ? this.getDefaultSponsoredCallsCallback(asset, onApproved) : asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
6355
+ const effectiveCallback = depositCallback ?? (this.paymasterServiceUrl ? this.getDefaultSponsoredCallsCallback(asset, onApproved, onSubmitted) : asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved, onSubmitted) : this.getDefaultSponsoredCallback(onApproved, onSubmitted));
5253
6356
  if (agentId) {
5254
6357
  if (this.isAgentDisabled(agentId)) {
5255
6358
  throw new OwneyError(
@@ -5268,7 +6371,8 @@ var OwneySDK = class {
5268
6371
  amount,
5269
6372
  asset,
5270
6373
  effectiveCallback,
5271
- depositCallback
6374
+ depositCallback,
6375
+ onProgress
5272
6376
  );
5273
6377
  }
5274
6378
  const totalAmount = BigInt(amount);
@@ -5332,7 +6436,7 @@ var OwneySDK = class {
5332
6436
  run: (callback) => withFailureReporting(
5333
6437
  this.apiKey,
5334
6438
  agent.id,
5335
- () => agent.deposit(state, chainId, amount2, asset, callback),
6439
+ () => onProgress ? agent.deposit(state, chainId, amount2, asset, callback, onProgress) : agent.deposit(state, chainId, amount2, asset, callback),
5336
6440
  this.routingApiBaseUrl
5337
6441
  )
5338
6442
  })),
@@ -5348,7 +6452,8 @@ var OwneySDK = class {
5348
6452
  );
5349
6453
  await this.approvePermit2(
5350
6454
  asset,
5351
- requiredAmount
6455
+ requiredAmount,
6456
+ cid
5352
6457
  );
5353
6458
  return batchTransfer(cid, transfers);
5354
6459
  }
@@ -5368,7 +6473,8 @@ var OwneySDK = class {
5368
6473
  agentAmount,
5369
6474
  asset,
5370
6475
  effectiveCallback,
5371
- depositCallback
6476
+ depositCallback,
6477
+ onProgress
5372
6478
  );
5373
6479
  } catch (error) {
5374
6480
  if (Object.keys(agentResults).length === 0) throw error;
@@ -5392,10 +6498,12 @@ var OwneySDK = class {
5392
6498
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
5393
6499
  * two independent auto-recovery mechanisms:
5394
6500
  *
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
6501
+ * 1. Missing Permit2 allowance outside the atomic Base-USDC path: when the
6502
+ * app did not supply its own callback and the attempt fails with
6503
+ * `PERMIT2_APPROVAL_REQUIRED`, this is the wallet's first Permit2 deposit
6504
+ * for that token. Base USDC bundles a gasless ERC-2612 approval inside
6505
+ * its sponsored deposit and never reaches this branch. Other tokens send
6506
+ * the one-time user-paid Permit2 approval via `approvePermit2()` and
5399
6507
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
5400
6508
  * per call so a wallet/agent that keeps reporting the allowance as
5401
6509
  * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
@@ -5413,17 +6521,11 @@ var OwneySDK = class {
5413
6521
  * propagates unchanged. Scoped per agent invocation so one agent's failure
5414
6522
  * doesn't force a retry of sibling agents in a multi-agent split.
5415
6523
  */
5416
- async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback) {
6524
+ async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback, onProgress) {
5417
6525
  const attempt = (callback) => withFailureReporting(
5418
6526
  this.apiKey,
5419
6527
  agent.id,
5420
- () => agent.deposit(
5421
- state,
5422
- chainId,
5423
- amount,
5424
- asset,
5425
- callback
5426
- ),
6528
+ () => onProgress ? agent.deposit(state, chainId, amount, asset, callback, onProgress) : agent.deposit(state, chainId, amount, asset, callback),
5427
6529
  this.routingApiBaseUrl
5428
6530
  );
5429
6531
  let approvalAttempted = false;
@@ -5438,7 +6540,8 @@ var OwneySDK = class {
5438
6540
  );
5439
6541
  await this.approvePermit2(
5440
6542
  asset,
5441
- BigInt(amount)
6543
+ BigInt(amount),
6544
+ chainId
5442
6545
  );
5443
6546
  continue;
5444
6547
  }
@@ -5585,6 +6688,98 @@ var OwneySDK = class {
5585
6688
  }
5586
6689
  return eligible;
5587
6690
  }
6691
+ /**
6692
+ * Run owner-approved withdrawals before relayer-only withdrawals. Wallet
6693
+ * approval is the only point at which the user can cancel the aggregate
6694
+ * operation, so no relayer leg should commit before it has completed.
6695
+ */
6696
+ orderAgentsForWithdrawal(agents) {
6697
+ return agents.map((agent, index) => ({ agent, index })).sort((left, right) => {
6698
+ const approvalOrder = Number(Boolean(right.agent.withdrawalRequiresWalletApproval)) - Number(Boolean(left.agent.withdrawalRequiresWalletApproval));
6699
+ return approvalOrder || left.index - right.index;
6700
+ }).map(({ agent }) => agent);
6701
+ }
6702
+ isUserRejectedWithdrawal(error) {
6703
+ let current = error;
6704
+ const seen = /* @__PURE__ */ new Set();
6705
+ while (current && typeof current === "object" && !seen.has(current)) {
6706
+ seen.add(current);
6707
+ const candidate = current;
6708
+ if (candidate.code === 4001 || candidate.code === "4001" || candidate.code === "ACTION_REJECTED") return true;
6709
+ const message = [candidate.message, candidate.shortMessage].filter((value) => typeof value === "string").join(" ");
6710
+ if (/user (?:rejected|denied)|rejected by user|request rejected/i.test(message)) {
6711
+ return true;
6712
+ }
6713
+ current = candidate.cause;
6714
+ }
6715
+ return false;
6716
+ }
6717
+ captureReadScope(requestedChain) {
6718
+ const original = this.requireState();
6719
+ const chainId = this.requireChainId();
6720
+ if (requestedChain !== void 0 && requestedChain !== chainId) {
6721
+ throw new OwneyError("NO_ACTIVE_CHAIN", "Account read scope changed. Please retry.");
6722
+ }
6723
+ const wallet = original.walletAddress;
6724
+ return { state: { ...original }, chainId, assertCurrent: () => {
6725
+ if (this.state !== original || this.state.walletAddress !== wallet || this.state.chainId !== chainId)
6726
+ throw new OwneyError("NOT_CONNECTED", "Account read scope changed. Please retry.");
6727
+ } };
6728
+ }
6729
+ /** Revalidate a saved transfer against the authenticated account and actual receipt. */
6730
+ async inspectFunding(evidence, direction, options = {}) {
6731
+ const { state, chainId, assertCurrent } = this.captureReadScope(evidence.chainId);
6732
+ if (state.walletAddress.toLowerCase() !== evidence.walletAddress.toLowerCase() || !evidence.txHash)
6733
+ throw new OwneyError("AGENT_API_ERROR", "Funding evidence does not match the connected account.");
6734
+ const agent = this.getAgent(evidence.agentId);
6735
+ const verifiedWallet = agent.getFundingWallet ? await agent.getFundingWallet(state, chainId, evidence.token) : (await agent.getUserProfile(state, chainId)).smartWallet;
6736
+ const agentWallet = evidence.agentWallet ?? verifiedWallet;
6737
+ if (!agentWallet || agentWallet.toLowerCase() !== verifiedWallet.toLowerCase())
6738
+ throw new OwneyError("AGENT_API_ERROR", "Funding wallet does not match this account.");
6739
+ const token = SupportedAssets.find((t) => t.chainId === chainId && t.symbol === evidence.token);
6740
+ if (!token) throw new OwneyError("CHAIN_UNSUPPORTED", "Unsupported funding asset.");
6741
+ const rpc = this.getPaidRpcClient(chainId);
6742
+ const receipt = await rpc.getTransactionReceipt({ hash: evidence.txHash });
6743
+ assertCurrent();
6744
+ let verified = inspectFundingReceipt(receipt, { ...evidence, agentWallet }, token.address, direction);
6745
+ if (direction === "withdraw" && evidence.agentId === "yieldseeker" && verified.transfer === "unknown" && verified.confirmedAmount && BigInt(verified.confirmedAmount) > 0n) {
6746
+ const transaction = await rpc.getTransaction({ hash: evidence.txHash });
6747
+ assertCurrent();
6748
+ if (isFullYieldseekerWithdrawal(transaction, { ...evidence, agentWallet }, token.address))
6749
+ verified = inspectFundingReceipt(receipt, { ...evidence, agentWallet, intendedAmount: "MAX" }, token.address, direction);
6750
+ }
6751
+ const key2 = `${state.walletAddress.toLowerCase()}:${chainId}:${evidence.agentId}:${evidence.token}:${evidence.txHash}`;
6752
+ if ((verified.transfer === "confirmed" || verified.transfer === "failed" && verified.confirmedAmount === "0") && !this.recoveredFundingReads.has(key2)) {
6753
+ if (options.refreshBalance !== false)
6754
+ agent.invalidateFundingRead?.(state, chainId, evidence.token, options.balanceRefreshUntil);
6755
+ this.recoveredFundingReads.add(key2);
6756
+ if (this.recoveredFundingReads.size > 256) this.recoveredFundingReads.delete(this.recoveredFundingReads.values().next().value);
6757
+ }
6758
+ return verified;
6759
+ }
6760
+ /** A display acknowledgement controls polling only; it never confirms a financial outcome. */
6761
+ endFundingBalanceRefresh(evidence, read2) {
6762
+ const { state, chainId } = this.captureReadScope(evidence.chainId);
6763
+ if (state.walletAddress.toLowerCase() !== evidence.walletAddress.toLowerCase() || read2.chainId !== chainId || read2.asset.toUpperCase() !== evidence.token) return false;
6764
+ return this.getAgent(evidence.agentId).endFundingBalanceRefresh?.(state, chainId, evidence.token, read2) ?? false;
6765
+ }
6766
+ /** History lookup for receipt recovery; adapters may not authenticate or sign here. */
6767
+ async getFundingHistory(evidence, fromDate, options) {
6768
+ const { state, chainId, assertCurrent } = this.captureReadScope(evidence.chainId);
6769
+ if (state.walletAddress.toLowerCase() !== evidence.walletAddress.toLowerCase())
6770
+ throw new OwneyError("NOT_CONNECTED", "Funding account changed.");
6771
+ const agent = this.getAgent(evidence.agentId);
6772
+ if (!agent.getFundingHistory) return { data: [], hasMore: false };
6773
+ const filters = { tokenSymbol: evidence.token, fromDate, limit: 50, ...options };
6774
+ const page = await this.readAgent(agent, "history", () => agent.getFundingHistory(state, chainId, filters), filters);
6775
+ assertCurrent();
6776
+ return page;
6777
+ }
6778
+ async recoverDepositRegistration(evidence) {
6779
+ const verified = await this.inspectFunding(evidence, "deposit");
6780
+ if (verified.agentId !== "zyfai") return verified;
6781
+ return this.getAgent("zyfai").retryDepositRegistration(this.requireState(), verified);
6782
+ }
5588
6783
  // --- Fund operations ---
5589
6784
  /**
5590
6785
  * Withdraw funds from a specific agent, or all agents that support the active chain+asset if agentId is omitted.
@@ -5596,10 +6791,66 @@ var OwneySDK = class {
5596
6791
  * @returns {AgentWithdrawResult} for a single agent, or {OwneyWithdrawResult} with per-agent results
5597
6792
  */
5598
6793
  async withdraw(options) {
5599
- const { asset, amount, agentId } = options;
5600
6794
  const state = this.requireState();
5601
6795
  const chainId = this.requireChainId();
6796
+ const ids = options.agentId ? [options.agentId] : this.getActiveAgentIds().filter((id) => !options.excludeAgentIds?.includes(id));
6797
+ const attempted = /* @__PURE__ */ new Set();
6798
+ try {
6799
+ return await this.withdrawFromAgents(options, state, chainId, attempted);
6800
+ } catch (error) {
6801
+ for (const id of ids) if (!attempted.has(id)) {
6802
+ notifyFunding(options.onProgress, {
6803
+ agentId: id,
6804
+ chainId,
6805
+ token: options.asset,
6806
+ walletAddress: state.walletAddress,
6807
+ intendedAmount: options.amount ?? "MAX",
6808
+ transfer: "failed",
6809
+ registration: "not-applicable",
6810
+ delivery: "failed"
6811
+ });
6812
+ }
6813
+ throw error;
6814
+ }
6815
+ }
6816
+ async withdrawFromAgents(options, state, chainId, attempted) {
6817
+ const { asset, amount, agentId, onAgentResult } = options;
6818
+ const notifyAgentResult = (id, result) => {
6819
+ try {
6820
+ if (result.evidence) notifyFunding(options.onProgress, result.evidence);
6821
+ onAgentResult?.(id, result);
6822
+ } catch (error) {
6823
+ console.warn("Withdrawal result observer failed:", error);
6824
+ }
6825
+ };
5602
6826
  const token = asset;
6827
+ const submissionEvidence = /* @__PURE__ */ new Map();
6828
+ const submit = async (agent, requested2) => {
6829
+ attempted.add(agent.id);
6830
+ let evidence = {
6831
+ agentId: agent.id,
6832
+ chainId,
6833
+ token,
6834
+ walletAddress: state.walletAddress,
6835
+ intendedAmount: requested2 ?? "MAX",
6836
+ transfer: "pending",
6837
+ registration: "not-applicable",
6838
+ delivery: "pending"
6839
+ };
6840
+ const progress = (next) => {
6841
+ evidence = { ...evidence, ...next };
6842
+ submissionEvidence.set(agent.id, evidence);
6843
+ notifyFunding(options.onProgress, evidence);
6844
+ };
6845
+ progress(evidence);
6846
+ try {
6847
+ return await agent.withdraw(state, chainId, token, requested2, progress);
6848
+ } catch (error) {
6849
+ if (!evidence.txHash && (this.isUserRejectedWithdrawal(error) || /insufficient (balance|funds)/i.test(error instanceof Error ? error.message : "")))
6850
+ progress({ ...evidence, transfer: "failed", delivery: "failed" });
6851
+ throw error;
6852
+ }
6853
+ };
5603
6854
  const assetInfo = SupportedAssets.find(
5604
6855
  (a) => a.chainId === chainId && a.symbol === token
5605
6856
  );
@@ -5612,21 +6863,27 @@ var OwneySDK = class {
5612
6863
  if (agentId) {
5613
6864
  const agent = this.getAgent(agentId);
5614
6865
  this.validateAssetSupport(agent, chainId, asset);
5615
- return withFailureReporting(
6866
+ const result = await withFailureReporting(
5616
6867
  this.apiKey,
5617
6868
  agent.id,
5618
- () => agent.withdraw(state, chainId, token, amount),
6869
+ () => submit(agent, amount),
5619
6870
  this.routingApiBaseUrl
5620
6871
  );
6872
+ notifyAgentResult(agent.id, result);
6873
+ return result;
5621
6874
  }
5622
- const eligibleAgents = this.getEligibleAgents(chainId, asset);
6875
+ const eligibleAgents = this.getEligibleAgents(chainId, asset).filter((agent) => !options.excludeAgentIds?.includes(agent.id));
6876
+ const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
5623
6877
  if (!amount) {
5624
6878
  const results2 = {};
5625
6879
  const agentErrors2 = {};
5626
- for (const agent of eligibleAgents) {
6880
+ for (const agent of withdrawalAgents) {
5627
6881
  try {
5628
- results2[agent.id] = await agent.withdraw(state, chainId, token);
6882
+ results2[agent.id] = await submit(agent);
6883
+ notifyAgentResult(agent.id, results2[agent.id]);
5629
6884
  } catch (err) {
6885
+ if (this.isUserRejectedWithdrawal(err)) throw err;
6886
+ if (err instanceof OwneyError && err.code === "WITHDRAW_INSUFFICIENT_BALANCE" && err.details?.available === "0" && submissionEvidence.get(agent.id)?.transfer === "failed") continue;
5630
6887
  console.error(`withdraw failed for agent "${agent.id}":`, err);
5631
6888
  agentErrors2[agent.id] = err instanceof Error ? err.message : String(err);
5632
6889
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5645,6 +6902,13 @@ var OwneySDK = class {
5645
6902
  { asset, agentErrors: agentErrors2 }
5646
6903
  );
5647
6904
  }
6905
+ if (Object.keys(agentErrors2).length > 0) {
6906
+ throw new OwneyError(
6907
+ "WITHDRAW_PARTIAL_FAILURE",
6908
+ "Some agents could not complete the withdrawal.",
6909
+ { asset, partialResults: results2, agentErrors: agentErrors2 }
6910
+ );
6911
+ }
5648
6912
  const amountsKnown = Object.values(results2).every(
5649
6913
  (result) => typeof result.amount === "string" && /^\d+$/.test(result.amount)
5650
6914
  );
@@ -5654,7 +6918,7 @@ var OwneySDK = class {
5654
6918
  };
5655
6919
  }
5656
6920
  const requested = BigInt(amount);
5657
- const aggregated = await this.getBalances();
6921
+ const aggregated = await this.getBalances(void 0, { chainId, asset: token });
5658
6922
  const unavailableAgents = eligibleAgents.filter(
5659
6923
  (agent) => !(agent.id in aggregated.agentBalances)
5660
6924
  );
@@ -5707,21 +6971,19 @@ var OwneySDK = class {
5707
6971
  planned: 0n
5708
6972
  }));
5709
6973
  const plans = [...disabledPlans, ...enabledPlans];
6974
+ const orderedPlans = this.orderAgentsForWithdrawal(plans.map((p) => p.agent)).map((agent) => plans.find((plan) => plan.agent === agent));
5710
6975
  const results = {};
5711
6976
  const agentErrors = {
5712
6977
  ...aggregated.agentErrors ?? {}
5713
6978
  };
5714
- for (let i = 0; i < plans.length; i++) {
5715
- const p = plans[i];
6979
+ for (let i = 0; i < orderedPlans.length; i++) {
6980
+ const p = orderedPlans[i];
5716
6981
  if (p.planned === 0n) continue;
5717
6982
  try {
5718
- results[p.agent.id] = await p.agent.withdraw(
5719
- state,
5720
- chainId,
5721
- token,
5722
- p.planned.toString()
5723
- );
6983
+ results[p.agent.id] = await submit(p.agent, p.planned.toString());
6984
+ notifyAgentResult(p.agent.id, results[p.agent.id]);
5724
6985
  } catch (err) {
6986
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5725
6987
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);
5726
6988
  agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
5727
6989
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5733,7 +6995,8 @@ var OwneySDK = class {
5733
6995
  );
5734
6996
  const failedAmount = p.planned;
5735
6997
  p.planned = 0n;
5736
- redistributeShare(plans, i, failedAmount);
6998
+ if (submissionEvidence.get(p.agent.id)?.transfer === "failed")
6999
+ redistributeShare(orderedPlans, i, failedAmount);
5737
7000
  }
5738
7001
  }
5739
7002
  if (Object.keys(results).length === 0) {
@@ -5770,12 +7033,19 @@ var OwneySDK = class {
5770
7033
  * @param agentId - Optional. Agent to query. Omit for aggregated balances.
5771
7034
  * @returns {AgentBalance} for a single agent, or {OwneyBalances} with totalBalance and per-agent breakdown
5772
7035
  */
5773
- async getBalances(agentId) {
5774
- const state = this.requireState();
5775
- const chainId = this.requireChainId();
7036
+ async getBalances(agentId, options) {
7037
+ const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
7038
+ const asset = options?.asset;
7039
+ const readBalance = (agent) => this.readAgent(
7040
+ agent,
7041
+ "balances",
7042
+ () => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
7043
+ asset ? { asset } : null
7044
+ );
5776
7045
  if (agentId) {
5777
7046
  const agent = this.getAgent(agentId);
5778
- const result = await this.readAgent(agent, "balances", () => agent.getBalances(state, chainId));
7047
+ const result = await readBalance(agent);
7048
+ assertCurrent();
5779
7049
  return {
5780
7050
  ...result,
5781
7051
  balanceComposition: agent.balanceComposition ?? "tokens-include-positions"
@@ -5786,7 +7056,7 @@ var OwneySDK = class {
5786
7056
  const entries = [...this.getActiveAgents().entries()];
5787
7057
  const balanceResults = await Promise.allSettled(
5788
7058
  entries.map(async ([id, agent]) => {
5789
- const b = await this.readAgent(agent, "balances", () => agent.getBalances(state, chainId));
7059
+ const b = await readBalance(agent);
5790
7060
  return [
5791
7061
  id,
5792
7062
  {
@@ -5796,6 +7066,7 @@ var OwneySDK = class {
5796
7066
  ];
5797
7067
  })
5798
7068
  );
7069
+ assertCurrent();
5799
7070
  let successCount = 0;
5800
7071
  const agentErrors = {};
5801
7072
  const agentRetryAt = {};
@@ -5843,23 +7114,11 @@ var OwneySDK = class {
5843
7114
  const agent = this.getAgent(agentId);
5844
7115
  return this.readAgent(agent, "earnings", () => agent.getEarnings(state, chainId));
5845
7116
  }
5846
- let totalEarnings = 0;
5847
- const results = {};
5848
7117
  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
- })
7118
+ return this.aggregateEarnings(
7119
+ entries,
7120
+ (agent) => this.readAgent(agent, "earnings", () => agent.getEarnings(state, chainId))
5854
7121
  );
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
7122
  }
5864
7123
  /**
5865
7124
  * Return refreshed earnings while letting each agent own its refresh policy.
@@ -5874,18 +7133,42 @@ var OwneySDK = class {
5874
7133
  () => agent.refreshEarnings?.(state, chainId) ?? agent.getEarnings(state, chainId)
5875
7134
  );
5876
7135
  if (agentId) return refreshAgent(this.getAgent(agentId));
5877
- let totalEarnings = 0;
7136
+ return this.aggregateEarnings([...this.getActiveAgents().entries()], refreshAgent);
7137
+ }
7138
+ async aggregateEarnings(entries, read2) {
7139
+ const settled = await Promise.allSettled(entries.map(([, agent]) => read2(agent)));
5878
7140
  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);
7141
+ const agentErrors = {};
7142
+ const agentRetryAt = {};
7143
+ const failures = [];
7144
+ let totalEarnings = 0;
7145
+ for (let i = 0; i < settled.length; i++) {
7146
+ const [id] = entries[i];
7147
+ const result = settled[i];
7148
+ if (result.status === "fulfilled") {
7149
+ agentEarnings[id] = result.value;
7150
+ totalEarnings += Number(result.value.lifetimeEarnings);
7151
+ continue;
7152
+ }
7153
+ const reason = result.reason;
7154
+ failures.push(reason);
7155
+ const retryDelay = rateLimitDelay(reason);
7156
+ if (retryDelay !== void 0) agentRetryAt[id] = Date.now() + retryDelay;
7157
+ agentErrors[id] = reason instanceof Error ? reason.message : String(reason);
7158
+ console.error(`[owney-sdk] Earnings fetch failed for agent "${id}":`, reason);
7159
+ }
7160
+ if (entries.length > 0 && Object.keys(agentEarnings).length === 0) {
7161
+ throw new OwneyError(
7162
+ "EARNINGS_ALL_FAILED",
7163
+ "Failed to fetch earnings for all active agents.",
7164
+ { agentErrors, failures }
7165
+ );
5887
7166
  }
5888
- return { totalEarnings: String(totalEarnings), agentEarnings };
7167
+ return {
7168
+ totalEarnings: String(totalEarnings),
7169
+ agentEarnings,
7170
+ ...Object.keys(agentErrors).length > 0 ? { agentErrors, agentRetryAt } : {}
7171
+ };
5889
7172
  }
5890
7173
  /**
5891
7174
  * Get the weighted APY for the user's account over a time period.
@@ -6051,12 +7334,13 @@ var OwneySDK = class {
6051
7334
  * @returns {OwneyAgentHistory} `data` (up to `limit` entries), `hasMore`, `nextCursor`.
6052
7335
  */
6053
7336
  async getHistory(options) {
6054
- const state = this.requireState();
6055
- const chainId = this.requireChainId();
7337
+ const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
6056
7338
  const { agentId, filters } = options ?? {};
6057
7339
  if (agentId) {
6058
7340
  const agent = this.getAgent(agentId);
6059
- return this.readAgent(agent, "history", () => agent.getHistory(state, chainId, filters), filters);
7341
+ const page = await this.readAgent(agent, "history", () => agent.getHistory(state, chainId, filters), filters);
7342
+ assertCurrent();
7343
+ return page;
6060
7344
  }
6061
7345
  const activeAgents = [...this.getActiveAgents().values()];
6062
7346
  const cursorMap = filters?.cursor ? decodeMultiAgentCursor(filters.cursor) : {};
@@ -6066,20 +7350,35 @@ var OwneySDK = class {
6066
7350
  if (filters?.cursor && agentCursor === void 0) {
6067
7351
  return { agentId: agent.id, page: null };
6068
7352
  }
6069
- const agentFilters = { ...filters, cursor: agentCursor };
6070
- const page = await this.readAgent(
6071
- agent,
6072
- "history",
6073
- () => agent.getHistory(state, chainId, agentFilters),
6074
- agentFilters
6075
- );
6076
- return { agentId: agent.id, page };
7353
+ const agentFilters = { ...filters, cursor: agentCursor || void 0 };
7354
+ try {
7355
+ const page = await this.readAgent(
7356
+ agent,
7357
+ "history",
7358
+ () => agent.getHistory(state, chainId, agentFilters),
7359
+ agentFilters
7360
+ );
7361
+ return { agentId: agent.id, page };
7362
+ } catch (error) {
7363
+ return { agentId: agent.id, page: null, error };
7364
+ }
6077
7365
  })
6078
7366
  );
7367
+ assertCurrent();
7368
+ const agentErrors = {};
7369
+ const agentRetryAt = {};
6079
7370
  const merged = [];
6080
7371
  const nextCursorMap = {};
6081
7372
  let hasMore = false;
6082
- for (const { agentId: id, page } of perAgent) {
7373
+ for (const result of perAgent) {
7374
+ const { agentId: id, page } = result;
7375
+ if ("error" in result) {
7376
+ agentErrors[id] = result.error instanceof Error ? result.error.message : String(result.error);
7377
+ const delay = rateLimitDelay(result.error);
7378
+ if (delay !== void 0) agentRetryAt[id] = Date.now() + delay;
7379
+ nextCursorMap[id] = cursorMap[id] ?? "";
7380
+ hasMore = true;
7381
+ }
6083
7382
  if (!page) continue;
6084
7383
  merged.push(...page.data);
6085
7384
  if (page.hasMore && page.nextCursor) {
@@ -6099,6 +7398,7 @@ var OwneySDK = class {
6099
7398
  });
6100
7399
  return {
6101
7400
  data: merged,
7401
+ ...Object.keys(agentErrors).length ? { agentErrors, agentRetryAt } : {},
6102
7402
  hasMore,
6103
7403
  nextCursor: hasMore ? encodeMultiAgentCursor(nextCursorMap) : void 0
6104
7404
  };
@@ -6159,16 +7459,18 @@ var OwneySDK = class {
6159
7459
  }
6160
7460
  /**
6161
7461
  * 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.
7462
+ * Grants the maximum ERC20 allowance so later deposits do not require another
7463
+ * approval. Resolves after one confirmation so the subsequent deposit attempt
7464
+ * sees the new allowance. Deposit retries pass their captured chain id so a
7465
+ * concurrent activation cannot redirect the approval to another network.
6165
7466
  *
6166
7467
  * @param requiredAmount Raw base-unit amount the pending deposit must cover.
7468
+ * @param expectedChainId Chain captured by the deposit that requested approval.
6167
7469
  * @returns the approval transaction hash.
6168
7470
  */
6169
- async approvePermit2(asset = "WETH", requiredAmount = 0n) {
7471
+ async approvePermit2(asset = "WETH", requiredAmount = 0n, expectedChainId) {
6170
7472
  const state = this.requireState();
6171
- const chainId = this.requireChainId();
7473
+ const chainId = expectedChainId ?? this.requireChainId();
6172
7474
  this.getEligibleAgents(chainId, asset, { excludeDisabled: true });
6173
7475
  const token = sponsoredTokensFor(asset)[chainId];
6174
7476
  if (!token) {
@@ -6178,16 +7480,18 @@ var OwneySDK = class {
6178
7480
  );
6179
7481
  }
6180
7482
  const provider = this.requireConnectedProvider();
6181
- const publicClient = createPublicClient4({
7483
+ const walletChainClient = createPublicClient4({
6182
7484
  chain: VIEM_CHAIN2[chainId],
6183
7485
  transport: custom3(provider)
6184
7486
  });
7487
+ const publicClient = this.getPaidRpcClient(chainId);
6185
7488
  const approvalAmount = permit2ApprovalAmount(requiredAmount);
6186
7489
  const wallet = createWalletClient3({
6187
7490
  account: state.walletAddress,
6188
7491
  chain: VIEM_CHAIN2[chainId],
6189
7492
  transport: custom3(provider)
6190
7493
  });
7494
+ await ensureWalletOnChain(walletChainClient, wallet, chainId);
6191
7495
  const hash = await wallet.writeContract({
6192
7496
  address: token,
6193
7497
  abi: ERC20_ALLOWANCE_ABI,
@@ -6196,14 +7500,53 @@ var OwneySDK = class {
6196
7500
  account: state.walletAddress,
6197
7501
  chain: VIEM_CHAIN2[chainId]
6198
7502
  });
6199
- const receipt = await publicClient.waitForTransactionReceipt({
6200
- hash,
6201
- confirmations: 1
6202
- });
7503
+ const receipt = await withPaidRpcDiagnostics(
7504
+ () => publicClient.waitForTransactionReceipt({
7505
+ hash,
7506
+ confirmations: 1
7507
+ }),
7508
+ chainId,
7509
+ "eth_getTransactionReceipt"
7510
+ );
6203
7511
  if (receipt.status !== "success") {
6204
7512
  throw new Error(`Permit2 approval reverted (tx ${hash})`);
6205
7513
  }
6206
- return hash;
7514
+ let observedAllowance = 0n;
7515
+ let verificationError;
7516
+ for (let attempt = 0; attempt < PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS; attempt += 1) {
7517
+ try {
7518
+ observedAllowance = await readPermit2Allowance(
7519
+ publicClient,
7520
+ token,
7521
+ state.walletAddress,
7522
+ attempt === 0 ? receipt.blockNumber : void 0
7523
+ );
7524
+ verificationError = void 0;
7525
+ if (observedAllowance >= requiredAmount) return hash;
7526
+ } catch (error) {
7527
+ verificationError = error;
7528
+ }
7529
+ if (attempt + 1 < PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS) {
7530
+ await new Promise(
7531
+ (resolve) => setTimeout(resolve, PERMIT2_ALLOWANCE_VERIFY_DELAY_MS)
7532
+ );
7533
+ }
7534
+ }
7535
+ throw new OwneyError(
7536
+ "PERMIT2_APPROVAL_REQUIRED",
7537
+ "Permit2 approval was confirmed, but the required token allowance was not observable.",
7538
+ {
7539
+ approvalConfirmed: true,
7540
+ approvalTxHash: hash,
7541
+ owner: state.walletAddress,
7542
+ token,
7543
+ spender: PERMIT2_ADDRESS,
7544
+ chainId,
7545
+ requiredAmount: requiredAmount.toString(),
7546
+ observedAllowance: observedAllowance.toString(),
7547
+ ...verificationError instanceof Error ? { verificationError: verificationError.message } : {}
7548
+ }
7549
+ );
6207
7550
  }
6208
7551
  // --- Discovery (no wallet required) ---
6209
7552
  /**