@owney/sdk 0.7.25-beta.3 → 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
  }
@@ -2328,12 +2476,13 @@ function randomPermit2Nonce() {
2328
2476
  globalThis.crypto.getRandomValues(bytes);
2329
2477
  return BigInt(bytesToHex2(bytes));
2330
2478
  }
2331
- async function readPermit2Allowance(publicClient, token, owner) {
2479
+ async function readPermit2Allowance(publicClient, token, owner, blockNumber) {
2332
2480
  return publicClient.readContract({
2333
2481
  address: token,
2334
2482
  abi: ERC20_ALLOWANCE_ABI,
2335
2483
  functionName: "allowance",
2336
- args: [owner, PERMIT2_ADDRESS]
2484
+ args: [owner, PERMIT2_ADDRESS],
2485
+ ...blockNumber === void 0 ? {} : { blockNumber }
2337
2486
  });
2338
2487
  }
2339
2488
  async function readErc20Balance(publicClient, token, owner) {
@@ -3190,6 +3339,9 @@ var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3190
3339
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3191
3340
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3192
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;
3193
3345
  function generateYieldseekerUsername() {
3194
3346
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
3195
3347
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -3233,6 +3385,7 @@ function query(params) {
3233
3385
  var YieldseekerAgent = class {
3234
3386
  id = "yieldseeker";
3235
3387
  balanceComposition = "tokens-plus-positions";
3388
+ withdrawalRequiresWalletApproval = true;
3236
3389
  supportedChainIds = [8453];
3237
3390
  supportedAssets = [
3238
3391
  {
@@ -3246,11 +3399,17 @@ var YieldseekerAgent = class {
3246
3399
  ];
3247
3400
  api;
3248
3401
  auth;
3402
+ rpcUrls;
3403
+ receiptClient;
3249
3404
  transactionExecutor;
3250
3405
  unwindReceiptWaiter;
3251
3406
  agentContexts = /* @__PURE__ */ new Map();
3252
3407
  users = /* @__PURE__ */ new Map();
3253
3408
  pendingAgents = /* @__PURE__ */ new Map();
3409
+ pendingWalletContexts = /* @__PURE__ */ new Map();
3410
+ readCache = /* @__PURE__ */ new Map();
3411
+ pendingReads = /* @__PURE__ */ new Map();
3412
+ readGeneration = 0;
3254
3413
  yieldOptions = /* @__PURE__ */ new Map();
3255
3414
  pendingYieldOptions = /* @__PURE__ */ new Map();
3256
3415
  constructor(owneyApiKey, options = {}) {
@@ -3260,10 +3419,16 @@ var YieldseekerAgent = class {
3260
3419
  options.fetchFn
3261
3420
  );
3262
3421
  this.auth = new YieldseekerAuth(options.auth);
3422
+ this.rpcUrls = options.rpcUrls;
3263
3423
  this.transactionExecutor = options.transactionExecutor;
3264
3424
  this.unwindReceiptWaiter = options.unwindReceiptWaiter;
3265
3425
  }
3426
+ getReceiptClient() {
3427
+ this.receiptClient ??= createPaidRpcClient(base3, this.rpcUrls);
3428
+ return this.receiptClient;
3429
+ }
3266
3430
  async disconnect() {
3431
+ this.readGeneration += 1;
3267
3432
  this.auth.clear();
3268
3433
  for (const key2 of this.users.keys()) {
3269
3434
  const [walletAddress, chainId] = key2.split(":");
@@ -3272,6 +3437,9 @@ var YieldseekerAgent = class {
3272
3437
  this.users.clear();
3273
3438
  this.agentContexts.clear();
3274
3439
  this.pendingAgents.clear();
3440
+ this.pendingWalletContexts.clear();
3441
+ this.readCache.clear();
3442
+ this.pendingReads.clear();
3275
3443
  }
3276
3444
  async activateAgent(state, chainId, asset) {
3277
3445
  this.assertChain(chainId);
@@ -3534,6 +3702,86 @@ var YieldseekerAgent = class {
3534
3702
  contextKey(state, chainId, asset) {
3535
3703
  return `${this.userKey(state, chainId)}:${asset}`;
3536
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
+ }
3537
3785
  async resolveUser(state, chainId) {
3538
3786
  const key2 = this.userKey(state, chainId);
3539
3787
  const inMemory = this.users.get(key2);
@@ -3630,16 +3878,9 @@ var YieldseekerAgent = class {
3630
3878
  }
3631
3879
  async resolveAgent(state, chainId, asset, createIfMissing) {
3632
3880
  const user = await this.resolveUser(state, chainId);
3633
- const response = await this.walletRequest(
3634
- state,
3635
- chainId,
3636
- `/users/${user.userId}/agents`
3637
- );
3638
- if (!Array.isArray(response?.agents)) {
3639
- throw this.invalidResponse("agent list");
3640
- }
3881
+ const agents = await this.listAgents(state, chainId, user);
3641
3882
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3642
- let agent = response.agents.find(
3883
+ let agent = agents.find(
3643
3884
  (candidate) => this.isOwneyAgent(candidate) && candidate.chainId === chainId && candidate.type === "vault" && candidate.assetAddress.toLowerCase() === metadata.address.toLowerCase()
3644
3885
  );
3645
3886
  if (!agent && createIfMissing) {
@@ -3660,83 +3901,91 @@ var YieldseekerAgent = class {
3660
3901
  }
3661
3902
  );
3662
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
+ }
3663
3912
  }
3664
3913
  if (!agent) return null;
3665
3914
  this.assertAgent(agent);
3666
- const walletResponse = await this.walletRequest(
3667
- state,
3668
- chainId,
3669
- `/users/${user.userId}/agents/${agent.agentId}/wallet`
3670
- );
3671
- if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3672
- throw this.invalidResponse("agent wallet");
3673
- }
3674
- return { user, agent, wallet: walletResponse.agentWallet, asset };
3915
+ return this.contextForAgent(state, chainId, user, agent, asset);
3675
3916
  }
3676
3917
  async loadPortfolio(state, chainId, options) {
3677
3918
  const user = await this.resolveUser(state, chainId);
3678
- const response = await this.walletRequest(
3679
- state,
3680
- chainId,
3681
- `/users/${user.userId}/agents`
3682
- );
3683
- if (!Array.isArray(response?.agents)) {
3684
- throw this.invalidResponse("agent list");
3685
- }
3919
+ const agents = await this.listAgents(state, chainId, user);
3686
3920
  const contexts = [];
3687
- for (const agent of response.agents) {
3921
+ for (const agent of agents) {
3688
3922
  const asset = this.assetForAgent(agent);
3689
3923
  if (!this.isOwneyAgent(agent) || !asset || agent.chainId !== chainId || agent.type !== "vault" || options.asset && options.asset !== asset) {
3690
3924
  continue;
3691
3925
  }
3692
3926
  this.assertAgent(agent);
3693
- const walletResponse = await this.walletRequest(
3694
- state,
3695
- chainId,
3696
- `/users/${user.userId}/agents/${agent.agentId}/wallet`
3697
- );
3698
- if (!walletResponse?.agentWallet || !isAddress2(walletResponse.agentWallet.walletAddress)) {
3699
- throw this.invalidResponse("agent wallet");
3700
- }
3701
- const context = {
3702
- user,
3703
- agent,
3704
- wallet: walletResponse.agentWallet,
3705
- asset
3706
- };
3707
- this.agentContexts.set(this.contextKey(state, chainId, asset), context);
3708
- contexts.push(context);
3927
+ contexts.push(this.contextForAgent(state, chainId, user, agent, asset));
3709
3928
  }
3929
+ const resolvedContexts = await Promise.all(contexts);
3710
3930
  return Promise.all(
3711
- contexts.map(
3931
+ resolvedContexts.map(
3712
3932
  (context) => this.loadPortfolioContext(state, chainId, context, options)
3713
3933
  )
3714
3934
  );
3715
3935
  }
3716
3936
  async loadPortfolioContext(state, chainId, context, options = {}) {
3937
+ const contextKey = this.contextKey(state, chainId, context.asset);
3717
3938
  const [snapshot, positions, historic, actions] = await Promise.all([
3718
- this.walletRequest(
3719
- state,
3720
- chainId,
3721
- `${this.agentPath(context, "snapshot")}${query({
3722
- shouldOnlyUseRecentValue: true,
3723
- shouldAllowStaleOnError: true
3724
- })}`
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
+ }
3725
3956
  ),
3726
- this.walletRequest(
3727
- state,
3728
- chainId,
3729
- 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
+ }
3730
3971
  ),
3731
- options.historic ? this.walletRequest(
3732
- state,
3733
- chainId,
3734
- 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
+ )
3735
3980
  ) : Promise.resolve(void 0),
3736
- options.actions ? this.walletRequest(
3737
- state,
3738
- chainId,
3739
- 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
+ )
3740
3989
  ) : Promise.resolve(void 0)
3741
3990
  ]);
3742
3991
  if (!snapshot?.agentSnapshot) {
@@ -3770,6 +4019,13 @@ var YieldseekerAgent = class {
3770
4019
  context.wallet = deployed.agentWallet;
3771
4020
  }
3772
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();
3773
4029
  try {
3774
4030
  const response = await this.walletRequest(
3775
4031
  state,
@@ -3786,6 +4042,8 @@ var YieldseekerAgent = class {
3786
4042
  `[owney-sdk] Yieldseeker ${movement} snapshot refresh failed:`,
3787
4043
  error
3788
4044
  );
4045
+ } finally {
4046
+ invalidatePortfolio();
3789
4047
  }
3790
4048
  }
3791
4049
  agentPath(context, suffix) {
@@ -3836,6 +4094,7 @@ var YieldseekerAgent = class {
3836
4094
  code,
3837
4095
  `Yieldseeker request failed: ${error.providerCode}.`,
3838
4096
  {
4097
+ rpcSource: "agent-api",
3839
4098
  statusCode: error.status,
3840
4099
  providerCode: error.providerCode,
3841
4100
  ...error.responseFields ? { fields: error.responseFields } : {}
@@ -3854,12 +4113,12 @@ var YieldseekerAgent = class {
3854
4113
  chain: base3,
3855
4114
  transport: custom2(state.provider)
3856
4115
  });
3857
- const publicClient = createPublicClient3({
4116
+ const walletChainClient = createPublicClient3({
3858
4117
  chain: base3,
3859
4118
  transport: custom2(state.provider)
3860
4119
  });
3861
4120
  await ensureWalletOnChain(
3862
- publicClient,
4121
+ walletChainClient,
3863
4122
  walletClient,
3864
4123
  8453
3865
4124
  );
@@ -3870,10 +4129,15 @@ var YieldseekerAgent = class {
3870
4129
  data: transaction.data,
3871
4130
  value: BigInt(transaction.value)
3872
4131
  });
3873
- const receipt = await publicClient.waitForTransactionReceipt({
3874
- hash,
3875
- confirmations: 1
3876
- });
4132
+ const receipt = await withPaidRpcDiagnostics(
4133
+ () => this.getReceiptClient().waitForTransactionReceipt({
4134
+ hash,
4135
+ confirmations: 1
4136
+ }),
4137
+ 8453,
4138
+ "eth_getTransactionReceipt",
4139
+ this.id
4140
+ );
3877
4141
  if (receipt.status !== "success") {
3878
4142
  throw new OwneyError(
3879
4143
  "AGENT_TRANSACTION_REVERTED",
@@ -3889,14 +4153,15 @@ var YieldseekerAgent = class {
3889
4153
  await this.unwindReceiptWaiter(state, chainId, transactionHash);
3890
4154
  return;
3891
4155
  }
3892
- const publicClient = createPublicClient3({
3893
- chain: base3,
3894
- transport: custom2(state.provider)
3895
- });
3896
- const receipt = await publicClient.waitForTransactionReceipt({
3897
- hash: transactionHash,
3898
- confirmations: 1
3899
- });
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
+ );
3900
4165
  if (receipt.status !== "success") {
3901
4166
  throw new OwneyError(
3902
4167
  "AGENT_TRANSACTION_REVERTED",
@@ -4268,6 +4533,41 @@ import {
4268
4533
  // src/lib/permit2-batch.ts
4269
4534
  import { parseAbi as parseAbi2, hashStruct } from "viem";
4270
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
+ }
4271
4571
  var PERMIT_BATCH_TYPES = {
4272
4572
  PermitBatchWitnessTransferFrom: [
4273
4573
  { name: "permitted", type: "TokenPermissions[]" },
@@ -4419,28 +4719,67 @@ async function execute(i, key2, plan) {
4419
4719
  "DEPOSIT_INSUFFICIENT_BALANCE",
4420
4720
  "Insufficient token balance for this deposit."
4421
4721
  );
4422
- if (allowance < total)
4722
+ if (allowance < total && (i.chainId !== 8453 || !isAddressEqual(i.token, BASE_USDC_ADDRESS)))
4423
4723
  throw new OwneyError(
4424
4724
  "PERMIT2_APPROVAL_REQUIRED",
4425
4725
  "token deposits need a one-time Permit2 approval."
4426
4726
  );
4427
- 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({
4428
4767
  apiKey: i.apiKey,
4429
4768
  baseUrl: i.baseUrl,
4430
4769
  chainId: i.chainId
4431
4770
  });
4432
- const now = (await i.pub.getBlock()).timestamp;
4433
4771
  const unsigned = {
4434
4772
  chainId: i.chainId,
4435
4773
  token: i.token,
4436
4774
  from: i.owner,
4437
4775
  transfers: i.transfers,
4438
4776
  nonce: randomPermit2Nonce().toString(),
4439
- deadline: (now + 900n).toString()
4777
+ deadline: (now + 900n).toString(),
4778
+ ...erc2612Permit ? { erc2612Permit } : {}
4440
4779
  };
4441
4780
  const signature = await i.wallet.signTypedData({
4442
4781
  account: i.owner,
4443
- ...batchTypedData(unsigned, relayer)
4782
+ ...batchTypedData(unsigned, spender)
4444
4783
  });
4445
4784
  i.onApproved?.();
4446
4785
  return send({ ...unsigned, signature });
@@ -4460,8 +4799,8 @@ function makeSponsoredTokenCallback(deps) {
4460
4799
  "CHAIN_UNSUPPORTED",
4461
4800
  `No sponsored token configured for chain ${chainId}`
4462
4801
  );
4463
- const pub = deps.getPublicClient(chainId), wallet = deps.getWalletClient(chainId);
4464
- 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);
4465
4804
  return sponsorTokenBatch({
4466
4805
  apiKey: deps.apiKey,
4467
4806
  baseUrl: deps.baseUrl,
@@ -4701,6 +5040,8 @@ function makeSponsoredCallsCallback(deps) {
4701
5040
  }
4702
5041
 
4703
5042
  // src/client.ts
5043
+ var PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS = 6;
5044
+ var PERMIT2_ALLOWANCE_VERIFY_DELAY_MS = 250;
4704
5045
  function encodeMultiAgentCursor(map) {
4705
5046
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
4706
5047
  }
@@ -4761,6 +5102,7 @@ var OwneySDK = class {
4761
5102
  // leave every user's agent profile alone.
4762
5103
  orgAgentConfig;
4763
5104
  orgAgentConfigPromise = null;
5105
+ rpcUrls;
4764
5106
  zyfaiRpcUrls;
4765
5107
  yieldseekerApiBaseUrl;
4766
5108
  yieldseekerSiweOrigin;
@@ -4785,6 +5127,7 @@ var OwneySDK = class {
4785
5127
  constructor(config) {
4786
5128
  this.apiKey = config.apiKey;
4787
5129
  if (config.debug) setOwneyDebug(true);
5130
+ this.rpcUrls = config.rpcUrls;
4788
5131
  this.zyfaiRpcUrls = config.zyfaiRpcUrls;
4789
5132
  this.yieldseekerApiBaseUrl = config.yieldseekerApiBaseUrl;
4790
5133
  this.yieldseekerSiweOrigin = config.yieldseekerSiweOrigin;
@@ -4875,6 +5218,9 @@ var OwneySDK = class {
4875
5218
  }
4876
5219
  return this.state.provider;
4877
5220
  }
5221
+ getPaidRpcClient(chainId) {
5222
+ return createPaidRpcClient(VIEM_CHAIN2[chainId], this.rpcUrls);
5223
+ }
4878
5224
  /** Builds the default USDC batch callback for the connected wallet. */
4879
5225
  getDefaultSponsoredCallback(onApproved) {
4880
5226
  if (!onApproved && this.cachedSponsoredCallback)
@@ -4890,7 +5236,8 @@ var OwneySDK = class {
4890
5236
  // Casts work around viem's chain-narrowed Client vs the generic
4891
5237
  // PublicClient/WalletClient param types — structurally identical at
4892
5238
  // runtime, but the two share a name TS treats as unrelated.
4893
- getPublicClient: (cid) => createPublicClient4({
5239
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5240
+ getWalletChainClient: (cid) => createPublicClient4({
4894
5241
  chain: VIEM_CHAIN2[cid],
4895
5242
  transport: custom3(provider)
4896
5243
  }),
@@ -4940,7 +5287,8 @@ var OwneySDK = class {
4940
5287
  // Casts work around viem's chain-narrowed Client vs the generic
4941
5288
  // PublicClient/WalletClient param types — structurally identical at
4942
5289
  // runtime, but the two share a name TS treats as unrelated.
4943
- getPublicClient: (cid) => createPublicClient4({
5290
+ getPublicClient: (cid) => this.getPaidRpcClient(cid),
5291
+ getWalletChainClient: (cid) => createPublicClient4({
4944
5292
  chain: VIEM_CHAIN2[cid],
4945
5293
  transport: custom3(provider)
4946
5294
  }),
@@ -5053,12 +5401,17 @@ var OwneySDK = class {
5053
5401
  createAgent(agentId, key2) {
5054
5402
  if (agentId === "zyfai") {
5055
5403
  if (!key2) return null;
5056
- return new ZyfaiAgent(key2, this.zyfaiRpcUrls, this.referralSource);
5404
+ return new ZyfaiAgent(
5405
+ key2,
5406
+ this.rpcUrls ?? this.zyfaiRpcUrls,
5407
+ this.referralSource
5408
+ );
5057
5409
  }
5058
5410
  if (agentId === "yieldseeker") {
5059
5411
  return new YieldseekerAgent(this.apiKey, {
5060
5412
  auth: { origin: this.yieldseekerSiweOrigin },
5061
- baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl)
5413
+ baseUrl: this.yieldseekerApiBaseUrl ?? getYieldseekerProxyBaseUrl(this.routingApiBaseUrl),
5414
+ rpcUrls: this.rpcUrls
5062
5415
  });
5063
5416
  }
5064
5417
  return null;
@@ -5349,7 +5702,8 @@ var OwneySDK = class {
5349
5702
  );
5350
5703
  await this.approvePermit2(
5351
5704
  asset,
5352
- requiredAmount
5705
+ requiredAmount,
5706
+ cid
5353
5707
  );
5354
5708
  return batchTransfer(cid, transfers);
5355
5709
  }
@@ -5393,10 +5747,12 @@ var OwneySDK = class {
5393
5747
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
5394
5748
  * two independent auto-recovery mechanisms:
5395
5749
  *
5396
- * 1. Missing Permit2 allowance: when the app did not supply its own
5397
- * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
5398
- * token deposit, this is the wallet's first Permit2 deposit for that token. We send
5399
- * 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
5400
5756
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
5401
5757
  * per call so a wallet/agent that keeps reporting the allowance as
5402
5758
  * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
@@ -5439,7 +5795,8 @@ var OwneySDK = class {
5439
5795
  );
5440
5796
  await this.approvePermit2(
5441
5797
  asset,
5442
- BigInt(amount)
5798
+ BigInt(amount),
5799
+ chainId
5443
5800
  );
5444
5801
  continue;
5445
5802
  }
@@ -5586,6 +5943,32 @@ var OwneySDK = class {
5586
5943
  }
5587
5944
  return eligible;
5588
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
+ }
5589
5972
  // --- Fund operations ---
5590
5973
  /**
5591
5974
  * Withdraw funds from a specific agent, or all agents that support the active chain+asset if agentId is omitted.
@@ -5621,13 +6004,15 @@ var OwneySDK = class {
5621
6004
  );
5622
6005
  }
5623
6006
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
6007
+ const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
5624
6008
  if (!amount) {
5625
6009
  const results2 = {};
5626
6010
  const agentErrors2 = {};
5627
- for (const agent of eligibleAgents) {
6011
+ for (const agent of withdrawalAgents) {
5628
6012
  try {
5629
6013
  results2[agent.id] = await agent.withdraw(state, chainId, token);
5630
6014
  } catch (err) {
6015
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5631
6016
  console.error(`withdraw failed for agent "${agent.id}":`, err);
5632
6017
  agentErrors2[agent.id] = err instanceof Error ? err.message : String(err);
5633
6018
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5708,12 +6093,13 @@ var OwneySDK = class {
5708
6093
  planned: 0n
5709
6094
  }));
5710
6095
  const plans = [...disabledPlans, ...enabledPlans];
6096
+ const orderedPlans = this.orderAgentsForWithdrawal(plans.map((p) => p.agent)).map((agent) => plans.find((plan) => plan.agent === agent));
5711
6097
  const results = {};
5712
6098
  const agentErrors = {
5713
6099
  ...aggregated.agentErrors ?? {}
5714
6100
  };
5715
- for (let i = 0; i < plans.length; i++) {
5716
- const p = plans[i];
6101
+ for (let i = 0; i < orderedPlans.length; i++) {
6102
+ const p = orderedPlans[i];
5717
6103
  if (p.planned === 0n) continue;
5718
6104
  try {
5719
6105
  results[p.agent.id] = await p.agent.withdraw(
@@ -5723,6 +6109,7 @@ var OwneySDK = class {
5723
6109
  p.planned.toString()
5724
6110
  );
5725
6111
  } catch (err) {
6112
+ if (this.isUserRejectedWithdrawal(err)) throw err;
5726
6113
  console.error(`withdraw failed for agent "${p.agent.id}":`, err);
5727
6114
  agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
5728
6115
  const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
@@ -5734,7 +6121,7 @@ var OwneySDK = class {
5734
6121
  );
5735
6122
  const failedAmount = p.planned;
5736
6123
  p.planned = 0n;
5737
- redistributeShare(plans, i, failedAmount);
6124
+ redistributeShare(orderedPlans, i, failedAmount);
5738
6125
  }
5739
6126
  }
5740
6127
  if (Object.keys(results).length === 0) {
@@ -6162,14 +6549,16 @@ var OwneySDK = class {
6162
6549
  * User-paid approval of Permit2 on the selected token for the active chain.
6163
6550
  * Grants the maximum ERC20 allowance so later deposits do not require another
6164
6551
  * approval. Resolves after one confirmation so the subsequent deposit attempt
6165
- * sees the new allowance.
6552
+ * sees the new allowance. Deposit retries pass their captured chain id so a
6553
+ * concurrent activation cannot redirect the approval to another network.
6166
6554
  *
6167
6555
  * @param requiredAmount Raw base-unit amount the pending deposit must cover.
6556
+ * @param expectedChainId Chain captured by the deposit that requested approval.
6168
6557
  * @returns the approval transaction hash.
6169
6558
  */
6170
- async approvePermit2(asset = "WETH", requiredAmount = 0n) {
6559
+ async approvePermit2(asset = "WETH", requiredAmount = 0n, expectedChainId) {
6171
6560
  const state = this.requireState();
6172
- const chainId = this.requireChainId();
6561
+ const chainId = expectedChainId ?? this.requireChainId();
6173
6562
  this.getEligibleAgents(chainId, asset, { excludeDisabled: true });
6174
6563
  const token = sponsoredTokensFor(asset)[chainId];
6175
6564
  if (!token) {
@@ -6179,16 +6568,18 @@ var OwneySDK = class {
6179
6568
  );
6180
6569
  }
6181
6570
  const provider = this.requireConnectedProvider();
6182
- const publicClient = createPublicClient4({
6571
+ const walletChainClient = createPublicClient4({
6183
6572
  chain: VIEM_CHAIN2[chainId],
6184
6573
  transport: custom3(provider)
6185
6574
  });
6575
+ const publicClient = this.getPaidRpcClient(chainId);
6186
6576
  const approvalAmount = permit2ApprovalAmount(requiredAmount);
6187
6577
  const wallet = createWalletClient3({
6188
6578
  account: state.walletAddress,
6189
6579
  chain: VIEM_CHAIN2[chainId],
6190
6580
  transport: custom3(provider)
6191
6581
  });
6582
+ await ensureWalletOnChain(walletChainClient, wallet, chainId);
6192
6583
  const hash = await wallet.writeContract({
6193
6584
  address: token,
6194
6585
  abi: ERC20_ALLOWANCE_ABI,
@@ -6197,14 +6588,53 @@ var OwneySDK = class {
6197
6588
  account: state.walletAddress,
6198
6589
  chain: VIEM_CHAIN2[chainId]
6199
6590
  });
6200
- const receipt = await publicClient.waitForTransactionReceipt({
6201
- hash,
6202
- confirmations: 1
6203
- });
6591
+ const receipt = await withPaidRpcDiagnostics(
6592
+ () => publicClient.waitForTransactionReceipt({
6593
+ hash,
6594
+ confirmations: 1
6595
+ }),
6596
+ chainId,
6597
+ "eth_getTransactionReceipt"
6598
+ );
6204
6599
  if (receipt.status !== "success") {
6205
6600
  throw new Error(`Permit2 approval reverted (tx ${hash})`);
6206
6601
  }
6207
- return hash;
6602
+ let observedAllowance = 0n;
6603
+ let verificationError;
6604
+ for (let attempt = 0; attempt < PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS; attempt += 1) {
6605
+ try {
6606
+ observedAllowance = await readPermit2Allowance(
6607
+ publicClient,
6608
+ token,
6609
+ state.walletAddress,
6610
+ attempt === 0 ? receipt.blockNumber : void 0
6611
+ );
6612
+ verificationError = void 0;
6613
+ if (observedAllowance >= requiredAmount) return hash;
6614
+ } catch (error) {
6615
+ verificationError = error;
6616
+ }
6617
+ if (attempt + 1 < PERMIT2_ALLOWANCE_VERIFY_ATTEMPTS) {
6618
+ await new Promise(
6619
+ (resolve) => setTimeout(resolve, PERMIT2_ALLOWANCE_VERIFY_DELAY_MS)
6620
+ );
6621
+ }
6622
+ }
6623
+ throw new OwneyError(
6624
+ "PERMIT2_APPROVAL_REQUIRED",
6625
+ "Permit2 approval was confirmed, but the required token allowance was not observable.",
6626
+ {
6627
+ approvalConfirmed: true,
6628
+ approvalTxHash: hash,
6629
+ owner: state.walletAddress,
6630
+ token,
6631
+ spender: PERMIT2_ADDRESS,
6632
+ chainId,
6633
+ requiredAmount: requiredAmount.toString(),
6634
+ observedAllowance: observedAllowance.toString(),
6635
+ ...verificationError instanceof Error ? { verificationError: verificationError.message } : {}
6636
+ }
6637
+ );
6208
6638
  }
6209
6639
  // --- Discovery (no wallet required) ---
6210
6640
  /**