@owney/sdk 0.7.21-beta.3 → 0.7.21-beta.5

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,54 +2157,53 @@ 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
@@ -2297,29 +2239,6 @@ var ERC20_ALLOWANCE_ABI = [
2297
2239
  outputs: [{ name: "", type: "uint256" }]
2298
2240
  }
2299
2241
  ];
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
2242
  function randomPermit2Nonce() {
2324
2243
  const bytes = new Uint8Array(32);
2325
2244
  globalThis.crypto.getRandomValues(bytes);
@@ -2362,88 +2281,6 @@ function makeVerificationAwareDepositCallback(implementation) {
2362
2281
  });
2363
2282
  return callback;
2364
2283
  }
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
2284
 
2448
2285
  // src/agents/yieldseeker/yieldseeker.auth.ts
2449
2286
  import { SiweMessage, generateNonce } from "siwe";
@@ -4311,98 +4148,302 @@ import {
4311
4148
  } from "viem";
4312
4149
  import { base as base4, arbitrum as arbitrum2, mainnet as mainnet2 } from "viem/chains";
4313
4150
 
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
4151
+ // src/lib/sponsored-token-batch.ts
4152
+ import {
4153
+ isAddressEqual,
4154
+ keccak256,
4155
+ toBytes
4156
+ } from "viem";
4157
+
4158
+ // src/lib/permit2-batch.ts
4159
+ import { parseAbi as parseAbi2, hashStruct } from "viem";
4160
+ var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
4161
+ var PERMIT_BATCH_TYPES = {
4162
+ PermitBatchWitnessTransferFrom: [
4163
+ { name: "permitted", type: "TokenPermissions[]" },
4164
+ { name: "spender", type: "address" },
4165
+ { name: "nonce", type: "uint256" },
4166
+ { name: "deadline", type: "uint256" },
4167
+ { name: "witness", type: "Deposit" }
4168
+ ],
4169
+ Deposit: [{ name: "recipients", type: "address[]" }],
4170
+ TokenPermissions: [
4171
+ { name: "token", type: "address" },
4172
+ { name: "amount", type: "uint256" }
4173
+ ]
4174
+ };
4175
+ var PERMIT2_BATCH_ABI = parseAbi2([
4176
+ "struct TokenPermissions { address token; uint256 amount; }",
4177
+ "struct PermitBatchTransferFrom { TokenPermissions[] permitted; uint256 nonce; uint256 deadline; }",
4178
+ "struct SignatureTransferDetails { address to; uint256 requestedAmount; }",
4179
+ "function permitWitnessTransferFrom(PermitBatchTransferFrom permit,SignatureTransferDetails[] transferDetails,address owner,bytes32 witness,string witnessTypeString,bytes signature)",
4180
+ "function nonceBitmap(address owner,uint256 wordPos) view returns (uint256)"
4181
+ ]);
4182
+ function batchPermit(b) {
4183
+ return {
4184
+ permitted: b.transfers.map((t) => ({
4185
+ token: b.token,
4186
+ amount: BigInt(t.amount)
4187
+ })),
4188
+ nonce: BigInt(b.nonce),
4189
+ deadline: BigInt(b.deadline)
4190
+ };
4191
+ }
4192
+ function batchTypedData(b, spender) {
4193
+ return {
4194
+ domain: {
4195
+ name: "Permit2",
4196
+ chainId: b.chainId,
4197
+ verifyingContract: BATCH_PERMIT2_ADDRESS
4198
+ },
4199
+ types: PERMIT_BATCH_TYPES,
4200
+ primaryType: "PermitBatchWitnessTransferFrom",
4201
+ message: {
4202
+ ...batchPermit(b),
4203
+ spender,
4204
+ witness: { recipients: b.transfers.map((t) => t.to) }
4205
+ }
4206
+ };
4207
+ }
4208
+
4209
+ // src/lib/sponsored-token-batch.ts
4210
+ var memory = /* @__PURE__ */ new Map();
4211
+ var inflight = /* @__PURE__ */ new Map();
4212
+ var planOf = (transfers) => JSON.stringify(
4213
+ transfers.map((t) => ({
4214
+ to: t.to.toLowerCase(),
4215
+ amount: BigInt(t.amount).toString()
4216
+ }))
4217
+ );
4218
+ function read(key2) {
4219
+ return typeof window === "undefined" ? memory.get(key2) : window.localStorage.getItem(key2);
4220
+ }
4221
+ function save(key2, body) {
4222
+ const value = JSON.stringify({
4223
+ ...body,
4224
+ transfers: body.transfers.map(({ to, amount }) => ({ to, amount }))
4225
+ });
4226
+ if (typeof window === "undefined") memory.set(key2, value);
4227
+ else window.localStorage.setItem(key2, value);
4228
+ }
4229
+ function clear(key2) {
4230
+ if (typeof window === "undefined") memory.delete(key2);
4231
+ else window.localStorage.removeItem(key2);
4232
+ }
4233
+ function sponsorTokenBatch(i) {
4234
+ const key2 = `owney.token-batch.v1:${keccak256(toBytes(i.apiKey))}:${i.baseUrl ?? "default"}:${i.chainId}:${i.owner.toLowerCase()}:${i.token.toLowerCase()}`;
4235
+ const plan = planOf(i.transfers);
4236
+ const active = inflight.get(key2);
4237
+ if (active) {
4238
+ if (active.plan !== plan)
4239
+ return Promise.reject(
4240
+ new Error(
4241
+ "A token deposit is already in progress. Wait for its result before depositing again."
4242
+ )
4353
4243
  );
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
- );
4244
+ return active.promise;
4245
+ }
4246
+ const promise = execute(i, key2, plan).finally(() => inflight.delete(key2));
4247
+ inflight.set(key2, { plan, promise });
4248
+ return promise;
4249
+ }
4250
+ async function execute(i, key2, plan) {
4251
+ if (!i.transfers.length || i.transfers.length > 16 || i.transfers.some(
4252
+ (t) => BigInt(t.amount) <= 0n || BigInt(t.amount) >= 1n << 256n
4253
+ ) || new Set(i.transfers.map((t) => t.to.toLowerCase())).size !== i.transfers.length)
4254
+ throw new Error("Invalid token deposit shares.");
4255
+ const send = async (initial) => {
4256
+ let body = initial;
4257
+ save(key2, body);
4258
+ try {
4259
+ if (!body.serializedTransaction) {
4260
+ const prepared = await postSponsorBatchTransfer({
4261
+ apiKey: i.apiKey,
4262
+ baseUrl: i.baseUrl,
4263
+ body
4264
+ });
4265
+ if (!prepared.serializedTransaction || keccak256(prepared.serializedTransaction) !== prepared.txHash)
4266
+ throw new Error(
4267
+ "Sponsorship API did not return a valid prepared transaction."
4268
+ );
4269
+ body = {
4270
+ ...body,
4271
+ serializedTransaction: prepared.serializedTransaction
4272
+ };
4273
+ save(key2, body);
4360
4274
  }
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
- }
4275
+ const result = await postSponsorBatchTransfer({
4276
+ apiKey: i.apiKey,
4277
+ baseUrl: i.baseUrl,
4278
+ body
4402
4279
  });
4280
+ if (result.txHash !== keccak256(body.serializedTransaction))
4281
+ throw new Error(
4282
+ "Sponsorship receipt does not match the pending transaction."
4283
+ );
4284
+ clear(key2);
4403
4285
  return result.txHash;
4286
+ } catch (error) {
4287
+ if (!body.serializedTransaction || error instanceof OwneyError && error.details?.notSubmitted === true)
4288
+ clear(key2);
4289
+ throw error;
4404
4290
  }
4291
+ };
4292
+ const saved = read(key2);
4293
+ if (saved) {
4294
+ const previous = JSON.parse(saved);
4295
+ if (previous.chainId !== i.chainId || !isAddressEqual(previous.from, i.owner) || !isAddressEqual(previous.token, i.token) || planOf(previous.transfers) !== plan)
4296
+ throw new Error(
4297
+ "Retry the previous token deposit and agent split first to reconcile its status."
4298
+ );
4299
+ i.onApproved?.();
4300
+ return send({ ...previous, transfers: i.transfers });
4301
+ }
4302
+ const total = i.transfers.reduce((sum, t) => sum + BigInt(t.amount), 0n);
4303
+ const [balance, allowance] = await Promise.all([
4304
+ readErc20Balance(i.pub, i.token, i.owner),
4305
+ readPermit2Allowance(i.pub, i.token, i.owner)
4306
+ ]);
4307
+ if (balance < total)
4308
+ throw new OwneyError(
4309
+ "DEPOSIT_INSUFFICIENT_BALANCE",
4310
+ "Insufficient token balance for this deposit."
4311
+ );
4312
+ if (allowance < total)
4313
+ throw new OwneyError(
4314
+ "PERMIT2_APPROVAL_REQUIRED",
4315
+ "token deposits need a one-time Permit2 approval."
4316
+ );
4317
+ const relayer = await getSponsorRelayerAddress({
4318
+ apiKey: i.apiKey,
4319
+ baseUrl: i.baseUrl,
4320
+ chainId: i.chainId
4321
+ });
4322
+ const now = (await i.pub.getBlock()).timestamp;
4323
+ const unsigned = {
4324
+ chainId: i.chainId,
4325
+ token: i.token,
4326
+ from: i.owner,
4327
+ transfers: i.transfers,
4328
+ nonce: randomPermit2Nonce().toString(),
4329
+ deadline: (now + 900n).toString()
4330
+ };
4331
+ const signature = await i.wallet.signTypedData({
4332
+ account: i.owner,
4333
+ ...batchTypedData(unsigned, relayer)
4334
+ });
4335
+ i.onApproved?.();
4336
+ return send({ ...unsigned, signature });
4337
+ }
4338
+
4339
+ // src/lib/sponsored-token-deposit.ts
4340
+ function makeSponsoredTokenCallback(deps) {
4341
+ const batch = async (chainId, transfers) => {
4342
+ if (chainId !== 8453 && chainId !== 42161 && chainId !== 1)
4343
+ throw new OwneyError(
4344
+ "CHAIN_UNSUPPORTED",
4345
+ `No sponsored token configured for chain ${chainId}`
4346
+ );
4347
+ const token = deps.tokenAddressByChain[chainId];
4348
+ if (!token)
4349
+ throw new OwneyError(
4350
+ "CHAIN_UNSUPPORTED",
4351
+ `No sponsored token configured for chain ${chainId}`
4352
+ );
4353
+ const pub = deps.getPublicClient(chainId), wallet = deps.getWalletClient(chainId);
4354
+ await ensureWalletOnChain(pub, wallet, chainId);
4355
+ return sponsorTokenBatch({
4356
+ apiKey: deps.apiKey,
4357
+ baseUrl: deps.baseUrl,
4358
+ owner: deps.ownerAddress,
4359
+ token,
4360
+ chainId,
4361
+ transfers,
4362
+ pub,
4363
+ wallet,
4364
+ onApproved: deps.onApproved
4365
+ });
4366
+ };
4367
+ const callback = makeVerificationAwareDepositCallback(
4368
+ (to, chainId, amount, verification) => batch(chainId, [toBatchTransfer(to, amount, verification)])
4405
4369
  );
4370
+ registerDepositBatch(callback, batch);
4371
+ return callback;
4372
+ }
4373
+
4374
+ // src/lib/agent-deposit-batch.ts
4375
+ function deferred() {
4376
+ let resolve, reject;
4377
+ const promise = new Promise((yes, no) => {
4378
+ resolve = yes;
4379
+ reject = no;
4380
+ });
4381
+ void promise.catch(() => {
4382
+ });
4383
+ return { promise, resolve, reject };
4384
+ }
4385
+ async function runAgentDepositBatch(chainId, legs, transfer) {
4386
+ const funding = deferred();
4387
+ const tasks = [];
4388
+ const transfers = [];
4389
+ try {
4390
+ for (const leg of legs) {
4391
+ const ready = deferred();
4392
+ let entered = false;
4393
+ const callback = makeVerificationAwareDepositCallback(
4394
+ (to, cid, amount, verification) => {
4395
+ if (entered || cid !== chainId || BigInt(amount) !== BigInt(leg.amount)) {
4396
+ const error = new Error(
4397
+ "Agent changed its prepared deposit share."
4398
+ );
4399
+ ready.reject(error);
4400
+ throw error;
4401
+ }
4402
+ entered = true;
4403
+ ready.resolve(toBatchTransfer(to, amount, verification));
4404
+ return funding.promise;
4405
+ }
4406
+ );
4407
+ const task = Promise.resolve().then(() => leg.run(callback));
4408
+ tasks.push(task);
4409
+ void task.then(
4410
+ () => {
4411
+ if (!entered)
4412
+ ready.reject(
4413
+ new Error("Agent did not prepare a deposit transfer.")
4414
+ );
4415
+ },
4416
+ (error) => ready.reject(error)
4417
+ );
4418
+ transfers.push(await ready.promise);
4419
+ }
4420
+ const txHash = await transfer(chainId, transfers);
4421
+ funding.resolve(txHash);
4422
+ const settled = await Promise.allSettled(tasks);
4423
+ const agentResults = {};
4424
+ const failures = [];
4425
+ for (const [index, result] of settled.entries()) {
4426
+ if (result.status === "fulfilled")
4427
+ agentResults[legs[index].id] = result.value;
4428
+ else failures.push(legs[index].id);
4429
+ }
4430
+ if (failures.length)
4431
+ throw new OwneyError(
4432
+ "DEPOSIT_PARTIAL_FAILURE",
4433
+ "The deposit was sent to all agents, but some agent updates could not be confirmed. Check activity before depositing again.",
4434
+ {
4435
+ txHash,
4436
+ fundsSubmitted: true,
4437
+ agentResults,
4438
+ failedAgentIds: failures
4439
+ }
4440
+ );
4441
+ return { agentResults };
4442
+ } catch (error) {
4443
+ funding.reject(error);
4444
+ await Promise.allSettled(tasks);
4445
+ throw error;
4446
+ }
4406
4447
  }
4407
4448
 
4408
4449
  // src/lib/sponsored-calls-deposit.ts
@@ -4432,7 +4473,7 @@ function makeSponsoredCallsCallback(deps) {
4432
4473
  }
4433
4474
  return new URL(configured, origin).toString();
4434
4475
  };
4435
- return makeVerificationAwareDepositCallback(async (smartWallet, chainId, amount, verification) => {
4476
+ const batch = async (chainId, transfers) => {
4436
4477
  const cid = chainId;
4437
4478
  const token = deps.tokenAddressByChain[cid];
4438
4479
  if (!token) {
@@ -4448,19 +4489,24 @@ function makeSponsoredCallsCallback(deps) {
4448
4489
  { chainId }
4449
4490
  );
4450
4491
  }
4451
- const data = encodeFunctionData2({
4452
- abi: erc20Abi2,
4453
- functionName: "transfer",
4454
- args: [smartWallet, BigInt(amount)]
4455
- });
4492
+ const calls = transfers.map((transfer) => ({
4493
+ to: token,
4494
+ value: "0x0",
4495
+ data: encodeFunctionData2({
4496
+ abi: erc20Abi2,
4497
+ functionName: "transfer",
4498
+ args: [transfer.to, BigInt(transfer.amount)]
4499
+ })
4500
+ }));
4456
4501
  let paymasterUrl = absolutePaymasterUrl();
4457
- if (verification?.agentId === "yieldseeker") {
4458
- if (chainId !== 8453) {
4502
+ for (const transfer of transfers) {
4503
+ const verification = transfer.yieldseeker;
4504
+ if (!verification) continue;
4505
+ if (chainId !== 8453)
4459
4506
  throw new OwneyError(
4460
4507
  "CHAIN_UNSUPPORTED",
4461
- `Yieldseeker Base Account sponsorship is not available on chain ${chainId}.`
4508
+ `Yieldseeker sponsorship is not available on chain ${chainId}.`
4462
4509
  );
4463
- }
4464
4510
  const { intent } = await postPaymasterIntent({
4465
4511
  baseUrl: deps.routingApiBaseUrl,
4466
4512
  apiKey: deps.apiKey,
@@ -4469,14 +4515,14 @@ function makeSponsoredCallsCallback(deps) {
4469
4515
  chainId,
4470
4516
  token,
4471
4517
  from: deps.ownerAddress,
4472
- to: smartWallet,
4473
- amount,
4518
+ to: transfer.to,
4519
+ amount: transfer.amount,
4474
4520
  yieldseekerUserId: verification.userId,
4475
- yieldseekerAgentId: verification.yieldseekerAgentId
4521
+ yieldseekerAgentId: verification.agentId
4476
4522
  }
4477
4523
  });
4478
4524
  const url = new URL(paymasterUrl);
4479
- url.searchParams.set("owneyIntent", intent);
4525
+ url.searchParams.append("owneyIntent", intent);
4480
4526
  paymasterUrl = url.toString();
4481
4527
  }
4482
4528
  const sendResult = await deps.provider.request({
@@ -4486,8 +4532,8 @@ function makeSponsoredCallsCallback(deps) {
4486
4532
  version: "2.0.0",
4487
4533
  from: deps.ownerAddress,
4488
4534
  chainId: toHex(chainId),
4489
- atomicRequired: false,
4490
- calls: [{ to: token, value: "0x0", data }],
4535
+ atomicRequired: transfers.length > 1,
4536
+ calls,
4491
4537
  capabilities: {
4492
4538
  paymasterService: { url: paymasterUrl }
4493
4539
  }
@@ -4509,7 +4555,24 @@ function makeSponsoredCallsCallback(deps) {
4509
4555
  params: [callsId]
4510
4556
  });
4511
4557
  const txHash = status?.receipts?.[0]?.transactionHash;
4512
- if (txHash) return txHash;
4558
+ if (status?.receipts?.some((receipt) => receipt.status === "0x0") || typeof status?.status === "number" && status.status >= 400) {
4559
+ throw new OwneyError(
4560
+ "SPONSOR_REQUEST_FAILED",
4561
+ "The sponsored deposit did not complete successfully.",
4562
+ { chainId, callsId, safeToFallback: false }
4563
+ );
4564
+ }
4565
+ if (txHash && (transfers.length === 1 || status?.status === 200 || status?.status === "CONFIRMED")) {
4566
+ if (status?.receipts?.some(
4567
+ (receipt) => receipt.transactionHash !== txHash
4568
+ ))
4569
+ throw new OwneyError(
4570
+ "SPONSORED_CALLS_NO_RECEIPT",
4571
+ "The wallet returned multiple transactions for an atomic deposit. Check activity before trying again.",
4572
+ { chainId, callsId }
4573
+ );
4574
+ return txHash;
4575
+ }
4513
4576
  if (pollIntervalMs > 0) {
4514
4577
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
4515
4578
  }
@@ -4519,7 +4582,12 @@ function makeSponsoredCallsCallback(deps) {
4519
4582
  `No receipt for calls ${callsId} after ${maxPolls} polls; the deposit may still settle.`,
4520
4583
  { chainId, callsId }
4521
4584
  );
4522
- });
4585
+ };
4586
+ const callback = makeVerificationAwareDepositCallback(
4587
+ (to, chainId, amount, verification) => batch(chainId, [toBatchTransfer(to, amount, verification)])
4588
+ );
4589
+ registerDepositBatch(callback, batch);
4590
+ return callback;
4523
4591
  }
4524
4592
 
4525
4593
  // src/client.ts
@@ -4558,6 +4626,11 @@ var SPONSORED_WETH_BY_CHAIN = {
4558
4626
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
4559
4627
  1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
4560
4628
  };
4629
+ var SPONSORED_TOKENS_BY_ASSET = { USDC: SPONSORED_USDC_BY_CHAIN, WETH: SPONSORED_WETH_BY_CHAIN };
4630
+ function sponsoredTokensFor(asset) {
4631
+ if (!Object.prototype.hasOwnProperty.call(SPONSORED_TOKENS_BY_ASSET, asset)) throw new OwneyError("ASSET_UNSUPPORTED", `No sponsored token configured for ${asset}`);
4632
+ return SPONSORED_TOKENS_BY_ASSET[asset];
4633
+ }
4561
4634
  function shouldFallbackToUserPaid(error, asset, appCallback) {
4562
4635
  return (asset === "WETH" || asset === "USDC") && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
4563
4636
  }
@@ -4692,18 +4765,13 @@ var OwneySDK = class {
4692
4765
  }
4693
4766
  return this.state.provider;
4694
4767
  }
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
- */
4768
+ /** Builds the default USDC batch callback for the connected wallet. */
4701
4769
  getDefaultSponsoredCallback(onApproved) {
4702
4770
  if (!onApproved && this.cachedSponsoredCallback)
4703
4771
  return this.cachedSponsoredCallback;
4704
4772
  const provider = this.requireConnectedProvider();
4705
4773
  const owner = this.state.walletAddress;
4706
- const callback = makeSponsoredDepositCallback({
4774
+ const callback = makeSponsoredTokenCallback({
4707
4775
  apiKey: this.apiKey,
4708
4776
  baseUrl: this.routingApiBaseUrl,
4709
4777
  ownerAddress: owner,
@@ -4725,12 +4793,7 @@ var OwneySDK = class {
4725
4793
  if (!onApproved) this.cachedSponsoredCallback = callback;
4726
4794
  return callback;
4727
4795
  }
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
- */
4796
+ /** Builds the wallet-native sponsored calls callback for compatible paymasters. */
4734
4797
  getDefaultSponsoredCallsCallback(asset, onApproved) {
4735
4798
  const cached = this.cachedSponsoredCallsCallbacks.get(asset);
4736
4799
  if (!onApproved && cached) return cached;
@@ -4742,7 +4805,7 @@ var OwneySDK = class {
4742
4805
  ownerAddress: this.state.walletAddress,
4743
4806
  paymasterServiceUrl: this.paymasterServiceUrl,
4744
4807
  onApproved,
4745
- tokenAddressByChain: asset === "WETH" ? SPONSORED_WETH_BY_CHAIN : SPONSORED_USDC_BY_CHAIN
4808
+ tokenAddressByChain: sponsoredTokensFor(asset)
4746
4809
  });
4747
4810
  if (!onApproved) this.cachedSponsoredCallsCallbacks.set(asset, callback);
4748
4811
  return callback;
@@ -4751,14 +4814,14 @@ var OwneySDK = class {
4751
4814
  * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
4752
4815
  * callback used when the caller omits `depositCallback` for a WETH deposit.
4753
4816
  * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
4754
- * `PermitTransferFrom` instead of an EIP-3009 authorization.
4817
+ * single-use batch authorization instead of an EIP-3009 authorization.
4755
4818
  */
4756
4819
  getDefaultWethSponsoredCallback(onApproved) {
4757
4820
  if (!onApproved && this.cachedWethSponsoredCallback)
4758
4821
  return this.cachedWethSponsoredCallback;
4759
4822
  const provider = this.requireConnectedProvider();
4760
4823
  const owner = this.state.walletAddress;
4761
- const callback = makeSponsoredWethCallback({
4824
+ const callback = makeSponsoredTokenCallback({
4762
4825
  apiKey: this.apiKey,
4763
4826
  baseUrl: this.routingApiBaseUrl,
4764
4827
  ownerAddress: owner,
@@ -5010,10 +5073,9 @@ var OwneySDK = class {
5010
5073
  * Serializing costs no real wall-clock: the user can only approve one prompt
5011
5074
  * at a time anyway.
5012
5075
  *
5013
- * Every agent is attempted even if an earlier one fails, so one declined
5014
- * signature can't deny the remaining agents their turn. Once all agents have
5015
- * had a chance, a partial failure identifies the agents that still need a
5016
- * retry; if none activated, the original provider error is preserved.
5076
+ * Stop at the first failure so a canceled sign-in does not open another
5077
+ * agent's wallet prompt. Report any earlier successes for diagnostics; the
5078
+ * app discards the session when the complete sign-in does not succeed.
5017
5079
  */
5018
5080
  async activateAgentsInTurn(agents, state, chainId, asset) {
5019
5081
  let firstError = null;
@@ -5041,6 +5103,7 @@ var OwneySDK = class {
5041
5103
  } else {
5042
5104
  console.error(`activateAgent(${agent.id}) failed:`, error);
5043
5105
  }
5106
+ break;
5044
5107
  }
5045
5108
  }
5046
5109
  if (firstError === null) return;
@@ -5064,7 +5127,8 @@ var OwneySDK = class {
5064
5127
  * @param options.asset - Asset symbol to deposit (e.g. "USDC")
5065
5128
  * @param options.depositCallback - Callback that performs the token transfer and returns a tx hash.
5066
5129
  * 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.
5130
+ * split amount and smart wallet address. Default sponsored deposits batch
5131
+ * all shares into one signature; custom callbacks still run once per agent.
5068
5132
  * @param options.agentId - Optional explicit target. Otherwise split equally,
5069
5133
  * or fund remaining agents when a recovery deposit cannot meet every minimum.
5070
5134
  * @returns {OwneyDepositResult} for a single agent, or {OwneyMultiDepositResult} with per-agent results
@@ -5149,6 +5213,32 @@ var OwneySDK = class {
5149
5213
  }
5150
5214
  );
5151
5215
  }
5216
+ const batchTransfer = getDepositBatchTransfer(effectiveCallback);
5217
+ if (!depositCallback && batchTransfer) {
5218
+ return runAgentDepositBatch(
5219
+ chainId,
5220
+ agentAmounts.map(({ agent, amount: amount2 }) => ({
5221
+ id: agent.id,
5222
+ amount: amount2,
5223
+ run: (callback) => withFailureReporting(
5224
+ this.apiKey,
5225
+ agent.id,
5226
+ () => agent.deposit(state, chainId, amount2, asset, callback),
5227
+ this.routingApiBaseUrl
5228
+ )
5229
+ })),
5230
+ async (cid, transfers) => {
5231
+ try {
5232
+ return await batchTransfer(cid, transfers);
5233
+ } catch (error) {
5234
+ if (!(error instanceof OwneyError) || error.code !== "PERMIT2_APPROVAL_REQUIRED")
5235
+ throw error;
5236
+ await this.approvePermit2(asset);
5237
+ return batchTransfer(cid, transfers);
5238
+ }
5239
+ }
5240
+ );
5241
+ }
5152
5242
  const agentResults = {};
5153
5243
  for (const [
5154
5244
  index,
@@ -5188,7 +5278,7 @@ var OwneySDK = class {
5188
5278
  *
5189
5279
  * 1. Missing Permit2 allowance: when the app did not supply its own
5190
5280
  * 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
5281
+ * token deposit, this is the wallet's first Permit2 deposit for that token. We send
5192
5282
  * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
5193
5283
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
5194
5284
  * per call so a wallet/agent that keeps reporting the allowance as
@@ -5225,12 +5315,12 @@ var OwneySDK = class {
5225
5315
  try {
5226
5316
  return await attempt(effectiveCallback);
5227
5317
  } catch (error) {
5228
- if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
5318
+ if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
5229
5319
  approvalAttempted = true;
5230
5320
  console.warn(
5231
- "[owney-sdk] First WETH deposit: sending one-time Permit2 approval..."
5321
+ "[owney-sdk] First token deposit: sending one-time Permit2 approval..."
5232
5322
  );
5233
- await this.approvePermit2();
5323
+ await this.approvePermit2(asset);
5234
5324
  continue;
5235
5325
  }
5236
5326
  if (!shouldFallbackToUserPaid(error, asset, appCallback)) throw error;
@@ -5883,23 +5973,22 @@ var OwneySDK = class {
5883
5973
  return pending;
5884
5974
  }
5885
5975
  /**
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
5976
+ * One-time, user-paid approval of Permit2 on the selected token for
5977
+ * the active chain. Required once per wallet, token and chain before sponsored
5888
5978
  * deposits; afterwards deposit() is signature-only. Resolves only after the
5889
5979
  * approval transaction is mined (1 confirmation), so a subsequent deposit()
5890
5980
  * will see the new allowance; throws if the transaction reverted.
5891
5981
  * @returns the approval transaction hash.
5892
5982
  */
5893
5983
  async approvePermit2(asset = "WETH") {
5894
- void asset;
5895
5984
  const state = this.requireState();
5896
5985
  const chainId = this.requireChainId();
5897
- this.validateAssetSupport(this.getAgent("zyfai"), chainId, "WETH");
5898
- const token = SPONSORED_WETH_BY_CHAIN[chainId];
5986
+ this.getEligibleAgents(chainId, asset, { excludeDisabled: true });
5987
+ const token = sponsoredTokensFor(asset)[chainId];
5899
5988
  if (!token) {
5900
5989
  throw new OwneyError(
5901
5990
  "CHAIN_UNSUPPORTED",
5902
- `No sponsored WETH on chain ${chainId}`
5991
+ `No sponsored token on chain ${chainId}`
5903
5992
  );
5904
5993
  }
5905
5994
  const provider = this.requireConnectedProvider();