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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -125,6 +125,34 @@ function rateLimitDelay(error, now = Date.now()) {
125
125
  }
126
126
 
127
127
  // src/lib/agent-reads.ts
128
+ function rateLimitDiagnostics(error) {
129
+ const seen = /* @__PURE__ */ new Set();
130
+ let details = {};
131
+ function visit(value, depth = 0) {
132
+ if (depth > 6 || !value || typeof value !== "object" || seen.has(value))
133
+ return;
134
+ seen.add(value);
135
+ const record = value;
136
+ for (const key2 of [
137
+ "rpcSource",
138
+ "chainId",
139
+ "rpcMethod",
140
+ "providerRequestId",
141
+ "statusCode"
142
+ ]) {
143
+ const candidate = record[key2];
144
+ if (candidate !== void 0 && details[key2] === void 0)
145
+ details = { ...details, [key2]: candidate };
146
+ }
147
+ for (const key2 of ["details", "cause", "response", "fields", "error"])
148
+ visit(record[key2], depth + 1);
149
+ }
150
+ visit(error);
151
+ return {
152
+ rpcSource: details.rpcSource ?? "agent-api",
153
+ ...details
154
+ };
155
+ }
128
156
  var AgentReads = class {
129
157
  inFlight = /* @__PURE__ */ new Map();
130
158
  cooldowns = /* @__PURE__ */ new Map();
@@ -132,14 +160,15 @@ var AgentReads = class {
132
160
  clearInFlight() {
133
161
  this.inFlight.clear();
134
162
  }
135
- limited(agentId, until) {
163
+ limited(agentId, until, diagnostics) {
136
164
  return new OwneyError(
137
165
  "AGENT_RATE_LIMITED",
138
166
  "Too many requests. Please wait before trying again.",
139
167
  {
140
168
  statusCode: 429,
141
169
  retryAt: until,
142
- retryAfterSeconds: Math.max(0, Math.ceil((until - Date.now()) / 1e3))
170
+ retryAfterSeconds: Math.max(0, Math.ceil((until - Date.now()) / 1e3)),
171
+ ...diagnostics
143
172
  },
144
173
  agentId
145
174
  );
@@ -147,7 +176,9 @@ var AgentReads = class {
147
176
  run(agentId, key2, fetch2) {
148
177
  const cooldown = this.cooldowns.get(agentId);
149
178
  if (cooldown && cooldown.until > Date.now()) {
150
- return Promise.reject(this.limited(agentId, cooldown.until));
179
+ return Promise.reject(
180
+ this.limited(agentId, cooldown.until, cooldown.diagnostics)
181
+ );
151
182
  }
152
183
  const requestKey = JSON.stringify([agentId, key2]);
153
184
  const existing = this.inFlight.get(requestKey);
@@ -168,8 +199,9 @@ var AgentReads = class {
168
199
  Math.min(3e4 * 2 ** (failures - 1), 3e5)
169
200
  );
170
201
  const until = Math.max(previous?.until ?? 0, Date.now() + delay);
171
- this.cooldowns.set(agentId, { until, failures });
172
- throw this.limited(agentId, until);
202
+ const diagnostics = rateLimitDiagnostics(error);
203
+ this.cooldowns.set(agentId, { until, failures, diagnostics });
204
+ throw this.limited(agentId, until, diagnostics);
173
205
  }
174
206
  ).finally(() => {
175
207
  if (this.inFlight.get(requestKey) === promise)
@@ -182,7 +214,7 @@ var AgentReads = class {
182
214
 
183
215
  // src/agents/zyfai/zyfai.agent.ts
184
216
  import { SMART_SESSIONS_VALIDATOR, ZyfaiSDK } from "@zyfai/sdk";
185
- import { createPublicClient, http, parseAbi } from "viem";
217
+ import { parseAbi } from "viem";
186
218
  import { base, arbitrum, mainnet } from "viem/chains";
187
219
 
188
220
  // src/types/config.ts
@@ -1063,15 +1095,148 @@ function protocolsPolicyNeedsUpdate(current, desiredProtocols, desiredAutoSelect
1063
1095
  return !protocolListsEqual(current.protocols, desiredProtocols);
1064
1096
  }
1065
1097
 
1098
+ // src/lib/paid-rpc.ts
1099
+ import {
1100
+ createPublicClient,
1101
+ http
1102
+ } from "viem";
1103
+ var PAID_RPC_RETRY_COUNT = 3;
1104
+ var PAID_RPC_RETRY_DELAY_MS = 1e3;
1105
+ function configuredRpcProxyBaseUrl(baseUrl) {
1106
+ const configured = (baseUrl ?? "")?.trim();
1107
+ if (!configured) {
1108
+ throw new Error(
1109
+ "OWNEY_ROUTING_API_BASE_URL is required for RPC proxy requests unless routingApiBaseUrl or all rpcUrls are configured."
1110
+ );
1111
+ }
1112
+ return configured;
1113
+ }
1114
+ function rpcProxyUrl(chainId, apiKey, baseUrl) {
1115
+ const url = new URL(
1116
+ `${configuredRpcProxyBaseUrl(baseUrl).replace(/\/$/, "")}/api/v1/rpc/${chainId}`
1117
+ );
1118
+ if (apiKey) url.searchParams.set("apiKey", apiKey);
1119
+ return url.toString();
1120
+ }
1121
+ function resolveRpcUrl(rpcUrls, chainId, apiKey = "", baseUrl) {
1122
+ const url = rpcUrls?.[chainId]?.trim();
1123
+ if (url) return url;
1124
+ return rpcProxyUrl(chainId, apiKey, baseUrl);
1125
+ }
1126
+ function resolveRpcUrls(rpcUrls, apiKey = "", baseUrl) {
1127
+ return {
1128
+ 1: resolveRpcUrl(rpcUrls, 1, apiKey, baseUrl),
1129
+ 8453: resolveRpcUrl(rpcUrls, 8453, apiKey, baseUrl),
1130
+ 42161: resolveRpcUrl(rpcUrls, 42161, apiKey, baseUrl)
1131
+ };
1132
+ }
1133
+ function createPaidRpcClient(chain, rpcUrls) {
1134
+ const url = resolveRpcUrl(
1135
+ rpcUrls,
1136
+ chain.id
1137
+ );
1138
+ return createPublicClient({
1139
+ chain,
1140
+ transport: http(url, {
1141
+ retryCount: PAID_RPC_RETRY_COUNT,
1142
+ retryDelay: PAID_RPC_RETRY_DELAY_MS
1143
+ })
1144
+ });
1145
+ }
1146
+ function headerValue(error, name) {
1147
+ const seen = /* @__PURE__ */ new Set();
1148
+ let value;
1149
+ function visit(candidate, depth = 0) {
1150
+ if (value || depth > 6 || !candidate || typeof candidate !== "object")
1151
+ return;
1152
+ if (seen.has(candidate)) return;
1153
+ seen.add(candidate);
1154
+ const record = candidate;
1155
+ const headers = record.headers;
1156
+ let found;
1157
+ if (typeof headers?.get === "function") {
1158
+ found = headers.get(name);
1159
+ } else {
1160
+ const headerRecord = headers;
1161
+ found = headerRecord?.[name] ?? headerRecord?.[name.toLowerCase()];
1162
+ }
1163
+ if (typeof found === "string" && found) {
1164
+ value = found;
1165
+ return;
1166
+ }
1167
+ for (const key2 of ["cause", "details", "response", "error"])
1168
+ visit(record[key2], depth + 1);
1169
+ }
1170
+ visit(error);
1171
+ return value;
1172
+ }
1173
+ function statusCode(error) {
1174
+ const seen = /* @__PURE__ */ new Set();
1175
+ let status;
1176
+ function visit(candidate, depth = 0) {
1177
+ if (status || depth > 6 || !candidate || typeof candidate !== "object")
1178
+ return;
1179
+ if (seen.has(candidate)) return;
1180
+ seen.add(candidate);
1181
+ const record = candidate;
1182
+ for (const key2 of ["status", "statusCode"]) {
1183
+ const parsed = Number(record[key2]);
1184
+ if (Number.isInteger(parsed) && parsed >= 100 && parsed <= 599) {
1185
+ status = parsed;
1186
+ return;
1187
+ }
1188
+ }
1189
+ for (const key2 of ["cause", "details", "response", "error"])
1190
+ visit(record[key2], depth + 1);
1191
+ }
1192
+ visit(error);
1193
+ return status;
1194
+ }
1195
+ function paidRpcError(error, chainId, rpcMethod, agentId) {
1196
+ const delay = rateLimitDelay(error);
1197
+ const providerRequestId = headerValue(error, "x-alchemy-request-id") ?? headerValue(error, "x-request-id");
1198
+ if (delay === void 0) {
1199
+ return new OwneyError(
1200
+ "AGENT_API_ERROR",
1201
+ "The blockchain RPC request failed.",
1202
+ {
1203
+ rpcSource: "paid-rpc",
1204
+ chainId,
1205
+ rpcMethod,
1206
+ ...statusCode(error) ? { statusCode: statusCode(error) } : {},
1207
+ ...providerRequestId ? { providerRequestId } : {}
1208
+ },
1209
+ agentId
1210
+ );
1211
+ }
1212
+ const retryAt = Date.now() + delay;
1213
+ return new OwneyError(
1214
+ "AGENT_RATE_LIMITED",
1215
+ "The blockchain RPC is rate limited. Please wait before trying again.",
1216
+ {
1217
+ rpcSource: "paid-rpc",
1218
+ chainId,
1219
+ rpcMethod,
1220
+ statusCode: 429,
1221
+ retryAt,
1222
+ retryAfterSeconds: Math.max(0, Math.ceil(delay / 1e3)),
1223
+ ...providerRequestId ? { providerRequestId } : {}
1224
+ },
1225
+ agentId
1226
+ );
1227
+ }
1228
+ async function withPaidRpcDiagnostics(operation, chainId, rpcMethod, agentId) {
1229
+ try {
1230
+ return await operation();
1231
+ } catch (error) {
1232
+ throw paidRpcError(error, chainId, rpcMethod, agentId);
1233
+ }
1234
+ }
1235
+
1066
1236
  // src/agents/zyfai/zyfai.agent.ts
1067
1237
  var ERC7579_IS_MODULE_INSTALLED_ABI = parseAbi([
1068
1238
  "function isModuleInstalled(uint256 moduleTypeId, address module, bytes additionalContext) view returns (bool)"
1069
1239
  ]);
1070
- var DEFAULT_ZYFAI_RPC_URLS = {
1071
- 8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1072
- 42161: "https://arb-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1073
- 1: "https://eth-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V"
1074
- };
1075
1240
  var WETH_ADDRESS_BY_CHAIN = {
1076
1241
  8453: "0x4200000000000000000000000000000000000006",
1077
1242
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
@@ -1137,7 +1302,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1137
1302
  earningsSnapshot = null;
1138
1303
  earningsGeneration = 0;
1139
1304
  constructor(apiKey, rpcUrls, referralSource) {
1140
- this.rpcUrls = rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS;
1305
+ this.rpcUrls = resolveRpcUrls(rpcUrls);
1141
1306
  this.sdk = new ZyfaiSDK({
1142
1307
  apiKey,
1143
1308
  rpcUrls: this.rpcUrls,
@@ -1151,10 +1316,10 @@ var ZyfaiAgent = class _ZyfaiAgent {
1151
1316
  getPublicClient(chainId) {
1152
1317
  const cached = this.publicClients.get(chainId);
1153
1318
  if (cached) return cached;
1154
- const client = createPublicClient({
1155
- chain: VIEM_CHAIN[chainId],
1156
- transport: http(this.rpcUrls[chainId])
1157
- });
1319
+ const client = createPaidRpcClient(
1320
+ VIEM_CHAIN[chainId],
1321
+ this.rpcUrls
1322
+ );
1158
1323
  this.publicClients.set(chainId, client);
1159
1324
  return client;
1160
1325
  }
@@ -3191,6 +3356,9 @@ var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3191
3356
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3192
3357
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3193
3358
  var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3359
+ var YIELDSEEKER_AGENT_LIST_CACHE_MS = 6e4;
3360
+ var YIELDSEEKER_PORTFOLIO_CACHE_MS = 3e4;
3361
+ var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
3194
3362
  function generateYieldseekerUsername() {
3195
3363
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
3196
3364
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -3234,6 +3402,7 @@ function query(params) {
3234
3402
  var YieldseekerAgent = class {
3235
3403
  id = "yieldseeker";
3236
3404
  balanceComposition = "tokens-plus-positions";
3405
+ withdrawalRequiresWalletApproval = true;
3237
3406
  supportedChainIds = [8453];
3238
3407
  supportedAssets = [
3239
3408
  {
@@ -3247,11 +3416,17 @@ var YieldseekerAgent = class {
3247
3416
  ];
3248
3417
  api;
3249
3418
  auth;
3419
+ rpcUrls;
3420
+ receiptClient;
3250
3421
  transactionExecutor;
3251
3422
  unwindReceiptWaiter;
3252
3423
  agentContexts = /* @__PURE__ */ new Map();
3253
3424
  users = /* @__PURE__ */ new Map();
3254
3425
  pendingAgents = /* @__PURE__ */ new Map();
3426
+ pendingWalletContexts = /* @__PURE__ */ new Map();
3427
+ readCache = /* @__PURE__ */ new Map();
3428
+ pendingReads = /* @__PURE__ */ new Map();
3429
+ readGeneration = 0;
3255
3430
  yieldOptions = /* @__PURE__ */ new Map();
3256
3431
  pendingYieldOptions = /* @__PURE__ */ new Map();
3257
3432
  constructor(owneyApiKey, options = {}) {
@@ -3261,10 +3436,16 @@ var YieldseekerAgent = class {
3261
3436
  options.fetchFn
3262
3437
  );
3263
3438
  this.auth = new YieldseekerAuth(options.auth);
3439
+ this.rpcUrls = options.rpcUrls;
3264
3440
  this.transactionExecutor = options.transactionExecutor;
3265
3441
  this.unwindReceiptWaiter = options.unwindReceiptWaiter;
3266
3442
  }
3443
+ getReceiptClient() {
3444
+ this.receiptClient ??= createPaidRpcClient(base3, this.rpcUrls);
3445
+ return this.receiptClient;
3446
+ }
3267
3447
  async disconnect() {
3448
+ this.readGeneration += 1;
3268
3449
  this.auth.clear();
3269
3450
  for (const key2 of this.users.keys()) {
3270
3451
  const [walletAddress, chainId] = key2.split(":");
@@ -3273,6 +3454,9 @@ var YieldseekerAgent = class {
3273
3454
  this.users.clear();
3274
3455
  this.agentContexts.clear();
3275
3456
  this.pendingAgents.clear();
3457
+ this.pendingWalletContexts.clear();
3458
+ this.readCache.clear();
3459
+ this.pendingReads.clear();
3276
3460
  }
3277
3461
  async activateAgent(state, chainId, asset) {
3278
3462
  this.assertChain(chainId);
@@ -3535,6 +3719,86 @@ var YieldseekerAgent = class {
3535
3719
  contextKey(state, chainId, asset) {
3536
3720
  return `${this.userKey(state, chainId)}:${asset}`;
3537
3721
  }
3722
+ cachedRead(key2, ttlMs, read2) {
3723
+ const cached = this.readCache.get(key2);
3724
+ if (cached && cached.expiresAt > Date.now()) {
3725
+ return Promise.resolve(cached.value);
3726
+ }
3727
+ const pending = this.pendingReads.get(key2);
3728
+ if (pending) return pending;
3729
+ const generation = this.readGeneration;
3730
+ const request = Promise.resolve().then(read2).then((value) => {
3731
+ if (this.readGeneration === generation) {
3732
+ this.readCache.set(key2, {
3733
+ expiresAt: Date.now() + ttlMs,
3734
+ value
3735
+ });
3736
+ }
3737
+ return value;
3738
+ }).finally(() => {
3739
+ if (this.pendingReads.get(key2) === request) {
3740
+ this.pendingReads.delete(key2);
3741
+ }
3742
+ });
3743
+ this.pendingReads.set(key2, request);
3744
+ return request;
3745
+ }
3746
+ agentListKey(state, chainId) {
3747
+ return `agents:${this.userKey(state, chainId)}`;
3748
+ }
3749
+ async listAgents(state, chainId, user) {
3750
+ const response = await this.cachedRead(
3751
+ this.agentListKey(state, chainId),
3752
+ YIELDSEEKER_AGENT_LIST_CACHE_MS,
3753
+ async () => {
3754
+ const response2 = await this.walletRequest(
3755
+ state,
3756
+ chainId,
3757
+ `/users/${user.userId}/agents`
3758
+ );
3759
+ if (!Array.isArray(response2?.agents)) {
3760
+ throw this.invalidResponse("agent list");
3761
+ }
3762
+ return response2;
3763
+ }
3764
+ );
3765
+ return response.agents;
3766
+ }
3767
+ async contextForAgent(state, chainId, user, agent, asset) {
3768
+ const key2 = this.contextKey(state, chainId, asset);
3769
+ const cached = this.agentContexts.get(key2);
3770
+ if (cached?.agent.agentId === agent.agentId) return cached;
3771
+ const pending = this.pendingWalletContexts.get(key2);
3772
+ if (pending) return pending;
3773
+ const generation = this.readGeneration;
3774
+ const request = this.walletRequest(
3775
+ state,
3776
+ chainId,
3777
+ `/users/${user.userId}/agents/${agent.agentId}/wallet`
3778
+ ).then((walletResponse) => {
3779
+ if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3780
+ throw this.invalidResponse("agent wallet");
3781
+ }
3782
+ const context = {
3783
+ user,
3784
+ agent,
3785
+ wallet: walletResponse.agentWallet,
3786
+ asset
3787
+ };
3788
+ if (this.readGeneration === generation) {
3789
+ this.agentContexts.set(key2, context);
3790
+ }
3791
+ return context;
3792
+ });
3793
+ this.pendingWalletContexts.set(key2, request);
3794
+ try {
3795
+ return await request;
3796
+ } finally {
3797
+ if (this.pendingWalletContexts.get(key2) === request) {
3798
+ this.pendingWalletContexts.delete(key2);
3799
+ }
3800
+ }
3801
+ }
3538
3802
  async resolveUser(state, chainId) {
3539
3803
  const key2 = this.userKey(state, chainId);
3540
3804
  const inMemory = this.users.get(key2);
@@ -3631,16 +3895,9 @@ var YieldseekerAgent = class {
3631
3895
  }
3632
3896
  async resolveAgent(state, chainId, asset, createIfMissing) {
3633
3897
  const user = await this.resolveUser(state, chainId);
3634
- const response = await this.walletRequest(
3635
- state,
3636
- chainId,
3637
- `/users/${user.userId}/agents`
3638
- );
3639
- if (!Array.isArray(response?.agents)) {
3640
- throw this.invalidResponse("agent list");
3641
- }
3898
+ const agents = await this.listAgents(state, chainId, user);
3642
3899
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3643
- let agent = response.agents.find(
3900
+ let agent = agents.find(
3644
3901
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
3645
3902
  );
3646
3903
  if (!agent && createIfMissing) {
@@ -3661,83 +3918,91 @@ var YieldseekerAgent = class {
3661
3918
  }
3662
3919
  );
3663
3920
  agent = created?.agent;
3921
+ if (agent) {
3922
+ this.readCache.set(this.agentListKey(state, chainId), {
3923
+ expiresAt: Date.now() + YIELDSEEKER_AGENT_LIST_CACHE_MS,
3924
+ value: {
3925
+ agents: [...agents, agent]
3926
+ }
3927
+ });
3928
+ }
3664
3929
  }
3665
3930
  if (!agent) return null;
3666
3931
  this.assertAgent(agent);
3667
- const walletResponse = await this.walletRequest(
3668
- state,
3669
- chainId,
3670
- `/users/${user.userId}/agents/${agent.agentId}/wallet`
3671
- );
3672
- if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3673
- throw this.invalidResponse("agent wallet");
3674
- }
3675
- return { user, agent, wallet: walletResponse.agentWallet, asset };
3932
+ return this.contextForAgent(state, chainId, user, agent, asset);
3676
3933
  }
3677
3934
  async loadPortfolio(state, chainId, options) {
3678
3935
  const user = await this.resolveUser(state, chainId);
3679
- const response = await this.walletRequest(
3680
- state,
3681
- chainId,
3682
- `/users/${user.userId}/agents`
3683
- );
3684
- if (!Array.isArray(response?.agents)) {
3685
- throw this.invalidResponse("agent list");
3686
- }
3936
+ const agents = await this.listAgents(state, chainId, user);
3687
3937
  const contexts = [];
3688
- for (const agent of response.agents) {
3938
+ for (const agent of agents) {
3689
3939
  const asset = this.assetForAgent(agent);
3690
3940
  if (!this.isOwneyAgent(agent) || !asset || agent.chainId !== chainId || agent.type !== "vault" || options.asset && options.asset !== asset) {
3691
3941
  continue;
3692
3942
  }
3693
3943
  this.assertAgent(agent);
3694
- const walletResponse = await this.walletRequest(
3695
- state,
3696
- chainId,
3697
- `/users/${user.userId}/agents/${agent.agentId}/wallet`
3698
- );
3699
- if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3700
- throw this.invalidResponse("agent wallet");
3701
- }
3702
- const context = {
3703
- user,
3704
- agent,
3705
- wallet: walletResponse.agentWallet,
3706
- asset
3707
- };
3708
- this.agentContexts.set(this.contextKey(state, chainId, asset), context);
3709
- contexts.push(context);
3944
+ contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
3710
3945
  }
3946
+ const resolvedContexts = await Promise.all(contexts);
3711
3947
  return Promise.all(
3712
- contexts.map(
3948
+ resolvedContexts.map(
3713
3949
  (context) => this.loadPortfolioContext(state, chainId, context, options)
3714
3950
  )
3715
3951
  );
3716
3952
  }
3717
3953
  async loadPortfolioContext(state, chainId, context, options = {}) {
3954
+ const contextKey = this.contextKey(state, chainId, context.asset);
3718
3955
  const [snapshot, positions, historic, actions] = await Promise.all([
3719
- this.walletRequest(
3720
- state,
3721
- chainId,
3722
- `${this.agentPath(context, "snapshot")}${query({
3723
- shouldOnlyUseRecentValue: true,
3724
- shouldAllowStaleOnError: true
3725
- })}`
3956
+ this.cachedRead(
3957
+ `snapshot:${contextKey}`,
3958
+ YIELDSEEKER_PORTFOLIO_CACHE_MS,
3959
+ async () => {
3960
+ const response = await this.walletRequest(
3961
+ state,
3962
+ chainId,
3963
+ `${this.agentPath(context, "snapshot")}${query({
3964
+ shouldOnlyUseRecentValue: true,
3965
+ shouldAllowStaleOnError: true
3966
+ })}`
3967
+ );
3968
+ if (!response?.agentSnapshot) {
3969
+ throw this.invalidResponse("agent snapshot");
3970
+ }
3971
+ return response;
3972
+ }
3726
3973
  ),
3727
- this.walletRequest(
3728
- state,
3729
- chainId,
3730
- this.agentPath(context, "yield-positions")
3974
+ this.cachedRead(
3975
+ `positions:${contextKey}`,
3976
+ YIELDSEEKER_PORTFOLIO_CACHE_MS,
3977
+ async () => {
3978
+ const response = await this.walletRequest(
3979
+ state,
3980
+ chainId,
3981
+ this.agentPath(context, "yield-positions")
3982
+ );
3983
+ if (!Array.isArray(response?.yieldPositions)) {
3984
+ throw this.invalidResponse("yield positions");
3985
+ }
3986
+ return response;
3987
+ }
3731
3988
  ),
3732
- options.historic ? this.walletRequest(
3733
- state,
3734
- chainId,
3735
- this.agentPath(context, "wallet/historic-position")
3989
+ options.historic ? this.cachedRead(
3990
+ `historic:${contextKey}`,
3991
+ YIELDSEEKER_ACTIVITY_CACHE_MS,
3992
+ () => this.walletRequest(
3993
+ state,
3994
+ chainId,
3995
+ this.agentPath(context, "wallet/historic-position")
3996
+ )
3736
3997
  ) : Promise.resolve(void 0),
3737
- options.actions ? this.walletRequest(
3738
- state,
3739
- chainId,
3740
- this.agentPath(context, "actions")
3998
+ options.actions ? this.cachedRead(
3999
+ `actions:${contextKey}`,
4000
+ YIELDSEEKER_ACTIVITY_CACHE_MS,
4001
+ () => this.walletRequest(
4002
+ state,
4003
+ chainId,
4004
+ this.agentPath(context, "actions")
4005
+ )
3741
4006
  ) : Promise.resolve(void 0)
3742
4007
  ]);
3743
4008
  if (!snapshot?.agentSnapshot) {
@@ -3771,6 +4036,13 @@ var YieldseekerAgent = class {
3771
4036
  context.wallet = deployed.agentWallet;
3772
4037
  }
3773
4038
  async refreshSnapshotAfterMovement(state, chainId, context, movement) {
4039
+ const contextKey = this.contextKey(state, chainId, context.asset);
4040
+ const invalidatePortfolio = () => {
4041
+ for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4042
+ this.readCache.delete(`${kind}:${contextKey}`);
4043
+ }
4044
+ };
4045
+ invalidatePortfolio();
3774
4046
  try {
3775
4047
  const response = await this.walletRequest(
3776
4048
  state,
@@ -3787,6 +4059,8 @@ var YieldseekerAgent = class {
3787
4059
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
3788
4060
  error
3789
4061
  );
4062
+ } finally {
4063
+ invalidatePortfolio();
3790
4064
  }
3791
4065
  }
3792
4066
  agentPath(context, suffix) {
@@ -3837,6 +4111,7 @@ var YieldseekerAgent = class {
3837
4111
  code,
3838
4112
  `Yieldseeker request failed: ${error.providerCode}.`,
3839
4113
  {
4114
+ rpcSource: "agent-api",
3840
4115
  statusCode: error.status,
3841
4116
  providerCode: error.providerCode,
3842
4117
  ...error.responseFields ? { fields: error.responseFields } : {}
@@ -3855,12 +4130,12 @@ var YieldseekerAgent = class {
3855
4130
  chain: base3,
3856
4131
  transport: custom2(state.provider)
3857
4132
  });
3858
- const publicClient = createPublicClient3({
4133
+ const walletChainClient = createPublicClient3({
3859
4134
  chain: base3,
3860
4135
  transport: custom2(state.provider)
3861
4136
  });
3862
4137
  await ensureWalletOnChain(
3863
- publicClient,
4138
+ walletChainClient,
3864
4139
  walletClient,
3865
4140
  8453
3866
4141
  );
@@ -3871,10 +4146,15 @@ var YieldseekerAgent = class {
3871
4146
  data: transaction.data,
3872
4147
  value: BigInt(transaction.value)
3873
4148
  });
3874
- const receipt = await publicClient.waitForTransactionReceipt({
3875
- hash,
3876
- confirmations: 1
3877
- });
4149
+ const receipt = await withPaidRpcDiagnostics(
4150
+ () => this.getReceiptClient().waitForTransactionReceipt({
4151
+ hash,
4152
+ confirmations: 1
4153
+ }),
4154
+ 8453,
4155
+ "eth_getTransactionReceipt",
4156
+ this.id
4157
+ );
3878
4158
  if (receipt.status !== "success") {
3879
4159
  throw new OwneyError(
3880
4160
  "AGENT_TRANSACTION_REVERTED",
@@ -3890,14 +4170,15 @@ var YieldseekerAgent = class {
3890
4170
  await this.unwindReceiptWaiter(state, chainId, transactionHash);
3891
4171
  return;
3892
4172
  }
3893
- const publicClient = createPublicClient3({
3894
- chain: base3,
3895
- transport: custom2(state.provider)
3896
- });
3897
- const receipt = await publicClient.waitForTransactionReceipt({
3898
- hash: transactionHash,
3899
- confirmations: 1
3900
- });
4173
+ const receipt = await withPaidRpcDiagnostics(
4174
+ () => this.getReceiptClient().waitForTransactionReceipt({
4175
+ hash: transactionHash,
4176
+ confirmations: 1
4177
+ }),
4178
+ 8453,
4179
+ "eth_getTransactionReceipt",
4180
+ this.id
4181
+ );
3901
4182
  if (receipt.status !== "success") {
3902
4183
  throw new OwneyError(
3903
4184
  "AGENT_TRANSACTION_REVERTED",
@@ -4269,6 +4550,41 @@ import {
4269
4550
  // src/lib/permit2-batch.ts
4270
4551
  import { parseAbi as parseAbi2, hashStruct } from "viem";
4271
4552
  var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
4553
+ var BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
4554
+ var MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
4555
+ var ERC2612_PERMIT_TYPES = {
4556
+ Permit: [
4557
+ { name: "owner", type: "address" },
4558
+ { name: "spender", type: "address" },
4559
+ { name: "value", type: "uint256" },
4560
+ { name: "nonce", type: "uint256" },
4561
+ { name: "deadline", type: "uint256" }
4562
+ ]
4563
+ };
4564
+ var ERC2612_READ_ABI = parseAbi2([
4565
+ "function name() view returns (string)",
4566
+ "function version() view returns (string)",
4567
+ "function nonces(address owner) view returns (uint256)"
4568
+ ]);
4569
+ function erc2612TypedData(input) {
4570
+ return {
4571
+ domain: {
4572
+ name: input.permit.tokenName,
4573
+ version: input.permit.tokenVersion,
4574
+ chainId: input.chainId,
4575
+ verifyingContract: input.token
4576
+ },
4577
+ types: ERC2612_PERMIT_TYPES,
4578
+ primaryType: "Permit",
4579
+ message: {
4580
+ owner: input.owner,
4581
+ spender: BATCH_PERMIT2_ADDRESS,
4582
+ value: BigInt(input.permit.value),
4583
+ nonce: BigInt(input.permit.nonce),
4584
+ deadline: BigInt(input.permit.deadline)
4585
+ }
4586
+ };
4587
+ }
4272
4588
  var PERMIT_BATCH_TYPES = {
4273
4589
  PermitBatchWitnessTransferFrom: [
4274
4590
  { name: "permitted", type: "TokenPermissions[]" },
@@ -4420,28 +4736,67 @@ async function execute(i, key2, plan) {
4420
4736
  "DEPOSIT_INSUFFICIENT_BALANCE",
4421
4737
  "Insufficient token balance for this deposit."
4422
4738
  );
4423
- if (allowance < total)
4739
+ if (allowance < total && (i.chainId !== 8453 || !isAddressEqual(i.token, BASE_USDC_ADDRESS)))
4424
4740
  throw new OwneyError(
4425
4741
  "PERMIT2_APPROVAL_REQUIRED",
4426
4742
  "token deposits need a one-time Permit2 approval."
4427
4743
  );
4428
- const relayer = await getSponsorRelayerAddress({
4744
+ const now = (await i.pub.getBlock()).timestamp;
4745
+ let erc2612Permit;
4746
+ if (allowance < total) {
4747
+ const [tokenName, tokenVersion, permitNonce] = await Promise.all([
4748
+ i.pub.readContract({
4749
+ address: i.token,
4750
+ abi: ERC2612_READ_ABI,
4751
+ functionName: "name"
4752
+ }),
4753
+ i.pub.readContract({
4754
+ address: i.token,
4755
+ abi: ERC2612_READ_ABI,
4756
+ functionName: "version"
4757
+ }),
4758
+ i.pub.readContract({
4759
+ address: i.token,
4760
+ abi: ERC2612_READ_ABI,
4761
+ functionName: "nonces",
4762
+ args: [i.owner]
4763
+ })
4764
+ ]);
4765
+ const unsignedPermit = {
4766
+ value: MAX_UINT256.toString(),
4767
+ nonce: permitNonce.toString(),
4768
+ deadline: (now + 900n).toString(),
4769
+ tokenName,
4770
+ tokenVersion
4771
+ };
4772
+ const signature2 = await i.wallet.signTypedData({
4773
+ account: i.owner,
4774
+ ...erc2612TypedData({
4775
+ chainId: 8453,
4776
+ token: i.token,
4777
+ owner: i.owner,
4778
+ permit: unsignedPermit
4779
+ })
4780
+ });
4781
+ erc2612Permit = { ...unsignedPermit, signature: signature2 };
4782
+ }
4783
+ const spender = erc2612Permit ? MULTICALL3_ADDRESS : await getSponsorRelayerAddress({
4429
4784
  apiKey: i.apiKey,
4430
4785
  baseUrl: i.baseUrl,
4431
4786
  chainId: i.chainId
4432
4787
  });
4433
- const now = (await i.pub.getBlock()).timestamp;
4434
4788
  const unsigned = {
4435
4789
  chainId: i.chainId,
4436
4790
  token: i.token,
4437
4791
  from: i.owner,
4438
4792
  transfers: i.transfers,
4439
4793
  nonce: randomPermit2Nonce().toString(),
4440
- deadline: (now + 900n).toString()
4794
+ deadline: (now + 900n).toString(),
4795
+ ...erc2612Permit ? { erc2612Permit } : {}
4441
4796
  };
4442
4797
  const signature = await i.wallet.signTypedData({
4443
4798
  account: i.owner,
4444
- ...batchTypedData(unsigned, relayer)
4799
+ ...batchTypedData(unsigned, spender)
4445
4800
  });
4446
4801
  i.onApproved?.();
4447
4802
  return send({ ...unsigned, signature });
@@ -4461,8 +4816,8 @@ function makeSponsoredTokenCallback(deps) {
4461
4816
  "CHAIN_UNSUPPORTED",
4462
4817
  `No sponsored token configured for chain ${chainId}`
4463
4818
  );
4464
- const pub = deps.getPublicClient(chainId), wallet = deps.getWalletClient(chainId);
4465
- await ensureWalletOnChain(pub, wallet, chainId);
4819
+ const pub = deps.getPublicClient(chainId), walletChain = deps.getWalletChainClient?.(chainId) ?? pub, wallet = deps.getWalletClient(chainId);
4820
+ await ensureWalletOnChain(walletChain, wallet, chainId);
4466
4821
  return sponsorTokenBatch({
4467
4822
  apiKey: deps.apiKey,
4468
4823
  baseUrl: deps.baseUrl,
@@ -4764,6 +5119,7 @@ var OwneySDK = class {
4764
5119
  // leave every user's agent profile alone.
4765
5120
  orgAgentConfig;
4766
5121
  orgAgentConfigPromise = null;
5122
+ rpcUrls;
4767
5123
  zyfaiRpcUrls;
4768
5124
  yieldseekerApiBaseUrl;
4769
5125
  yieldseekerSiweOrigin;
@@ -4788,7 +5144,12 @@ var OwneySDK = class {
4788
5144
  constructor(config) {
4789
5145
  this.apiKey = config.apiKey;
4790
5146
  if (config.debug) setOwneyDebug(true);
4791
- this.zyfaiRpcUrls = config.zyfaiRpcUrls;
5147
+ this.rpcUrls = resolveRpcUrls(
5148
+ config.rpcUrls,
5149
+ config.apiKey,
5150
+ config.routingApiBaseUrl
5151
+ );
5152
+ this.zyfaiRpcUrls = config.rpcUrls ? void 0 : config.zyfaiRpcUrls;
4792
5153
  this.yieldseekerApiBaseUrl = config.yieldseekerApiBaseUrl;
4793
5154
  this.yieldseekerSiweOrigin = config.yieldseekerSiweOrigin;
4794
5155
  this.routingApiBaseUrl = config.routingApiBaseUrl;
@@ -4878,6 +5239,9 @@ var OwneySDK = class {
4878
5239
  }
4879
5240
  return this.state.provider;
4880
5241
  }
5242
+ getPaidRpcClient(chainId) {
5243
+ return createPaidRpcClient(VIEM_CHAIN2[chainId], this.rpcUrls);
5244
+ }
4881
5245
  /** Builds the default USDC batch callback for the connected wallet. */
4882
5246
  getDefaultSponsoredCallback(onApproved) {
4883
5247
  if (!onApproved && this.cachedSponsoredCallback)
@@ -4893,7 +5257,8 @@ var OwneySDK = class {
4893
5257
  // Casts work around viem's chain-narrowed Client vs the generic
4894
5258
  // PublicClient/WalletClient param types — structurally identical at
4895
5259
  // runtime, but the two share a name TS treats as unrelated.
4896
- getPublicClient: (cid) => createPublicClient4({
5260
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5261
+ getWalletChainClient: (cid) => createPublicClient4({
4897
5262
  chain: VIEM_CHAIN2[cid],
4898
5263
  transport: custom3(provider)
4899
5264
  }),
@@ -4943,7 +5308,8 @@ var OwneySDK = class {
4943
5308
  // Casts work around viem's chain-narrowed Client vs the generic
4944
5309
  // PublicClient/WalletClient param types — structurally identical at
4945
5310
  // runtime, but the two share a name TS treats as unrelated.
4946
- getPublicClient: (cid) => createPublicClient4({
5311
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5312
+ getWalletChainClient: (cid) => createPublicClient4({
4947
5313
  chain: VIEM_CHAIN2[cid],
4948
5314
  transport: custom3(provider)
4949
5315
  }),
@@ -5056,12 +5422,21 @@ var OwneySDK = class {
5056
5422
  createAgent(agentId, key2) {
5057
5423
  if (agentId === "zyfai") {
5058
5424
  if (!key2) return null;
5059
- return new ZyfaiAgent(key2, this.zyfaiRpcUrls, this.referralSource);
5425
+ return new ZyfaiAgent(
5426
+ key2,
5427
+ this.zyfaiRpcUrls ? resolveRpcUrls(
5428
+ this.zyfaiRpcUrls,
5429
+ this.apiKey,
5430
+ this.routingApiBaseUrl
5431
+ ) : this.rpcUrls,
5432
+ this.referralSource
5433
+ );
5060
5434
  }
5061
5435
  if (agentId === "yieldseeker") {
5062
5436
  return new YieldseekerAgent(this.apiKey, {
5063
5437
  auth: { origin: this.yieldseekerSiweOrigin },
5064
- baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl)
5438
+ baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl),
5439
+ rpcUrls: this.rpcUrls
5065
5440
  });
5066
5441
  }
5067
5442
  return null;
@@ -5397,10 +5772,12 @@ var OwneySDK = class {
5397
5772
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
5398
5773
  * two independent auto-recovery mechanisms:
5399
5774
  *
5400
- * 1. Missing Permit2 allowance: when the app did not supply its own
5401
- * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
5402
- * token deposit, this is the wallet's first Permit2 deposit for that token. We send
5403
- * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
5775
+ * 1. Missing Permit2 allowance outside the atomic Base-USDC path: when the
5776
+ * app did not supply its own callback and the attempt fails with
5777
+ * `PERMIT2_APPROVAL_REQUIRED`, this is the wallet's first Permit2 deposit
5778
+ * for that token. Base USDC bundles a gasless ERC-2612 approval inside
5779
+ * its sponsored deposit and never reaches this branch. Other tokens send
5780
+ * the one-time user-paid Permit2 approval via `approvePermit2()` and
5404
5781
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
5405
5782
  * per call so a wallet/agent that keeps reporting the allowance as
5406
5783
  * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
@@ -5591,6 +5968,32 @@ var OwneySDK = class {
5591
5968
  }
5592
5969
  return eligible;
5593
5970
  }
5971
+ /**
5972
+ * Run owner-approved withdrawals before relayer-only withdrawals. Wallet
5973
+ * approval is the only point at which the user can cancel the aggregate
5974
+ * operation, so no relayer leg should commit before it has completed.
5975
+ */
5976
+ orderAgentsForWithdrawal(agents) {
5977
+ return agents.map((agent, index) => ({ agent, index })).sort((left, right) => {
5978
+ const approvalOrder = Number(Boolean(right.agent.withdrawalRequiresWalletApproval)) - Number(Boolean(left.agent.withdrawalRequiresWalletApproval));
5979
+ return approvalOrder || left.index - right.index;
5980
+ }).map(({ agent }) => agent);
5981
+ }
5982
+ isUserRejectedWithdrawal(error) {
5983
+ let current = error;
5984
+ const seen = /* @__PURE__ */ new Set();
5985
+ while (current && typeof current === "object" && !seen.has(current)) {
5986
+ seen.add(current);
5987
+ const candidate = current;
5988
+ if (candidate.code === 4001 || candidate.code === "4001") return true;
5989
+ const message = [candidate.message, candidate.shortMessage].filter((value) => typeof value === "string").join(" ");
5990
+ if (/user (?:rejected|denied)|rejected by user/i.test(message)) {
5991
+ return true;
5992
+ }
5993
+ current = candidate.cause;
5994
+ }
5995
+ return false;
5996
+ }
5594
5997
  // --- Fund operations ---
5595
5998
  /**
5596
5999
  * Withdraw funds from a specific agent, or all agents that support the active chain+asset if agentId is omitted.
@@ -5626,13 +6029,15 @@ var OwneySDK = class {
5626
6029
  );
5627
6030
  }
5628
6031
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
6032
+ const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
5629
6033
  if (!amount) {
5630
6034
  const results2 = {};
5631
6035
  const agentErrors2 = {};
5632
- for (const agent of eligibleAgents) {
6036
+ for (const agent of withdrawalAgents) {
5633
6037
  try {
5634
6038
  results2[agent.id] = await agent.withdraw(state, chainId, token);
5635
6039
  } catch (err) {
6040
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5636
6041
  console.error(`withdraw failed for agent "${agent.id}":`, err);
5637
6042
  agentErrors2[agent.id] = err instanceof Error ? err.message : String(err);
5638
6043
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5713,12 +6118,13 @@ var OwneySDK = class {
5713
6118
  planned: 0n
5714
6119
  }));
5715
6120
  const plans = [...disabledPlans, ...enabledPlans];
6121
+ const orderedPlans = this.orderAgentsForWithdrawal(plans.map((p) => p.agent)).map((agent) => plans.find((plan) => plan.agent === agent));
5716
6122
  const results = {};
5717
6123
  const agentErrors = {
5718
6124
  ...aggregated.agentErrors ?? {}
5719
6125
  };
5720
- for (let i = 0; i < plans.length; i++) {
5721
- const p = plans[i];
6126
+ for (let i = 0; i < orderedPlans.length; i++) {
6127
+ const p = orderedPlans[i];
5722
6128
  if (p.planned === 0n) continue;
5723
6129
  try {
5724
6130
  results[p.agent.id] = await p.agent.withdraw(
@@ -5728,6 +6134,7 @@ var OwneySDK = class {
5728
6134
  p.planned.toString()
5729
6135
  );
5730
6136
  } catch (err) {
6137
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5731
6138
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);
5732
6139
  agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
5733
6140
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5739,7 +6146,7 @@ var OwneySDK = class {
5739
6146
  );
5740
6147
  const failedAmount = p.planned;
5741
6148
  p.planned = 0n;
5742
- redistributeShare(plans, i, failedAmount);
6149
+ redistributeShare(orderedPlans, i, failedAmount);
5743
6150
  }
5744
6151
  }
5745
6152
  if (Object.keys(results).length === 0) {
@@ -6186,17 +6593,18 @@ var OwneySDK = class {
6186
6593
  );
6187
6594
  }
6188
6595
  const provider = this.requireConnectedProvider();
6189
- const publicClient = createPublicClient4({
6596
+ const walletChainClient = createPublicClient4({
6190
6597
  chain: VIEM_CHAIN2[chainId],
6191
6598
  transport: custom3(provider)
6192
6599
  });
6600
+ const publicClient = this.getPaidRpcClient(chainId);
6193
6601
  const approvalAmount = permit2ApprovalAmount(requiredAmount);
6194
6602
  const wallet = createWalletClient3({
6195
6603
  account: state.walletAddress,
6196
6604
  chain: VIEM_CHAIN2[chainId],
6197
6605
  transport: custom3(provider)
6198
6606
  });
6199
- await ensureWalletOnChain(publicClient, wallet, chainId);
6607
+ await ensureWalletOnChain(walletChainClient, wallet, chainId);
6200
6608
  const hash = await wallet.writeContract({
6201
6609
  address: token,
6202
6610
  abi: ERC20_ALLOWANCE_ABI,
@@ -6205,10 +6613,14 @@ var OwneySDK = class {
6205
6613
  account: state.walletAddress,
6206
6614
  chain: VIEM_CHAIN2[chainId]
6207
6615
  });
6208
- const receipt = await publicClient.waitForTransactionReceipt({
6209
- hash,
6210
- confirmations: 1
6211
- });
6616
+ const receipt = await withPaidRpcDiagnostics(
6617
+ () => publicClient.waitForTransactionReceipt({
6618
+ hash,
6619
+ confirmations: 1
6620
+ }),
6621
+ chainId,
6622
+ "eth_getTransactionReceipt"
6623
+ );
6212
6624
  if (receipt.status !== "success") {
6213
6625
  throw new Error(`Permit2 approval reverted (tx ${hash})`);
6214
6626
  }