@owney/sdk 0.7.7-beta.1 → 0.7.7-beta.2

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.cjs CHANGED
@@ -1336,43 +1336,7 @@ var ZyfaiAgent = class {
1336
1336
  * recoverable (ensureWalletDeployed tries the other deploy call), but
1337
1337
  * failing the deposit outright is not.
1338
1338
  */
1339
- /**
1340
- * Zyfai's own answer to "what kind of wallet is this, and can the agent act on
1341
- * it", read from the positions payload (`GET /data/position`, which
1342
- * `getPositions` wraps).
1343
- *
1344
- * `predeployed` and `hasActiveSessionKey` are both returned at runtime but
1345
- * declared on neither `Portfolio` nor their docs, hence the cast. Verified
1346
- * live: an old wallet reports `{predeployed: false, hasActiveSessionKey:
1347
- * false}` while a pool wallet reports `{true, true}`.
1348
- *
1349
- * Preferred over the SDK's private `isPredeployed`, which is cached from the
1350
- * login response and can go stale mid-session. Returns an empty object on any
1351
- * failure so every caller degrades to its own fallback.
1352
- */
1353
- async readWalletState(ownerAddress) {
1354
- try {
1355
- const { portfolio } = await this.sdk.getPositions(ownerAddress);
1356
- const raw = portfolio;
1357
- return {
1358
- predeployed: raw?.predeployed,
1359
- hasActiveSessionKey: raw?.hasActiveSessionKey
1360
- };
1361
- } catch (error) {
1362
- console.warn(
1363
- "[zyfai] getPositions wallet-state read failed (non-fatal):",
1364
- error instanceof Error ? error.message : String(error)
1365
- );
1366
- return {};
1367
- }
1368
- }
1369
1339
  async isPredeployedPoolWallet(ownerAddress, smartWallet) {
1370
- const { predeployed } = await this.readWalletState(ownerAddress);
1371
- if (typeof predeployed === "boolean") return predeployed;
1372
- const internals = this.sdk;
1373
- if (typeof internals.isPredeployed === "boolean") {
1374
- return internals.isPredeployed;
1375
- }
1376
1340
  try {
1377
1341
  const byEoa = await this.sdk.getSmartWalletByEOA(ownerAddress);
1378
1342
  return typeof byEoa?.smartWallet === "string" && byEoa.smartWallet.toLowerCase() === smartWallet.toLowerCase();
@@ -1414,13 +1378,7 @@ var ZyfaiAgent = class {
1414
1378
  chainId
1415
1379
  );
1416
1380
  if (!wallet.isDeployed) return;
1417
- const { hasActiveSessionKey } = await this.readWalletState(ownerAddress);
1418
- await this.ensureSessionKey(
1419
- ownerAddress,
1420
- chainId,
1421
- wallet.address,
1422
- hasActiveSessionKey
1423
- );
1381
+ await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
1424
1382
  } catch (error) {
1425
1383
  console.warn(
1426
1384
  "[zyfai] session-key check at activation failed (non-fatal):",
@@ -1428,19 +1386,15 @@ var ZyfaiAgent = class {
1428
1386
  );
1429
1387
  }
1430
1388
  }
1431
- async ensureSessionKey(ownerAddress, chainId, smartWallet, hasActiveSessionKey) {
1389
+ async ensureSessionKey(ownerAddress, chainId, smartWallet) {
1432
1390
  try {
1433
- if (hasActiveSessionKey === false) {
1434
- console.warn(
1435
- `[zyfai] no session key on record for ${ownerAddress}; creating one for chain ${chainId}.`
1436
- );
1437
- await this.sdk.createSessionKey(ownerAddress, chainId);
1438
- return;
1439
- }
1440
- const installed = await this.readSmartSessionsInstalled(
1441
- chainId,
1442
- smartWallet
1443
- );
1391
+ const installed = await this.getPublicClient(chainId).readContract({
1392
+ address: smartWallet,
1393
+ abi: ERC7579_IS_MODULE_INSTALLED_ABI,
1394
+ functionName: "isModuleInstalled",
1395
+ // moduleTypeId 1 = validator (ERC-7579)
1396
+ args: [1n, import_sdk.SMART_SESSIONS_VALIDATOR, "0x"]
1397
+ });
1444
1398
  if (installed) return;
1445
1399
  console.warn(
1446
1400
  `[zyfai] Safe ${smartWallet} has no smart-sessions validator on chain ${chainId}; creating a session key so the agent can allocate.`
@@ -1453,16 +1407,6 @@ var ZyfaiAgent = class {
1453
1407
  );
1454
1408
  }
1455
1409
  }
1456
- /** Ground truth for "can Zyfai's automation act on this Safe, on this chain". */
1457
- async readSmartSessionsInstalled(chainId, smartWallet) {
1458
- return this.getPublicClient(chainId).readContract({
1459
- address: smartWallet,
1460
- abi: ERC7579_IS_MODULE_INSTALLED_ABI,
1461
- functionName: "isModuleInstalled",
1462
- // moduleTypeId 1 = validator (ERC-7579)
1463
- args: [1n, import_sdk.SMART_SESSIONS_VALIDATOR, "0x"]
1464
- });
1465
- }
1466
1410
  /**
1467
1411
  * Guarantee the user's Safe has code on `chainId` before anything transfers
1468
1412
  * funds into it, and return its address.
@@ -1484,13 +1428,7 @@ var ZyfaiAgent = class {
1484
1428
  async ensureWalletDeployed(ownerAddress, chainId) {
1485
1429
  const wallet = await this.sdk.getSmartWalletAddress(ownerAddress, chainId);
1486
1430
  if (wallet.isDeployed) {
1487
- const { hasActiveSessionKey } = await this.readWalletState(ownerAddress);
1488
- await this.ensureSessionKey(
1489
- ownerAddress,
1490
- chainId,
1491
- wallet.address,
1492
- hasActiveSessionKey
1493
- );
1431
+ await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
1494
1432
  return wallet.address;
1495
1433
  }
1496
1434
  const poolFirst = await this.isPredeployedPoolWallet(
@@ -1974,9 +1912,12 @@ async function postSponsorTransferAuth(input) {
1974
1912
  {
1975
1913
  statusCode: res.status,
1976
1914
  responseBody: text.slice(0, 500),
1977
- // 503 SPONSOR_UNAVAILABLE = relayer out of gas; the tx was rejected
1978
- // before broadcast, so it is safe to fall back to a user-paid deposit.
1979
- safeToFallback: res.status === 503
1915
+ // A 4xx is an explicit refusal before execution (DTO validation,
1916
+ // signature rejection, simulation) — nothing was broadcast, so a
1917
+ // user-paid retry cannot double-spend. 503 SPONSOR_UNAVAILABLE means
1918
+ // the relayer was out of gas, also pre-broadcast. Any other 5xx stays
1919
+ // ambiguous: the transfer may already be in flight.
1920
+ safeToFallback: res.status >= 400 && res.status < 500 || res.status === 503
1980
1921
  }
1981
1922
  );
1982
1923
  }
@@ -2248,9 +2189,9 @@ function makeSponsoredDepositCallback(deps) {
2248
2189
  };
2249
2190
  }
2250
2191
 
2251
- // src/lib/sponsored-weth-deposit.ts
2192
+ // src/lib/sponsored-permit2-deposit.ts
2252
2193
  var PERMIT_WINDOW_SECONDS = 15 * 60;
2253
- function makeSponsoredWethCallback(deps) {
2194
+ function makeSponsoredPermit2Callback(deps) {
2254
2195
  const get = deps.httpGet ?? getSponsorRelayerAddress;
2255
2196
  const post = deps.httpPost ?? postSponsorPermit2Transfer;
2256
2197
  return async (smartWallet, chainId, amount) => {
@@ -2259,7 +2200,7 @@ function makeSponsoredWethCallback(deps) {
2259
2200
  if (!token) {
2260
2201
  throw new OwneyError(
2261
2202
  "CHAIN_UNSUPPORTED",
2262
- `No sponsored WETH configured for chain ${chainId}`
2203
+ `No sponsored ${deps.assetLabel} configured for chain ${chainId}`
2263
2204
  );
2264
2205
  }
2265
2206
  const amountWei = BigInt(amount);
@@ -2271,14 +2212,14 @@ function makeSponsoredWethCallback(deps) {
2271
2212
  if (balance < amountWei) {
2272
2213
  throw new OwneyError(
2273
2214
  "DEPOSIT_INSUFFICIENT_BALANCE",
2274
- "Insufficient WETH balance for this deposit.",
2215
+ `Insufficient ${deps.assetLabel} balance for this deposit.`,
2275
2216
  { token, chainId: cid, balance: balance.toString(), amount }
2276
2217
  );
2277
2218
  }
2278
2219
  } catch (err) {
2279
2220
  if (err instanceof OwneyError) throw err;
2280
2221
  console.warn(
2281
- "[owney-sdk] WETH balance pre-check failed (non-fatal):",
2222
+ `[owney-sdk] ${deps.assetLabel} balance pre-check failed (non-fatal):`,
2282
2223
  err instanceof Error ? err.message : String(err)
2283
2224
  );
2284
2225
  }
@@ -2286,7 +2227,7 @@ function makeSponsoredWethCallback(deps) {
2286
2227
  if (allowance < amountWei) {
2287
2228
  throw new OwneyError(
2288
2229
  "PERMIT2_APPROVAL_REQUIRED",
2289
- "WETH gasless deposits need a one-time Permit2 approval; call approvePermit2() first",
2230
+ `${deps.assetLabel} gasless deposits need a one-time Permit2 approval; call approvePermit2("${deps.assetLabel}") first`,
2290
2231
  { token, chainId: cid, allowance: allowance.toString(), amount }
2291
2232
  );
2292
2233
  }
@@ -2367,6 +2308,8 @@ var SPONSORED_WETH_BY_CHAIN = {
2367
2308
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
2368
2309
  1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
2369
2310
  };
2311
+ var PERMIT2_ALLOWANCE_POLL_MAX_ATTEMPTS = 10;
2312
+ var PERMIT2_ALLOWANCE_POLL_INTERVAL_MS = 1500;
2370
2313
  function shouldFallbackToUserPaid(error, asset, appCallback) {
2371
2314
  return (asset === "WETH" || asset === "USDC") && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
2372
2315
  }
@@ -2391,7 +2334,7 @@ var OwneySDK = class {
2391
2334
  routingApiBaseUrl;
2392
2335
  referralSource;
2393
2336
  cachedSponsoredCallback = null;
2394
- cachedWethSponsoredCallback = null;
2337
+ cachedPermit2Callbacks = /* @__PURE__ */ new Map();
2395
2338
  initializingAgentsPromise = null;
2396
2339
  constructor(config) {
2397
2340
  this.apiKey = config.apiKey;
@@ -2419,7 +2362,7 @@ var OwneySDK = class {
2419
2362
  }
2420
2363
  this.state = { provider, walletAddress, chainId: null };
2421
2364
  this.cachedSponsoredCallback = null;
2422
- this.cachedWethSponsoredCallback = null;
2365
+ this.cachedPermit2Callbacks.clear();
2423
2366
  }
2424
2367
  /**
2425
2368
  * Disconnect from all agents and clear connection state.
@@ -2433,7 +2376,7 @@ var OwneySDK = class {
2433
2376
  this.disabledAgents.clear();
2434
2377
  this.state = null;
2435
2378
  this.cachedSponsoredCallback = null;
2436
- this.cachedWethSponsoredCallback = null;
2379
+ this.cachedPermit2Callbacks.clear();
2437
2380
  }
2438
2381
  isAgentDisabled(agentId) {
2439
2382
  return this.disabledAgents.has(agentId);
@@ -2510,25 +2453,23 @@ var OwneySDK = class {
2510
2453
  return callback;
2511
2454
  }
2512
2455
  /**
2513
- * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
2514
- * callback used when the caller omits `depositCallback` for a WETH
2515
- * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
2516
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
2456
+ * Lazily builds (and caches) the Permit2 sponsored callback for one asset.
2457
+ * Used for WETH always, and for USDC when the connected owner is a smart
2458
+ * account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
2459
+ * contract signature, while Permit2 supports ERC-1271.
2517
2460
  */
2518
- getDefaultWethSponsoredCallback(onApproved) {
2519
- if (!onApproved && this.cachedWethSponsoredCallback)
2520
- return this.cachedWethSponsoredCallback;
2461
+ getDefaultPermit2Callback(asset, onApproved) {
2462
+ const cached = this.cachedPermit2Callbacks.get(asset);
2463
+ if (!onApproved && cached) return cached;
2521
2464
  const provider = this.requireConnectedProvider();
2522
2465
  const owner = this.state.walletAddress;
2523
- const callback = makeSponsoredWethCallback({
2466
+ const callback = makeSponsoredPermit2Callback({
2524
2467
  apiKey: this.apiKey,
2525
2468
  baseUrl: this.routingApiBaseUrl,
2526
2469
  ownerAddress: owner,
2470
+ assetLabel: asset,
2527
2471
  onApproved,
2528
- tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
2529
- // Casts work around viem's chain-narrowed Client vs the generic
2530
- // PublicClient/WalletClient param types — structurally identical at
2531
- // runtime, but the two share a name TS treats as unrelated.
2472
+ tokenAddressByChain: asset === "USDC" ? SPONSORED_USDC_BY_CHAIN : SPONSORED_WETH_BY_CHAIN,
2532
2473
  getPublicClient: (cid) => (0, import_viem5.createPublicClient)({
2533
2474
  chain: VIEM_CHAIN2[cid],
2534
2475
  transport: (0, import_viem5.custom)(provider)
@@ -2539,9 +2480,34 @@ var OwneySDK = class {
2539
2480
  transport: (0, import_viem5.custom)(provider)
2540
2481
  })
2541
2482
  });
2542
- if (!onApproved) this.cachedWethSponsoredCallback = callback;
2483
+ if (!onApproved) this.cachedPermit2Callbacks.set(asset, callback);
2543
2484
  return callback;
2544
2485
  }
2486
+ /**
2487
+ * True when the connected owner has bytecode — a deployed smart wallet, or an
2488
+ * EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
2489
+ * Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
2490
+ *
2491
+ * Fails OPEN: an unreadable code check returns false so a working EOA deposit
2492
+ * is never blocked by a flaky provider.
2493
+ */
2494
+ async ownerHasCode() {
2495
+ try {
2496
+ const provider = this.requireConnectedProvider();
2497
+ const code = await provider.request({
2498
+ method: "eth_getCode",
2499
+ params: [this.state.walletAddress, "latest"]
2500
+ });
2501
+ return Boolean(code) && code !== "0x";
2502
+ } catch (error) {
2503
+ debugLog(
2504
+ "deposit",
2505
+ "owner code check failed; assuming EOA",
2506
+ error instanceof Error ? error.message : String(error)
2507
+ );
2508
+ return false;
2509
+ }
2510
+ }
2545
2511
  getAgent(agentId) {
2546
2512
  const agent = this.agents.get(agentId);
2547
2513
  if (!agent) {
@@ -2737,7 +2703,11 @@ var OwneySDK = class {
2737
2703
  }
2738
2704
  const state = this.requireState();
2739
2705
  const chainId = this.requireChainId();
2740
- const effectiveCallback = depositCallback ?? (asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
2706
+ const ownerIsSmartAccount = depositCallback === void 0 && asset !== "WETH" ? await this.ownerHasCode() : false;
2707
+ const effectiveCallback = depositCallback ?? (asset === "WETH" || ownerIsSmartAccount ? this.getDefaultPermit2Callback(
2708
+ asset === "WETH" ? "WETH" : "USDC",
2709
+ onApproved
2710
+ ) : this.getDefaultSponsoredCallback(onApproved));
2741
2711
  if (agentId) {
2742
2712
  if (this.isAgentDisabled(agentId)) {
2743
2713
  throw new OwneyError(
@@ -2806,13 +2776,16 @@ var OwneySDK = class {
2806
2776
  * 1. Missing Permit2 allowance: when the app did not supply its own
2807
2777
  * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
2808
2778
  * WETH deposit, this is the wallet's first gasless WETH deposit. We send
2809
- * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
2810
- * retry the SAME sponsored attempt once. Bounded to one approval attempt
2811
- * per call so a wallet/agent that keeps reporting the allowance as
2812
- * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
2813
- * the user rejects the wallet prompt), that error propagates as-is —
2814
- * the user said no to a transaction, so we must not turn around and ask
2815
- * them to pay for a different one via the fallback below.
2779
+ * the one-time (user-paid) Permit2 approval via `approvePermit2()`, wait
2780
+ * for the new allowance to actually be readable via
2781
+ * `waitForPermit2Allowance()` (the wallet's own RPC can lag a freshly
2782
+ * mined tx), then retry the SAME sponsored attempt once. Bounded to one
2783
+ * approval attempt per call so a wallet/agent that keeps reporting the
2784
+ * allowance as missing can't loop forever. If `approvePermit2()` itself
2785
+ * throws (e.g. the user rejects the wallet prompt), that error
2786
+ * propagates as-is — the user said no to a transaction, so we must not
2787
+ * turn around and ask them to pay for a different one via the fallback
2788
+ * below.
2816
2789
  * 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
2817
2790
  * the infrastructure layer in a provably safe-to-retry way, so we warn
2818
2791
  * and retry once with `undefined`, falling through to a user-paid
@@ -2824,6 +2797,49 @@ var OwneySDK = class {
2824
2797
  * propagates unchanged. Scoped per agent invocation so one agent's failure
2825
2798
  * doesn't force a retry of sibling agents in a multi-agent split.
2826
2799
  */
2800
+ /**
2801
+ * Polls the owner's Permit2 allowance for `asset` until it covers `amount`
2802
+ * or the attempt budget runs out. Called right after `approvePermit2()`
2803
+ * resolves: the approval tx is mined, but the wallet's own RPC can still
2804
+ * serve a stale (zero) allowance on the very next read (replica lag), which
2805
+ * would otherwise make the immediate retry fail with the same
2806
+ * PERMIT2_APPROVAL_REQUIRED error the user just approved past. Never
2807
+ * throws — if the allowance still hasn't caught up after the budget, the
2808
+ * caller's retry will surface the honest PERMIT2_APPROVAL_REQUIRED error
2809
+ * rather than this helper inventing a different one.
2810
+ */
2811
+ async waitForPermit2Allowance(asset, amount) {
2812
+ const state = this.requireState();
2813
+ const chainId = this.requireChainId();
2814
+ const provider = this.requireConnectedProvider();
2815
+ const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
2816
+ if (!token) return;
2817
+ const publicClient = (0, import_viem5.createPublicClient)({
2818
+ chain: VIEM_CHAIN2[chainId],
2819
+ transport: (0, import_viem5.custom)(provider)
2820
+ });
2821
+ for (let attempt = 0; attempt < PERMIT2_ALLOWANCE_POLL_MAX_ATTEMPTS; attempt++) {
2822
+ if (attempt > 0) {
2823
+ await new Promise(
2824
+ (resolve) => setTimeout(resolve, PERMIT2_ALLOWANCE_POLL_INTERVAL_MS)
2825
+ );
2826
+ }
2827
+ try {
2828
+ const allowance = await readPermit2Allowance(
2829
+ publicClient,
2830
+ token,
2831
+ state.walletAddress
2832
+ );
2833
+ if (allowance >= amount) return;
2834
+ } catch (error) {
2835
+ debugLog(
2836
+ "deposit",
2837
+ "Permit2 allowance poll read failed; retrying",
2838
+ error instanceof Error ? error.message : String(error)
2839
+ );
2840
+ }
2841
+ }
2842
+ }
2827
2843
  async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback) {
2828
2844
  const attempt = (callback) => withFailureReporting(
2829
2845
  this.apiKey,
@@ -2841,12 +2857,16 @@ var OwneySDK = class {
2841
2857
  try {
2842
2858
  return await attempt(effectiveCallback);
2843
2859
  } catch (error) {
2844
- if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
2860
+ if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
2845
2861
  approvalAttempted = true;
2846
2862
  console.warn(
2847
- "[owney-sdk] First WETH deposit: sending one-time Permit2 approval..."
2863
+ `[owney-sdk] First ${asset} gasless deposit: sending one-time Permit2 approval...`
2864
+ );
2865
+ await this.approvePermit2(asset);
2866
+ await this.waitForPermit2Allowance(
2867
+ asset,
2868
+ BigInt(amount)
2848
2869
  );
2849
- await this.approvePermit2();
2850
2870
  continue;
2851
2871
  }
2852
2872
  if (!shouldFallbackToUserPaid(error, asset, appCallback)) throw error;
@@ -3360,23 +3380,32 @@ var OwneySDK = class {
3360
3380
  return agent.ensureAutoSelectProtocols(state, chainId, asset);
3361
3381
  }
3362
3382
  /**
3363
- * One-time, user-paid approval of Permit2 on the sponsored WETH token for
3364
- * the active chain. Required once per wallet per chain before gasless WETH
3365
- * deposits; afterwards deposit() is signature-only. Resolves only after the
3366
- * approval transaction is mined (1 confirmation), so a subsequent deposit()
3367
- * will see the new allowance; throws if the transaction reverted.
3383
+ * One-time, user-paid approval of Permit2 on the given sponsored token for
3384
+ * the active chain. Required once per wallet per chain per token before a
3385
+ * gasless deposit of that asset over Permit2; afterwards deposit() is
3386
+ * signature-only for it. This call itself is a real transaction the user
3387
+ * pays gas for. Resolves only after the approval transaction is mined (1
3388
+ * confirmation), so a subsequent deposit() will see the new allowance;
3389
+ * throws if the transaction reverted.
3368
3390
  * @returns the approval transaction hash.
3369
3391
  */
3370
3392
  async approvePermit2(asset = "WETH") {
3371
- void asset;
3372
3393
  const state = this.requireState();
3373
3394
  const chainId = this.requireChainId();
3374
- this.validateAssetSupport(this.getAgent("zyfai"), chainId, "WETH");
3375
- const token = SPONSORED_WETH_BY_CHAIN[chainId];
3395
+ let guard = this.agents.get("zyfai");
3396
+ if (!guard) {
3397
+ try {
3398
+ guard = this.getEligibleAgents(chainId, asset)[0];
3399
+ } catch {
3400
+ guard = void 0;
3401
+ }
3402
+ }
3403
+ if (guard) this.validateAssetSupport(guard, chainId, asset);
3404
+ const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
3376
3405
  if (!token) {
3377
3406
  throw new OwneyError(
3378
3407
  "CHAIN_UNSUPPORTED",
3379
- `No sponsored WETH on chain ${chainId}`
3408
+ `No sponsored ${asset} on chain ${chainId}`
3380
3409
  );
3381
3410
  }
3382
3411
  const provider = this.requireConnectedProvider();
package/dist/index.d.cts CHANGED
@@ -392,7 +392,7 @@ declare class OwneySDK {
392
392
  private routingApiBaseUrl?;
393
393
  private referralSource?;
394
394
  private cachedSponsoredCallback;
395
- private cachedWethSponsoredCallback;
395
+ private cachedPermit2Callbacks;
396
396
  private initializingAgentsPromise;
397
397
  constructor(config: OwneySDKConfig);
398
398
  /**
@@ -428,12 +428,21 @@ declare class OwneySDK {
428
428
  */
429
429
  private getDefaultSponsoredCallback;
430
430
  /**
431
- * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
432
- * callback used when the caller omits `depositCallback` for a WETH
433
- * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
434
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
431
+ * Lazily builds (and caches) the Permit2 sponsored callback for one asset.
432
+ * Used for WETH always, and for USDC when the connected owner is a smart
433
+ * account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
434
+ * contract signature, while Permit2 supports ERC-1271.
435
435
  */
436
- private getDefaultWethSponsoredCallback;
436
+ private getDefaultPermit2Callback;
437
+ /**
438
+ * True when the connected owner has bytecode — a deployed smart wallet, or an
439
+ * EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
440
+ * Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
441
+ *
442
+ * Fails OPEN: an unreadable code check returns false so a working EOA deposit
443
+ * is never blocked by a flaky provider.
444
+ */
445
+ private ownerHasCode;
437
446
  private getAgent;
438
447
  private getActiveAgents;
439
448
  /**
@@ -483,13 +492,16 @@ declare class OwneySDK {
483
492
  * 1. Missing Permit2 allowance: when the app did not supply its own
484
493
  * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
485
494
  * WETH deposit, this is the wallet's first gasless WETH deposit. We send
486
- * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
487
- * retry the SAME sponsored attempt once. Bounded to one approval attempt
488
- * per call so a wallet/agent that keeps reporting the allowance as
489
- * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
490
- * the user rejects the wallet prompt), that error propagates as-is —
491
- * the user said no to a transaction, so we must not turn around and ask
492
- * them to pay for a different one via the fallback below.
495
+ * the one-time (user-paid) Permit2 approval via `approvePermit2()`, wait
496
+ * for the new allowance to actually be readable via
497
+ * `waitForPermit2Allowance()` (the wallet's own RPC can lag a freshly
498
+ * mined tx), then retry the SAME sponsored attempt once. Bounded to one
499
+ * approval attempt per call so a wallet/agent that keeps reporting the
500
+ * allowance as missing can't loop forever. If `approvePermit2()` itself
501
+ * throws (e.g. the user rejects the wallet prompt), that error
502
+ * propagates as-is — the user said no to a transaction, so we must not
503
+ * turn around and ask them to pay for a different one via the fallback
504
+ * below.
493
505
  * 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
494
506
  * the infrastructure layer in a provably safe-to-retry way, so we warn
495
507
  * and retry once with `undefined`, falling through to a user-paid
@@ -501,6 +513,18 @@ declare class OwneySDK {
501
513
  * propagates unchanged. Scoped per agent invocation so one agent's failure
502
514
  * doesn't force a retry of sibling agents in a multi-agent split.
503
515
  */
516
+ /**
517
+ * Polls the owner's Permit2 allowance for `asset` until it covers `amount`
518
+ * or the attempt budget runs out. Called right after `approvePermit2()`
519
+ * resolves: the approval tx is mined, but the wallet's own RPC can still
520
+ * serve a stale (zero) allowance on the very next read (replica lag), which
521
+ * would otherwise make the immediate retry fail with the same
522
+ * PERMIT2_APPROVAL_REQUIRED error the user just approved past. Never
523
+ * throws — if the allowance still hasn't caught up after the budget, the
524
+ * caller's retry will surface the honest PERMIT2_APPROVAL_REQUIRED error
525
+ * rather than this helper inventing a different one.
526
+ */
527
+ private waitForPermit2Allowance;
504
528
  private depositWithFallback;
505
529
  private getMinDepositAmount;
506
530
  private splitDepositAmount;
@@ -578,14 +602,16 @@ declare class OwneySDK {
578
602
  */
579
603
  ensureAutoSelectProtocols(asset: "USDC" | "WETH", agentId?: AgentId): Promise<boolean>;
580
604
  /**
581
- * One-time, user-paid approval of Permit2 on the sponsored WETH token for
582
- * the active chain. Required once per wallet per chain before gasless WETH
583
- * deposits; afterwards deposit() is signature-only. Resolves only after the
584
- * approval transaction is mined (1 confirmation), so a subsequent deposit()
585
- * will see the new allowance; throws if the transaction reverted.
605
+ * One-time, user-paid approval of Permit2 on the given sponsored token for
606
+ * the active chain. Required once per wallet per chain per token before a
607
+ * gasless deposit of that asset over Permit2; afterwards deposit() is
608
+ * signature-only for it. This call itself is a real transaction the user
609
+ * pays gas for. Resolves only after the approval transaction is mined (1
610
+ * confirmation), so a subsequent deposit() will see the new allowance;
611
+ * throws if the transaction reverted.
586
612
  * @returns the approval transaction hash.
587
613
  */
588
- approvePermit2(asset?: "WETH"): Promise<`0x${string}`>;
614
+ approvePermit2(asset?: "WETH" | "USDC"): Promise<`0x${string}`>;
589
615
  /**
590
616
  * Get the agent's average APY performance over a time period. Does not require a wallet connection.
591
617
  * @param options - Contains agentId (optional) and days ("7D", "14D", or "30D")
package/dist/index.d.ts CHANGED
@@ -392,7 +392,7 @@ declare class OwneySDK {
392
392
  private routingApiBaseUrl?;
393
393
  private referralSource?;
394
394
  private cachedSponsoredCallback;
395
- private cachedWethSponsoredCallback;
395
+ private cachedPermit2Callbacks;
396
396
  private initializingAgentsPromise;
397
397
  constructor(config: OwneySDKConfig);
398
398
  /**
@@ -428,12 +428,21 @@ declare class OwneySDK {
428
428
  */
429
429
  private getDefaultSponsoredCallback;
430
430
  /**
431
- * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
432
- * callback used when the caller omits `depositCallback` for a WETH
433
- * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
434
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
431
+ * Lazily builds (and caches) the Permit2 sponsored callback for one asset.
432
+ * Used for WETH always, and for USDC when the connected owner is a smart
433
+ * account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
434
+ * contract signature, while Permit2 supports ERC-1271.
435
435
  */
436
- private getDefaultWethSponsoredCallback;
436
+ private getDefaultPermit2Callback;
437
+ /**
438
+ * True when the connected owner has bytecode — a deployed smart wallet, or an
439
+ * EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
440
+ * Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
441
+ *
442
+ * Fails OPEN: an unreadable code check returns false so a working EOA deposit
443
+ * is never blocked by a flaky provider.
444
+ */
445
+ private ownerHasCode;
437
446
  private getAgent;
438
447
  private getActiveAgents;
439
448
  /**
@@ -483,13 +492,16 @@ declare class OwneySDK {
483
492
  * 1. Missing Permit2 allowance: when the app did not supply its own
484
493
  * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
485
494
  * WETH deposit, this is the wallet's first gasless WETH deposit. We send
486
- * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
487
- * retry the SAME sponsored attempt once. Bounded to one approval attempt
488
- * per call so a wallet/agent that keeps reporting the allowance as
489
- * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
490
- * the user rejects the wallet prompt), that error propagates as-is —
491
- * the user said no to a transaction, so we must not turn around and ask
492
- * them to pay for a different one via the fallback below.
495
+ * the one-time (user-paid) Permit2 approval via `approvePermit2()`, wait
496
+ * for the new allowance to actually be readable via
497
+ * `waitForPermit2Allowance()` (the wallet's own RPC can lag a freshly
498
+ * mined tx), then retry the SAME sponsored attempt once. Bounded to one
499
+ * approval attempt per call so a wallet/agent that keeps reporting the
500
+ * allowance as missing can't loop forever. If `approvePermit2()` itself
501
+ * throws (e.g. the user rejects the wallet prompt), that error
502
+ * propagates as-is — the user said no to a transaction, so we must not
503
+ * turn around and ask them to pay for a different one via the fallback
504
+ * below.
493
505
  * 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
494
506
  * the infrastructure layer in a provably safe-to-retry way, so we warn
495
507
  * and retry once with `undefined`, falling through to a user-paid
@@ -501,6 +513,18 @@ declare class OwneySDK {
501
513
  * propagates unchanged. Scoped per agent invocation so one agent's failure
502
514
  * doesn't force a retry of sibling agents in a multi-agent split.
503
515
  */
516
+ /**
517
+ * Polls the owner's Permit2 allowance for `asset` until it covers `amount`
518
+ * or the attempt budget runs out. Called right after `approvePermit2()`
519
+ * resolves: the approval tx is mined, but the wallet's own RPC can still
520
+ * serve a stale (zero) allowance on the very next read (replica lag), which
521
+ * would otherwise make the immediate retry fail with the same
522
+ * PERMIT2_APPROVAL_REQUIRED error the user just approved past. Never
523
+ * throws — if the allowance still hasn't caught up after the budget, the
524
+ * caller's retry will surface the honest PERMIT2_APPROVAL_REQUIRED error
525
+ * rather than this helper inventing a different one.
526
+ */
527
+ private waitForPermit2Allowance;
504
528
  private depositWithFallback;
505
529
  private getMinDepositAmount;
506
530
  private splitDepositAmount;
@@ -578,14 +602,16 @@ declare class OwneySDK {
578
602
  */
579
603
  ensureAutoSelectProtocols(asset: "USDC" | "WETH", agentId?: AgentId): Promise<boolean>;
580
604
  /**
581
- * One-time, user-paid approval of Permit2 on the sponsored WETH token for
582
- * the active chain. Required once per wallet per chain before gasless WETH
583
- * deposits; afterwards deposit() is signature-only. Resolves only after the
584
- * approval transaction is mined (1 confirmation), so a subsequent deposit()
585
- * will see the new allowance; throws if the transaction reverted.
605
+ * One-time, user-paid approval of Permit2 on the given sponsored token for
606
+ * the active chain. Required once per wallet per chain per token before a
607
+ * gasless deposit of that asset over Permit2; afterwards deposit() is
608
+ * signature-only for it. This call itself is a real transaction the user
609
+ * pays gas for. Resolves only after the approval transaction is mined (1
610
+ * confirmation), so a subsequent deposit() will see the new allowance;
611
+ * throws if the transaction reverted.
586
612
  * @returns the approval transaction hash.
587
613
  */
588
- approvePermit2(asset?: "WETH"): Promise<`0x${string}`>;
614
+ approvePermit2(asset?: "WETH" | "USDC"): Promise<`0x${string}`>;
589
615
  /**
590
616
  * Get the agent's average APY performance over a time period. Does not require a wallet connection.
591
617
  * @param options - Contains agentId (optional) and days ("7D", "14D", or "30D")
package/dist/index.js CHANGED
@@ -1303,43 +1303,7 @@ var ZyfaiAgent = class {
1303
1303
  * recoverable (ensureWalletDeployed tries the other deploy call), but
1304
1304
  * failing the deposit outright is not.
1305
1305
  */
1306
- /**
1307
- * Zyfai's own answer to "what kind of wallet is this, and can the agent act on
1308
- * it", read from the positions payload (`GET /data/position`, which
1309
- * `getPositions` wraps).
1310
- *
1311
- * `predeployed` and `hasActiveSessionKey` are both returned at runtime but
1312
- * declared on neither `Portfolio` nor their docs, hence the cast. Verified
1313
- * live: an old wallet reports `{predeployed: false, hasActiveSessionKey:
1314
- * false}` while a pool wallet reports `{true, true}`.
1315
- *
1316
- * Preferred over the SDK's private `isPredeployed`, which is cached from the
1317
- * login response and can go stale mid-session. Returns an empty object on any
1318
- * failure so every caller degrades to its own fallback.
1319
- */
1320
- async readWalletState(ownerAddress) {
1321
- try {
1322
- const { portfolio } = await this.sdk.getPositions(ownerAddress);
1323
- const raw = portfolio;
1324
- return {
1325
- predeployed: raw?.predeployed,
1326
- hasActiveSessionKey: raw?.hasActiveSessionKey
1327
- };
1328
- } catch (error) {
1329
- console.warn(
1330
- "[zyfai] getPositions wallet-state read failed (non-fatal):",
1331
- error instanceof Error ? error.message : String(error)
1332
- );
1333
- return {};
1334
- }
1335
- }
1336
1306
  async isPredeployedPoolWallet(ownerAddress, smartWallet) {
1337
- const { predeployed } = await this.readWalletState(ownerAddress);
1338
- if (typeof predeployed === "boolean") return predeployed;
1339
- const internals = this.sdk;
1340
- if (typeof internals.isPredeployed === "boolean") {
1341
- return internals.isPredeployed;
1342
- }
1343
1307
  try {
1344
1308
  const byEoa = await this.sdk.getSmartWalletByEOA(ownerAddress);
1345
1309
  return typeof byEoa?.smartWallet === "string" && byEoa.smartWallet.toLowerCase() === smartWallet.toLowerCase();
@@ -1381,13 +1345,7 @@ var ZyfaiAgent = class {
1381
1345
  chainId
1382
1346
  );
1383
1347
  if (!wallet.isDeployed) return;
1384
- const { hasActiveSessionKey } = await this.readWalletState(ownerAddress);
1385
- await this.ensureSessionKey(
1386
- ownerAddress,
1387
- chainId,
1388
- wallet.address,
1389
- hasActiveSessionKey
1390
- );
1348
+ await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
1391
1349
  } catch (error) {
1392
1350
  console.warn(
1393
1351
  "[zyfai] session-key check at activation failed (non-fatal):",
@@ -1395,19 +1353,15 @@ var ZyfaiAgent = class {
1395
1353
  );
1396
1354
  }
1397
1355
  }
1398
- async ensureSessionKey(ownerAddress, chainId, smartWallet, hasActiveSessionKey) {
1356
+ async ensureSessionKey(ownerAddress, chainId, smartWallet) {
1399
1357
  try {
1400
- if (hasActiveSessionKey === false) {
1401
- console.warn(
1402
- `[zyfai] no session key on record for ${ownerAddress}; creating one for chain ${chainId}.`
1403
- );
1404
- await this.sdk.createSessionKey(ownerAddress, chainId);
1405
- return;
1406
- }
1407
- const installed = await this.readSmartSessionsInstalled(
1408
- chainId,
1409
- smartWallet
1410
- );
1358
+ const installed = await this.getPublicClient(chainId).readContract({
1359
+ address: smartWallet,
1360
+ abi: ERC7579_IS_MODULE_INSTALLED_ABI,
1361
+ functionName: "isModuleInstalled",
1362
+ // moduleTypeId 1 = validator (ERC-7579)
1363
+ args: [1n, SMART_SESSIONS_VALIDATOR, "0x"]
1364
+ });
1411
1365
  if (installed) return;
1412
1366
  console.warn(
1413
1367
  `[zyfai] Safe ${smartWallet} has no smart-sessions validator on chain ${chainId}; creating a session key so the agent can allocate.`
@@ -1420,16 +1374,6 @@ var ZyfaiAgent = class {
1420
1374
  );
1421
1375
  }
1422
1376
  }
1423
- /** Ground truth for "can Zyfai's automation act on this Safe, on this chain". */
1424
- async readSmartSessionsInstalled(chainId, smartWallet) {
1425
- return this.getPublicClient(chainId).readContract({
1426
- address: smartWallet,
1427
- abi: ERC7579_IS_MODULE_INSTALLED_ABI,
1428
- functionName: "isModuleInstalled",
1429
- // moduleTypeId 1 = validator (ERC-7579)
1430
- args: [1n, SMART_SESSIONS_VALIDATOR, "0x"]
1431
- });
1432
- }
1433
1377
  /**
1434
1378
  * Guarantee the user's Safe has code on `chainId` before anything transfers
1435
1379
  * funds into it, and return its address.
@@ -1451,13 +1395,7 @@ var ZyfaiAgent = class {
1451
1395
  async ensureWalletDeployed(ownerAddress, chainId) {
1452
1396
  const wallet = await this.sdk.getSmartWalletAddress(ownerAddress, chainId);
1453
1397
  if (wallet.isDeployed) {
1454
- const { hasActiveSessionKey } = await this.readWalletState(ownerAddress);
1455
- await this.ensureSessionKey(
1456
- ownerAddress,
1457
- chainId,
1458
- wallet.address,
1459
- hasActiveSessionKey
1460
- );
1398
+ await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
1461
1399
  return wallet.address;
1462
1400
  }
1463
1401
  const poolFirst = await this.isPredeployedPoolWallet(
@@ -1945,9 +1883,12 @@ async function postSponsorTransferAuth(input) {
1945
1883
  {
1946
1884
  statusCode: res.status,
1947
1885
  responseBody: text.slice(0, 500),
1948
- // 503 SPONSOR_UNAVAILABLE = relayer out of gas; the tx was rejected
1949
- // before broadcast, so it is safe to fall back to a user-paid deposit.
1950
- safeToFallback: res.status === 503
1886
+ // A 4xx is an explicit refusal before execution (DTO validation,
1887
+ // signature rejection, simulation) — nothing was broadcast, so a
1888
+ // user-paid retry cannot double-spend. 503 SPONSOR_UNAVAILABLE means
1889
+ // the relayer was out of gas, also pre-broadcast. Any other 5xx stays
1890
+ // ambiguous: the transfer may already be in flight.
1891
+ safeToFallback: res.status >= 400 && res.status < 500 || res.status === 503
1951
1892
  }
1952
1893
  );
1953
1894
  }
@@ -2219,9 +2160,9 @@ function makeSponsoredDepositCallback(deps) {
2219
2160
  };
2220
2161
  }
2221
2162
 
2222
- // src/lib/sponsored-weth-deposit.ts
2163
+ // src/lib/sponsored-permit2-deposit.ts
2223
2164
  var PERMIT_WINDOW_SECONDS = 15 * 60;
2224
- function makeSponsoredWethCallback(deps) {
2165
+ function makeSponsoredPermit2Callback(deps) {
2225
2166
  const get = deps.httpGet ?? getSponsorRelayerAddress;
2226
2167
  const post = deps.httpPost ?? postSponsorPermit2Transfer;
2227
2168
  return async (smartWallet, chainId, amount) => {
@@ -2230,7 +2171,7 @@ function makeSponsoredWethCallback(deps) {
2230
2171
  if (!token) {
2231
2172
  throw new OwneyError(
2232
2173
  "CHAIN_UNSUPPORTED",
2233
- `No sponsored WETH configured for chain ${chainId}`
2174
+ `No sponsored ${deps.assetLabel} configured for chain ${chainId}`
2234
2175
  );
2235
2176
  }
2236
2177
  const amountWei = BigInt(amount);
@@ -2242,14 +2183,14 @@ function makeSponsoredWethCallback(deps) {
2242
2183
  if (balance < amountWei) {
2243
2184
  throw new OwneyError(
2244
2185
  "DEPOSIT_INSUFFICIENT_BALANCE",
2245
- "Insufficient WETH balance for this deposit.",
2186
+ `Insufficient ${deps.assetLabel} balance for this deposit.`,
2246
2187
  { token, chainId: cid, balance: balance.toString(), amount }
2247
2188
  );
2248
2189
  }
2249
2190
  } catch (err) {
2250
2191
  if (err instanceof OwneyError) throw err;
2251
2192
  console.warn(
2252
- "[owney-sdk] WETH balance pre-check failed (non-fatal):",
2193
+ `[owney-sdk] ${deps.assetLabel} balance pre-check failed (non-fatal):`,
2253
2194
  err instanceof Error ? err.message : String(err)
2254
2195
  );
2255
2196
  }
@@ -2257,7 +2198,7 @@ function makeSponsoredWethCallback(deps) {
2257
2198
  if (allowance < amountWei) {
2258
2199
  throw new OwneyError(
2259
2200
  "PERMIT2_APPROVAL_REQUIRED",
2260
- "WETH gasless deposits need a one-time Permit2 approval; call approvePermit2() first",
2201
+ `${deps.assetLabel} gasless deposits need a one-time Permit2 approval; call approvePermit2("${deps.assetLabel}") first`,
2261
2202
  { token, chainId: cid, allowance: allowance.toString(), amount }
2262
2203
  );
2263
2204
  }
@@ -2338,6 +2279,8 @@ var SPONSORED_WETH_BY_CHAIN = {
2338
2279
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
2339
2280
  1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
2340
2281
  };
2282
+ var PERMIT2_ALLOWANCE_POLL_MAX_ATTEMPTS = 10;
2283
+ var PERMIT2_ALLOWANCE_POLL_INTERVAL_MS = 1500;
2341
2284
  function shouldFallbackToUserPaid(error, asset, appCallback) {
2342
2285
  return (asset === "WETH" || asset === "USDC") && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
2343
2286
  }
@@ -2362,7 +2305,7 @@ var OwneySDK = class {
2362
2305
  routingApiBaseUrl;
2363
2306
  referralSource;
2364
2307
  cachedSponsoredCallback = null;
2365
- cachedWethSponsoredCallback = null;
2308
+ cachedPermit2Callbacks = /* @__PURE__ */ new Map();
2366
2309
  initializingAgentsPromise = null;
2367
2310
  constructor(config) {
2368
2311
  this.apiKey = config.apiKey;
@@ -2390,7 +2333,7 @@ var OwneySDK = class {
2390
2333
  }
2391
2334
  this.state = { provider, walletAddress, chainId: null };
2392
2335
  this.cachedSponsoredCallback = null;
2393
- this.cachedWethSponsoredCallback = null;
2336
+ this.cachedPermit2Callbacks.clear();
2394
2337
  }
2395
2338
  /**
2396
2339
  * Disconnect from all agents and clear connection state.
@@ -2404,7 +2347,7 @@ var OwneySDK = class {
2404
2347
  this.disabledAgents.clear();
2405
2348
  this.state = null;
2406
2349
  this.cachedSponsoredCallback = null;
2407
- this.cachedWethSponsoredCallback = null;
2350
+ this.cachedPermit2Callbacks.clear();
2408
2351
  }
2409
2352
  isAgentDisabled(agentId) {
2410
2353
  return this.disabledAgents.has(agentId);
@@ -2481,25 +2424,23 @@ var OwneySDK = class {
2481
2424
  return callback;
2482
2425
  }
2483
2426
  /**
2484
- * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
2485
- * callback used when the caller omits `depositCallback` for a WETH
2486
- * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
2487
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
2427
+ * Lazily builds (and caches) the Permit2 sponsored callback for one asset.
2428
+ * Used for WETH always, and for USDC when the connected owner is a smart
2429
+ * account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
2430
+ * contract signature, while Permit2 supports ERC-1271.
2488
2431
  */
2489
- getDefaultWethSponsoredCallback(onApproved) {
2490
- if (!onApproved && this.cachedWethSponsoredCallback)
2491
- return this.cachedWethSponsoredCallback;
2432
+ getDefaultPermit2Callback(asset, onApproved) {
2433
+ const cached = this.cachedPermit2Callbacks.get(asset);
2434
+ if (!onApproved && cached) return cached;
2492
2435
  const provider = this.requireConnectedProvider();
2493
2436
  const owner = this.state.walletAddress;
2494
- const callback = makeSponsoredWethCallback({
2437
+ const callback = makeSponsoredPermit2Callback({
2495
2438
  apiKey: this.apiKey,
2496
2439
  baseUrl: this.routingApiBaseUrl,
2497
2440
  ownerAddress: owner,
2441
+ assetLabel: asset,
2498
2442
  onApproved,
2499
- tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
2500
- // Casts work around viem's chain-narrowed Client vs the generic
2501
- // PublicClient/WalletClient param types — structurally identical at
2502
- // runtime, but the two share a name TS treats as unrelated.
2443
+ tokenAddressByChain: asset === "USDC" ? SPONSORED_USDC_BY_CHAIN : SPONSORED_WETH_BY_CHAIN,
2503
2444
  getPublicClient: (cid) => createPublicClient2({
2504
2445
  chain: VIEM_CHAIN2[cid],
2505
2446
  transport: custom(provider)
@@ -2510,9 +2451,34 @@ var OwneySDK = class {
2510
2451
  transport: custom(provider)
2511
2452
  })
2512
2453
  });
2513
- if (!onApproved) this.cachedWethSponsoredCallback = callback;
2454
+ if (!onApproved) this.cachedPermit2Callbacks.set(asset, callback);
2514
2455
  return callback;
2515
2456
  }
2457
+ /**
2458
+ * True when the connected owner has bytecode — a deployed smart wallet, or an
2459
+ * EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
2460
+ * Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
2461
+ *
2462
+ * Fails OPEN: an unreadable code check returns false so a working EOA deposit
2463
+ * is never blocked by a flaky provider.
2464
+ */
2465
+ async ownerHasCode() {
2466
+ try {
2467
+ const provider = this.requireConnectedProvider();
2468
+ const code = await provider.request({
2469
+ method: "eth_getCode",
2470
+ params: [this.state.walletAddress, "latest"]
2471
+ });
2472
+ return Boolean(code) && code !== "0x";
2473
+ } catch (error) {
2474
+ debugLog(
2475
+ "deposit",
2476
+ "owner code check failed; assuming EOA",
2477
+ error instanceof Error ? error.message : String(error)
2478
+ );
2479
+ return false;
2480
+ }
2481
+ }
2516
2482
  getAgent(agentId) {
2517
2483
  const agent = this.agents.get(agentId);
2518
2484
  if (!agent) {
@@ -2708,7 +2674,11 @@ var OwneySDK = class {
2708
2674
  }
2709
2675
  const state = this.requireState();
2710
2676
  const chainId = this.requireChainId();
2711
- const effectiveCallback = depositCallback ?? (asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
2677
+ const ownerIsSmartAccount = depositCallback === void 0 && asset !== "WETH" ? await this.ownerHasCode() : false;
2678
+ const effectiveCallback = depositCallback ?? (asset === "WETH" || ownerIsSmartAccount ? this.getDefaultPermit2Callback(
2679
+ asset === "WETH" ? "WETH" : "USDC",
2680
+ onApproved
2681
+ ) : this.getDefaultSponsoredCallback(onApproved));
2712
2682
  if (agentId) {
2713
2683
  if (this.isAgentDisabled(agentId)) {
2714
2684
  throw new OwneyError(
@@ -2777,13 +2747,16 @@ var OwneySDK = class {
2777
2747
  * 1. Missing Permit2 allowance: when the app did not supply its own
2778
2748
  * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
2779
2749
  * WETH deposit, this is the wallet's first gasless WETH deposit. We send
2780
- * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
2781
- * retry the SAME sponsored attempt once. Bounded to one approval attempt
2782
- * per call so a wallet/agent that keeps reporting the allowance as
2783
- * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
2784
- * the user rejects the wallet prompt), that error propagates as-is —
2785
- * the user said no to a transaction, so we must not turn around and ask
2786
- * them to pay for a different one via the fallback below.
2750
+ * the one-time (user-paid) Permit2 approval via `approvePermit2()`, wait
2751
+ * for the new allowance to actually be readable via
2752
+ * `waitForPermit2Allowance()` (the wallet's own RPC can lag a freshly
2753
+ * mined tx), then retry the SAME sponsored attempt once. Bounded to one
2754
+ * approval attempt per call so a wallet/agent that keeps reporting the
2755
+ * allowance as missing can't loop forever. If `approvePermit2()` itself
2756
+ * throws (e.g. the user rejects the wallet prompt), that error
2757
+ * propagates as-is — the user said no to a transaction, so we must not
2758
+ * turn around and ask them to pay for a different one via the fallback
2759
+ * below.
2787
2760
  * 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
2788
2761
  * the infrastructure layer in a provably safe-to-retry way, so we warn
2789
2762
  * and retry once with `undefined`, falling through to a user-paid
@@ -2795,6 +2768,49 @@ var OwneySDK = class {
2795
2768
  * propagates unchanged. Scoped per agent invocation so one agent's failure
2796
2769
  * doesn't force a retry of sibling agents in a multi-agent split.
2797
2770
  */
2771
+ /**
2772
+ * Polls the owner's Permit2 allowance for `asset` until it covers `amount`
2773
+ * or the attempt budget runs out. Called right after `approvePermit2()`
2774
+ * resolves: the approval tx is mined, but the wallet's own RPC can still
2775
+ * serve a stale (zero) allowance on the very next read (replica lag), which
2776
+ * would otherwise make the immediate retry fail with the same
2777
+ * PERMIT2_APPROVAL_REQUIRED error the user just approved past. Never
2778
+ * throws — if the allowance still hasn't caught up after the budget, the
2779
+ * caller's retry will surface the honest PERMIT2_APPROVAL_REQUIRED error
2780
+ * rather than this helper inventing a different one.
2781
+ */
2782
+ async waitForPermit2Allowance(asset, amount) {
2783
+ const state = this.requireState();
2784
+ const chainId = this.requireChainId();
2785
+ const provider = this.requireConnectedProvider();
2786
+ const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
2787
+ if (!token) return;
2788
+ const publicClient = createPublicClient2({
2789
+ chain: VIEM_CHAIN2[chainId],
2790
+ transport: custom(provider)
2791
+ });
2792
+ for (let attempt = 0; attempt < PERMIT2_ALLOWANCE_POLL_MAX_ATTEMPTS; attempt++) {
2793
+ if (attempt > 0) {
2794
+ await new Promise(
2795
+ (resolve) => setTimeout(resolve, PERMIT2_ALLOWANCE_POLL_INTERVAL_MS)
2796
+ );
2797
+ }
2798
+ try {
2799
+ const allowance = await readPermit2Allowance(
2800
+ publicClient,
2801
+ token,
2802
+ state.walletAddress
2803
+ );
2804
+ if (allowance >= amount) return;
2805
+ } catch (error) {
2806
+ debugLog(
2807
+ "deposit",
2808
+ "Permit2 allowance poll read failed; retrying",
2809
+ error instanceof Error ? error.message : String(error)
2810
+ );
2811
+ }
2812
+ }
2813
+ }
2798
2814
  async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback) {
2799
2815
  const attempt = (callback) => withFailureReporting(
2800
2816
  this.apiKey,
@@ -2812,12 +2828,16 @@ var OwneySDK = class {
2812
2828
  try {
2813
2829
  return await attempt(effectiveCallback);
2814
2830
  } catch (error) {
2815
- if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
2831
+ if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
2816
2832
  approvalAttempted = true;
2817
2833
  console.warn(
2818
- "[owney-sdk] First WETH deposit: sending one-time Permit2 approval..."
2834
+ `[owney-sdk] First ${asset} gasless deposit: sending one-time Permit2 approval...`
2835
+ );
2836
+ await this.approvePermit2(asset);
2837
+ await this.waitForPermit2Allowance(
2838
+ asset,
2839
+ BigInt(amount)
2819
2840
  );
2820
- await this.approvePermit2();
2821
2841
  continue;
2822
2842
  }
2823
2843
  if (!shouldFallbackToUserPaid(error, asset, appCallback)) throw error;
@@ -3331,23 +3351,32 @@ var OwneySDK = class {
3331
3351
  return agent.ensureAutoSelectProtocols(state, chainId, asset);
3332
3352
  }
3333
3353
  /**
3334
- * One-time, user-paid approval of Permit2 on the sponsored WETH token for
3335
- * the active chain. Required once per wallet per chain before gasless WETH
3336
- * deposits; afterwards deposit() is signature-only. Resolves only after the
3337
- * approval transaction is mined (1 confirmation), so a subsequent deposit()
3338
- * will see the new allowance; throws if the transaction reverted.
3354
+ * One-time, user-paid approval of Permit2 on the given sponsored token for
3355
+ * the active chain. Required once per wallet per chain per token before a
3356
+ * gasless deposit of that asset over Permit2; afterwards deposit() is
3357
+ * signature-only for it. This call itself is a real transaction the user
3358
+ * pays gas for. Resolves only after the approval transaction is mined (1
3359
+ * confirmation), so a subsequent deposit() will see the new allowance;
3360
+ * throws if the transaction reverted.
3339
3361
  * @returns the approval transaction hash.
3340
3362
  */
3341
3363
  async approvePermit2(asset = "WETH") {
3342
- void asset;
3343
3364
  const state = this.requireState();
3344
3365
  const chainId = this.requireChainId();
3345
- this.validateAssetSupport(this.getAgent("zyfai"), chainId, "WETH");
3346
- const token = SPONSORED_WETH_BY_CHAIN[chainId];
3366
+ let guard = this.agents.get("zyfai");
3367
+ if (!guard) {
3368
+ try {
3369
+ guard = this.getEligibleAgents(chainId, asset)[0];
3370
+ } catch {
3371
+ guard = void 0;
3372
+ }
3373
+ }
3374
+ if (guard) this.validateAssetSupport(guard, chainId, asset);
3375
+ const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
3347
3376
  if (!token) {
3348
3377
  throw new OwneyError(
3349
3378
  "CHAIN_UNSUPPORTED",
3350
- `No sponsored WETH on chain ${chainId}`
3379
+ `No sponsored ${asset} on chain ${chainId}`
3351
3380
  );
3352
3381
  }
3353
3382
  const provider = this.requireConnectedProvider();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.7.7-beta.1",
3
+ "version": "0.7.7-beta.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",