@owney/sdk 0.7.21-beta.4 → 0.7.21-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
@@ -1,3 +1,23 @@
1
+ // src/lib/deposit-batch-callback.ts
2
+ var batchCallbacks = /* @__PURE__ */ new WeakMap();
3
+ var getDepositBatchTransfer = (callback) => callback ? batchCallbacks.get(callback) : void 0;
4
+ function toBatchTransfer(to, amount, verification) {
5
+ return {
6
+ to,
7
+ amount,
8
+ ...verification ? {
9
+ yieldseeker: {
10
+ signature: verification.signature,
11
+ userId: verification.userId,
12
+ agentId: verification.yieldseekerAgentId
13
+ }
14
+ } : {}
15
+ };
16
+ }
17
+ function registerDepositBatch(callback, transfer) {
18
+ batchCallbacks.set(callback, transfer);
19
+ }
20
+
1
21
  // src/errors.ts
2
22
  var OwneyError = class extends Error {
3
23
  code;
@@ -2072,39 +2092,6 @@ async function ensureWalletOnChain(pub, wallet, expected) {
2072
2092
 
2073
2093
  // src/lib/transfer-auth.ts
2074
2094
  import { bytesToHex } from "viem";
2075
- var ERC20_META_ABI = [
2076
- { type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "string" }] },
2077
- { type: "function", name: "version", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "string" }] }
2078
- ];
2079
- function buildTransferWithAuthorizationTypedData(input) {
2080
- return {
2081
- domain: { name: input.tokenName, version: input.tokenVersion, chainId: input.chainId, verifyingContract: input.token },
2082
- types: {
2083
- TransferWithAuthorization: [
2084
- { name: "from", type: "address" },
2085
- { name: "to", type: "address" },
2086
- { name: "value", type: "uint256" },
2087
- { name: "validAfter", type: "uint256" },
2088
- { name: "validBefore", type: "uint256" },
2089
- { name: "nonce", type: "bytes32" }
2090
- ]
2091
- },
2092
- primaryType: "TransferWithAuthorization",
2093
- message: input.message
2094
- };
2095
- }
2096
- async function readTokenMeta(publicClient, token) {
2097
- const [tokenName, tokenVersion] = await Promise.all([
2098
- publicClient.readContract({ address: token, abi: ERC20_META_ABI, functionName: "name" }),
2099
- publicClient.readContract({ address: token, abi: ERC20_META_ABI, functionName: "version" }).catch(() => "2")
2100
- ]);
2101
- return { tokenName, tokenVersion };
2102
- }
2103
- function randomAuthNonce() {
2104
- const bytes = new Uint8Array(32);
2105
- globalThis.crypto.getRandomValues(bytes);
2106
- return bytesToHex(bytes);
2107
- }
2108
2095
 
2109
2096
  // src/lib/sponsor-client.ts
2110
2097
  var ROUTING_API_BASE_URL = "https://owney-routing-api-243946518160.europe-west4.run.app";
@@ -2147,65 +2134,21 @@ async function postPaymasterIntent(input) {
2147
2134
  }
2148
2135
  return parsed.data;
2149
2136
  }
2150
- async function postSponsorTransferAuth(input) {
2137
+ async function getSponsorRelayerAddress(input) {
2151
2138
  const base5 = input.baseUrl ?? ROUTING_API_BASE_URL;
2152
2139
  let res;
2153
2140
  try {
2154
- res = await fetch(`${base5}/api/v1/sponsor/erc20-transfer-auth`, {
2155
- method: "POST",
2156
- headers: {
2157
- "content-type": "application/json",
2158
- "x-owney-api-key": input.apiKey,
2159
- ...input.yieldseekerSignature ? { Authorization: `Signature ${input.yieldseekerSignature}` } : {}
2160
- },
2161
- body: JSON.stringify(input.body)
2162
- });
2163
- } catch (networkError) {
2164
- throw new OwneyError(
2165
- "SPONSOR_REQUEST_FAILED",
2166
- `Sponsor API network error: ${networkError instanceof Error ? networkError.message : String(networkError)}`,
2167
- { cause: String(networkError) }
2168
- );
2169
- }
2170
- const text = await res.text();
2171
- let parsed = null;
2172
- try {
2173
- parsed = JSON.parse(text);
2174
- } catch {
2175
- }
2176
- if (!res.ok || !parsed?.success || !parsed.data) {
2177
- throw new OwneyError(
2178
- "SPONSOR_REQUEST_FAILED",
2179
- `Sponsor API error ${res.status}: ${parsed?.message ?? text.slice(0, 500)}`,
2141
+ res = await fetch(
2142
+ `${base5}/api/v1/sponsor/relayer-address?chainId=${input.chainId}`,
2180
2143
  {
2181
- statusCode: res.status,
2182
- responseBody: text.slice(0, 500),
2183
- // 503 SPONSOR_UNAVAILABLE = relayer out of gas; the tx was rejected
2184
- // before broadcast, so it is safe to fall back to a user-paid deposit.
2185
- safeToFallback: res.status === 503
2144
+ headers: { "x-owney-api-key": input.apiKey }
2186
2145
  }
2187
2146
  );
2188
- }
2189
- return parsed.data;
2190
- }
2191
- async function postSponsorPermit2Transfer(input) {
2192
- const base5 = input.baseUrl ?? ROUTING_API_BASE_URL;
2193
- let res;
2194
- try {
2195
- res = await fetch(`${base5}/api/v1/sponsor/permit2-transfer`, {
2196
- method: "POST",
2197
- headers: {
2198
- "content-type": "application/json",
2199
- "x-owney-api-key": input.apiKey,
2200
- ...input.yieldseekerSignature ? { Authorization: `Signature ${input.yieldseekerSignature}` } : {}
2201
- },
2202
- body: JSON.stringify(input.body)
2203
- });
2204
2147
  } catch (networkError) {
2205
2148
  throw new OwneyError(
2206
2149
  "SPONSOR_REQUEST_FAILED",
2207
2150
  `Sponsor API network error: ${networkError instanceof Error ? networkError.message : String(networkError)}`,
2208
- { cause: String(networkError), safeToFallback: false }
2151
+ { cause: String(networkError), safeToFallback: true }
2209
2152
  );
2210
2153
  }
2211
2154
  const text = await res.text();
@@ -2214,60 +2157,64 @@ async function postSponsorPermit2Transfer(input) {
2214
2157
  parsed = JSON.parse(text);
2215
2158
  } catch {
2216
2159
  }
2217
- if (!res.ok || !parsed?.success || !parsed.data) {
2160
+ if (!res.ok || !parsed?.success || !parsed.data?.relayer) {
2218
2161
  throw new OwneyError(
2219
2162
  "SPONSOR_REQUEST_FAILED",
2220
2163
  `Sponsor API error ${res.status}: ${parsed?.message ?? text.slice(0, 500)}`,
2221
2164
  {
2222
2165
  statusCode: res.status,
2223
2166
  responseBody: text.slice(0, 500),
2224
- safeToFallback: res.status >= 400 && res.status < 500 || res.status === 503
2167
+ safeToFallback: true
2225
2168
  }
2226
2169
  );
2227
2170
  }
2228
- return parsed.data;
2171
+ return parsed.data.relayer;
2229
2172
  }
2230
- async function getSponsorRelayerAddress(input) {
2231
- const base5 = input.baseUrl ?? ROUTING_API_BASE_URL;
2173
+ async function postSponsorBatchTransfer(input) {
2232
2174
  let res;
2233
2175
  try {
2234
2176
  res = await fetch(
2235
- `${base5}/api/v1/sponsor/relayer-address?chainId=${input.chainId}`,
2177
+ `${input.baseUrl ?? ROUTING_API_BASE_URL}/api/v1/sponsor/permit2-batch`,
2236
2178
  {
2237
- headers: { "x-owney-api-key": input.apiKey }
2179
+ method: "POST",
2180
+ headers: {
2181
+ "content-type": "application/json",
2182
+ "x-owney-api-key": input.apiKey
2183
+ },
2184
+ body: JSON.stringify(input.body)
2238
2185
  }
2239
2186
  );
2240
- } catch (networkError) {
2187
+ } catch {
2241
2188
  throw new OwneyError(
2242
2189
  "SPONSOR_REQUEST_FAILED",
2243
- `Sponsor API network error: ${networkError instanceof Error ? networkError.message : String(networkError)}`,
2244
- { cause: String(networkError), safeToFallback: true }
2190
+ "Deposit status is unknown. Retry the same amount to check it.",
2191
+ { safeToFallback: false }
2245
2192
  );
2246
2193
  }
2247
- const text = await res.text();
2248
- let parsed = null;
2249
- try {
2250
- parsed = JSON.parse(text);
2251
- } catch {
2252
- }
2253
- if (!res.ok || !parsed?.success || !parsed.data?.relayer) {
2194
+ const parsed = await res.json().catch(() => null);
2195
+ if (!res.ok || !parsed?.success || !/^0x[0-9a-fA-F]{64}$/.test(parsed.data?.txHash ?? "")) {
2254
2196
  throw new OwneyError(
2255
2197
  "SPONSOR_REQUEST_FAILED",
2256
- `Sponsor API error ${res.status}: ${parsed?.message ?? text.slice(0, 500)}`,
2198
+ "Deposit could not be confirmed. Retry the same amount to check its status.",
2257
2199
  {
2258
2200
  statusCode: res.status,
2259
- responseBody: text.slice(0, 500),
2260
- safeToFallback: true
2201
+ safeToFallback: false,
2202
+ notSubmitted: parsed?.notSubmitted === true || parsed?.error?.notSubmitted === true || parsed?.error?.details?.notSubmitted === true
2261
2203
  }
2262
2204
  );
2263
2205
  }
2264
- return parsed.data.relayer;
2206
+ return parsed.data;
2265
2207
  }
2266
2208
 
2267
2209
  // src/lib/permit2.ts
2268
2210
  import { bytesToHex as bytesToHex2 } from "viem";
2269
2211
  var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
2270
- var MAX_UINT256 = 2n ** 256n - 1n;
2212
+ function permit2ApprovalAmount(requiredAmount) {
2213
+ if (requiredAmount <= 0n) {
2214
+ throw new Error("Permit2 approval requires a positive deposit amount");
2215
+ }
2216
+ return requiredAmount;
2217
+ }
2271
2218
  var ERC20_ALLOWANCE_ABI = [
2272
2219
  {
2273
2220
  type: "function",
@@ -2297,29 +2244,6 @@ var ERC20_ALLOWANCE_ABI = [
2297
2244
  outputs: [{ name: "", type: "uint256" }]
2298
2245
  }
2299
2246
  ];
2300
- function buildPermitTransferFromTypedData(input) {
2301
- return {
2302
- domain: {
2303
- name: "Permit2",
2304
- chainId: input.chainId,
2305
- verifyingContract: PERMIT2_ADDRESS
2306
- },
2307
- types: {
2308
- PermitTransferFrom: [
2309
- { name: "permitted", type: "TokenPermissions" },
2310
- { name: "spender", type: "address" },
2311
- { name: "nonce", type: "uint256" },
2312
- { name: "deadline", type: "uint256" }
2313
- ],
2314
- TokenPermissions: [
2315
- { name: "token", type: "address" },
2316
- { name: "amount", type: "uint256" }
2317
- ]
2318
- },
2319
- primaryType: "PermitTransferFrom",
2320
- message: input.message
2321
- };
2322
- }
2323
2247
  function randomPermit2Nonce() {
2324
2248
  const bytes = new Uint8Array(32);
2325
2249
  globalThis.crypto.getRandomValues(bytes);
@@ -2362,88 +2286,6 @@ function makeVerificationAwareDepositCallback(implementation) {
2362
2286
  });
2363
2287
  return callback;
2364
2288
  }
2365
- function makeSponsoredDepositCallback(deps) {
2366
- const post = deps.httpPost ?? postSponsorTransferAuth;
2367
- return makeVerificationAwareDepositCallback(
2368
- async (smartWallet, chainId, amount, verification) => {
2369
- const cid = chainId;
2370
- const token = deps.tokenAddressByChain[cid];
2371
- if (!token) {
2372
- throw new OwneyError(
2373
- "CHAIN_UNSUPPORTED",
2374
- `No sponsored token configured for chain ${chainId}`
2375
- );
2376
- }
2377
- const pub = deps.getPublicClient(cid);
2378
- const wallet = deps.getWalletClient(cid);
2379
- await ensureWalletOnChain(pub, wallet, cid);
2380
- try {
2381
- const balance = await readErc20Balance(pub, token, deps.ownerAddress);
2382
- if (balance < BigInt(amount)) {
2383
- throw new OwneyError(
2384
- "DEPOSIT_INSUFFICIENT_BALANCE",
2385
- "Insufficient balance for this deposit.",
2386
- { token, chainId: cid, balance: balance.toString(), amount }
2387
- );
2388
- }
2389
- } catch (err) {
2390
- if (err instanceof OwneyError) throw err;
2391
- console.warn(
2392
- "[owney-sdk] Deposit balance pre-check failed (non-fatal):",
2393
- err instanceof Error ? err.message : String(err)
2394
- );
2395
- }
2396
- const { tokenName, tokenVersion } = await readTokenMeta(pub, token);
2397
- const validAfter = 0n;
2398
- const validBefore = BigInt(
2399
- Math.floor(Date.now() / 1e3) + AUTH_WINDOW_SECONDS
2400
- );
2401
- const nonce = randomAuthNonce();
2402
- const typedData = buildTransferWithAuthorizationTypedData({
2403
- token,
2404
- chainId: cid,
2405
- tokenName,
2406
- tokenVersion,
2407
- message: {
2408
- from: deps.ownerAddress,
2409
- to: smartWallet,
2410
- value: BigInt(amount),
2411
- validAfter,
2412
- validBefore,
2413
- nonce
2414
- }
2415
- });
2416
- const authSignature = await wallet.signTypedData({
2417
- account: deps.ownerAddress,
2418
- ...typedData
2419
- });
2420
- deps.onApproved?.();
2421
- const result = await post({
2422
- baseUrl: deps.baseUrl,
2423
- apiKey: deps.apiKey,
2424
- ...verification?.agentId === "yieldseeker" ? { yieldseekerSignature: verification.signature } : {},
2425
- body: {
2426
- chainId: cid,
2427
- token,
2428
- from: deps.ownerAddress,
2429
- to: smartWallet,
2430
- value: amount,
2431
- validAfter: validAfter.toString(),
2432
- validBefore: validBefore.toString(),
2433
- nonce,
2434
- authSignature,
2435
- tokenName,
2436
- tokenVersion,
2437
- ...verification?.agentId === "yieldseeker" ? {
2438
- yieldseekerUserId: verification.userId,
2439
- yieldseekerAgentId: verification.yieldseekerAgentId
2440
- } : {}
2441
- }
2442
- });
2443
- return result.txHash;
2444
- }
2445
- );
2446
- }
2447
2289
 
2448
2290
  // src/agents/yieldseeker/yieldseeker.auth.ts
2449
2291
  import { SiweMessage, generateNonce } from "siwe";
@@ -3127,7 +2969,7 @@ function depositDestination(context, movement) {
3127
2969
  if (receipt && minted) return { protocol: "Vault", pool: receipt.name || receipt.symbol, tokenSymbol: context.asset };
3128
2970
  return void 0;
3129
2971
  }
3130
- function movementEntry(movement, wallet, agent, asset, ownerAddress, destination) {
2972
+ function movementEntry(movement, wallet, agent, asset, ownerAddress, vaultAddresses, destination) {
3131
2973
  const from = movement.fromAddress.toLowerCase();
3132
2974
  const to = movement.toAddress.toLowerCase();
3133
2975
  const owner = ownerAddress.toLowerCase();
@@ -3137,7 +2979,7 @@ function movementEntry(movement, wallet, agent, asset, ownerAddress, destination
3137
2979
  return void 0;
3138
2980
  }
3139
2981
  let action;
3140
- if (from === owner && to === agentWallet) {
2982
+ if (to === agentWallet && !vaultAddresses.has(from)) {
3141
2983
  action = "Top up";
3142
2984
  } else if (from === agentWallet && to === owner) {
3143
2985
  action = "Withdraw";
@@ -3187,6 +3029,7 @@ function mapYieldseekerHistory(contexts, options) {
3187
3029
  context.agent,
3188
3030
  context.asset,
3189
3031
  options.ownerAddress,
3032
+ options.vaultAddresses,
3190
3033
  depositDestination(context, movement)
3191
3034
  )
3192
3035
  ),
@@ -3267,6 +3110,7 @@ var OWNEY_AGENT_NAME = "owney";
3267
3110
  var YIELDSEEKER_USERNAME_PREFIX = "owney_";
3268
3111
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
3269
3112
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
3113
+ var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
3270
3114
  function generateYieldseekerUsername() {
3271
3115
  const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
3272
3116
  return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
@@ -3328,6 +3172,8 @@ var YieldseekerAgent = class {
3328
3172
  agentContexts = /* @__PURE__ */ new Map();
3329
3173
  users = /* @__PURE__ */ new Map();
3330
3174
  pendingAgents = /* @__PURE__ */ new Map();
3175
+ yieldOptions = /* @__PURE__ */ new Map();
3176
+ pendingYieldOptions = /* @__PURE__ */ new Map();
3331
3177
  constructor(owneyApiKey, options = {}) {
3332
3178
  this.api = new YieldseekerApiClient(
3333
3179
  owneyApiKey,
@@ -3394,7 +3240,6 @@ var YieldseekerAgent = class {
3394
3240
  chainId
3395
3241
  });
3396
3242
  }
3397
- await this.deployAfterFunding(state, chainId, context);
3398
3243
  } finally {
3399
3244
  await this.refreshSnapshotAfterMovement(
3400
3245
  state,
@@ -3546,9 +3391,20 @@ var YieldseekerAgent = class {
3546
3391
  historic: true,
3547
3392
  actions: true
3548
3393
  });
3394
+ const catalog = await Promise.all(
3395
+ [...new Set(contexts.map((context) => context.asset))].map(
3396
+ (contextAsset) => this.loadYieldOptions(contextAsset)
3397
+ )
3398
+ );
3399
+ const vaultAddresses = new Set(
3400
+ catalog.flat().filter(
3401
+ (yieldOption) => yieldOption.chainId === chainId && isAddress2(yieldOption.address)
3402
+ ).map((yieldOption) => yieldOption.address.toLowerCase())
3403
+ );
3549
3404
  return mapYieldseekerHistory(contexts, {
3550
3405
  limit: options?.limit ?? 10,
3551
3406
  ownerAddress: state.walletAddress,
3407
+ vaultAddresses,
3552
3408
  ...options?.fromDate ? { fromDate: options.fromDate } : {},
3553
3409
  ...options?.toDate ? { toDate: options.toDate } : {}
3554
3410
  });
@@ -3567,18 +3423,32 @@ var YieldseekerAgent = class {
3567
3423
  const assets = requested ? [requested] : ["USDC", "WETH"];
3568
3424
  const values = await Promise.all(
3569
3425
  assets.map(async (asset) => {
3570
- const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3571
- const response = await this.api.request(
3572
- `/chains/8453/assets/${metadata.address}/yield-options`
3573
- );
3574
- if (!Array.isArray(response?.yieldOptions)) {
3575
- throw this.invalidResponse("yield options");
3576
- }
3577
- return { asset, yieldOptions: response.yieldOptions };
3426
+ return { asset, yieldOptions: await this.loadYieldOptions(asset) };
3578
3427
  })
3579
3428
  );
3580
3429
  return mapYieldseekerAgentApy(values, days);
3581
3430
  }
3431
+ async loadYieldOptions(asset) {
3432
+ const cached = this.yieldOptions.get(asset);
3433
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
3434
+ const pending = this.pendingYieldOptions.get(asset);
3435
+ if (pending) return pending;
3436
+ const metadata = YIELDSEEKER_ASSET_METADATA[asset];
3437
+ const request = this.api.request(
3438
+ `/chains/8453/assets/${metadata.address}/yield-options`
3439
+ ).then((response) => {
3440
+ if (!Array.isArray(response?.yieldOptions)) {
3441
+ throw this.invalidResponse("yield options");
3442
+ }
3443
+ this.yieldOptions.set(asset, {
3444
+ expiresAt: Date.now() + YIELDSEEKER_YIELD_OPTIONS_CACHE_MS,
3445
+ value: response.yieldOptions
3446
+ });
3447
+ return response.yieldOptions;
3448
+ }).finally(() => this.pendingYieldOptions.delete(asset));
3449
+ this.pendingYieldOptions.set(asset, request);
3450
+ return request;
3451
+ }
3582
3452
  userKey(state, chainId) {
3583
3453
  return `${state.walletAddress.toLowerCase()}:${chainId}`;
3584
3454
  }
@@ -3657,8 +3527,9 @@ var YieldseekerAgent = class {
3657
3527
  const pending = this.pendingAgents.get(key2);
3658
3528
  if (pending) return pending;
3659
3529
  const request = this.resolveAgent(state, chainId, asset, true).then(
3660
- (context) => {
3530
+ async (context) => {
3661
3531
  if (!context) throw this.invalidResponse("agent creation");
3532
+ await this.deployAgent(state, chainId, context);
3662
3533
  this.agentContexts.set(key2, context);
3663
3534
  return context;
3664
3535
  }
@@ -3803,24 +3674,21 @@ var YieldseekerAgent = class {
3803
3674
  ...actions?.actions ? { actions: actions.actions } : {}
3804
3675
  };
3805
3676
  }
3806
- async deployAfterFunding(state, chainId, context) {
3677
+ async deployAgent(state, chainId, context) {
3807
3678
  if (context.wallet.initializedDate != null) return;
3808
- try {
3809
- const deployed = await this.walletRequest(
3810
- state,
3811
- chainId,
3812
- this.agentPath(context, "deploy"),
3813
- { method: "POST", body: {} }
3814
- );
3815
- if (deployed?.agentWallet) {
3816
- context.wallet = deployed.agentWallet;
3817
- }
3818
- } catch (error) {
3819
- console.warn(
3820
- "[owney-sdk] Yieldseeker deposit is funded but not deployable yet:",
3821
- error
3822
- );
3679
+ const walletAddress = context.wallet.walletAddress.toLowerCase();
3680
+ const deployed = await this.walletRequest(
3681
+ state,
3682
+ chainId,
3683
+ this.agentPath(context, "deploy"),
3684
+ { method: "POST", body: {} }
3685
+ );
3686
+ if (!deployed?.agentWallet || !isAddress2(deployed.agentWallet.walletAddress) || deployed.agentWallet.walletAddress.toLowerCase() !== walletAddress) {
3687
+ throw this.invalidResponse("agent deployment", {
3688
+ reason: "Deploy did not return the expected Agent Wallet."
3689
+ });
3823
3690
  }
3691
+ context.wallet = deployed.agentWallet;
3824
3692
  }
3825
3693
  async refreshSnapshotAfterMovement(state, chainId, context, movement) {
3826
3694
  try {
@@ -4311,98 +4179,302 @@ import {
4311
4179
  } from "viem";
4312
4180
  import { base as base4, arbitrum as arbitrum2, mainnet as mainnet2 } from "viem/chains";
4313
4181
 
4314
- // src/lib/sponsored-weth-deposit.ts
4315
- var PERMIT_WINDOW_SECONDS = 15 * 60;
4316
- function makeSponsoredWethCallback(deps) {
4317
- const get = deps.httpGet ?? getSponsorRelayerAddress;
4318
- const post = deps.httpPost ?? postSponsorPermit2Transfer;
4319
- return makeVerificationAwareDepositCallback(
4320
- async (smartWallet, chainId, amount, verification) => {
4321
- const cid = chainId;
4322
- const token = deps.tokenAddressByChain[cid];
4323
- if (!token) {
4324
- throw new OwneyError(
4325
- "CHAIN_UNSUPPORTED",
4326
- `No sponsored WETH configured for chain ${chainId}`
4327
- );
4328
- }
4329
- const amountWei = BigInt(amount);
4330
- const pub = deps.getPublicClient(cid);
4331
- const wallet = deps.getWalletClient(cid);
4332
- await ensureWalletOnChain(pub, wallet, cid);
4333
- try {
4334
- const balance = await readErc20Balance(pub, token, deps.ownerAddress);
4335
- if (balance < amountWei) {
4336
- throw new OwneyError(
4337
- "DEPOSIT_INSUFFICIENT_BALANCE",
4338
- "Insufficient WETH balance for this deposit.",
4339
- { token, chainId: cid, balance: balance.toString(), amount }
4340
- );
4341
- }
4342
- } catch (err) {
4343
- if (err instanceof OwneyError) throw err;
4344
- console.warn(
4345
- "[owney-sdk] WETH balance pre-check failed (non-fatal):",
4346
- err instanceof Error ? err.message : String(err)
4347
- );
4348
- }
4349
- const allowance = await readPermit2Allowance(
4350
- pub,
4351
- token,
4352
- deps.ownerAddress
4182
+ // src/lib/sponsored-token-batch.ts
4183
+ import {
4184
+ isAddressEqual,
4185
+ keccak256,
4186
+ toBytes
4187
+ } from "viem";
4188
+
4189
+ // src/lib/permit2-batch.ts
4190
+ import { parseAbi as parseAbi2, hashStruct } from "viem";
4191
+ var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
4192
+ var PERMIT_BATCH_TYPES = {
4193
+ PermitBatchWitnessTransferFrom: [
4194
+ { name: "permitted", type: "TokenPermissions[]" },
4195
+ { name: "spender", type: "address" },
4196
+ { name: "nonce", type: "uint256" },
4197
+ { name: "deadline", type: "uint256" },
4198
+ { name: "witness", type: "Deposit" }
4199
+ ],
4200
+ Deposit: [{ name: "recipients", type: "address[]" }],
4201
+ TokenPermissions: [
4202
+ { name: "token", type: "address" },
4203
+ { name: "amount", type: "uint256" }
4204
+ ]
4205
+ };
4206
+ var PERMIT2_BATCH_ABI = parseAbi2([
4207
+ "struct TokenPermissions { address token; uint256 amount; }",
4208
+ "struct PermitBatchTransferFrom { TokenPermissions[] permitted; uint256 nonce; uint256 deadline; }",
4209
+ "struct SignatureTransferDetails { address to; uint256 requestedAmount; }",
4210
+ "function permitWitnessTransferFrom(PermitBatchTransferFrom permit,SignatureTransferDetails[] transferDetails,address owner,bytes32 witness,string witnessTypeString,bytes signature)",
4211
+ "function nonceBitmap(address owner,uint256 wordPos) view returns (uint256)"
4212
+ ]);
4213
+ function batchPermit(b) {
4214
+ return {
4215
+ permitted: b.transfers.map((t) => ({
4216
+ token: b.token,
4217
+ amount: BigInt(t.amount)
4218
+ })),
4219
+ nonce: BigInt(b.nonce),
4220
+ deadline: BigInt(b.deadline)
4221
+ };
4222
+ }
4223
+ function batchTypedData(b, spender) {
4224
+ return {
4225
+ domain: {
4226
+ name: "Permit2",
4227
+ chainId: b.chainId,
4228
+ verifyingContract: BATCH_PERMIT2_ADDRESS
4229
+ },
4230
+ types: PERMIT_BATCH_TYPES,
4231
+ primaryType: "PermitBatchWitnessTransferFrom",
4232
+ message: {
4233
+ ...batchPermit(b),
4234
+ spender,
4235
+ witness: { recipients: b.transfers.map((t) => t.to) }
4236
+ }
4237
+ };
4238
+ }
4239
+
4240
+ // src/lib/sponsored-token-batch.ts
4241
+ var memory = /* @__PURE__ */ new Map();
4242
+ var inflight = /* @__PURE__ */ new Map();
4243
+ var planOf = (transfers) => JSON.stringify(
4244
+ transfers.map((t) => ({
4245
+ to: t.to.toLowerCase(),
4246
+ amount: BigInt(t.amount).toString()
4247
+ }))
4248
+ );
4249
+ function read(key2) {
4250
+ return typeof window === "undefined" ? memory.get(key2) : window.localStorage.getItem(key2);
4251
+ }
4252
+ function save(key2, body) {
4253
+ const value = JSON.stringify({
4254
+ ...body,
4255
+ transfers: body.transfers.map(({ to, amount }) => ({ to, amount }))
4256
+ });
4257
+ if (typeof window === "undefined") memory.set(key2, value);
4258
+ else window.localStorage.setItem(key2, value);
4259
+ }
4260
+ function clear(key2) {
4261
+ if (typeof window === "undefined") memory.delete(key2);
4262
+ else window.localStorage.removeItem(key2);
4263
+ }
4264
+ function sponsorTokenBatch(i) {
4265
+ const key2 = `owney.token-batch.v1:${keccak256(toBytes(i.apiKey))}:${i.baseUrl ?? "default"}:${i.chainId}:${i.owner.toLowerCase()}:${i.token.toLowerCase()}`;
4266
+ const plan = planOf(i.transfers);
4267
+ const active = inflight.get(key2);
4268
+ if (active) {
4269
+ if (active.plan !== plan)
4270
+ return Promise.reject(
4271
+ new Error(
4272
+ "A token deposit is already in progress. Wait for its result before depositing again."
4273
+ )
4353
4274
  );
4354
- if (allowance < amountWei) {
4355
- throw new OwneyError(
4356
- "PERMIT2_APPROVAL_REQUIRED",
4357
- "WETH gasless deposits need a one-time Permit2 approval; call approvePermit2() first",
4358
- { token, chainId: cid, allowance: allowance.toString(), amount }
4359
- );
4275
+ return active.promise;
4276
+ }
4277
+ const promise = execute(i, key2, plan).finally(() => inflight.delete(key2));
4278
+ inflight.set(key2, { plan, promise });
4279
+ return promise;
4280
+ }
4281
+ async function execute(i, key2, plan) {
4282
+ if (!i.transfers.length || i.transfers.length > 16 || i.transfers.some(
4283
+ (t) => BigInt(t.amount) <= 0n || BigInt(t.amount) >= 1n << 256n
4284
+ ) || new Set(i.transfers.map((t) => t.to.toLowerCase())).size !== i.transfers.length)
4285
+ throw new Error("Invalid token deposit shares.");
4286
+ const send = async (initial) => {
4287
+ let body = initial;
4288
+ save(key2, body);
4289
+ try {
4290
+ if (!body.serializedTransaction) {
4291
+ const prepared = await postSponsorBatchTransfer({
4292
+ apiKey: i.apiKey,
4293
+ baseUrl: i.baseUrl,
4294
+ body
4295
+ });
4296
+ if (!prepared.serializedTransaction || keccak256(prepared.serializedTransaction) !== prepared.txHash)
4297
+ throw new Error(
4298
+ "Sponsorship API did not return a valid prepared transaction."
4299
+ );
4300
+ body = {
4301
+ ...body,
4302
+ serializedTransaction: prepared.serializedTransaction
4303
+ };
4304
+ save(key2, body);
4360
4305
  }
4361
- const relayer = await get({
4362
- baseUrl: deps.baseUrl,
4363
- apiKey: deps.apiKey,
4364
- chainId: cid
4365
- });
4366
- const nonce = randomPermit2Nonce();
4367
- const deadline = BigInt(
4368
- Math.floor(Date.now() / 1e3) + PERMIT_WINDOW_SECONDS
4369
- );
4370
- const typedData = buildPermitTransferFromTypedData({
4371
- chainId: cid,
4372
- message: {
4373
- permitted: { token, amount: amountWei },
4374
- spender: relayer,
4375
- nonce,
4376
- deadline
4377
- }
4378
- });
4379
- const signature = await wallet.signTypedData({
4380
- account: deps.ownerAddress,
4381
- ...typedData
4382
- });
4383
- deps.onApproved?.();
4384
- const result = await post({
4385
- baseUrl: deps.baseUrl,
4386
- apiKey: deps.apiKey,
4387
- ...verification?.agentId === "yieldseeker" ? { yieldseekerSignature: verification.signature } : {},
4388
- body: {
4389
- chainId: cid,
4390
- token,
4391
- from: deps.ownerAddress,
4392
- to: smartWallet,
4393
- amount,
4394
- nonce: nonce.toString(),
4395
- deadline: deadline.toString(),
4396
- signature,
4397
- ...verification?.agentId === "yieldseeker" ? {
4398
- yieldseekerUserId: verification.userId,
4399
- yieldseekerAgentId: verification.yieldseekerAgentId
4400
- } : {}
4401
- }
4306
+ const result = await postSponsorBatchTransfer({
4307
+ apiKey: i.apiKey,
4308
+ baseUrl: i.baseUrl,
4309
+ body
4402
4310
  });
4311
+ if (result.txHash !== keccak256(body.serializedTransaction))
4312
+ throw new Error(
4313
+ "Sponsorship receipt does not match the pending transaction."
4314
+ );
4315
+ clear(key2);
4403
4316
  return result.txHash;
4317
+ } catch (error) {
4318
+ if (!body.serializedTransaction || error instanceof OwneyError && error.details?.notSubmitted === true)
4319
+ clear(key2);
4320
+ throw error;
4404
4321
  }
4322
+ };
4323
+ const saved = read(key2);
4324
+ if (saved) {
4325
+ const previous = JSON.parse(saved);
4326
+ if (previous.chainId !== i.chainId || !isAddressEqual(previous.from, i.owner) || !isAddressEqual(previous.token, i.token) || planOf(previous.transfers) !== plan)
4327
+ throw new Error(
4328
+ "Retry the previous token deposit and agent split first to reconcile its status."
4329
+ );
4330
+ i.onApproved?.();
4331
+ return send({ ...previous, transfers: i.transfers });
4332
+ }
4333
+ const total = i.transfers.reduce((sum, t) => sum + BigInt(t.amount), 0n);
4334
+ const [balance, allowance] = await Promise.all([
4335
+ readErc20Balance(i.pub, i.token, i.owner),
4336
+ readPermit2Allowance(i.pub, i.token, i.owner)
4337
+ ]);
4338
+ if (balance < total)
4339
+ throw new OwneyError(
4340
+ "DEPOSIT_INSUFFICIENT_BALANCE",
4341
+ "Insufficient token balance for this deposit."
4342
+ );
4343
+ if (allowance < total)
4344
+ throw new OwneyError(
4345
+ "PERMIT2_APPROVAL_REQUIRED",
4346
+ "token deposits need a one-time Permit2 approval."
4347
+ );
4348
+ const relayer = await getSponsorRelayerAddress({
4349
+ apiKey: i.apiKey,
4350
+ baseUrl: i.baseUrl,
4351
+ chainId: i.chainId
4352
+ });
4353
+ const now = (await i.pub.getBlock()).timestamp;
4354
+ const unsigned = {
4355
+ chainId: i.chainId,
4356
+ token: i.token,
4357
+ from: i.owner,
4358
+ transfers: i.transfers,
4359
+ nonce: randomPermit2Nonce().toString(),
4360
+ deadline: (now + 900n).toString()
4361
+ };
4362
+ const signature = await i.wallet.signTypedData({
4363
+ account: i.owner,
4364
+ ...batchTypedData(unsigned, relayer)
4365
+ });
4366
+ i.onApproved?.();
4367
+ return send({ ...unsigned, signature });
4368
+ }
4369
+
4370
+ // src/lib/sponsored-token-deposit.ts
4371
+ function makeSponsoredTokenCallback(deps) {
4372
+ const batch = async (chainId, transfers) => {
4373
+ if (chainId !== 8453 && chainId !== 42161 && chainId !== 1)
4374
+ throw new OwneyError(
4375
+ "CHAIN_UNSUPPORTED",
4376
+ `No sponsored token configured for chain ${chainId}`
4377
+ );
4378
+ const token = deps.tokenAddressByChain[chainId];
4379
+ if (!token)
4380
+ throw new OwneyError(
4381
+ "CHAIN_UNSUPPORTED",
4382
+ `No sponsored token configured for chain ${chainId}`
4383
+ );
4384
+ const pub = deps.getPublicClient(chainId), wallet = deps.getWalletClient(chainId);
4385
+ await ensureWalletOnChain(pub, wallet, chainId);
4386
+ return sponsorTokenBatch({
4387
+ apiKey: deps.apiKey,
4388
+ baseUrl: deps.baseUrl,
4389
+ owner: deps.ownerAddress,
4390
+ token,
4391
+ chainId,
4392
+ transfers,
4393
+ pub,
4394
+ wallet,
4395
+ onApproved: deps.onApproved
4396
+ });
4397
+ };
4398
+ const callback = makeVerificationAwareDepositCallback(
4399
+ (to, chainId, amount, verification) => batch(chainId, [toBatchTransfer(to, amount, verification)])
4405
4400
  );
4401
+ registerDepositBatch(callback, batch);
4402
+ return callback;
4403
+ }
4404
+
4405
+ // src/lib/agent-deposit-batch.ts
4406
+ function deferred() {
4407
+ let resolve, reject;
4408
+ const promise = new Promise((yes, no) => {
4409
+ resolve = yes;
4410
+ reject = no;
4411
+ });
4412
+ void promise.catch(() => {
4413
+ });
4414
+ return { promise, resolve, reject };
4415
+ }
4416
+ async function runAgentDepositBatch(chainId, legs, transfer) {
4417
+ const funding = deferred();
4418
+ const tasks = [];
4419
+ const transfers = [];
4420
+ try {
4421
+ for (const leg of legs) {
4422
+ const ready = deferred();
4423
+ let entered = false;
4424
+ const callback = makeVerificationAwareDepositCallback(
4425
+ (to, cid, amount, verification) => {
4426
+ if (entered || cid !== chainId || BigInt(amount) !== BigInt(leg.amount)) {
4427
+ const error = new Error(
4428
+ "Agent changed its prepared deposit share."
4429
+ );
4430
+ ready.reject(error);
4431
+ throw error;
4432
+ }
4433
+ entered = true;
4434
+ ready.resolve(toBatchTransfer(to, amount, verification));
4435
+ return funding.promise;
4436
+ }
4437
+ );
4438
+ const task = Promise.resolve().then(() => leg.run(callback));
4439
+ tasks.push(task);
4440
+ void task.then(
4441
+ () => {
4442
+ if (!entered)
4443
+ ready.reject(
4444
+ new Error("Agent did not prepare a deposit transfer.")
4445
+ );
4446
+ },
4447
+ (error) => ready.reject(error)
4448
+ );
4449
+ transfers.push(await ready.promise);
4450
+ }
4451
+ const txHash = await transfer(chainId, transfers);
4452
+ funding.resolve(txHash);
4453
+ const settled = await Promise.allSettled(tasks);
4454
+ const agentResults = {};
4455
+ const failures = [];
4456
+ for (const [index, result] of settled.entries()) {
4457
+ if (result.status === "fulfilled")
4458
+ agentResults[legs[index].id] = result.value;
4459
+ else failures.push(legs[index].id);
4460
+ }
4461
+ if (failures.length)
4462
+ throw new OwneyError(
4463
+ "DEPOSIT_PARTIAL_FAILURE",
4464
+ "The deposit was sent to all agents, but some agent updates could not be confirmed. Check activity before depositing again.",
4465
+ {
4466
+ txHash,
4467
+ fundsSubmitted: true,
4468
+ agentResults,
4469
+ failedAgentIds: failures
4470
+ }
4471
+ );
4472
+ return { agentResults };
4473
+ } catch (error) {
4474
+ funding.reject(error);
4475
+ await Promise.allSettled(tasks);
4476
+ throw error;
4477
+ }
4406
4478
  }
4407
4479
 
4408
4480
  // src/lib/sponsored-calls-deposit.ts
@@ -4432,7 +4504,7 @@ function makeSponsoredCallsCallback(deps) {
4432
4504
  }
4433
4505
  return new URL(configured, origin).toString();
4434
4506
  };
4435
- return makeVerificationAwareDepositCallback(async (smartWallet, chainId, amount, verification) => {
4507
+ const batch = async (chainId, transfers) => {
4436
4508
  const cid = chainId;
4437
4509
  const token = deps.tokenAddressByChain[cid];
4438
4510
  if (!token) {
@@ -4448,19 +4520,24 @@ function makeSponsoredCallsCallback(deps) {
4448
4520
  { chainId }
4449
4521
  );
4450
4522
  }
4451
- const data = encodeFunctionData2({
4452
- abi: erc20Abi2,
4453
- functionName: "transfer",
4454
- args: [smartWallet, BigInt(amount)]
4455
- });
4523
+ const calls = transfers.map((transfer) => ({
4524
+ to: token,
4525
+ value: "0x0",
4526
+ data: encodeFunctionData2({
4527
+ abi: erc20Abi2,
4528
+ functionName: "transfer",
4529
+ args: [transfer.to, BigInt(transfer.amount)]
4530
+ })
4531
+ }));
4456
4532
  let paymasterUrl = absolutePaymasterUrl();
4457
- if (verification?.agentId === "yieldseeker") {
4458
- if (chainId !== 8453) {
4533
+ for (const transfer of transfers) {
4534
+ const verification = transfer.yieldseeker;
4535
+ if (!verification) continue;
4536
+ if (chainId !== 8453)
4459
4537
  throw new OwneyError(
4460
4538
  "CHAIN_UNSUPPORTED",
4461
- `Yieldseeker Base Account sponsorship is not available on chain ${chainId}.`
4539
+ `Yieldseeker sponsorship is not available on chain ${chainId}.`
4462
4540
  );
4463
- }
4464
4541
  const { intent } = await postPaymasterIntent({
4465
4542
  baseUrl: deps.routingApiBaseUrl,
4466
4543
  apiKey: deps.apiKey,
@@ -4469,14 +4546,14 @@ function makeSponsoredCallsCallback(deps) {
4469
4546
  chainId,
4470
4547
  token,
4471
4548
  from: deps.ownerAddress,
4472
- to: smartWallet,
4473
- amount,
4549
+ to: transfer.to,
4550
+ amount: transfer.amount,
4474
4551
  yieldseekerUserId: verification.userId,
4475
- yieldseekerAgentId: verification.yieldseekerAgentId
4552
+ yieldseekerAgentId: verification.agentId
4476
4553
  }
4477
4554
  });
4478
4555
  const url = new URL(paymasterUrl);
4479
- url.searchParams.set("owneyIntent", intent);
4556
+ url.searchParams.append("owneyIntent", intent);
4480
4557
  paymasterUrl = url.toString();
4481
4558
  }
4482
4559
  const sendResult = await deps.provider.request({
@@ -4486,8 +4563,8 @@ function makeSponsoredCallsCallback(deps) {
4486
4563
  version: "2.0.0",
4487
4564
  from: deps.ownerAddress,
4488
4565
  chainId: toHex(chainId),
4489
- atomicRequired: false,
4490
- calls: [{ to: token, value: "0x0", data }],
4566
+ atomicRequired: transfers.length > 1,
4567
+ calls,
4491
4568
  capabilities: {
4492
4569
  paymasterService: { url: paymasterUrl }
4493
4570
  }
@@ -4509,7 +4586,24 @@ function makeSponsoredCallsCallback(deps) {
4509
4586
  params: [callsId]
4510
4587
  });
4511
4588
  const txHash = status?.receipts?.[0]?.transactionHash;
4512
- if (txHash) return txHash;
4589
+ if (status?.receipts?.some((receipt) => receipt.status === "0x0") || typeof status?.status === "number" && status.status >= 400) {
4590
+ throw new OwneyError(
4591
+ "SPONSOR_REQUEST_FAILED",
4592
+ "The sponsored deposit did not complete successfully.",
4593
+ { chainId, callsId, safeToFallback: false }
4594
+ );
4595
+ }
4596
+ if (txHash && (transfers.length === 1 || status?.status === 200 || status?.status === "CONFIRMED")) {
4597
+ if (status?.receipts?.some(
4598
+ (receipt) => receipt.transactionHash !== txHash
4599
+ ))
4600
+ throw new OwneyError(
4601
+ "SPONSORED_CALLS_NO_RECEIPT",
4602
+ "The wallet returned multiple transactions for an atomic deposit. Check activity before trying again.",
4603
+ { chainId, callsId }
4604
+ );
4605
+ return txHash;
4606
+ }
4513
4607
  if (pollIntervalMs > 0) {
4514
4608
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
4515
4609
  }
@@ -4519,7 +4613,12 @@ function makeSponsoredCallsCallback(deps) {
4519
4613
  `No receipt for calls ${callsId} after ${maxPolls} polls; the deposit may still settle.`,
4520
4614
  { chainId, callsId }
4521
4615
  );
4522
- });
4616
+ };
4617
+ const callback = makeVerificationAwareDepositCallback(
4618
+ (to, chainId, amount, verification) => batch(chainId, [toBatchTransfer(to, amount, verification)])
4619
+ );
4620
+ registerDepositBatch(callback, batch);
4621
+ return callback;
4523
4622
  }
4524
4623
 
4525
4624
  // src/client.ts
@@ -4558,6 +4657,11 @@ var SPONSORED_WETH_BY_CHAIN = {
4558
4657
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
4559
4658
  1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
4560
4659
  };
4660
+ var SPONSORED_TOKENS_BY_ASSET = { USDC: SPONSORED_USDC_BY_CHAIN, WETH: SPONSORED_WETH_BY_CHAIN };
4661
+ function sponsoredTokensFor(asset) {
4662
+ if (!Object.prototype.hasOwnProperty.call(SPONSORED_TOKENS_BY_ASSET, asset)) throw new OwneyError("ASSET_UNSUPPORTED", `No sponsored token configured for ${asset}`);
4663
+ return SPONSORED_TOKENS_BY_ASSET[asset];
4664
+ }
4561
4665
  function shouldFallbackToUserPaid(error, asset, appCallback) {
4562
4666
  return (asset === "WETH" || asset === "USDC") && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
4563
4667
  }
@@ -4692,18 +4796,13 @@ var OwneySDK = class {
4692
4796
  }
4693
4797
  return this.state.provider;
4694
4798
  }
4695
- /**
4696
- * Lazily builds (and caches) the default EIP-3009 sponsored deposit callback
4697
- * used when the caller omits `depositCallback`. Wraps the connected EIP-1193
4698
- * provider with viem `custom(provider)` to read token meta and sign the
4699
- * `TransferWithAuthorization`, then POSTs to the sponsor API.
4700
- */
4799
+ /** Builds the default USDC batch callback for the connected wallet. */
4701
4800
  getDefaultSponsoredCallback(onApproved) {
4702
4801
  if (!onApproved && this.cachedSponsoredCallback)
4703
4802
  return this.cachedSponsoredCallback;
4704
4803
  const provider = this.requireConnectedProvider();
4705
4804
  const owner = this.state.walletAddress;
4706
- const callback = makeSponsoredDepositCallback({
4805
+ const callback = makeSponsoredTokenCallback({
4707
4806
  apiKey: this.apiKey,
4708
4807
  baseUrl: this.routingApiBaseUrl,
4709
4808
  ownerAddress: owner,
@@ -4725,12 +4824,7 @@ var OwneySDK = class {
4725
4824
  if (!onApproved) this.cachedSponsoredCallback = callback;
4726
4825
  return callback;
4727
4826
  }
4728
- /**
4729
- * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
4730
- * callback used when the caller omits `depositCallback` for a WETH
4731
- * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
4732
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
4733
- */
4827
+ /** Builds the wallet-native sponsored calls callback for compatible paymasters. */
4734
4828
  getDefaultSponsoredCallsCallback(asset, onApproved) {
4735
4829
  const cached = this.cachedSponsoredCallsCallbacks.get(asset);
4736
4830
  if (!onApproved && cached) return cached;
@@ -4742,7 +4836,7 @@ var OwneySDK = class {
4742
4836
  ownerAddress: this.state.walletAddress,
4743
4837
  paymasterServiceUrl: this.paymasterServiceUrl,
4744
4838
  onApproved,
4745
- tokenAddressByChain: asset === "WETH" ? SPONSORED_WETH_BY_CHAIN : SPONSORED_USDC_BY_CHAIN
4839
+ tokenAddressByChain: sponsoredTokensFor(asset)
4746
4840
  });
4747
4841
  if (!onApproved) this.cachedSponsoredCallsCallbacks.set(asset, callback);
4748
4842
  return callback;
@@ -4751,14 +4845,14 @@ var OwneySDK = class {
4751
4845
  * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
4752
4846
  * callback used when the caller omits `depositCallback` for a WETH deposit.
4753
4847
  * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
4754
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
4848
+ * single-use batch authorization instead of an EIP-3009 authorization.
4755
4849
  */
4756
4850
  getDefaultWethSponsoredCallback(onApproved) {
4757
4851
  if (!onApproved && this.cachedWethSponsoredCallback)
4758
4852
  return this.cachedWethSponsoredCallback;
4759
4853
  const provider = this.requireConnectedProvider();
4760
4854
  const owner = this.state.walletAddress;
4761
- const callback = makeSponsoredWethCallback({
4855
+ const callback = makeSponsoredTokenCallback({
4762
4856
  apiKey: this.apiKey,
4763
4857
  baseUrl: this.routingApiBaseUrl,
4764
4858
  ownerAddress: owner,
@@ -5064,7 +5158,8 @@ var OwneySDK = class {
5064
5158
  * @param options.asset - Asset symbol to deposit (e.g. "USDC")
5065
5159
  * @param options.depositCallback - Callback that performs the token transfer and returns a tx hash.
5066
5160
  * When agentId is omitted, this callback is invoked once per eligible agent with that agent's
5067
- * split amount and smart wallet address — expect multiple wallet prompts.
5161
+ * split amount and smart wallet address. Default sponsored deposits batch
5162
+ * all shares into one signature; custom callbacks still run once per agent.
5068
5163
  * @param options.agentId - Optional explicit target. Otherwise split equally,
5069
5164
  * or fund remaining agents when a recovery deposit cannot meet every minimum.
5070
5165
  * @returns {OwneyDepositResult} for a single agent, or {OwneyMultiDepositResult} with per-agent results
@@ -5149,6 +5244,39 @@ var OwneySDK = class {
5149
5244
  }
5150
5245
  );
5151
5246
  }
5247
+ const batchTransfer = getDepositBatchTransfer(effectiveCallback);
5248
+ if (!depositCallback && batchTransfer) {
5249
+ return runAgentDepositBatch(
5250
+ chainId,
5251
+ agentAmounts.map(({ agent, amount: amount2 }) => ({
5252
+ id: agent.id,
5253
+ amount: amount2,
5254
+ run: (callback) => withFailureReporting(
5255
+ this.apiKey,
5256
+ agent.id,
5257
+ () => agent.deposit(state, chainId, amount2, asset, callback),
5258
+ this.routingApiBaseUrl
5259
+ )
5260
+ })),
5261
+ async (cid, transfers) => {
5262
+ try {
5263
+ return await batchTransfer(cid, transfers);
5264
+ } catch (error) {
5265
+ if (!(error instanceof OwneyError) || error.code !== "PERMIT2_APPROVAL_REQUIRED")
5266
+ throw error;
5267
+ const requiredAmount = transfers.reduce(
5268
+ (sum, transfer) => sum + BigInt(transfer.amount),
5269
+ 0n
5270
+ );
5271
+ await this.approvePermit2(
5272
+ asset,
5273
+ requiredAmount
5274
+ );
5275
+ return batchTransfer(cid, transfers);
5276
+ }
5277
+ }
5278
+ );
5279
+ }
5152
5280
  const agentResults = {};
5153
5281
  for (const [
5154
5282
  index,
@@ -5188,7 +5316,7 @@ var OwneySDK = class {
5188
5316
  *
5189
5317
  * 1. Missing Permit2 allowance: when the app did not supply its own
5190
5318
  * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
5191
- * WETH deposit, this is the wallet's first gasless WETH deposit. We send
5319
+ * token deposit, this is the wallet's first Permit2 deposit for that token. We send
5192
5320
  * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
5193
5321
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
5194
5322
  * per call so a wallet/agent that keeps reporting the allowance as
@@ -5225,12 +5353,15 @@ var OwneySDK = class {
5225
5353
  try {
5226
5354
  return await attempt(effectiveCallback);
5227
5355
  } catch (error) {
5228
- if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
5356
+ if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
5229
5357
  approvalAttempted = true;
5230
5358
  console.warn(
5231
- "[owney-sdk] First WETH deposit: sending one-time Permit2 approval..."
5359
+ "[owney-sdk] First token deposit: sending one-time Permit2 approval..."
5360
+ );
5361
+ await this.approvePermit2(
5362
+ asset,
5363
+ BigInt(amount)
5232
5364
  );
5233
- await this.approvePermit2();
5234
5365
  continue;
5235
5366
  }
5236
5367
  if (!shouldFallbackToUserPaid(error, asset, appCallback)) throw error;
@@ -5883,26 +6014,31 @@ var OwneySDK = class {
5883
6014
  return pending;
5884
6015
  }
5885
6016
  /**
5886
- * One-time, user-paid approval of Permit2 on the sponsored WETH token for
5887
- * the active chain. Required once per wallet per chain before gasless WETH
5888
- * deposits; afterwards deposit() is signature-only. Resolves only after the
5889
- * approval transaction is mined (1 confirmation), so a subsequent deposit()
5890
- * will see the new allowance; throws if the transaction reverted.
6017
+ * User-paid approval of Permit2 on the selected token for the active chain.
6018
+ * Approves exactly the pending deposit amount. Another approval is required
6019
+ * for a later deposit once this allowance has been consumed. Resolves after
6020
+ * one confirmation so the subsequent deposit attempt sees the new allowance.
6021
+ *
6022
+ * @param requiredAmount Raw base-unit amount the pending deposit must cover.
5891
6023
  * @returns the approval transaction hash.
5892
6024
  */
5893
- async approvePermit2(asset = "WETH") {
5894
- void asset;
6025
+ async approvePermit2(asset = "WETH", requiredAmount = 0n) {
5895
6026
  const state = this.requireState();
5896
6027
  const chainId = this.requireChainId();
5897
- this.validateAssetSupport(this.getAgent("zyfai"), chainId, "WETH");
5898
- const token = SPONSORED_WETH_BY_CHAIN[chainId];
6028
+ this.getEligibleAgents(chainId, asset, { excludeDisabled: true });
6029
+ const token = sponsoredTokensFor(asset)[chainId];
5899
6030
  if (!token) {
5900
6031
  throw new OwneyError(
5901
6032
  "CHAIN_UNSUPPORTED",
5902
- `No sponsored WETH on chain ${chainId}`
6033
+ `No sponsored token on chain ${chainId}`
5903
6034
  );
5904
6035
  }
5905
6036
  const provider = this.requireConnectedProvider();
6037
+ const publicClient = createPublicClient4({
6038
+ chain: VIEM_CHAIN2[chainId],
6039
+ transport: custom3(provider)
6040
+ });
6041
+ const approvalAmount = permit2ApprovalAmount(requiredAmount);
5906
6042
  const wallet = createWalletClient3({
5907
6043
  account: state.walletAddress,
5908
6044
  chain: VIEM_CHAIN2[chainId],
@@ -5912,14 +6048,10 @@ var OwneySDK = class {
5912
6048
  address: token,
5913
6049
  abi: ERC20_ALLOWANCE_ABI,
5914
6050
  functionName: "approve",
5915
- args: [PERMIT2_ADDRESS, MAX_UINT256],
6051
+ args: [PERMIT2_ADDRESS, approvalAmount],
5916
6052
  account: state.walletAddress,
5917
6053
  chain: VIEM_CHAIN2[chainId]
5918
6054
  });
5919
- const publicClient = createPublicClient4({
5920
- chain: VIEM_CHAIN2[chainId],
5921
- transport: custom3(provider)
5922
- });
5923
6055
  const receipt = await publicClient.waitForTransactionReceipt({
5924
6056
  hash,
5925
6057
  confirmations: 1