@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.cjs CHANGED
@@ -40,7 +40,7 @@ var P_SUPPORTED_CHAINS = {
40
40
  chainIds: [8453, 42161],
41
41
  chains: ["BASE", "ARBITRUM"]
42
42
  };
43
- var SUPPORTED_TOKENS = ["USDC", "USDT"];
43
+ var SUPPORTED_TOKENS = ["USDC", "USDT", "WETH"];
44
44
  var SUPPORTED_CHAIN_IDS = P_SUPPORTED_CHAINS.chainIds;
45
45
  var SUPPORTED_CHAINS = P_SUPPORTED_CHAINS.chains;
46
46
  var SupportedAssets = [
@@ -61,6 +61,18 @@ var SupportedAssets = [
61
61
  symbol: "USDT",
62
62
  chainId: 42161,
63
63
  decimals: 6
64
+ },
65
+ {
66
+ address: `0x4200000000000000000000000000000000000006`,
67
+ symbol: "WETH",
68
+ chainId: 8453,
69
+ decimals: 18
70
+ },
71
+ {
72
+ address: `0x82aF49447D8a07e3bd95BD0d56f35241523fBab1`,
73
+ symbol: "WETH",
74
+ chainId: 42161,
75
+ decimals: 18
64
76
  }
65
77
  ];
66
78
 
@@ -141,6 +153,20 @@ var DayFilterMapping = {
141
153
  };
142
154
 
143
155
  // src/agents/zyfai/zyfai.mapper.ts
156
+ var DECIMALS_BY_SYMBOL = {
157
+ usdc: 6,
158
+ usdt: 6,
159
+ weth: 18,
160
+ eth: 18,
161
+ cbeth: 18,
162
+ wbtc: 8,
163
+ cbbtc: 8
164
+ };
165
+ function tokenDecimals(symbol, explicit) {
166
+ if (typeof explicit === "number" && Number.isFinite(explicit))
167
+ return explicit;
168
+ return DECIMALS_BY_SYMBOL[(symbol ?? "").toLowerCase()] ?? 6;
169
+ }
144
170
  function mapDeposit(raw) {
145
171
  return {
146
172
  txHash: raw.txHash,
@@ -198,9 +224,16 @@ function mapBalances(raw, _chainId, smartWallet) {
198
224
  pool: p.pool,
199
225
  asset: p.token_symbol ?? "",
200
226
  // Position amount is hex-encoded base units (like the token balances
201
- // above). Supported assets (USDC/USDT) are 6-decimal — hexToDecimal's
202
- // default — so no per-token decimals lookup is needed.
203
- amount: String(hexToDecimal(p.amount || "0")),
227
+ // above). `PositionSlot` doesn't type a `decimals` field, but runtime
228
+ // getBalances payloads often carry one (e.g. 18 for WETH) — prefer that
229
+ // when present, otherwise fall back to the per-symbol lookup so
230
+ // non-6-decimal assets like WETH don't render 1e12 too large.
231
+ amount: String(
232
+ hexToDecimal(
233
+ p.amount || "0",
234
+ tokenDecimals(p.token_symbol, p.decimals)
235
+ )
236
+ ),
204
237
  apy: p.pool_apy,
205
238
  tvl: p.pool_tvl,
206
239
  // PositionSlot has no `liquidity` field yet; Zyfai will add it. Narrow
@@ -323,7 +356,15 @@ function mapEntries(rawEntries, chainId) {
323
356
  fromPool: from.pool,
324
357
  toPool: to.pool,
325
358
  tokenSymbol: matched?.token_symbol ?? "",
326
- amount: matched ? String(hexToDecimal(matched.amount || "0x")) : "",
359
+ amount: matched ? String(
360
+ hexToDecimal(
361
+ matched.amount || "0x",
362
+ tokenDecimals(
363
+ matched.token_symbol,
364
+ matched.decimals
365
+ )
366
+ )
367
+ ) : "",
327
368
  status: "success"
328
369
  }
329
370
  ];
@@ -342,7 +383,12 @@ function mapEntries(rawEntries, chainId) {
342
383
  txHashes: entry.transactionHash ? [entry.transactionHash] : [],
343
384
  chainId: entry.chainId,
344
385
  tokenSymbol: entry?.positions?.[0]?.token_symbol,
345
- amount: hasAmount ? String(hexToDecimal(rawAmount)) : void 0
386
+ amount: hasAmount ? String(
387
+ hexToDecimal(
388
+ rawAmount,
389
+ tokenDecimals(entry?.positions?.[0]?.token_symbol)
390
+ )
391
+ ) : void 0
346
392
  }
347
393
  ],
348
394
  rebalanceLog
@@ -545,11 +591,26 @@ var clearSession = (address, chainId) => {
545
591
  }
546
592
  };
547
593
 
594
+ // src/agents/zyfai/autoSelect.ts
595
+ function buildAutoSelectUpdate(current, asset) {
596
+ if (current.autoSelectProtocols === true)
597
+ return null;
598
+ return {
599
+ asset,
600
+ autoSelectProtocols: true,
601
+ ...current.chains ? { chains: current.chains } : {}
602
+ };
603
+ }
604
+
548
605
  // src/agents/zyfai/zyfai.agent.ts
549
606
  var DEFAULT_ZYFAI_RPC_URLS = {
550
607
  8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
551
608
  42161: "https://arb-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V"
552
609
  };
610
+ var WETH_ADDRESS_BY_CHAIN = {
611
+ 8453: "0x4200000000000000000000000000000000000006",
612
+ 42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
613
+ };
553
614
  var VIEM_CHAIN = { 8453: import_chains.base, 42161: import_chains.arbitrum };
554
615
  var ZyfaiAgent = class {
555
616
  id = "zyfai";
@@ -558,12 +619,18 @@ var ZyfaiAgent = class {
558
619
  {
559
620
  chainId: 8453,
560
621
  chain: "BASE",
561
- assets: [{ symbol: "USDC", minDepositAmount: "1500000" }]
622
+ assets: [
623
+ { symbol: "USDC", minDepositAmount: "1500000" },
624
+ { symbol: "WETH", minDepositAmount: "500000000000000" }
625
+ ]
562
626
  },
563
627
  {
564
628
  chainId: 42161,
565
629
  chain: "ARBITRUM",
566
- assets: [{ symbol: "USDC", minDepositAmount: "1500000" }]
630
+ assets: [
631
+ { symbol: "USDC", minDepositAmount: "1500000" },
632
+ { symbol: "WETH", minDepositAmount: "500000000000000" }
633
+ ]
567
634
  }
568
635
  ];
569
636
  sdk;
@@ -598,22 +665,24 @@ var ZyfaiAgent = class {
598
665
  }
599
666
  /**
600
667
  * Fills in the true withdrawn amount for `Withdraw` entries by decoding the
601
- * USDC `Transfer` that lands in the user's wallet on-chain. The Zyfai history
668
+ * on-chain `Transfer` that lands in the user's wallet. The Zyfai history
602
669
  * payload only reports the *remaining* position for a partial withdraw (see
603
670
  * the note in `mapEntries`), so without this the UI shows the leftover
604
671
  * balance instead of the amount withdrawn.
605
672
  *
673
+ * The withdrawn asset varies per entry (USDC or WETH), so it's resolved
674
+ * from `tx.tokenSymbol` (set by `mapEntries` from the entry's own
675
+ * position) rather than assumed to always be USDC. Zyfai sometimes
676
+ * denominates WETH as lowercase "eth" — that's normalized to WETH.
677
+ * Entries with no symbol (older payload shapes) fall back to USDC to
678
+ * preserve prior behavior.
679
+ *
606
680
  * Mutates `entries` in place. Runs all lookups concurrently and is tolerant
607
681
  * of RPC failures: an entry whose amount can't be resolved keeps
608
682
  * `amount: undefined`, which consumers render as a hidden/retryable row
609
683
  * rather than a wrong number.
610
684
  */
611
685
  async resolveWithdrawnAmounts(entries, chainId, recipient) {
612
- const asset = SupportedAssets.find(
613
- (a) => a.chainId === chainId && a.symbol === "USDC"
614
- );
615
- if (!asset)
616
- return;
617
686
  const withdrawals = entries.filter(
618
687
  (e) => e.action === "Withdraw" && e.transactions[0]?.txHashes[0]
619
688
  );
@@ -624,7 +693,17 @@ var ZyfaiAgent = class {
624
693
  withdrawals.map(async (entry) => {
625
694
  const tx = entry.transactions[0];
626
695
  const txHash = tx.txHashes[0];
627
- const cached = this.withdrawAmountCache.get(txHash);
696
+ const rawSymbol = tx.tokenSymbol;
697
+ const normalizedSymbol = rawSymbol ? rawSymbol.toLowerCase() === "eth" ? "WETH" : rawSymbol.toUpperCase() : "USDC";
698
+ const asset = SupportedAssets.find(
699
+ (a) => a.chainId === chainId && a.symbol === normalizedSymbol
700
+ ) ?? SupportedAssets.find(
701
+ (a) => a.chainId === chainId && a.symbol === "USDC"
702
+ );
703
+ if (!asset)
704
+ return;
705
+ const cacheKey = `${txHash}:${asset.address}`;
706
+ const cached = this.withdrawAmountCache.get(cacheKey);
628
707
  if (cached !== void 0) {
629
708
  tx.amount = cached;
630
709
  return;
@@ -640,7 +719,7 @@ var ZyfaiAgent = class {
640
719
  asset.decimals
641
720
  );
642
721
  if (amount !== void 0) {
643
- this.withdrawAmountCache.set(txHash, amount);
722
+ this.withdrawAmountCache.set(cacheKey, amount);
644
723
  tx.amount = amount;
645
724
  }
646
725
  } catch {
@@ -758,6 +837,22 @@ var ZyfaiAgent = class {
758
837
  this.smartWalletCache.set(`${address}_${validChainId}`, wallet.address);
759
838
  if (!wallet.isDeployed) {
760
839
  await this.sdk.deploySafe(address, validChainId, "aggressive", true);
840
+ try {
841
+ if (validChainId === 42161) {
842
+ await this.sdk.updateUserProfile({
843
+ asset: "WETH",
844
+ chains: [validChainId],
845
+ protocols: ["c24a33fa-4a87-4376-b756-93bd3957d3ea"],
846
+ // AAVE protocol ID
847
+ autoSelectProtocols: false
848
+ });
849
+ }
850
+ } catch (err) {
851
+ console.warn(
852
+ "Failed to configure AAVE protocol after Safe deployment:",
853
+ err instanceof Error ? err.message : String(err)
854
+ );
855
+ }
761
856
  }
762
857
  const user = await this.sdk.getUserDetails();
763
858
  if (!user.hasActiveSessionKey) {
@@ -799,7 +894,7 @@ var ZyfaiAgent = class {
799
894
  throw error;
800
895
  }
801
896
  }
802
- async deposit(state, chainId, amount, depositCallback) {
897
+ async deposit(state, chainId, amount, asset, depositCallback) {
803
898
  try {
804
899
  await this.connectFull(state, chainId);
805
900
  const validChainId = isValidChainId(chainId);
@@ -813,13 +908,15 @@ var ZyfaiAgent = class {
813
908
  validChainId,
814
909
  amount
815
910
  );
816
- await this.sdk.logDeposit(validChainId, txHash, amount);
911
+ const tokenAddress = asset === "WETH" ? WETH_ADDRESS_BY_CHAIN[validChainId] : void 0;
912
+ await (tokenAddress ? this.sdk.logDeposit(validChainId, txHash, amount, tokenAddress) : this.sdk.logDeposit(validChainId, txHash, amount));
817
913
  return { txHash, smartWallet: wallet.address, amount };
818
914
  }
819
915
  const raw = await this.sdk.depositFunds(
820
916
  this.getAddress(),
821
917
  validChainId,
822
- amount
918
+ amount,
919
+ asset
823
920
  );
824
921
  return mapDeposit(raw);
825
922
  } catch (error) {
@@ -915,6 +1012,21 @@ var ZyfaiAgent = class {
915
1012
  }
916
1013
  return mapUserProfile(raw, this.connectedAddress);
917
1014
  }
1015
+ async ensureAutoSelectProtocols(state, chainId, asset) {
1016
+ await this.connectAuth(state, chainId);
1017
+ const current = await this.sdk.getUserDetails(asset);
1018
+ const update = buildAutoSelectUpdate(
1019
+ {
1020
+ autoSelectProtocols: current.autoSelectProtocols,
1021
+ chains: current.chains
1022
+ },
1023
+ asset
1024
+ );
1025
+ if (!update)
1026
+ return false;
1027
+ await this.sdk.updateUserProfile(update);
1028
+ return true;
1029
+ }
918
1030
  // --- IAgent: Discovery (no wallet required) ---
919
1031
  async getAgentApy(days, options) {
920
1032
  const raw = await this.sdk.getAPYPerStrategy(
@@ -1118,7 +1230,7 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
1118
1230
  }
1119
1231
 
1120
1232
  // src/client.ts
1121
- var import_viem4 = require("viem");
1233
+ var import_viem5 = require("viem");
1122
1234
  var import_chains2 = require("viem/chains");
1123
1235
 
1124
1236
  // src/lib/transfer-auth.ts
@@ -1190,6 +1302,70 @@ async function postSponsorTransferAuth(input) {
1190
1302
  }
1191
1303
  return parsed.data;
1192
1304
  }
1305
+ async function postSponsorPermit2Transfer(input) {
1306
+ const base3 = input.baseUrl ?? ROUTING_API_BASE_URL3;
1307
+ let res;
1308
+ try {
1309
+ res = await fetch(`${base3}/api/v1/sponsor/permit2-transfer`, {
1310
+ method: "POST",
1311
+ headers: { "content-type": "application/json", "x-owney-api-key": input.apiKey },
1312
+ body: JSON.stringify(input.body)
1313
+ });
1314
+ } catch (networkError) {
1315
+ throw new OwneyError(
1316
+ "SPONSOR_REQUEST_FAILED",
1317
+ `Sponsor API network error: ${networkError instanceof Error ? networkError.message : String(networkError)}`,
1318
+ { cause: String(networkError), safeToFallback: false }
1319
+ );
1320
+ }
1321
+ const text = await res.text();
1322
+ let parsed = null;
1323
+ try {
1324
+ parsed = JSON.parse(text);
1325
+ } catch {
1326
+ }
1327
+ if (!res.ok || !parsed?.success || !parsed.data) {
1328
+ throw new OwneyError(
1329
+ "SPONSOR_REQUEST_FAILED",
1330
+ `Sponsor API error ${res.status}: ${parsed?.message ?? text.slice(0, 500)}`,
1331
+ {
1332
+ statusCode: res.status,
1333
+ responseBody: text.slice(0, 500),
1334
+ safeToFallback: res.status >= 400 && res.status < 500
1335
+ }
1336
+ );
1337
+ }
1338
+ return parsed.data;
1339
+ }
1340
+ async function getSponsorRelayerAddress(input) {
1341
+ const base3 = input.baseUrl ?? ROUTING_API_BASE_URL3;
1342
+ let res;
1343
+ try {
1344
+ res = await fetch(`${base3}/api/v1/sponsor/relayer-address?chainId=${input.chainId}`, {
1345
+ headers: { "x-owney-api-key": input.apiKey }
1346
+ });
1347
+ } catch (networkError) {
1348
+ throw new OwneyError(
1349
+ "SPONSOR_REQUEST_FAILED",
1350
+ `Sponsor API network error: ${networkError instanceof Error ? networkError.message : String(networkError)}`,
1351
+ { cause: String(networkError), safeToFallback: true }
1352
+ );
1353
+ }
1354
+ const text = await res.text();
1355
+ let parsed = null;
1356
+ try {
1357
+ parsed = JSON.parse(text);
1358
+ } catch {
1359
+ }
1360
+ if (!res.ok || !parsed?.success || !parsed.data?.relayer) {
1361
+ throw new OwneyError(
1362
+ "SPONSOR_REQUEST_FAILED",
1363
+ `Sponsor API error ${res.status}: ${parsed?.message ?? text.slice(0, 500)}`,
1364
+ { statusCode: res.status, responseBody: text.slice(0, 500), safeToFallback: true }
1365
+ );
1366
+ }
1367
+ return parsed.data.relayer;
1368
+ }
1193
1369
 
1194
1370
  // src/lib/sponsored-deposit.ts
1195
1371
  var AUTH_WINDOW_SECONDS = 15 * 60;
@@ -1241,6 +1417,114 @@ function makeSponsoredDepositCallback(deps) {
1241
1417
  };
1242
1418
  }
1243
1419
 
1420
+ // src/lib/permit2.ts
1421
+ var import_viem4 = require("viem");
1422
+ var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
1423
+ var MAX_UINT256 = 2n ** 256n - 1n;
1424
+ var ERC20_ALLOWANCE_ABI = [
1425
+ {
1426
+ type: "function",
1427
+ name: "allowance",
1428
+ stateMutability: "view",
1429
+ inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }],
1430
+ outputs: [{ name: "", type: "uint256" }]
1431
+ },
1432
+ {
1433
+ type: "function",
1434
+ name: "approve",
1435
+ stateMutability: "nonpayable",
1436
+ inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }],
1437
+ outputs: [{ name: "", type: "bool" }]
1438
+ }
1439
+ ];
1440
+ function buildPermitTransferFromTypedData(input) {
1441
+ return {
1442
+ domain: { name: "Permit2", chainId: input.chainId, verifyingContract: PERMIT2_ADDRESS },
1443
+ types: {
1444
+ PermitTransferFrom: [
1445
+ { name: "permitted", type: "TokenPermissions" },
1446
+ { name: "spender", type: "address" },
1447
+ { name: "nonce", type: "uint256" },
1448
+ { name: "deadline", type: "uint256" }
1449
+ ],
1450
+ TokenPermissions: [
1451
+ { name: "token", type: "address" },
1452
+ { name: "amount", type: "uint256" }
1453
+ ]
1454
+ },
1455
+ primaryType: "PermitTransferFrom",
1456
+ message: input.message
1457
+ };
1458
+ }
1459
+ function randomPermit2Nonce() {
1460
+ const bytes = new Uint8Array(32);
1461
+ globalThis.crypto.getRandomValues(bytes);
1462
+ return BigInt((0, import_viem4.bytesToHex)(bytes));
1463
+ }
1464
+ async function readPermit2Allowance(publicClient, token, owner) {
1465
+ return publicClient.readContract({
1466
+ address: token,
1467
+ abi: ERC20_ALLOWANCE_ABI,
1468
+ functionName: "allowance",
1469
+ args: [owner, PERMIT2_ADDRESS]
1470
+ });
1471
+ }
1472
+
1473
+ // src/lib/sponsored-weth-deposit.ts
1474
+ var PERMIT_WINDOW_SECONDS = 15 * 60;
1475
+ function makeSponsoredWethCallback(deps) {
1476
+ const get = deps.httpGet ?? getSponsorRelayerAddress;
1477
+ const post = deps.httpPost ?? postSponsorPermit2Transfer;
1478
+ return async (smartWallet, chainId, amount) => {
1479
+ const cid = chainId;
1480
+ const token = deps.tokenAddressByChain[cid];
1481
+ if (!token) {
1482
+ throw new OwneyError("CHAIN_UNSUPPORTED", `No sponsored WETH configured for chain ${chainId}`);
1483
+ }
1484
+ const amountWei = BigInt(amount);
1485
+ const pub = deps.getPublicClient(cid);
1486
+ const allowance = await readPermit2Allowance(pub, token, deps.ownerAddress);
1487
+ if (allowance < amountWei) {
1488
+ throw new OwneyError(
1489
+ "PERMIT2_APPROVAL_REQUIRED",
1490
+ "WETH gasless deposits need a one-time Permit2 approval; call approvePermit2() first",
1491
+ { token, chainId: cid, allowance: allowance.toString(), amount }
1492
+ );
1493
+ }
1494
+ const relayer = await get({ baseUrl: deps.baseUrl, apiKey: deps.apiKey, chainId: cid });
1495
+ const nonce = randomPermit2Nonce();
1496
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + PERMIT_WINDOW_SECONDS);
1497
+ const typedData = buildPermitTransferFromTypedData({
1498
+ chainId: cid,
1499
+ message: {
1500
+ permitted: { token, amount: amountWei },
1501
+ spender: relayer,
1502
+ nonce,
1503
+ deadline
1504
+ }
1505
+ });
1506
+ const wallet = deps.getWalletClient(cid);
1507
+ const signature = await wallet.signTypedData(
1508
+ { account: deps.ownerAddress, ...typedData }
1509
+ );
1510
+ const result = await post({
1511
+ baseUrl: deps.baseUrl,
1512
+ apiKey: deps.apiKey,
1513
+ body: {
1514
+ chainId: cid,
1515
+ token,
1516
+ from: deps.ownerAddress,
1517
+ to: smartWallet,
1518
+ amount,
1519
+ nonce: nonce.toString(),
1520
+ deadline: deadline.toString(),
1521
+ signature
1522
+ }
1523
+ });
1524
+ return result.txHash;
1525
+ };
1526
+ }
1527
+
1244
1528
  // src/client.ts
1245
1529
  function encodeMultiAgentCursor(map) {
1246
1530
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
@@ -1270,6 +1554,13 @@ var VIEM_CHAIN2 = {
1270
1554
  8453: import_chains2.base,
1271
1555
  42161: import_chains2.arbitrum
1272
1556
  };
1557
+ var SPONSORED_WETH_BY_CHAIN = {
1558
+ 8453: "0x4200000000000000000000000000000000000006",
1559
+ 42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
1560
+ };
1561
+ function shouldFallbackToUserPaid(error, asset, appCallback) {
1562
+ return asset === "WETH" && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
1563
+ }
1273
1564
  var OwneySDK = class {
1274
1565
  agents = /* @__PURE__ */ new Map();
1275
1566
  activeAgents = /* @__PURE__ */ new Set();
@@ -1283,6 +1574,7 @@ var OwneySDK = class {
1283
1574
  apiKey;
1284
1575
  zyfaiRpcUrls;
1285
1576
  cachedSponsoredCallback = null;
1577
+ cachedWethSponsoredCallback = null;
1286
1578
  initializingAgentsPromise = null;
1287
1579
  constructor(config) {
1288
1580
  this.apiKey = config.apiKey;
@@ -1307,6 +1599,7 @@ var OwneySDK = class {
1307
1599
  }
1308
1600
  this.state = { provider, walletAddress, chainId: null };
1309
1601
  this.cachedSponsoredCallback = null;
1602
+ this.cachedWethSponsoredCallback = null;
1310
1603
  }
1311
1604
  /**
1312
1605
  * Disconnect from all agents and clear connection state.
@@ -1320,6 +1613,7 @@ var OwneySDK = class {
1320
1613
  this.disabledAgents.clear();
1321
1614
  this.state = null;
1322
1615
  this.cachedSponsoredCallback = null;
1616
+ this.cachedWethSponsoredCallback = null;
1323
1617
  }
1324
1618
  isAgentDisabled(agentId) {
1325
1619
  return this.disabledAgents.has(agentId);
@@ -1381,18 +1675,48 @@ var OwneySDK = class {
1381
1675
  // Casts work around viem's chain-narrowed Client vs the generic
1382
1676
  // PublicClient/WalletClient param types — structurally identical at
1383
1677
  // runtime, but the two share a name TS treats as unrelated.
1384
- getPublicClient: (cid) => (0, import_viem4.createPublicClient)({
1678
+ getPublicClient: (cid) => (0, import_viem5.createPublicClient)({
1385
1679
  chain: VIEM_CHAIN2[cid],
1386
- transport: (0, import_viem4.custom)(provider)
1680
+ transport: (0, import_viem5.custom)(provider)
1387
1681
  }),
1388
- getWalletClient: (cid) => (0, import_viem4.createWalletClient)({
1682
+ getWalletClient: (cid) => (0, import_viem5.createWalletClient)({
1389
1683
  account: owner,
1390
1684
  chain: VIEM_CHAIN2[cid],
1391
- transport: (0, import_viem4.custom)(provider)
1685
+ transport: (0, import_viem5.custom)(provider)
1392
1686
  })
1393
1687
  });
1394
1688
  return this.cachedSponsoredCallback;
1395
1689
  }
1690
+ /**
1691
+ * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
1692
+ * callback used when the caller omits `depositCallback` for a WETH
1693
+ * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
1694
+ * `PermitTransferFrom` instead of an EIP-3009 authorization.
1695
+ */
1696
+ getDefaultWethSponsoredCallback() {
1697
+ if (this.cachedWethSponsoredCallback)
1698
+ return this.cachedWethSponsoredCallback;
1699
+ const provider = this.requireConnectedProvider();
1700
+ const owner = this.state.walletAddress;
1701
+ this.cachedWethSponsoredCallback = makeSponsoredWethCallback({
1702
+ apiKey: this.apiKey,
1703
+ ownerAddress: owner,
1704
+ tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
1705
+ // Casts work around viem's chain-narrowed Client vs the generic
1706
+ // PublicClient/WalletClient param types — structurally identical at
1707
+ // runtime, but the two share a name TS treats as unrelated.
1708
+ getPublicClient: (cid) => (0, import_viem5.createPublicClient)({
1709
+ chain: VIEM_CHAIN2[cid],
1710
+ transport: (0, import_viem5.custom)(provider)
1711
+ }),
1712
+ getWalletClient: (cid) => (0, import_viem5.createWalletClient)({
1713
+ account: owner,
1714
+ chain: VIEM_CHAIN2[cid],
1715
+ transport: (0, import_viem5.custom)(provider)
1716
+ })
1717
+ });
1718
+ return this.cachedWethSponsoredCallback;
1719
+ }
1396
1720
  getAgent(agentId) {
1397
1721
  const agent = this.agents.get(agentId);
1398
1722
  if (!agent) {
@@ -1546,7 +1870,7 @@ var OwneySDK = class {
1546
1870
  }
1547
1871
  const state = this.requireState();
1548
1872
  const chainId = this.requireChainId();
1549
- const effectiveCallback = depositCallback ?? this.getDefaultSponsoredCallback();
1873
+ const effectiveCallback = depositCallback ?? (asset === "WETH" ? this.getDefaultWethSponsoredCallback() : this.getDefaultSponsoredCallback());
1550
1874
  if (agentId) {
1551
1875
  if (this.isAgentDisabled(agentId)) {
1552
1876
  throw new OwneyError(
@@ -1558,10 +1882,14 @@ var OwneySDK = class {
1558
1882
  const agent = this.getAgent(agentId);
1559
1883
  this.validateAssetSupport(agent, chainId, asset);
1560
1884
  await this.validateMinDepositAmount(agent, state, chainId, asset, amount);
1561
- return withFailureReporting(
1562
- this.apiKey,
1563
- agent.id,
1564
- () => agent.deposit(state, chainId, amount, effectiveCallback)
1885
+ return this.depositWithFallback(
1886
+ agent,
1887
+ state,
1888
+ chainId,
1889
+ amount,
1890
+ asset,
1891
+ effectiveCallback,
1892
+ depositCallback
1565
1893
  );
1566
1894
  }
1567
1895
  const totalAmount = BigInt(amount);
@@ -1592,14 +1920,78 @@ var OwneySDK = class {
1592
1920
  }
1593
1921
  const agentResults = {};
1594
1922
  for (const { agent, amount: agentAmount } of agentAmounts) {
1595
- agentResults[agent.id] = await withFailureReporting(
1596
- this.apiKey,
1597
- agent.id,
1598
- () => agent.deposit(state, chainId, agentAmount, effectiveCallback)
1923
+ agentResults[agent.id] = await this.depositWithFallback(
1924
+ agent,
1925
+ state,
1926
+ chainId,
1927
+ agentAmount,
1928
+ asset,
1929
+ effectiveCallback,
1930
+ depositCallback
1599
1931
  );
1600
1932
  }
1601
1933
  return { agentResults };
1602
1934
  }
1935
+ /**
1936
+ * Invokes `agent.deposit` with the resolved sponsored callback, composing
1937
+ * two independent auto-recovery mechanisms:
1938
+ *
1939
+ * 1. Missing Permit2 allowance: when the app did not supply its own
1940
+ * callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
1941
+ * WETH deposit, this is the wallet's first gasless WETH deposit. We send
1942
+ * the one-time (user-paid) Permit2 approval via `approvePermit2()` and
1943
+ * retry the SAME sponsored attempt once. Bounded to one approval attempt
1944
+ * per call so a wallet/agent that keeps reporting the allowance as
1945
+ * missing can't loop forever. If `approvePermit2()` itself throws (e.g.
1946
+ * the user rejects the wallet prompt), that error propagates as-is —
1947
+ * the user said no to a transaction, so we must not turn around and ask
1948
+ * them to pay for a different one via the fallback below.
1949
+ * 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
1950
+ * the infrastructure layer in a provably safe-to-retry way, so we warn
1951
+ * and retry once with `undefined`, falling through to a user-paid
1952
+ * native deposit. This still applies after a successful Permit2
1953
+ * approval retry (the retried sponsored attempt can itself hit a safe
1954
+ * sponsor failure).
1955
+ *
1956
+ * Any other error — an app-supplied callback's own failure in particular —
1957
+ * propagates unchanged. Scoped per agent invocation so one agent's failure
1958
+ * doesn't force a retry of sibling agents in a multi-agent split.
1959
+ */
1960
+ async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback) {
1961
+ const attempt = (callback) => withFailureReporting(
1962
+ this.apiKey,
1963
+ agent.id,
1964
+ () => agent.deposit(
1965
+ state,
1966
+ chainId,
1967
+ amount,
1968
+ asset,
1969
+ callback
1970
+ )
1971
+ );
1972
+ let approvalAttempted = false;
1973
+ for (; ; ) {
1974
+ try {
1975
+ return await attempt(effectiveCallback);
1976
+ } catch (error) {
1977
+ if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
1978
+ approvalAttempted = true;
1979
+ console.warn(
1980
+ "[owney-sdk] First WETH deposit: sending one-time Permit2 approval..."
1981
+ );
1982
+ await this.approvePermit2();
1983
+ continue;
1984
+ }
1985
+ if (!shouldFallbackToUserPaid(error, asset, appCallback))
1986
+ throw error;
1987
+ console.warn(
1988
+ "[owney-sdk] WETH sponsorship unavailable, falling back to user-paid deposit:",
1989
+ error
1990
+ );
1991
+ return attempt(void 0);
1992
+ }
1993
+ }
1994
+ }
1603
1995
  getMinDepositAmount(agent, chainId, asset) {
1604
1996
  const chainAssets = agent.supportedAssets.find(
1605
1997
  (sa) => sa.chainId === chainId
@@ -2087,6 +2479,66 @@ var OwneySDK = class {
2087
2479
  agentUserProfile: results
2088
2480
  };
2089
2481
  }
2482
+ /**
2483
+ * Ensure the given asset uses backend protocol auto-selection for the active
2484
+ * chain's account. No-op (returns false) for agents that don't support it.
2485
+ * @param asset - "USDC" or "WETH".
2486
+ * @param agentId - Agent to target. Defaults to "zyfai".
2487
+ * @returns whether a profile write occurred.
2488
+ */
2489
+ async ensureAutoSelectProtocols(asset, agentId = "zyfai") {
2490
+ const state = this.requireState();
2491
+ const chainId = this.requireChainId();
2492
+ const agent = this.getAgent(agentId);
2493
+ if (typeof agent.ensureAutoSelectProtocols !== "function")
2494
+ return false;
2495
+ return agent.ensureAutoSelectProtocols(state, chainId, asset);
2496
+ }
2497
+ /**
2498
+ * One-time, user-paid approval of Permit2 on the sponsored WETH token for
2499
+ * the active chain. Required once per wallet per chain before gasless WETH
2500
+ * deposits; afterwards deposit() is signature-only. Resolves only after the
2501
+ * approval transaction is mined (1 confirmation), so a subsequent deposit()
2502
+ * will see the new allowance; throws if the transaction reverted.
2503
+ * @returns the approval transaction hash.
2504
+ */
2505
+ async approvePermit2(asset = "WETH") {
2506
+ const state = this.requireState();
2507
+ const chainId = this.requireChainId();
2508
+ const token = SPONSORED_WETH_BY_CHAIN[chainId];
2509
+ if (!token) {
2510
+ throw new OwneyError(
2511
+ "CHAIN_UNSUPPORTED",
2512
+ `No sponsored WETH on chain ${chainId}`
2513
+ );
2514
+ }
2515
+ const provider = this.requireConnectedProvider();
2516
+ const wallet = (0, import_viem5.createWalletClient)({
2517
+ account: state.walletAddress,
2518
+ chain: VIEM_CHAIN2[chainId],
2519
+ transport: (0, import_viem5.custom)(provider)
2520
+ });
2521
+ const hash = await wallet.writeContract({
2522
+ address: token,
2523
+ abi: ERC20_ALLOWANCE_ABI,
2524
+ functionName: "approve",
2525
+ args: [PERMIT2_ADDRESS, MAX_UINT256],
2526
+ account: state.walletAddress,
2527
+ chain: VIEM_CHAIN2[chainId]
2528
+ });
2529
+ const publicClient = (0, import_viem5.createPublicClient)({
2530
+ chain: VIEM_CHAIN2[chainId],
2531
+ transport: (0, import_viem5.custom)(provider)
2532
+ });
2533
+ const receipt = await publicClient.waitForTransactionReceipt({
2534
+ hash,
2535
+ confirmations: 1
2536
+ });
2537
+ if (receipt.status !== "success") {
2538
+ throw new Error(`Permit2 approval reverted (tx ${hash})`);
2539
+ }
2540
+ return hash;
2541
+ }
2090
2542
  // --- Discovery (no wallet required) ---
2091
2543
  /**
2092
2544
  * Get the agent's average APY performance over a time period. Does not require a wallet connection.
@@ -2185,7 +2637,7 @@ var OwneySDK = class {
2185
2637
  };
2186
2638
 
2187
2639
  // src/agents/zyfai/zyfai.siwx.ts
2188
- var import_viem5 = require("viem");
2640
+ var import_viem6 = require("viem");
2189
2641
  var import_siwe = require("siwe");
2190
2642
  var import_sdk2 = require("@zyfai/sdk");
2191
2643
 
@@ -2267,7 +2719,7 @@ function buildSIWXConfig(deps) {
2267
2719
  issuedAt,
2268
2720
  toString() {
2269
2721
  return new import_siwe.SiweMessage({
2270
- address: (0, import_viem5.getAddress)(accountAddress),
2722
+ address: (0, import_viem6.getAddress)(accountAddress),
2271
2723
  chainId: numericChainId(chainId),
2272
2724
  domain,
2273
2725
  uri,