@owney/sdk 0.6.4 → 0.6.5-beta.1

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
@@ -8,7 +8,7 @@ var P_SUPPORTED_CHAINS = {
8
8
  chainIds: [8453, 42161],
9
9
  chains: ["BASE", "ARBITRUM"]
10
10
  };
11
- var SUPPORTED_TOKENS = ["USDC", "USDT"];
11
+ var SUPPORTED_TOKENS = ["USDC", "USDT", "WETH"];
12
12
  var SUPPORTED_CHAIN_IDS = P_SUPPORTED_CHAINS.chainIds;
13
13
  var SUPPORTED_CHAINS = P_SUPPORTED_CHAINS.chains;
14
14
  var SupportedAssets = [
@@ -29,6 +29,18 @@ var SupportedAssets = [
29
29
  symbol: "USDT",
30
30
  chainId: 42161,
31
31
  decimals: 6
32
+ },
33
+ {
34
+ address: `0x4200000000000000000000000000000000000006`,
35
+ symbol: "WETH",
36
+ chainId: 8453,
37
+ decimals: 18
38
+ },
39
+ {
40
+ address: `0x82aF49447D8a07e3bd95BD0d56f35241523fBab1`,
41
+ symbol: "WETH",
42
+ chainId: 42161,
43
+ decimals: 18
32
44
  }
33
45
  ];
34
46
 
@@ -109,6 +121,20 @@ var DayFilterMapping = {
109
121
  };
110
122
 
111
123
  // src/agents/zyfai/zyfai.mapper.ts
124
+ var DECIMALS_BY_SYMBOL = {
125
+ usdc: 6,
126
+ usdt: 6,
127
+ weth: 18,
128
+ eth: 18,
129
+ cbeth: 18,
130
+ wbtc: 8,
131
+ cbbtc: 8
132
+ };
133
+ function tokenDecimals(symbol, explicit) {
134
+ if (typeof explicit === "number" && Number.isFinite(explicit))
135
+ return explicit;
136
+ return DECIMALS_BY_SYMBOL[(symbol ?? "").toLowerCase()] ?? 6;
137
+ }
112
138
  function mapDeposit(raw) {
113
139
  return {
114
140
  txHash: raw.txHash,
@@ -166,9 +192,16 @@ function mapBalances(raw, _chainId, smartWallet) {
166
192
  pool: p.pool,
167
193
  asset: p.token_symbol ?? "",
168
194
  // Position amount is hex-encoded base units (like the token balances
169
- // above). Supported assets (USDC/USDT) are 6-decimal — hexToDecimal's
170
- // default — so no per-token decimals lookup is needed.
171
- amount: String(hexToDecimal(p.amount || "0")),
195
+ // above). `PositionSlot` doesn't type a `decimals` field, but runtime
196
+ // getBalances payloads often carry one (e.g. 18 for WETH) — prefer that
197
+ // when present, otherwise fall back to the per-symbol lookup so
198
+ // non-6-decimal assets like WETH don't render 1e12 too large.
199
+ amount: String(
200
+ hexToDecimal(
201
+ p.amount || "0",
202
+ tokenDecimals(p.token_symbol, p.decimals)
203
+ )
204
+ ),
172
205
  apy: p.pool_apy,
173
206
  tvl: p.pool_tvl,
174
207
  // PositionSlot has no `liquidity` field yet; Zyfai will add it. Narrow
@@ -291,7 +324,15 @@ function mapEntries(rawEntries, chainId) {
291
324
  fromPool: from.pool,
292
325
  toPool: to.pool,
293
326
  tokenSymbol: matched?.token_symbol ?? "",
294
- amount: matched ? String(hexToDecimal(matched.amount || "0x")) : "",
327
+ amount: matched ? String(
328
+ hexToDecimal(
329
+ matched.amount || "0x",
330
+ tokenDecimals(
331
+ matched.token_symbol,
332
+ matched.decimals
333
+ )
334
+ )
335
+ ) : "",
295
336
  status: "success"
296
337
  }
297
338
  ];
@@ -310,7 +351,12 @@ function mapEntries(rawEntries, chainId) {
310
351
  txHashes: entry.transactionHash ? [entry.transactionHash] : [],
311
352
  chainId: entry.chainId,
312
353
  tokenSymbol: entry?.positions?.[0]?.token_symbol,
313
- amount: hasAmount ? String(hexToDecimal(rawAmount)) : void 0
354
+ amount: hasAmount ? String(
355
+ hexToDecimal(
356
+ rawAmount,
357
+ tokenDecimals(entry?.positions?.[0]?.token_symbol)
358
+ )
359
+ ) : void 0
314
360
  }
315
361
  ],
316
362
  rebalanceLog
@@ -513,11 +559,26 @@ var clearSession = (address, chainId) => {
513
559
  }
514
560
  };
515
561
 
562
+ // src/agents/zyfai/autoSelect.ts
563
+ function buildAutoSelectUpdate(current, asset) {
564
+ if (current.autoSelectProtocols === true)
565
+ return null;
566
+ return {
567
+ asset,
568
+ autoSelectProtocols: true,
569
+ ...current.chains ? { chains: current.chains } : {}
570
+ };
571
+ }
572
+
516
573
  // src/agents/zyfai/zyfai.agent.ts
517
574
  var DEFAULT_ZYFAI_RPC_URLS = {
518
575
  8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
519
576
  42161: "https://arb-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V"
520
577
  };
578
+ var WETH_ADDRESS_BY_CHAIN = {
579
+ 8453: "0x4200000000000000000000000000000000000006",
580
+ 42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
581
+ };
521
582
  var VIEM_CHAIN = { 8453: base, 42161: arbitrum };
522
583
  var ZyfaiAgent = class {
523
584
  id = "zyfai";
@@ -526,12 +587,18 @@ var ZyfaiAgent = class {
526
587
  {
527
588
  chainId: 8453,
528
589
  chain: "BASE",
529
- assets: [{ symbol: "USDC", minDepositAmount: "1500000" }]
590
+ assets: [
591
+ { symbol: "USDC", minDepositAmount: "1500000" },
592
+ { symbol: "WETH", minDepositAmount: "500000000000000" }
593
+ ]
530
594
  },
531
595
  {
532
596
  chainId: 42161,
533
597
  chain: "ARBITRUM",
534
- assets: [{ symbol: "USDC", minDepositAmount: "1500000" }]
598
+ assets: [
599
+ { symbol: "USDC", minDepositAmount: "1500000" },
600
+ { symbol: "WETH", minDepositAmount: "500000000000000" }
601
+ ]
535
602
  }
536
603
  ];
537
604
  sdk;
@@ -566,22 +633,24 @@ var ZyfaiAgent = class {
566
633
  }
567
634
  /**
568
635
  * Fills in the true withdrawn amount for `Withdraw` entries by decoding the
569
- * USDC `Transfer` that lands in the user's wallet on-chain. The Zyfai history
636
+ * on-chain `Transfer` that lands in the user's wallet. The Zyfai history
570
637
  * payload only reports the *remaining* position for a partial withdraw (see
571
638
  * the note in `mapEntries`), so without this the UI shows the leftover
572
639
  * balance instead of the amount withdrawn.
573
640
  *
641
+ * The withdrawn asset varies per entry (USDC or WETH), so it's resolved
642
+ * from `tx.tokenSymbol` (set by `mapEntries` from the entry's own
643
+ * position) rather than assumed to always be USDC. Zyfai sometimes
644
+ * denominates WETH as lowercase "eth" — that's normalized to WETH.
645
+ * Entries with no symbol (older payload shapes) fall back to USDC to
646
+ * preserve prior behavior.
647
+ *
574
648
  * Mutates `entries` in place. Runs all lookups concurrently and is tolerant
575
649
  * of RPC failures: an entry whose amount can't be resolved keeps
576
650
  * `amount: undefined`, which consumers render as a hidden/retryable row
577
651
  * rather than a wrong number.
578
652
  */
579
653
  async resolveWithdrawnAmounts(entries, chainId, recipient) {
580
- const asset = SupportedAssets.find(
581
- (a) => a.chainId === chainId && a.symbol === "USDC"
582
- );
583
- if (!asset)
584
- return;
585
654
  const withdrawals = entries.filter(
586
655
  (e) => e.action === "Withdraw" && e.transactions[0]?.txHashes[0]
587
656
  );
@@ -592,7 +661,17 @@ var ZyfaiAgent = class {
592
661
  withdrawals.map(async (entry) => {
593
662
  const tx = entry.transactions[0];
594
663
  const txHash = tx.txHashes[0];
595
- const cached = this.withdrawAmountCache.get(txHash);
664
+ const rawSymbol = tx.tokenSymbol;
665
+ const normalizedSymbol = rawSymbol ? rawSymbol.toLowerCase() === "eth" ? "WETH" : rawSymbol.toUpperCase() : "USDC";
666
+ const asset = SupportedAssets.find(
667
+ (a) => a.chainId === chainId && a.symbol === normalizedSymbol
668
+ ) ?? SupportedAssets.find(
669
+ (a) => a.chainId === chainId && a.symbol === "USDC"
670
+ );
671
+ if (!asset)
672
+ return;
673
+ const cacheKey = `${txHash}:${asset.address}`;
674
+ const cached = this.withdrawAmountCache.get(cacheKey);
596
675
  if (cached !== void 0) {
597
676
  tx.amount = cached;
598
677
  return;
@@ -608,7 +687,7 @@ var ZyfaiAgent = class {
608
687
  asset.decimals
609
688
  );
610
689
  if (amount !== void 0) {
611
- this.withdrawAmountCache.set(txHash, amount);
690
+ this.withdrawAmountCache.set(cacheKey, amount);
612
691
  tx.amount = amount;
613
692
  }
614
693
  } catch {
@@ -726,6 +805,22 @@ var ZyfaiAgent = class {
726
805
  this.smartWalletCache.set(`${address}_${validChainId}`, wallet.address);
727
806
  if (!wallet.isDeployed) {
728
807
  await this.sdk.deploySafe(address, validChainId, "aggressive", true);
808
+ try {
809
+ if (validChainId === 42161) {
810
+ await this.sdk.updateUserProfile({
811
+ asset: "WETH",
812
+ chains: [validChainId],
813
+ protocols: ["c24a33fa-4a87-4376-b756-93bd3957d3ea"],
814
+ // AAVE protocol ID
815
+ autoSelectProtocols: false
816
+ });
817
+ }
818
+ } catch (err) {
819
+ console.warn(
820
+ "Failed to configure AAVE protocol after Safe deployment:",
821
+ err instanceof Error ? err.message : String(err)
822
+ );
823
+ }
729
824
  }
730
825
  const user = await this.sdk.getUserDetails();
731
826
  if (!user.hasActiveSessionKey) {
@@ -767,7 +862,7 @@ var ZyfaiAgent = class {
767
862
  throw error;
768
863
  }
769
864
  }
770
- async deposit(state, chainId, amount, depositCallback) {
865
+ async deposit(state, chainId, amount, asset, depositCallback) {
771
866
  try {
772
867
  await this.connectFull(state, chainId);
773
868
  const validChainId = isValidChainId(chainId);
@@ -781,13 +876,15 @@ var ZyfaiAgent = class {
781
876
  validChainId,
782
877
  amount
783
878
  );
784
- await this.sdk.logDeposit(validChainId, txHash, amount);
879
+ const tokenAddress = asset === "WETH" ? WETH_ADDRESS_BY_CHAIN[validChainId] : void 0;
880
+ await (tokenAddress ? this.sdk.logDeposit(validChainId, txHash, amount, tokenAddress) : this.sdk.logDeposit(validChainId, txHash, amount));
785
881
  return { txHash, smartWallet: wallet.address, amount };
786
882
  }
787
883
  const raw = await this.sdk.depositFunds(
788
884
  this.getAddress(),
789
885
  validChainId,
790
- amount
886
+ amount,
887
+ asset
791
888
  );
792
889
  return mapDeposit(raw);
793
890
  } catch (error) {
@@ -883,6 +980,21 @@ var ZyfaiAgent = class {
883
980
  }
884
981
  return mapUserProfile(raw, this.connectedAddress);
885
982
  }
983
+ async ensureAutoSelectProtocols(state, chainId, asset) {
984
+ await this.connectAuth(state, chainId);
985
+ const current = await this.sdk.getUserDetails(asset);
986
+ const update = buildAutoSelectUpdate(
987
+ {
988
+ autoSelectProtocols: current.autoSelectProtocols,
989
+ chains: current.chains
990
+ },
991
+ asset
992
+ );
993
+ if (!update)
994
+ return false;
995
+ await this.sdk.updateUserProfile(update);
996
+ return true;
997
+ }
886
998
  // --- IAgent: Discovery (no wallet required) ---
887
999
  async getAgentApy(days, options) {
888
1000
  const raw = await this.sdk.getAPYPerStrategy(
@@ -1162,6 +1274,70 @@ async function postSponsorTransferAuth(input) {
1162
1274
  }
1163
1275
  return parsed.data;
1164
1276
  }
1277
+ async function postSponsorPermit2Transfer(input) {
1278
+ const base3 = input.baseUrl ?? ROUTING_API_BASE_URL3;
1279
+ let res;
1280
+ try {
1281
+ res = await fetch(`${base3}/api/v1/sponsor/permit2-transfer`, {
1282
+ method: "POST",
1283
+ headers: { "content-type": "application/json", "x-owney-api-key": input.apiKey },
1284
+ body: JSON.stringify(input.body)
1285
+ });
1286
+ } catch (networkError) {
1287
+ throw new OwneyError(
1288
+ "SPONSOR_REQUEST_FAILED",
1289
+ `Sponsor API network error: ${networkError instanceof Error ? networkError.message : String(networkError)}`,
1290
+ { cause: String(networkError), safeToFallback: false }
1291
+ );
1292
+ }
1293
+ const text = await res.text();
1294
+ let parsed = null;
1295
+ try {
1296
+ parsed = JSON.parse(text);
1297
+ } catch {
1298
+ }
1299
+ if (!res.ok || !parsed?.success || !parsed.data) {
1300
+ throw new OwneyError(
1301
+ "SPONSOR_REQUEST_FAILED",
1302
+ `Sponsor API error ${res.status}: ${parsed?.message ?? text.slice(0, 500)}`,
1303
+ {
1304
+ statusCode: res.status,
1305
+ responseBody: text.slice(0, 500),
1306
+ safeToFallback: res.status >= 400 && res.status < 500
1307
+ }
1308
+ );
1309
+ }
1310
+ return parsed.data;
1311
+ }
1312
+ async function getSponsorRelayerAddress(input) {
1313
+ const base3 = input.baseUrl ?? ROUTING_API_BASE_URL3;
1314
+ let res;
1315
+ try {
1316
+ res = await fetch(`${base3}/api/v1/sponsor/relayer-address?chainId=${input.chainId}`, {
1317
+ headers: { "x-owney-api-key": input.apiKey }
1318
+ });
1319
+ } catch (networkError) {
1320
+ throw new OwneyError(
1321
+ "SPONSOR_REQUEST_FAILED",
1322
+ `Sponsor API network error: ${networkError instanceof Error ? networkError.message : String(networkError)}`,
1323
+ { cause: String(networkError), safeToFallback: true }
1324
+ );
1325
+ }
1326
+ const text = await res.text();
1327
+ let parsed = null;
1328
+ try {
1329
+ parsed = JSON.parse(text);
1330
+ } catch {
1331
+ }
1332
+ if (!res.ok || !parsed?.success || !parsed.data?.relayer) {
1333
+ throw new OwneyError(
1334
+ "SPONSOR_REQUEST_FAILED",
1335
+ `Sponsor API error ${res.status}: ${parsed?.message ?? text.slice(0, 500)}`,
1336
+ { statusCode: res.status, responseBody: text.slice(0, 500), safeToFallback: true }
1337
+ );
1338
+ }
1339
+ return parsed.data.relayer;
1340
+ }
1165
1341
 
1166
1342
  // src/lib/sponsored-deposit.ts
1167
1343
  var AUTH_WINDOW_SECONDS = 15 * 60;
@@ -1213,6 +1389,114 @@ function makeSponsoredDepositCallback(deps) {
1213
1389
  };
1214
1390
  }
1215
1391
 
1392
+ // src/lib/permit2.ts
1393
+ import { bytesToHex as bytesToHex2 } from "viem";
1394
+ var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
1395
+ var MAX_UINT256 = 2n ** 256n - 1n;
1396
+ var ERC20_ALLOWANCE_ABI = [
1397
+ {
1398
+ type: "function",
1399
+ name: "allowance",
1400
+ stateMutability: "view",
1401
+ inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }],
1402
+ outputs: [{ name: "", type: "uint256" }]
1403
+ },
1404
+ {
1405
+ type: "function",
1406
+ name: "approve",
1407
+ stateMutability: "nonpayable",
1408
+ inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }],
1409
+ outputs: [{ name: "", type: "bool" }]
1410
+ }
1411
+ ];
1412
+ function buildPermitTransferFromTypedData(input) {
1413
+ return {
1414
+ domain: { name: "Permit2", chainId: input.chainId, verifyingContract: PERMIT2_ADDRESS },
1415
+ types: {
1416
+ PermitTransferFrom: [
1417
+ { name: "permitted", type: "TokenPermissions" },
1418
+ { name: "spender", type: "address" },
1419
+ { name: "nonce", type: "uint256" },
1420
+ { name: "deadline", type: "uint256" }
1421
+ ],
1422
+ TokenPermissions: [
1423
+ { name: "token", type: "address" },
1424
+ { name: "amount", type: "uint256" }
1425
+ ]
1426
+ },
1427
+ primaryType: "PermitTransferFrom",
1428
+ message: input.message
1429
+ };
1430
+ }
1431
+ function randomPermit2Nonce() {
1432
+ const bytes = new Uint8Array(32);
1433
+ globalThis.crypto.getRandomValues(bytes);
1434
+ return BigInt(bytesToHex2(bytes));
1435
+ }
1436
+ async function readPermit2Allowance(publicClient, token, owner) {
1437
+ return publicClient.readContract({
1438
+ address: token,
1439
+ abi: ERC20_ALLOWANCE_ABI,
1440
+ functionName: "allowance",
1441
+ args: [owner, PERMIT2_ADDRESS]
1442
+ });
1443
+ }
1444
+
1445
+ // src/lib/sponsored-weth-deposit.ts
1446
+ var PERMIT_WINDOW_SECONDS = 15 * 60;
1447
+ function makeSponsoredWethCallback(deps) {
1448
+ const get = deps.httpGet ?? getSponsorRelayerAddress;
1449
+ const post = deps.httpPost ?? postSponsorPermit2Transfer;
1450
+ return async (smartWallet, chainId, amount) => {
1451
+ const cid = chainId;
1452
+ const token = deps.tokenAddressByChain[cid];
1453
+ if (!token) {
1454
+ throw new OwneyError("CHAIN_UNSUPPORTED", `No sponsored WETH configured for chain ${chainId}`);
1455
+ }
1456
+ const amountWei = BigInt(amount);
1457
+ const pub = deps.getPublicClient(cid);
1458
+ const allowance = await readPermit2Allowance(pub, token, deps.ownerAddress);
1459
+ if (allowance < amountWei) {
1460
+ throw new OwneyError(
1461
+ "PERMIT2_APPROVAL_REQUIRED",
1462
+ "WETH gasless deposits need a one-time Permit2 approval; call approvePermit2() first",
1463
+ { token, chainId: cid, allowance: allowance.toString(), amount }
1464
+ );
1465
+ }
1466
+ const relayer = await get({ baseUrl: deps.baseUrl, apiKey: deps.apiKey, chainId: cid });
1467
+ const nonce = randomPermit2Nonce();
1468
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + PERMIT_WINDOW_SECONDS);
1469
+ const typedData = buildPermitTransferFromTypedData({
1470
+ chainId: cid,
1471
+ message: {
1472
+ permitted: { token, amount: amountWei },
1473
+ spender: relayer,
1474
+ nonce,
1475
+ deadline
1476
+ }
1477
+ });
1478
+ const wallet = deps.getWalletClient(cid);
1479
+ const signature = await wallet.signTypedData(
1480
+ { account: deps.ownerAddress, ...typedData }
1481
+ );
1482
+ const result = await post({
1483
+ baseUrl: deps.baseUrl,
1484
+ apiKey: deps.apiKey,
1485
+ body: {
1486
+ chainId: cid,
1487
+ token,
1488
+ from: deps.ownerAddress,
1489
+ to: smartWallet,
1490
+ amount,
1491
+ nonce: nonce.toString(),
1492
+ deadline: deadline.toString(),
1493
+ signature
1494
+ }
1495
+ });
1496
+ return result.txHash;
1497
+ };
1498
+ }
1499
+
1216
1500
  // src/client.ts
1217
1501
  function encodeMultiAgentCursor(map) {
1218
1502
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
@@ -1242,6 +1526,13 @@ var VIEM_CHAIN2 = {
1242
1526
  8453: base2,
1243
1527
  42161: arbitrum2
1244
1528
  };
1529
+ var SPONSORED_WETH_BY_CHAIN = {
1530
+ 8453: "0x4200000000000000000000000000000000000006",
1531
+ 42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
1532
+ };
1533
+ function shouldFallbackToUserPaid(error, asset, appCallback) {
1534
+ return asset === "WETH" && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
1535
+ }
1245
1536
  var OwneySDK = class {
1246
1537
  agents = /* @__PURE__ */ new Map();
1247
1538
  activeAgents = /* @__PURE__ */ new Set();
@@ -1255,6 +1546,7 @@ var OwneySDK = class {
1255
1546
  apiKey;
1256
1547
  zyfaiRpcUrls;
1257
1548
  cachedSponsoredCallback = null;
1549
+ cachedWethSponsoredCallback = null;
1258
1550
  initializingAgentsPromise = null;
1259
1551
  constructor(config) {
1260
1552
  this.apiKey = config.apiKey;
@@ -1279,6 +1571,7 @@ var OwneySDK = class {
1279
1571
  }
1280
1572
  this.state = { provider, walletAddress, chainId: null };
1281
1573
  this.cachedSponsoredCallback = null;
1574
+ this.cachedWethSponsoredCallback = null;
1282
1575
  }
1283
1576
  /**
1284
1577
  * Disconnect from all agents and clear connection state.
@@ -1292,6 +1585,7 @@ var OwneySDK = class {
1292
1585
  this.disabledAgents.clear();
1293
1586
  this.state = null;
1294
1587
  this.cachedSponsoredCallback = null;
1588
+ this.cachedWethSponsoredCallback = null;
1295
1589
  }
1296
1590
  isAgentDisabled(agentId) {
1297
1591
  return this.disabledAgents.has(agentId);
@@ -1365,6 +1659,36 @@ var OwneySDK = class {
1365
1659
  });
1366
1660
  return this.cachedSponsoredCallback;
1367
1661
  }
1662
+ /**
1663
+ * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
1664
+ * callback used when the caller omits `depositCallback` for a WETH
1665
+ * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
1666
+ * `PermitTransferFrom` instead of an EIP-3009 authorization.
1667
+ */
1668
+ getDefaultWethSponsoredCallback() {
1669
+ if (this.cachedWethSponsoredCallback)
1670
+ return this.cachedWethSponsoredCallback;
1671
+ const provider = this.requireConnectedProvider();
1672
+ const owner = this.state.walletAddress;
1673
+ this.cachedWethSponsoredCallback = makeSponsoredWethCallback({
1674
+ apiKey: this.apiKey,
1675
+ ownerAddress: owner,
1676
+ tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
1677
+ // Casts work around viem's chain-narrowed Client vs the generic
1678
+ // PublicClient/WalletClient param types — structurally identical at
1679
+ // runtime, but the two share a name TS treats as unrelated.
1680
+ getPublicClient: (cid) => createPublicClient2({
1681
+ chain: VIEM_CHAIN2[cid],
1682
+ transport: custom(provider)
1683
+ }),
1684
+ getWalletClient: (cid) => createWalletClient({
1685
+ account: owner,
1686
+ chain: VIEM_CHAIN2[cid],
1687
+ transport: custom(provider)
1688
+ })
1689
+ });
1690
+ return this.cachedWethSponsoredCallback;
1691
+ }
1368
1692
  getAgent(agentId) {
1369
1693
  const agent = this.agents.get(agentId);
1370
1694
  if (!agent) {
@@ -1518,7 +1842,7 @@ var OwneySDK = class {
1518
1842
  }
1519
1843
  const state = this.requireState();
1520
1844
  const chainId = this.requireChainId();
1521
- const effectiveCallback = depositCallback ?? this.getDefaultSponsoredCallback();
1845
+ const effectiveCallback = depositCallback ?? (asset === "WETH" ? this.getDefaultWethSponsoredCallback() : this.getDefaultSponsoredCallback());
1522
1846
  if (agentId) {
1523
1847
  if (this.isAgentDisabled(agentId)) {
1524
1848
  throw new OwneyError(
@@ -1530,10 +1854,14 @@ var OwneySDK = class {
1530
1854
  const agent = this.getAgent(agentId);
1531
1855
  this.validateAssetSupport(agent, chainId, asset);
1532
1856
  await this.validateMinDepositAmount(agent, state, chainId, asset, amount);
1533
- return withFailureReporting(
1534
- this.apiKey,
1535
- agent.id,
1536
- () => agent.deposit(state, chainId, amount, effectiveCallback)
1857
+ return this.depositWithFallback(
1858
+ agent,
1859
+ state,
1860
+ chainId,
1861
+ amount,
1862
+ asset,
1863
+ effectiveCallback,
1864
+ depositCallback
1537
1865
  );
1538
1866
  }
1539
1867
  const totalAmount = BigInt(amount);
@@ -1564,14 +1892,78 @@ var OwneySDK = class {
1564
1892
  }
1565
1893
  const agentResults = {};
1566
1894
  for (const { agent, amount: agentAmount } of agentAmounts) {
1567
- agentResults[agent.id] = await withFailureReporting(
1568
- this.apiKey,
1569
- agent.id,
1570
- () => agent.deposit(state, chainId, agentAmount, effectiveCallback)
1895
+ agentResults[agent.id] = await this.depositWithFallback(
1896
+ agent,
1897
+ state,
1898
+ chainId,
1899
+ agentAmount,
1900
+ asset,
1901
+ effectiveCallback,
1902
+ depositCallback
1571
1903
  );
1572
1904
  }
1573
1905
  return { agentResults };
1574
1906
  }
1907
+ /**
1908
+ * Invokes `agent.deposit` with the resolved sponsored callback, composing
1909
+ * two independent auto-recovery mechanisms:
1910
+ *
1911
+ * 1. Missing Permit2 allowance: when the app did not supply its own
1912
+ * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
1913
+ * WETH deposit, this is the wallet's first gasless WETH deposit. We send
1914
+ * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
1915
+ * retry the SAME sponsored attempt once. Bounded to one approval attempt
1916
+ * per call so a wallet/agent that keeps reporting the allowance as
1917
+ * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
1918
+ * the user rejects the wallet prompt), that error propagates as-is —
1919
+ * the user said no to a transaction, so we must not turn around and ask
1920
+ * them to pay for a different one via the fallback below.
1921
+ * 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
1922
+ * the infrastructure layer in a provably safe-to-retry way, so we warn
1923
+ * and retry once with `undefined`, falling through to a user-paid
1924
+ * native deposit. This still applies after a successful Permit2
1925
+ * approval retry (the retried sponsored attempt can itself hit a safe
1926
+ * sponsor failure).
1927
+ *
1928
+ * Any other error — an app-supplied callback's own failure in particular —
1929
+ * propagates unchanged. Scoped per agent invocation so one agent's failure
1930
+ * doesn't force a retry of sibling agents in a multi-agent split.
1931
+ */
1932
+ async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback) {
1933
+ const attempt = (callback) => withFailureReporting(
1934
+ this.apiKey,
1935
+ agent.id,
1936
+ () => agent.deposit(
1937
+ state,
1938
+ chainId,
1939
+ amount,
1940
+ asset,
1941
+ callback
1942
+ )
1943
+ );
1944
+ let approvalAttempted = false;
1945
+ for (; ; ) {
1946
+ try {
1947
+ return await attempt(effectiveCallback);
1948
+ } catch (error) {
1949
+ if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
1950
+ approvalAttempted = true;
1951
+ console.warn(
1952
+ "[owney-sdk] First WETH deposit: sending one-time Permit2 approval..."
1953
+ );
1954
+ await this.approvePermit2();
1955
+ continue;
1956
+ }
1957
+ if (!shouldFallbackToUserPaid(error, asset, appCallback))
1958
+ throw error;
1959
+ console.warn(
1960
+ "[owney-sdk] WETH sponsorship unavailable, falling back to user-paid deposit:",
1961
+ error
1962
+ );
1963
+ return attempt(void 0);
1964
+ }
1965
+ }
1966
+ }
1575
1967
  getMinDepositAmount(agent, chainId, asset) {
1576
1968
  const chainAssets = agent.supportedAssets.find(
1577
1969
  (sa) => sa.chainId === chainId
@@ -2059,6 +2451,66 @@ var OwneySDK = class {
2059
2451
  agentUserProfile: results
2060
2452
  };
2061
2453
  }
2454
+ /**
2455
+ * Ensure the given asset uses backend protocol auto-selection for the active
2456
+ * chain's account. No-op (returns false) for agents that don't support it.
2457
+ * @param asset - "USDC" or "WETH".
2458
+ * @param agentId - Agent to target. Defaults to "zyfai".
2459
+ * @returns whether a profile write occurred.
2460
+ */
2461
+ async ensureAutoSelectProtocols(asset, agentId = "zyfai") {
2462
+ const state = this.requireState();
2463
+ const chainId = this.requireChainId();
2464
+ const agent = this.getAgent(agentId);
2465
+ if (typeof agent.ensureAutoSelectProtocols !== "function")
2466
+ return false;
2467
+ return agent.ensureAutoSelectProtocols(state, chainId, asset);
2468
+ }
2469
+ /**
2470
+ * One-time, user-paid approval of Permit2 on the sponsored WETH token for
2471
+ * the active chain. Required once per wallet per chain before gasless WETH
2472
+ * deposits; afterwards deposit() is signature-only. Resolves only after the
2473
+ * approval transaction is mined (1 confirmation), so a subsequent deposit()
2474
+ * will see the new allowance; throws if the transaction reverted.
2475
+ * @returns the approval transaction hash.
2476
+ */
2477
+ async approvePermit2(asset = "WETH") {
2478
+ const state = this.requireState();
2479
+ const chainId = this.requireChainId();
2480
+ const token = SPONSORED_WETH_BY_CHAIN[chainId];
2481
+ if (!token) {
2482
+ throw new OwneyError(
2483
+ "CHAIN_UNSUPPORTED",
2484
+ `No sponsored WETH on chain ${chainId}`
2485
+ );
2486
+ }
2487
+ const provider = this.requireConnectedProvider();
2488
+ const wallet = createWalletClient({
2489
+ account: state.walletAddress,
2490
+ chain: VIEM_CHAIN2[chainId],
2491
+ transport: custom(provider)
2492
+ });
2493
+ const hash = await wallet.writeContract({
2494
+ address: token,
2495
+ abi: ERC20_ALLOWANCE_ABI,
2496
+ functionName: "approve",
2497
+ args: [PERMIT2_ADDRESS, MAX_UINT256],
2498
+ account: state.walletAddress,
2499
+ chain: VIEM_CHAIN2[chainId]
2500
+ });
2501
+ const publicClient = createPublicClient2({
2502
+ chain: VIEM_CHAIN2[chainId],
2503
+ transport: custom(provider)
2504
+ });
2505
+ const receipt = await publicClient.waitForTransactionReceipt({
2506
+ hash,
2507
+ confirmations: 1
2508
+ });
2509
+ if (receipt.status !== "success") {
2510
+ throw new Error(`Permit2 approval reverted (tx ${hash})`);
2511
+ }
2512
+ return hash;
2513
+ }
2062
2514
  // --- Discovery (no wallet required) ---
2063
2515
  /**
2064
2516
  * Get the agent's average APY performance over a time period. Does not require a wallet connection.