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

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,131 @@ 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
+ var DEFAULT_RPC_URLS = {
1106
+ 1: "https://eth-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1107
+ 8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
1108
+ 42161: "https://arb-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V"
1109
+ };
1110
+ function resolveRpcUrl(rpcUrls, chainId) {
1111
+ const url = rpcUrls?.[chainId]?.trim();
1112
+ if (url) return url;
1113
+ return DEFAULT_RPC_URLS[chainId];
1114
+ }
1115
+ function resolveRpcUrls(rpcUrls) {
1116
+ return {
1117
+ 1: resolveRpcUrl(rpcUrls, 1),
1118
+ 8453: resolveRpcUrl(rpcUrls, 8453),
1119
+ 42161: resolveRpcUrl(rpcUrls, 42161)
1120
+ };
1121
+ }
1122
+ function createPaidRpcClient(chain, rpcUrls) {
1123
+ const url = resolveRpcUrl(
1124
+ rpcUrls,
1125
+ chain.id
1126
+ );
1127
+ return createPublicClient({
1128
+ chain,
1129
+ transport: http(url, {
1130
+ retryCount: PAID_RPC_RETRY_COUNT,
1131
+ retryDelay: PAID_RPC_RETRY_DELAY_MS
1132
+ })
1133
+ });
1134
+ }
1135
+ function headerValue(error, name) {
1136
+ const seen = /* @__PURE__ */ new Set();
1137
+ let value;
1138
+ function visit(candidate, depth = 0) {
1139
+ if (value || depth > 6 || !candidate || typeof candidate !== "object")
1140
+ return;
1141
+ if (seen.has(candidate)) return;
1142
+ seen.add(candidate);
1143
+ const record = candidate;
1144
+ const headers = record.headers;
1145
+ const found = typeof headers?.get === "function" ? headers.get(name) : headers?.[name] ?? headers?.[name.toLowerCase()];
1146
+ if (typeof found === "string" && found) {
1147
+ value = found;
1148
+ return;
1149
+ }
1150
+ for (const key2 of ["cause", "details", "response", "error"])
1151
+ visit(record[key2], depth + 1);
1152
+ }
1153
+ visit(error);
1154
+ return value;
1155
+ }
1156
+ function statusCode(error) {
1157
+ const seen = /* @__PURE__ */ new Set();
1158
+ let status;
1159
+ function visit(candidate, depth = 0) {
1160
+ if (status || depth > 6 || !candidate || typeof candidate !== "object")
1161
+ return;
1162
+ if (seen.has(candidate)) return;
1163
+ seen.add(candidate);
1164
+ const record = candidate;
1165
+ for (const key2 of ["status", "statusCode"]) {
1166
+ const parsed = Number(record[key2]);
1167
+ if (Number.isInteger(parsed) && parsed >= 100 && parsed <= 599) {
1168
+ status = parsed;
1169
+ return;
1170
+ }
1171
+ }
1172
+ for (const key2 of ["cause", "details", "response", "error"])
1173
+ visit(record[key2], depth + 1);
1174
+ }
1175
+ visit(error);
1176
+ return status;
1177
+ }
1178
+ function paidRpcError(error, chainId, rpcMethod, agentId) {
1179
+ const delay = rateLimitDelay(error);
1180
+ const providerRequestId = headerValue(error, "x-alchemy-request-id") ?? headerValue(error, "x-request-id");
1181
+ if (delay === void 0) {
1182
+ return new OwneyError(
1183
+ "AGENT_API_ERROR",
1184
+ "The blockchain RPC request failed.",
1185
+ {
1186
+ rpcSource: "paid-rpc",
1187
+ chainId,
1188
+ rpcMethod,
1189
+ ...statusCode(error) ? { statusCode: statusCode(error) } : {},
1190
+ ...providerRequestId ? { providerRequestId } : {}
1191
+ },
1192
+ agentId
1193
+ );
1194
+ }
1195
+ const retryAt = Date.now() + delay;
1196
+ return new OwneyError(
1197
+ "AGENT_RATE_LIMITED",
1198
+ "The blockchain RPC is rate limited. Please wait before trying again.",
1199
+ {
1200
+ rpcSource: "paid-rpc",
1201
+ chainId,
1202
+ rpcMethod,
1203
+ statusCode: 429,
1204
+ retryAt,
1205
+ retryAfterSeconds: Math.max(0, Math.ceil(delay / 1e3)),
1206
+ ...providerRequestId ? { providerRequestId } : {}
1207
+ },
1208
+ agentId
1209
+ );
1210
+ }
1211
+ async function withPaidRpcDiagnostics(operation, chainId, rpcMethod, agentId) {
1212
+ try {
1213
+ return await operation();
1214
+ } catch (error) {
1215
+ throw paidRpcError(error, chainId, rpcMethod, agentId);
1216
+ }
1217
+ }
1218
+
1066
1219
  // src/agents/zyfai/zyfai.agent.ts
1067
1220
  var ERC7579_IS_MODULE_INSTALLED_ABI = parseAbi([
1068
1221
  "function isModuleInstalled(uint256 moduleTypeId, address module, bytes additionalContext) view returns (bool)"
1069
1222
  ]);
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
1223
  var WETH_ADDRESS_BY_CHAIN = {
1076
1224
  8453: "0x4200000000000000000000000000000000000006",
1077
1225
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
@@ -1137,7 +1285,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1137
1285
  earningsSnapshot = null;
1138
1286
  earningsGeneration = 0;
1139
1287
  constructor(apiKey, rpcUrls, referralSource) {
1140
- this.rpcUrls = rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS;
1288
+ this.rpcUrls = resolveRpcUrls(rpcUrls);
1141
1289
  this.sdk = new ZyfaiSDK({
1142
1290
  apiKey,
1143
1291
  rpcUrls: this.rpcUrls,
@@ -1151,10 +1299,10 @@ var ZyfaiAgent = class _ZyfaiAgent {
1151
1299
  getPublicClient(chainId) {
1152
1300
  const cached = this.publicClients.get(chainId);
1153
1301
  if (cached) return cached;
1154
- const client = createPublicClient({
1155
- chain: VIEM_CHAIN[chainId],
1156
- transport: http(this.rpcUrls[chainId])
1157
- });
1302
+ const client = createPaidRpcClient(
1303
+ VIEM_CHAIN[chainId],
1304
+ this.rpcUrls
1305
+ );
1158
1306
  this.publicClients.set(chainId, client);
1159
1307
  return client;
1160
1308
  }
@@ -3191,6 +3339,9 @@ var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3191
3339
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3192
3340
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3193
3341
  var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3342
+ var YIELDSEEKER_AGENT_LIST_CACHE_MS = 6e4;
3343
+ var YIELDSEEKER_PORTFOLIO_CACHE_MS = 3e4;
3344
+ var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
3194
3345
  function generateYieldseekerUsername() {
3195
3346
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
3196
3347
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -3234,6 +3385,7 @@ function query(params) {
3234
3385
  var YieldseekerAgent = class {
3235
3386
  id = "yieldseeker";
3236
3387
  balanceComposition = "tokens-plus-positions";
3388
+ withdrawalRequiresWalletApproval = true;
3237
3389
  supportedChainIds = [8453];
3238
3390
  supportedAssets = [
3239
3391
  {
@@ -3247,11 +3399,17 @@ var YieldseekerAgent = class {
3247
3399
  ];
3248
3400
  api;
3249
3401
  auth;
3402
+ rpcUrls;
3403
+ receiptClient;
3250
3404
  transactionExecutor;
3251
3405
  unwindReceiptWaiter;
3252
3406
  agentContexts = /* @__PURE__ */ new Map();
3253
3407
  users = /* @__PURE__ */ new Map();
3254
3408
  pendingAgents = /* @__PURE__ */ new Map();
3409
+ pendingWalletContexts = /* @__PURE__ */ new Map();
3410
+ readCache = /* @__PURE__ */ new Map();
3411
+ pendingReads = /* @__PURE__ */ new Map();
3412
+ readGeneration = 0;
3255
3413
  yieldOptions = /* @__PURE__ */ new Map();
3256
3414
  pendingYieldOptions = /* @__PURE__ */ new Map();
3257
3415
  constructor(owneyApiKey, options = {}) {
@@ -3261,10 +3419,16 @@ var YieldseekerAgent = class {
3261
3419
  options.fetchFn
3262
3420
  );
3263
3421
  this.auth = new YieldseekerAuth(options.auth);
3422
+ this.rpcUrls = options.rpcUrls;
3264
3423
  this.transactionExecutor = options.transactionExecutor;
3265
3424
  this.unwindReceiptWaiter = options.unwindReceiptWaiter;
3266
3425
  }
3426
+ getReceiptClient() {
3427
+ this.receiptClient ??= createPaidRpcClient(base3, this.rpcUrls);
3428
+ return this.receiptClient;
3429
+ }
3267
3430
  async disconnect() {
3431
+ this.readGeneration += 1;
3268
3432
  this.auth.clear();
3269
3433
  for (const key2 of this.users.keys()) {
3270
3434
  const [walletAddress, chainId] = key2.split(":");
@@ -3273,6 +3437,9 @@ var YieldseekerAgent = class {
3273
3437
  this.users.clear();
3274
3438
  this.agentContexts.clear();
3275
3439
  this.pendingAgents.clear();
3440
+ this.pendingWalletContexts.clear();
3441
+ this.readCache.clear();
3442
+ this.pendingReads.clear();
3276
3443
  }
3277
3444
  async activateAgent(state, chainId, asset) {
3278
3445
  this.assertChain(chainId);
@@ -3535,6 +3702,86 @@ var YieldseekerAgent = class {
3535
3702
  contextKey(state, chainId, asset) {
3536
3703
  return `${this.userKey(state, chainId)}:${asset}`;
3537
3704
  }
3705
+ cachedRead(key2, ttlMs, read2) {
3706
+ const cached = this.readCache.get(key2);
3707
+ if (cached && cached.expiresAt > Date.now()) {
3708
+ return Promise.resolve(cached.value);
3709
+ }
3710
+ const pending = this.pendingReads.get(key2);
3711
+ if (pending) return pending;
3712
+ const generation = this.readGeneration;
3713
+ const request = Promise.resolve().then(read2).then((value) => {
3714
+ if (this.readGeneration === generation) {
3715
+ this.readCache.set(key2, {
3716
+ expiresAt: Date.now() + ttlMs,
3717
+ value
3718
+ });
3719
+ }
3720
+ return value;
3721
+ }).finally(() => {
3722
+ if (this.pendingReads.get(key2) === request) {
3723
+ this.pendingReads.delete(key2);
3724
+ }
3725
+ });
3726
+ this.pendingReads.set(key2, request);
3727
+ return request;
3728
+ }
3729
+ agentListKey(state, chainId) {
3730
+ return `agents:${this.userKey(state, chainId)}`;
3731
+ }
3732
+ async listAgents(state, chainId, user) {
3733
+ const response = await this.cachedRead(
3734
+ this.agentListKey(state, chainId),
3735
+ YIELDSEEKER_AGENT_LIST_CACHE_MS,
3736
+ async () => {
3737
+ const response2 = await this.walletRequest(
3738
+ state,
3739
+ chainId,
3740
+ `/users/${user.userId}/agents`
3741
+ );
3742
+ if (!Array.isArray(response2?.agents)) {
3743
+ throw this.invalidResponse("agent list");
3744
+ }
3745
+ return response2;
3746
+ }
3747
+ );
3748
+ return response.agents;
3749
+ }
3750
+ async contextForAgent(state, chainId, user, agent, asset) {
3751
+ const key2 = this.contextKey(state, chainId, asset);
3752
+ const cached = this.agentContexts.get(key2);
3753
+ if (cached?.agent.agentId === agent.agentId) return cached;
3754
+ const pending = this.pendingWalletContexts.get(key2);
3755
+ if (pending) return pending;
3756
+ const generation = this.readGeneration;
3757
+ const request = this.walletRequest(
3758
+ state,
3759
+ chainId,
3760
+ `/users/${user.userId}/agents/${agent.agentId}/wallet`
3761
+ ).then((walletResponse) => {
3762
+ if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3763
+ throw this.invalidResponse("agent wallet");
3764
+ }
3765
+ const context = {
3766
+ user,
3767
+ agent,
3768
+ wallet: walletResponse.agentWallet,
3769
+ asset
3770
+ };
3771
+ if (this.readGeneration === generation) {
3772
+ this.agentContexts.set(key2, context);
3773
+ }
3774
+ return context;
3775
+ });
3776
+ this.pendingWalletContexts.set(key2, request);
3777
+ try {
3778
+ return await request;
3779
+ } finally {
3780
+ if (this.pendingWalletContexts.get(key2) === request) {
3781
+ this.pendingWalletContexts.delete(key2);
3782
+ }
3783
+ }
3784
+ }
3538
3785
  async resolveUser(state, chainId) {
3539
3786
  const key2 = this.userKey(state, chainId);
3540
3787
  const inMemory = this.users.get(key2);
@@ -3631,16 +3878,9 @@ var YieldseekerAgent = class {
3631
3878
  }
3632
3879
  async resolveAgent(state, chainId, asset, createIfMissing) {
3633
3880
  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
- }
3881
+ const agents = await this.listAgents(state, chainId, user);
3642
3882
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3643
- let agent = response.agents.find(
3883
+ let agent = agents.find(
3644
3884
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
3645
3885
  );
3646
3886
  if (!agent && createIfMissing) {
@@ -3661,83 +3901,91 @@ var YieldseekerAgent = class {
3661
3901
  }
3662
3902
  );
3663
3903
  agent = created?.agent;
3904
+ if (agent) {
3905
+ this.readCache.set(this.agentListKey(state, chainId), {
3906
+ expiresAt: Date.now() + YIELDSEEKER_AGENT_LIST_CACHE_MS,
3907
+ value: {
3908
+ agents: [...agents, agent]
3909
+ }
3910
+ });
3911
+ }
3664
3912
  }
3665
3913
  if (!agent) return null;
3666
3914
  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 };
3915
+ return this.contextForAgent(state, chainId, user, agent, asset);
3676
3916
  }
3677
3917
  async loadPortfolio(state, chainId, options) {
3678
3918
  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
- }
3919
+ const agents = await this.listAgents(state, chainId, user);
3687
3920
  const contexts = [];
3688
- for (const agent of response.agents) {
3921
+ for (const agent of agents) {
3689
3922
  const asset = this.assetForAgent(agent);
3690
3923
  if (!this.isOwneyAgent(agent) || !asset || agent.chainId !== chainId || agent.type !== "vault" || options.asset && options.asset !== asset) {
3691
3924
  continue;
3692
3925
  }
3693
3926
  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);
3927
+ contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
3710
3928
  }
3929
+ const resolvedContexts = await Promise.all(contexts);
3711
3930
  return Promise.all(
3712
- contexts.map(
3931
+ resolvedContexts.map(
3713
3932
  (context) => this.loadPortfolioContext(state, chainId, context, options)
3714
3933
  )
3715
3934
  );
3716
3935
  }
3717
3936
  async loadPortfolioContext(state, chainId, context, options = {}) {
3937
+ const contextKey = this.contextKey(state, chainId, context.asset);
3718
3938
  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
- })}`
3939
+ this.cachedRead(
3940
+ `snapshot:${contextKey}`,
3941
+ YIELDSEEKER_PORTFOLIO_CACHE_MS,
3942
+ async () => {
3943
+ const response = await this.walletRequest(
3944
+ state,
3945
+ chainId,
3946
+ `${this.agentPath(context, "snapshot")}${query({
3947
+ shouldOnlyUseRecentValue: true,
3948
+ shouldAllowStaleOnError: true
3949
+ })}`
3950
+ );
3951
+ if (!response?.agentSnapshot) {
3952
+ throw this.invalidResponse("agent snapshot");
3953
+ }
3954
+ return response;
3955
+ }
3726
3956
  ),
3727
- this.walletRequest(
3728
- state,
3729
- chainId,
3730
- this.agentPath(context, "yield-positions")
3957
+ this.cachedRead(
3958
+ `positions:${contextKey}`,
3959
+ YIELDSEEKER_PORTFOLIO_CACHE_MS,
3960
+ async () => {
3961
+ const response = await this.walletRequest(
3962
+ state,
3963
+ chainId,
3964
+ this.agentPath(context, "yield-positions")
3965
+ );
3966
+ if (!Array.isArray(response?.yieldPositions)) {
3967
+ throw this.invalidResponse("yield positions");
3968
+ }
3969
+ return response;
3970
+ }
3731
3971
  ),
3732
- options.historic ? this.walletRequest(
3733
- state,
3734
- chainId,
3735
- this.agentPath(context, "wallet/historic-position")
3972
+ options.historic ? this.cachedRead(
3973
+ `historic:${contextKey}`,
3974
+ YIELDSEEKER_ACTIVITY_CACHE_MS,
3975
+ () => this.walletRequest(
3976
+ state,
3977
+ chainId,
3978
+ this.agentPath(context, "wallet/historic-position")
3979
+ )
3736
3980
  ) : Promise.resolve(void 0),
3737
- options.actions ? this.walletRequest(
3738
- state,
3739
- chainId,
3740
- this.agentPath(context, "actions")
3981
+ options.actions ? this.cachedRead(
3982
+ `actions:${contextKey}`,
3983
+ YIELDSEEKER_ACTIVITY_CACHE_MS,
3984
+ () => this.walletRequest(
3985
+ state,
3986
+ chainId,
3987
+ this.agentPath(context, "actions")
3988
+ )
3741
3989
  ) : Promise.resolve(void 0)
3742
3990
  ]);
3743
3991
  if (!snapshot?.agentSnapshot) {
@@ -3771,6 +4019,13 @@ var YieldseekerAgent = class {
3771
4019
  context.wallet = deployed.agentWallet;
3772
4020
  }
3773
4021
  async refreshSnapshotAfterMovement(state, chainId, context, movement) {
4022
+ const contextKey = this.contextKey(state, chainId, context.asset);
4023
+ const invalidatePortfolio = () => {
4024
+ for (const kind of ["snapshot", "positions", "historic", "actions"]) {
4025
+ this.readCache.delete(`${kind}:${contextKey}`);
4026
+ }
4027
+ };
4028
+ invalidatePortfolio();
3774
4029
  try {
3775
4030
  const response = await this.walletRequest(
3776
4031
  state,
@@ -3787,6 +4042,8 @@ var YieldseekerAgent = class {
3787
4042
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
3788
4043
  error
3789
4044
  );
4045
+ } finally {
4046
+ invalidatePortfolio();
3790
4047
  }
3791
4048
  }
3792
4049
  agentPath(context, suffix) {
@@ -3837,6 +4094,7 @@ var YieldseekerAgent = class {
3837
4094
  code,
3838
4095
  `Yieldseeker request failed: ${error.providerCode}.`,
3839
4096
  {
4097
+ rpcSource: "agent-api",
3840
4098
  statusCode: error.status,
3841
4099
  providerCode: error.providerCode,
3842
4100
  ...error.responseFields ? { fields: error.responseFields } : {}
@@ -3855,12 +4113,12 @@ var YieldseekerAgent = class {
3855
4113
  chain: base3,
3856
4114
  transport: custom2(state.provider)
3857
4115
  });
3858
- const publicClient = createPublicClient3({
4116
+ const walletChainClient = createPublicClient3({
3859
4117
  chain: base3,
3860
4118
  transport: custom2(state.provider)
3861
4119
  });
3862
4120
  await ensureWalletOnChain(
3863
- publicClient,
4121
+ walletChainClient,
3864
4122
  walletClient,
3865
4123
  8453
3866
4124
  );
@@ -3871,10 +4129,15 @@ var YieldseekerAgent = class {
3871
4129
  data: transaction.data,
3872
4130
  value: BigInt(transaction.value)
3873
4131
  });
3874
- const receipt = await publicClient.waitForTransactionReceipt({
3875
- hash,
3876
- confirmations: 1
3877
- });
4132
+ const receipt = await withPaidRpcDiagnostics(
4133
+ () => this.getReceiptClient().waitForTransactionReceipt({
4134
+ hash,
4135
+ confirmations: 1
4136
+ }),
4137
+ 8453,
4138
+ "eth_getTransactionReceipt",
4139
+ this.id
4140
+ );
3878
4141
  if (receipt.status !== "success") {
3879
4142
  throw new OwneyError(
3880
4143
  "AGENT_TRANSACTION_REVERTED",
@@ -3890,14 +4153,15 @@ var YieldseekerAgent = class {
3890
4153
  await this.unwindReceiptWaiter(state, chainId, transactionHash);
3891
4154
  return;
3892
4155
  }
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
- });
4156
+ const receipt = await withPaidRpcDiagnostics(
4157
+ () => this.getReceiptClient().waitForTransactionReceipt({
4158
+ hash: transactionHash,
4159
+ confirmations: 1
4160
+ }),
4161
+ 8453,
4162
+ "eth_getTransactionReceipt",
4163
+ this.id
4164
+ );
3901
4165
  if (receipt.status !== "success") {
3902
4166
  throw new OwneyError(
3903
4167
  "AGENT_TRANSACTION_REVERTED",
@@ -4269,6 +4533,41 @@ import {
4269
4533
  // src/lib/permit2-batch.ts
4270
4534
  import { parseAbi as parseAbi2, hashStruct } from "viem";
4271
4535
  var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
4536
+ var BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
4537
+ var MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
4538
+ var ERC2612_PERMIT_TYPES = {
4539
+ Permit: [
4540
+ { name: "owner", type: "address" },
4541
+ { name: "spender", type: "address" },
4542
+ { name: "value", type: "uint256" },
4543
+ { name: "nonce", type: "uint256" },
4544
+ { name: "deadline", type: "uint256" }
4545
+ ]
4546
+ };
4547
+ var ERC2612_READ_ABI = parseAbi2([
4548
+ "function name() view returns (string)",
4549
+ "function version() view returns (string)",
4550
+ "function nonces(address owner) view returns (uint256)"
4551
+ ]);
4552
+ function erc2612TypedData(input) {
4553
+ return {
4554
+ domain: {
4555
+ name: input.permit.tokenName,
4556
+ version: input.permit.tokenVersion,
4557
+ chainId: input.chainId,
4558
+ verifyingContract: input.token
4559
+ },
4560
+ types: ERC2612_PERMIT_TYPES,
4561
+ primaryType: "Permit",
4562
+ message: {
4563
+ owner: input.owner,
4564
+ spender: BATCH_PERMIT2_ADDRESS,
4565
+ value: BigInt(input.permit.value),
4566
+ nonce: BigInt(input.permit.nonce),
4567
+ deadline: BigInt(input.permit.deadline)
4568
+ }
4569
+ };
4570
+ }
4272
4571
  var PERMIT_BATCH_TYPES = {
4273
4572
  PermitBatchWitnessTransferFrom: [
4274
4573
  { name: "permitted", type: "TokenPermissions[]" },
@@ -4420,28 +4719,67 @@ async function execute(i, key2, plan) {
4420
4719
  "DEPOSIT_INSUFFICIENT_BALANCE",
4421
4720
  "Insufficient token balance for this deposit."
4422
4721
  );
4423
- if (allowance < total)
4722
+ if (allowance < total && (i.chainId !== 8453 || !isAddressEqual(i.token, BASE_USDC_ADDRESS)))
4424
4723
  throw new OwneyError(
4425
4724
  "PERMIT2_APPROVAL_REQUIRED",
4426
4725
  "token deposits need a one-time Permit2 approval."
4427
4726
  );
4428
- const relayer = await getSponsorRelayerAddress({
4727
+ const now = (await i.pub.getBlock()).timestamp;
4728
+ let erc2612Permit;
4729
+ if (allowance < total) {
4730
+ const [tokenName, tokenVersion, permitNonce] = await Promise.all([
4731
+ i.pub.readContract({
4732
+ address: i.token,
4733
+ abi: ERC2612_READ_ABI,
4734
+ functionName: "name"
4735
+ }),
4736
+ i.pub.readContract({
4737
+ address: i.token,
4738
+ abi: ERC2612_READ_ABI,
4739
+ functionName: "version"
4740
+ }),
4741
+ i.pub.readContract({
4742
+ address: i.token,
4743
+ abi: ERC2612_READ_ABI,
4744
+ functionName: "nonces",
4745
+ args: [i.owner]
4746
+ })
4747
+ ]);
4748
+ const unsignedPermit = {
4749
+ value: MAX_UINT256.toString(),
4750
+ nonce: permitNonce.toString(),
4751
+ deadline: (now + 900n).toString(),
4752
+ tokenName,
4753
+ tokenVersion
4754
+ };
4755
+ const signature2 = await i.wallet.signTypedData({
4756
+ account: i.owner,
4757
+ ...erc2612TypedData({
4758
+ chainId: 8453,
4759
+ token: i.token,
4760
+ owner: i.owner,
4761
+ permit: unsignedPermit
4762
+ })
4763
+ });
4764
+ erc2612Permit = { ...unsignedPermit, signature: signature2 };
4765
+ }
4766
+ const spender = erc2612Permit ? MULTICALL3_ADDRESS : await getSponsorRelayerAddress({
4429
4767
  apiKey: i.apiKey,
4430
4768
  baseUrl: i.baseUrl,
4431
4769
  chainId: i.chainId
4432
4770
  });
4433
- const now = (await i.pub.getBlock()).timestamp;
4434
4771
  const unsigned = {
4435
4772
  chainId: i.chainId,
4436
4773
  token: i.token,
4437
4774
  from: i.owner,
4438
4775
  transfers: i.transfers,
4439
4776
  nonce: randomPermit2Nonce().toString(),
4440
- deadline: (now + 900n).toString()
4777
+ deadline: (now + 900n).toString(),
4778
+ ...erc2612Permit ? { erc2612Permit } : {}
4441
4779
  };
4442
4780
  const signature = await i.wallet.signTypedData({
4443
4781
  account: i.owner,
4444
- ...batchTypedData(unsigned, relayer)
4782
+ ...batchTypedData(unsigned, spender)
4445
4783
  });
4446
4784
  i.onApproved?.();
4447
4785
  return send({ ...unsigned, signature });
@@ -4461,8 +4799,8 @@ function makeSponsoredTokenCallback(deps) {
4461
4799
  "CHAIN_UNSUPPORTED",
4462
4800
  `No sponsored token configured for chain ${chainId}`
4463
4801
  );
4464
- const pub = deps.getPublicClient(chainId), wallet = deps.getWalletClient(chainId);
4465
- await ensureWalletOnChain(pub, wallet, chainId);
4802
+ const pub = deps.getPublicClient(chainId), walletChain = deps.getWalletChainClient?.(chainId) ?? pub, wallet = deps.getWalletClient(chainId);
4803
+ await ensureWalletOnChain(walletChain, wallet, chainId);
4466
4804
  return sponsorTokenBatch({
4467
4805
  apiKey: deps.apiKey,
4468
4806
  baseUrl: deps.baseUrl,
@@ -4764,6 +5102,7 @@ var OwneySDK = class {
4764
5102
  // leave every user's agent profile alone.
4765
5103
  orgAgentConfig;
4766
5104
  orgAgentConfigPromise = null;
5105
+ rpcUrls;
4767
5106
  zyfaiRpcUrls;
4768
5107
  yieldseekerApiBaseUrl;
4769
5108
  yieldseekerSiweOrigin;
@@ -4788,6 +5127,7 @@ var OwneySDK = class {
4788
5127
  constructor(config) {
4789
5128
  this.apiKey = config.apiKey;
4790
5129
  if (config.debug) setOwneyDebug(true);
5130
+ this.rpcUrls = config.rpcUrls;
4791
5131
  this.zyfaiRpcUrls = config.zyfaiRpcUrls;
4792
5132
  this.yieldseekerApiBaseUrl = config.yieldseekerApiBaseUrl;
4793
5133
  this.yieldseekerSiweOrigin = config.yieldseekerSiweOrigin;
@@ -4878,6 +5218,9 @@ var OwneySDK = class {
4878
5218
  }
4879
5219
  return this.state.provider;
4880
5220
  }
5221
+ getPaidRpcClient(chainId) {
5222
+ return createPaidRpcClient(VIEM_CHAIN2[chainId], this.rpcUrls);
5223
+ }
4881
5224
  /** Builds the default USDC batch callback for the connected wallet. */
4882
5225
  getDefaultSponsoredCallback(onApproved) {
4883
5226
  if (!onApproved && this.cachedSponsoredCallback)
@@ -4893,7 +5236,8 @@ var OwneySDK = class {
4893
5236
  // Casts work around viem's chain-narrowed Client vs the generic
4894
5237
  // PublicClient/WalletClient param types — structurally identical at
4895
5238
  // runtime, but the two share a name TS treats as unrelated.
4896
- getPublicClient: (cid) => createPublicClient4({
5239
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5240
+ getWalletChainClient: (cid) => createPublicClient4({
4897
5241
  chain: VIEM_CHAIN2[cid],
4898
5242
  transport: custom3(provider)
4899
5243
  }),
@@ -4943,7 +5287,8 @@ var OwneySDK = class {
4943
5287
  // Casts work around viem's chain-narrowed Client vs the generic
4944
5288
  // PublicClient/WalletClient param types — structurally identical at
4945
5289
  // runtime, but the two share a name TS treats as unrelated.
4946
- getPublicClient: (cid) => createPublicClient4({
5290
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5291
+ getWalletChainClient: (cid) => createPublicClient4({
4947
5292
  chain: VIEM_CHAIN2[cid],
4948
5293
  transport: custom3(provider)
4949
5294
  }),
@@ -5056,12 +5401,17 @@ var OwneySDK = class {
5056
5401
  createAgent(agentId, key2) {
5057
5402
  if (agentId === "zyfai") {
5058
5403
  if (!key2) return null;
5059
- return new ZyfaiAgent(key2, this.zyfaiRpcUrls, this.referralSource);
5404
+ return new ZyfaiAgent(
5405
+ key2,
5406
+ this.rpcUrls ?? this.zyfaiRpcUrls,
5407
+ this.referralSource
5408
+ );
5060
5409
  }
5061
5410
  if (agentId === "yieldseeker") {
5062
5411
  return new YieldseekerAgent(this.apiKey, {
5063
5412
  auth: { origin: this.yieldseekerSiweOrigin },
5064
- baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl)
5413
+ baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl),
5414
+ rpcUrls: this.rpcUrls
5065
5415
  });
5066
5416
  }
5067
5417
  return null;
@@ -5397,10 +5747,12 @@ var OwneySDK = class {
5397
5747
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
5398
5748
  * two independent auto-recovery mechanisms:
5399
5749
  *
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
5750
+ * 1. Missing Permit2 allowance outside the atomic Base-USDC path: when the
5751
+ * app did not supply its own callback and the attempt fails with
5752
+ * `PERMIT2_APPROVAL_REQUIRED`, this is the wallet's first Permit2 deposit
5753
+ * for that token. Base USDC bundles a gasless ERC-2612 approval inside
5754
+ * its sponsored deposit and never reaches this branch. Other tokens send
5755
+ * the one-time user-paid Permit2 approval via `approvePermit2()` and
5404
5756
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
5405
5757
  * per call so a wallet/agent that keeps reporting the allowance as
5406
5758
  * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
@@ -5591,6 +5943,32 @@ var OwneySDK = class {
5591
5943
  }
5592
5944
  return eligible;
5593
5945
  }
5946
+ /**
5947
+ * Run owner-approved withdrawals before relayer-only withdrawals. Wallet
5948
+ * approval is the only point at which the user can cancel the aggregate
5949
+ * operation, so no relayer leg should commit before it has completed.
5950
+ */
5951
+ orderAgentsForWithdrawal(agents) {
5952
+ return agents.map((agent, index) => ({ agent, index })).sort((left, right) => {
5953
+ const approvalOrder = Number(Boolean(right.agent.withdrawalRequiresWalletApproval)) - Number(Boolean(left.agent.withdrawalRequiresWalletApproval));
5954
+ return approvalOrder || left.index - right.index;
5955
+ }).map(({ agent }) => agent);
5956
+ }
5957
+ isUserRejectedWithdrawal(error) {
5958
+ let current = error;
5959
+ const seen = /* @__PURE__ */ new Set();
5960
+ while (current && typeof current === "object" && !seen.has(current)) {
5961
+ seen.add(current);
5962
+ const candidate = current;
5963
+ if (candidate.code === 4001 || candidate.code === "4001") return true;
5964
+ const message = [candidate.message, candidate.shortMessage].filter((value) => typeof value === "string").join(" ");
5965
+ if (/user (?:rejected|denied)|rejected by user/i.test(message)) {
5966
+ return true;
5967
+ }
5968
+ current = candidate.cause;
5969
+ }
5970
+ return false;
5971
+ }
5594
5972
  // --- Fund operations ---
5595
5973
  /**
5596
5974
  * Withdraw funds from a specific agent, or all agents that support the active chain+asset if agentId is omitted.
@@ -5626,13 +6004,15 @@ var OwneySDK = class {
5626
6004
  );
5627
6005
  }
5628
6006
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
6007
+ const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
5629
6008
  if (!amount) {
5630
6009
  const results2 = {};
5631
6010
  const agentErrors2 = {};
5632
- for (const agent of eligibleAgents) {
6011
+ for (const agent of withdrawalAgents) {
5633
6012
  try {
5634
6013
  results2[agent.id] = await agent.withdraw(state, chainId, token);
5635
6014
  } catch (err) {
6015
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5636
6016
  console.error(`withdraw failed for agent "${agent.id}":`, err);
5637
6017
  agentErrors2[agent.id] = err instanceof Error ? err.message : String(err);
5638
6018
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5713,12 +6093,13 @@ var OwneySDK = class {
5713
6093
  planned: 0n
5714
6094
  }));
5715
6095
  const plans = [...disabledPlans, ...enabledPlans];
6096
+ const orderedPlans = this.orderAgentsForWithdrawal(plans.map((p) => p.agent)).map((agent) => plans.find((plan) => plan.agent === agent));
5716
6097
  const results = {};
5717
6098
  const agentErrors = {
5718
6099
  ...aggregated.agentErrors ?? {}
5719
6100
  };
5720
- for (let i = 0; i < plans.length; i++) {
5721
- const p = plans[i];
6101
+ for (let i = 0; i < orderedPlans.length; i++) {
6102
+ const p = orderedPlans[i];
5722
6103
  if (p.planned === 0n) continue;
5723
6104
  try {
5724
6105
  results[p.agent.id] = await p.agent.withdraw(
@@ -5728,6 +6109,7 @@ var OwneySDK = class {
5728
6109
  p.planned.toString()
5729
6110
  );
5730
6111
  } catch (err) {
6112
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5731
6113
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);
5732
6114
  agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
5733
6115
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5739,7 +6121,7 @@ var OwneySDK = class {
5739
6121
  );
5740
6122
  const failedAmount = p.planned;
5741
6123
  p.planned = 0n;
5742
- redistributeShare(plans, i, failedAmount);
6124
+ redistributeShare(orderedPlans, i, failedAmount);
5743
6125
  }
5744
6126
  }
5745
6127
  if (Object.keys(results).length === 0) {
@@ -6186,17 +6568,18 @@ var OwneySDK = class {
6186
6568
  );
6187
6569
  }
6188
6570
  const provider = this.requireConnectedProvider();
6189
- const publicClient = createPublicClient4({
6571
+ const walletChainClient = createPublicClient4({
6190
6572
  chain: VIEM_CHAIN2[chainId],
6191
6573
  transport: custom3(provider)
6192
6574
  });
6575
+ const publicClient = this.getPaidRpcClient(chainId);
6193
6576
  const approvalAmount = permit2ApprovalAmount(requiredAmount);
6194
6577
  const wallet = createWalletClient3({
6195
6578
  account: state.walletAddress,
6196
6579
  chain: VIEM_CHAIN2[chainId],
6197
6580
  transport: custom3(provider)
6198
6581
  });
6199
- await ensureWalletOnChain(publicClient, wallet, chainId);
6582
+ await ensureWalletOnChain(walletChainClient, wallet, chainId);
6200
6583
  const hash = await wallet.writeContract({
6201
6584
  address: token,
6202
6585
  abi: ERC20_ALLOWANCE_ABI,
@@ -6205,10 +6588,14 @@ var OwneySDK = class {
6205
6588
  account: state.walletAddress,
6206
6589
  chain: VIEM_CHAIN2[chainId]
6207
6590
  });
6208
- const receipt = await publicClient.waitForTransactionReceipt({
6209
- hash,
6210
- confirmations: 1
6211
- });
6591
+ const receipt = await withPaidRpcDiagnostics(
6592
+ () => publicClient.waitForTransactionReceipt({
6593
+ hash,
6594
+ confirmations: 1
6595
+ }),
6596
+ chainId,
6597
+ "eth_getTransactionReceipt"
6598
+ );
6212
6599
  if (receipt.status !== "success") {
6213
6600
  throw new Error(`Permit2 approval reverted (tx ${hash})`);
6214
6601
  }