@owney/sdk 0.7.6 → 0.7.7-beta.0

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
@@ -1912,9 +1912,12 @@ async function postSponsorTransferAuth(input) {
1912
1912
  {
1913
1913
  statusCode: res.status,
1914
1914
  responseBody: text.slice(0, 500),
1915
- // 503 SPONSOR_UNAVAILABLE = relayer out of gas; the tx was rejected
1916
- // before broadcast, so it is safe to fall back to a user-paid deposit.
1917
- 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
1918
1921
  }
1919
1922
  );
1920
1923
  }
@@ -2186,9 +2189,9 @@ function makeSponsoredDepositCallback(deps) {
2186
2189
  };
2187
2190
  }
2188
2191
 
2189
- // src/lib/sponsored-weth-deposit.ts
2192
+ // src/lib/sponsored-permit2-deposit.ts
2190
2193
  var PERMIT_WINDOW_SECONDS = 15 * 60;
2191
- function makeSponsoredWethCallback(deps) {
2194
+ function makeSponsoredPermit2Callback(deps) {
2192
2195
  const get = deps.httpGet ?? getSponsorRelayerAddress;
2193
2196
  const post = deps.httpPost ?? postSponsorPermit2Transfer;
2194
2197
  return async (smartWallet, chainId, amount) => {
@@ -2197,7 +2200,7 @@ function makeSponsoredWethCallback(deps) {
2197
2200
  if (!token) {
2198
2201
  throw new OwneyError(
2199
2202
  "CHAIN_UNSUPPORTED",
2200
- `No sponsored WETH configured for chain ${chainId}`
2203
+ `No sponsored ${deps.assetLabel} configured for chain ${chainId}`
2201
2204
  );
2202
2205
  }
2203
2206
  const amountWei = BigInt(amount);
@@ -2209,14 +2212,14 @@ function makeSponsoredWethCallback(deps) {
2209
2212
  if (balance < amountWei) {
2210
2213
  throw new OwneyError(
2211
2214
  "DEPOSIT_INSUFFICIENT_BALANCE",
2212
- "Insufficient WETH balance for this deposit.",
2215
+ `Insufficient ${deps.assetLabel} balance for this deposit.`,
2213
2216
  { token, chainId: cid, balance: balance.toString(), amount }
2214
2217
  );
2215
2218
  }
2216
2219
  } catch (err) {
2217
2220
  if (err instanceof OwneyError) throw err;
2218
2221
  console.warn(
2219
- "[owney-sdk] WETH balance pre-check failed (non-fatal):",
2222
+ `[owney-sdk] ${deps.assetLabel} balance pre-check failed (non-fatal):`,
2220
2223
  err instanceof Error ? err.message : String(err)
2221
2224
  );
2222
2225
  }
@@ -2224,7 +2227,7 @@ function makeSponsoredWethCallback(deps) {
2224
2227
  if (allowance < amountWei) {
2225
2228
  throw new OwneyError(
2226
2229
  "PERMIT2_APPROVAL_REQUIRED",
2227
- "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`,
2228
2231
  { token, chainId: cid, allowance: allowance.toString(), amount }
2229
2232
  );
2230
2233
  }
@@ -2329,7 +2332,7 @@ var OwneySDK = class {
2329
2332
  routingApiBaseUrl;
2330
2333
  referralSource;
2331
2334
  cachedSponsoredCallback = null;
2332
- cachedWethSponsoredCallback = null;
2335
+ cachedPermit2Callbacks = /* @__PURE__ */ new Map();
2333
2336
  initializingAgentsPromise = null;
2334
2337
  constructor(config) {
2335
2338
  this.apiKey = config.apiKey;
@@ -2357,7 +2360,7 @@ var OwneySDK = class {
2357
2360
  }
2358
2361
  this.state = { provider, walletAddress, chainId: null };
2359
2362
  this.cachedSponsoredCallback = null;
2360
- this.cachedWethSponsoredCallback = null;
2363
+ this.cachedPermit2Callbacks.clear();
2361
2364
  }
2362
2365
  /**
2363
2366
  * Disconnect from all agents and clear connection state.
@@ -2371,7 +2374,7 @@ var OwneySDK = class {
2371
2374
  this.disabledAgents.clear();
2372
2375
  this.state = null;
2373
2376
  this.cachedSponsoredCallback = null;
2374
- this.cachedWethSponsoredCallback = null;
2377
+ this.cachedPermit2Callbacks.clear();
2375
2378
  }
2376
2379
  isAgentDisabled(agentId) {
2377
2380
  return this.disabledAgents.has(agentId);
@@ -2448,25 +2451,23 @@ var OwneySDK = class {
2448
2451
  return callback;
2449
2452
  }
2450
2453
  /**
2451
- * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
2452
- * callback used when the caller omits `depositCallback` for a WETH
2453
- * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
2454
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
2454
+ * Lazily builds (and caches) the Permit2 sponsored callback for one asset.
2455
+ * Used for WETH always, and for USDC when the connected owner is a smart
2456
+ * account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
2457
+ * contract signature, while Permit2 supports ERC-1271.
2455
2458
  */
2456
- getDefaultWethSponsoredCallback(onApproved) {
2457
- if (!onApproved && this.cachedWethSponsoredCallback)
2458
- return this.cachedWethSponsoredCallback;
2459
+ getDefaultPermit2Callback(asset, onApproved) {
2460
+ const cached = this.cachedPermit2Callbacks.get(asset);
2461
+ if (!onApproved && cached) return cached;
2459
2462
  const provider = this.requireConnectedProvider();
2460
2463
  const owner = this.state.walletAddress;
2461
- const callback = makeSponsoredWethCallback({
2464
+ const callback = makeSponsoredPermit2Callback({
2462
2465
  apiKey: this.apiKey,
2463
2466
  baseUrl: this.routingApiBaseUrl,
2464
2467
  ownerAddress: owner,
2468
+ assetLabel: asset,
2465
2469
  onApproved,
2466
- tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
2467
- // Casts work around viem's chain-narrowed Client vs the generic
2468
- // PublicClient/WalletClient param types — structurally identical at
2469
- // runtime, but the two share a name TS treats as unrelated.
2470
+ tokenAddressByChain: asset === "USDC" ? SPONSORED_USDC_BY_CHAIN : SPONSORED_WETH_BY_CHAIN,
2470
2471
  getPublicClient: (cid) => (0, import_viem5.createPublicClient)({
2471
2472
  chain: VIEM_CHAIN2[cid],
2472
2473
  transport: (0, import_viem5.custom)(provider)
@@ -2477,9 +2478,34 @@ var OwneySDK = class {
2477
2478
  transport: (0, import_viem5.custom)(provider)
2478
2479
  })
2479
2480
  });
2480
- if (!onApproved) this.cachedWethSponsoredCallback = callback;
2481
+ if (!onApproved) this.cachedPermit2Callbacks.set(asset, callback);
2481
2482
  return callback;
2482
2483
  }
2484
+ /**
2485
+ * True when the connected owner has bytecode — a deployed smart wallet, or an
2486
+ * EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
2487
+ * Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
2488
+ *
2489
+ * Fails OPEN: an unreadable code check returns false so a working EOA deposit
2490
+ * is never blocked by a flaky provider.
2491
+ */
2492
+ async ownerHasCode() {
2493
+ try {
2494
+ const provider = this.requireConnectedProvider();
2495
+ const code = await provider.request({
2496
+ method: "eth_getCode",
2497
+ params: [this.state.walletAddress, "latest"]
2498
+ });
2499
+ return Boolean(code) && code !== "0x";
2500
+ } catch (error) {
2501
+ debugLog(
2502
+ "deposit",
2503
+ "owner code check failed; assuming EOA",
2504
+ error instanceof Error ? error.message : String(error)
2505
+ );
2506
+ return false;
2507
+ }
2508
+ }
2483
2509
  getAgent(agentId) {
2484
2510
  const agent = this.agents.get(agentId);
2485
2511
  if (!agent) {
@@ -2675,7 +2701,11 @@ var OwneySDK = class {
2675
2701
  }
2676
2702
  const state = this.requireState();
2677
2703
  const chainId = this.requireChainId();
2678
- const effectiveCallback = depositCallback ?? (asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
2704
+ const ownerIsSmartAccount = depositCallback === void 0 && asset !== "WETH" ? await this.ownerHasCode() : false;
2705
+ const effectiveCallback = depositCallback ?? (asset === "WETH" || ownerIsSmartAccount ? this.getDefaultPermit2Callback(
2706
+ asset === "WETH" ? "WETH" : "USDC",
2707
+ onApproved
2708
+ ) : this.getDefaultSponsoredCallback(onApproved));
2679
2709
  if (agentId) {
2680
2710
  if (this.isAgentDisabled(agentId)) {
2681
2711
  throw new OwneyError(
@@ -2779,12 +2809,12 @@ var OwneySDK = class {
2779
2809
  try {
2780
2810
  return await attempt(effectiveCallback);
2781
2811
  } catch (error) {
2782
- if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
2812
+ if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
2783
2813
  approvalAttempted = true;
2784
2814
  console.warn(
2785
- "[owney-sdk] First WETH deposit: sending one-time Permit2 approval..."
2815
+ `[owney-sdk] First ${asset} gasless deposit: sending one-time Permit2 approval...`
2786
2816
  );
2787
- await this.approvePermit2();
2817
+ await this.approvePermit2(asset);
2788
2818
  continue;
2789
2819
  }
2790
2820
  if (!shouldFallbackToUserPaid(error, asset, appCallback)) throw error;
@@ -3298,23 +3328,32 @@ var OwneySDK = class {
3298
3328
  return agent.ensureAutoSelectProtocols(state, chainId, asset);
3299
3329
  }
3300
3330
  /**
3301
- * One-time, user-paid approval of Permit2 on the sponsored WETH token for
3302
- * the active chain. Required once per wallet per chain before gasless WETH
3303
- * deposits; afterwards deposit() is signature-only. Resolves only after the
3304
- * approval transaction is mined (1 confirmation), so a subsequent deposit()
3305
- * will see the new allowance; throws if the transaction reverted.
3331
+ * One-time, user-paid approval of Permit2 on the given sponsored token for
3332
+ * the active chain. Required once per wallet per chain per token before a
3333
+ * gasless deposit of that asset over Permit2; afterwards deposit() is
3334
+ * signature-only for it. This call itself is a real transaction the user
3335
+ * pays gas for. Resolves only after the approval transaction is mined (1
3336
+ * confirmation), so a subsequent deposit() will see the new allowance;
3337
+ * throws if the transaction reverted.
3306
3338
  * @returns the approval transaction hash.
3307
3339
  */
3308
3340
  async approvePermit2(asset = "WETH") {
3309
- void asset;
3310
3341
  const state = this.requireState();
3311
3342
  const chainId = this.requireChainId();
3312
- this.validateAssetSupport(this.getAgent("zyfai"), chainId, "WETH");
3313
- const token = SPONSORED_WETH_BY_CHAIN[chainId];
3343
+ let guard = this.agents.get("zyfai");
3344
+ if (!guard) {
3345
+ try {
3346
+ guard = this.getEligibleAgents(chainId, asset)[0];
3347
+ } catch {
3348
+ guard = void 0;
3349
+ }
3350
+ }
3351
+ if (guard) this.validateAssetSupport(guard, chainId, asset);
3352
+ const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
3314
3353
  if (!token) {
3315
3354
  throw new OwneyError(
3316
3355
  "CHAIN_UNSUPPORTED",
3317
- `No sponsored WETH on chain ${chainId}`
3356
+ `No sponsored ${asset} on chain ${chainId}`
3318
3357
  );
3319
3358
  }
3320
3359
  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
  /**
@@ -578,14 +587,16 @@ declare class OwneySDK {
578
587
  */
579
588
  ensureAutoSelectProtocols(asset: "USDC" | "WETH", agentId?: AgentId): Promise<boolean>;
580
589
  /**
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.
590
+ * One-time, user-paid approval of Permit2 on the given sponsored token for
591
+ * the active chain. Required once per wallet per chain per token before a
592
+ * gasless deposit of that asset over Permit2; afterwards deposit() is
593
+ * signature-only for it. This call itself is a real transaction the user
594
+ * pays gas for. Resolves only after the approval transaction is mined (1
595
+ * confirmation), so a subsequent deposit() will see the new allowance;
596
+ * throws if the transaction reverted.
586
597
  * @returns the approval transaction hash.
587
598
  */
588
- approvePermit2(asset?: "WETH"): Promise<`0x${string}`>;
599
+ approvePermit2(asset?: "WETH" | "USDC"): Promise<`0x${string}`>;
589
600
  /**
590
601
  * Get the agent's average APY performance over a time period. Does not require a wallet connection.
591
602
  * @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
  /**
@@ -578,14 +587,16 @@ declare class OwneySDK {
578
587
  */
579
588
  ensureAutoSelectProtocols(asset: "USDC" | "WETH", agentId?: AgentId): Promise<boolean>;
580
589
  /**
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.
590
+ * One-time, user-paid approval of Permit2 on the given sponsored token for
591
+ * the active chain. Required once per wallet per chain per token before a
592
+ * gasless deposit of that asset over Permit2; afterwards deposit() is
593
+ * signature-only for it. This call itself is a real transaction the user
594
+ * pays gas for. Resolves only after the approval transaction is mined (1
595
+ * confirmation), so a subsequent deposit() will see the new allowance;
596
+ * throws if the transaction reverted.
586
597
  * @returns the approval transaction hash.
587
598
  */
588
- approvePermit2(asset?: "WETH"): Promise<`0x${string}`>;
599
+ approvePermit2(asset?: "WETH" | "USDC"): Promise<`0x${string}`>;
589
600
  /**
590
601
  * Get the agent's average APY performance over a time period. Does not require a wallet connection.
591
602
  * @param options - Contains agentId (optional) and days ("7D", "14D", or "30D")
package/dist/index.js CHANGED
@@ -1883,9 +1883,12 @@ async function postSponsorTransferAuth(input) {
1883
1883
  {
1884
1884
  statusCode: res.status,
1885
1885
  responseBody: text.slice(0, 500),
1886
- // 503 SPONSOR_UNAVAILABLE = relayer out of gas; the tx was rejected
1887
- // before broadcast, so it is safe to fall back to a user-paid deposit.
1888
- 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
1889
1892
  }
1890
1893
  );
1891
1894
  }
@@ -2157,9 +2160,9 @@ function makeSponsoredDepositCallback(deps) {
2157
2160
  };
2158
2161
  }
2159
2162
 
2160
- // src/lib/sponsored-weth-deposit.ts
2163
+ // src/lib/sponsored-permit2-deposit.ts
2161
2164
  var PERMIT_WINDOW_SECONDS = 15 * 60;
2162
- function makeSponsoredWethCallback(deps) {
2165
+ function makeSponsoredPermit2Callback(deps) {
2163
2166
  const get = deps.httpGet ?? getSponsorRelayerAddress;
2164
2167
  const post = deps.httpPost ?? postSponsorPermit2Transfer;
2165
2168
  return async (smartWallet, chainId, amount) => {
@@ -2168,7 +2171,7 @@ function makeSponsoredWethCallback(deps) {
2168
2171
  if (!token) {
2169
2172
  throw new OwneyError(
2170
2173
  "CHAIN_UNSUPPORTED",
2171
- `No sponsored WETH configured for chain ${chainId}`
2174
+ `No sponsored ${deps.assetLabel} configured for chain ${chainId}`
2172
2175
  );
2173
2176
  }
2174
2177
  const amountWei = BigInt(amount);
@@ -2180,14 +2183,14 @@ function makeSponsoredWethCallback(deps) {
2180
2183
  if (balance < amountWei) {
2181
2184
  throw new OwneyError(
2182
2185
  "DEPOSIT_INSUFFICIENT_BALANCE",
2183
- "Insufficient WETH balance for this deposit.",
2186
+ `Insufficient ${deps.assetLabel} balance for this deposit.`,
2184
2187
  { token, chainId: cid, balance: balance.toString(), amount }
2185
2188
  );
2186
2189
  }
2187
2190
  } catch (err) {
2188
2191
  if (err instanceof OwneyError) throw err;
2189
2192
  console.warn(
2190
- "[owney-sdk] WETH balance pre-check failed (non-fatal):",
2193
+ `[owney-sdk] ${deps.assetLabel} balance pre-check failed (non-fatal):`,
2191
2194
  err instanceof Error ? err.message : String(err)
2192
2195
  );
2193
2196
  }
@@ -2195,7 +2198,7 @@ function makeSponsoredWethCallback(deps) {
2195
2198
  if (allowance < amountWei) {
2196
2199
  throw new OwneyError(
2197
2200
  "PERMIT2_APPROVAL_REQUIRED",
2198
- "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`,
2199
2202
  { token, chainId: cid, allowance: allowance.toString(), amount }
2200
2203
  );
2201
2204
  }
@@ -2300,7 +2303,7 @@ var OwneySDK = class {
2300
2303
  routingApiBaseUrl;
2301
2304
  referralSource;
2302
2305
  cachedSponsoredCallback = null;
2303
- cachedWethSponsoredCallback = null;
2306
+ cachedPermit2Callbacks = /* @__PURE__ */ new Map();
2304
2307
  initializingAgentsPromise = null;
2305
2308
  constructor(config) {
2306
2309
  this.apiKey = config.apiKey;
@@ -2328,7 +2331,7 @@ var OwneySDK = class {
2328
2331
  }
2329
2332
  this.state = { provider, walletAddress, chainId: null };
2330
2333
  this.cachedSponsoredCallback = null;
2331
- this.cachedWethSponsoredCallback = null;
2334
+ this.cachedPermit2Callbacks.clear();
2332
2335
  }
2333
2336
  /**
2334
2337
  * Disconnect from all agents and clear connection state.
@@ -2342,7 +2345,7 @@ var OwneySDK = class {
2342
2345
  this.disabledAgents.clear();
2343
2346
  this.state = null;
2344
2347
  this.cachedSponsoredCallback = null;
2345
- this.cachedWethSponsoredCallback = null;
2348
+ this.cachedPermit2Callbacks.clear();
2346
2349
  }
2347
2350
  isAgentDisabled(agentId) {
2348
2351
  return this.disabledAgents.has(agentId);
@@ -2419,25 +2422,23 @@ var OwneySDK = class {
2419
2422
  return callback;
2420
2423
  }
2421
2424
  /**
2422
- * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
2423
- * callback used when the caller omits `depositCallback` for a WETH
2424
- * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
2425
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
2425
+ * Lazily builds (and caches) the Permit2 sponsored callback for one asset.
2426
+ * Used for WETH always, and for USDC when the connected owner is a smart
2427
+ * account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
2428
+ * contract signature, while Permit2 supports ERC-1271.
2426
2429
  */
2427
- getDefaultWethSponsoredCallback(onApproved) {
2428
- if (!onApproved && this.cachedWethSponsoredCallback)
2429
- return this.cachedWethSponsoredCallback;
2430
+ getDefaultPermit2Callback(asset, onApproved) {
2431
+ const cached = this.cachedPermit2Callbacks.get(asset);
2432
+ if (!onApproved && cached) return cached;
2430
2433
  const provider = this.requireConnectedProvider();
2431
2434
  const owner = this.state.walletAddress;
2432
- const callback = makeSponsoredWethCallback({
2435
+ const callback = makeSponsoredPermit2Callback({
2433
2436
  apiKey: this.apiKey,
2434
2437
  baseUrl: this.routingApiBaseUrl,
2435
2438
  ownerAddress: owner,
2439
+ assetLabel: asset,
2436
2440
  onApproved,
2437
- tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
2438
- // Casts work around viem's chain-narrowed Client vs the generic
2439
- // PublicClient/WalletClient param types — structurally identical at
2440
- // runtime, but the two share a name TS treats as unrelated.
2441
+ tokenAddressByChain: asset === "USDC" ? SPONSORED_USDC_BY_CHAIN : SPONSORED_WETH_BY_CHAIN,
2441
2442
  getPublicClient: (cid) => createPublicClient2({
2442
2443
  chain: VIEM_CHAIN2[cid],
2443
2444
  transport: custom(provider)
@@ -2448,9 +2449,34 @@ var OwneySDK = class {
2448
2449
  transport: custom(provider)
2449
2450
  })
2450
2451
  });
2451
- if (!onApproved) this.cachedWethSponsoredCallback = callback;
2452
+ if (!onApproved) this.cachedPermit2Callbacks.set(asset, callback);
2452
2453
  return callback;
2453
2454
  }
2455
+ /**
2456
+ * True when the connected owner has bytecode — a deployed smart wallet, or an
2457
+ * EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
2458
+ * Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
2459
+ *
2460
+ * Fails OPEN: an unreadable code check returns false so a working EOA deposit
2461
+ * is never blocked by a flaky provider.
2462
+ */
2463
+ async ownerHasCode() {
2464
+ try {
2465
+ const provider = this.requireConnectedProvider();
2466
+ const code = await provider.request({
2467
+ method: "eth_getCode",
2468
+ params: [this.state.walletAddress, "latest"]
2469
+ });
2470
+ return Boolean(code) && code !== "0x";
2471
+ } catch (error) {
2472
+ debugLog(
2473
+ "deposit",
2474
+ "owner code check failed; assuming EOA",
2475
+ error instanceof Error ? error.message : String(error)
2476
+ );
2477
+ return false;
2478
+ }
2479
+ }
2454
2480
  getAgent(agentId) {
2455
2481
  const agent = this.agents.get(agentId);
2456
2482
  if (!agent) {
@@ -2646,7 +2672,11 @@ var OwneySDK = class {
2646
2672
  }
2647
2673
  const state = this.requireState();
2648
2674
  const chainId = this.requireChainId();
2649
- const effectiveCallback = depositCallback ?? (asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
2675
+ const ownerIsSmartAccount = depositCallback === void 0 && asset !== "WETH" ? await this.ownerHasCode() : false;
2676
+ const effectiveCallback = depositCallback ?? (asset === "WETH" || ownerIsSmartAccount ? this.getDefaultPermit2Callback(
2677
+ asset === "WETH" ? "WETH" : "USDC",
2678
+ onApproved
2679
+ ) : this.getDefaultSponsoredCallback(onApproved));
2650
2680
  if (agentId) {
2651
2681
  if (this.isAgentDisabled(agentId)) {
2652
2682
  throw new OwneyError(
@@ -2750,12 +2780,12 @@ var OwneySDK = class {
2750
2780
  try {
2751
2781
  return await attempt(effectiveCallback);
2752
2782
  } catch (error) {
2753
- if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
2783
+ if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
2754
2784
  approvalAttempted = true;
2755
2785
  console.warn(
2756
- "[owney-sdk] First WETH deposit: sending one-time Permit2 approval..."
2786
+ `[owney-sdk] First ${asset} gasless deposit: sending one-time Permit2 approval...`
2757
2787
  );
2758
- await this.approvePermit2();
2788
+ await this.approvePermit2(asset);
2759
2789
  continue;
2760
2790
  }
2761
2791
  if (!shouldFallbackToUserPaid(error, asset, appCallback)) throw error;
@@ -3269,23 +3299,32 @@ var OwneySDK = class {
3269
3299
  return agent.ensureAutoSelectProtocols(state, chainId, asset);
3270
3300
  }
3271
3301
  /**
3272
- * One-time, user-paid approval of Permit2 on the sponsored WETH token for
3273
- * the active chain. Required once per wallet per chain before gasless WETH
3274
- * deposits; afterwards deposit() is signature-only. Resolves only after the
3275
- * approval transaction is mined (1 confirmation), so a subsequent deposit()
3276
- * will see the new allowance; throws if the transaction reverted.
3302
+ * One-time, user-paid approval of Permit2 on the given sponsored token for
3303
+ * the active chain. Required once per wallet per chain per token before a
3304
+ * gasless deposit of that asset over Permit2; afterwards deposit() is
3305
+ * signature-only for it. This call itself is a real transaction the user
3306
+ * pays gas for. Resolves only after the approval transaction is mined (1
3307
+ * confirmation), so a subsequent deposit() will see the new allowance;
3308
+ * throws if the transaction reverted.
3277
3309
  * @returns the approval transaction hash.
3278
3310
  */
3279
3311
  async approvePermit2(asset = "WETH") {
3280
- void asset;
3281
3312
  const state = this.requireState();
3282
3313
  const chainId = this.requireChainId();
3283
- this.validateAssetSupport(this.getAgent("zyfai"), chainId, "WETH");
3284
- const token = SPONSORED_WETH_BY_CHAIN[chainId];
3314
+ let guard = this.agents.get("zyfai");
3315
+ if (!guard) {
3316
+ try {
3317
+ guard = this.getEligibleAgents(chainId, asset)[0];
3318
+ } catch {
3319
+ guard = void 0;
3320
+ }
3321
+ }
3322
+ if (guard) this.validateAssetSupport(guard, chainId, asset);
3323
+ const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
3285
3324
  if (!token) {
3286
3325
  throw new OwneyError(
3287
3326
  "CHAIN_UNSUPPORTED",
3288
- `No sponsored WETH on chain ${chainId}`
3327
+ `No sponsored ${asset} on chain ${chainId}`
3289
3328
  );
3290
3329
  }
3291
3330
  const provider = this.requireConnectedProvider();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.7.6",
3
+ "version": "0.7.7-beta.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",