@owney/sdk 0.7.28-beta.1 → 0.7.28-beta.3

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
@@ -510,7 +510,7 @@ function rawPoolApyForChain(entry, chainId, tokenSymbol) {
510
510
  let weightedSum = 0;
511
511
  let totalBalance = 0;
512
512
  for (const p of entry.positions ?? []) {
513
- if (p.chainId !== chainId) continue;
513
+ if (chainId !== void 0 && p.chainId !== chainId) continue;
514
514
  if (tokenSymbol && p.tokenSymbol?.toUpperCase() !== tokenSymbol.toUpperCase()) continue;
515
515
  const balance = p.balance ?? 0;
516
516
  const apy = netApy(p.apy_withFee, p.apy, "apy_withFee");
@@ -520,6 +520,28 @@ function rawPoolApyForChain(entry, chainId, tokenSymbol) {
520
520
  }
521
521
  return totalBalance > 0 ? weightedSum / totalBalance : null;
522
522
  }
523
+ function historyCoverage(raw) {
524
+ const chains = /* @__PURE__ */ new Map();
525
+ const assets = /* @__PURE__ */ new Map();
526
+ for (const date of Object.keys(raw.history ?? {}).sort()) {
527
+ void date;
528
+ const seenChains = /* @__PURE__ */ new Set();
529
+ const seenAssets = /* @__PURE__ */ new Set();
530
+ for (const p of (raw.history ?? {})[date].positions ?? []) {
531
+ const balance = p.balance ?? 0;
532
+ const apy = netApy(p.apy_withFee, p.apy, "apy_withFee");
533
+ if (balance <= 0 || typeof apy !== "number") continue;
534
+ seenChains.add(p.chainId);
535
+ if (p.tokenSymbol) seenAssets.add(p.tokenSymbol.toUpperCase());
536
+ }
537
+ for (const c of seenChains) chains.set(c, (chains.get(c) ?? 0) + 1);
538
+ for (const a of seenAssets) assets.set(a, (assets.get(a) ?? 0) + 1);
539
+ }
540
+ return {
541
+ chains: [...chains].map(([chainId, days]) => ({ chainId, days })),
542
+ assets: [...assets].map(([asset, days]) => ({ asset, days }))
543
+ };
544
+ }
523
545
  function mapApyHistory(raw, chainId, tokenSymbol) {
524
546
  const history = Object.entries(raw.history ?? {}).map(([date, entry]) => ({
525
547
  date,
@@ -530,7 +552,7 @@ function mapApyHistory(raw, chainId, tokenSymbol) {
530
552
  ...tokenSymbol ? {
531
553
  historicalBalance: {
532
554
  amount: (entry.positions ?? []).filter(
533
- (p) => p.chainId === chainId && p.tokenSymbol?.toUpperCase() === tokenSymbol.toUpperCase() && Number.isFinite(p.balance) && p.balance > 0 && Number.isFinite(p.apy_withFee ?? p.apy)
555
+ (p) => (chainId === void 0 || p.chainId === chainId) && p.tokenSymbol?.toUpperCase() === tokenSymbol.toUpperCase() && Number.isFinite(p.balance) && p.balance > 0 && Number.isFinite(p.apy_withFee ?? p.apy)
534
556
  ).reduce((sum, p) => sum + p.balance, 0),
535
557
  unit: tokenSymbol.toUpperCase()
536
558
  }
@@ -540,7 +562,8 @@ function mapApyHistory(raw, chainId, tokenSymbol) {
540
562
  walletAddress: raw.walletAddress,
541
563
  weightedApyAfterFee: raw.weightedApyAfterFee ? sumTokenValues(raw.weightedApyAfterFee) : void 0,
542
564
  apyByChainAndAsset: mapWeightedApyByChain(raw.weightedApyAfterFeeByChain),
543
- history
565
+ history,
566
+ coverage: historyCoverage(raw)
544
567
  };
545
568
  }
546
569
  function splitOpportunity(opportunity) {
@@ -999,6 +1022,9 @@ function splittingNeedsUpdate(current, desired) {
999
1022
  }
1000
1023
 
1001
1024
  // src/agents/zyfai/pool-policy.ts
1025
+ function orgPolicyForConnect(policy, userPoolCustomization) {
1026
+ return userPoolCustomization ? { ...policy, poolPolicy: null } : policy;
1027
+ }
1002
1028
  function toZyfaiCustomizations(policy, availableByProtocol, chainsByProtocol) {
1003
1029
  const selectionByKey = /* @__PURE__ */ new Map();
1004
1030
  for (const selection of policy.selections) {
@@ -1090,6 +1116,54 @@ function protocolsPolicyNeedsUpdate(current, desiredProtocols, desiredAutoSelect
1090
1116
  if (desiredProtocols === null) return false;
1091
1117
  return !protocolListsEqual(current.protocols, desiredProtocols);
1092
1118
  }
1119
+ function mergeScopedPoolNames(current, scoped, allAvailableNames) {
1120
+ const managed = new Set(scoped.flatMap((entry) => entry.availablePools));
1121
+ const existing = current.autoselect ? allAvailableNames : current.pools;
1122
+ const preserved = existing.filter((name) => !managed.has(name));
1123
+ const selected = scoped.flatMap((entry) => entry.pools);
1124
+ return [.../* @__PURE__ */ new Set([...preserved, ...selected])];
1125
+ }
1126
+ function mapAvailablePools(raw) {
1127
+ const byChain = raw ?? {};
1128
+ if (typeof byChain !== "object" || byChain === null) return [];
1129
+ const result = [];
1130
+ for (const [chainIdStr, byToken] of Object.entries(byChain)) {
1131
+ const chainId = Number(chainIdStr);
1132
+ if (!Number.isFinite(chainId)) continue;
1133
+ for (const [token, entry] of Object.entries(byToken ?? {})) {
1134
+ for (const pool of entry?.pools ?? []) {
1135
+ if (!pool?.name) continue;
1136
+ result.push({
1137
+ chainId,
1138
+ token,
1139
+ name: pool.name,
1140
+ apy: pool.info?.combined_apy ?? null,
1141
+ tvlUsd: pool.info?.tvlUsd ?? null,
1142
+ link: pool.info?.link ?? null
1143
+ });
1144
+ }
1145
+ }
1146
+ }
1147
+ return result;
1148
+ }
1149
+ function assemblePoolCatalog(protocols, poolsByProtocol, asset, chains) {
1150
+ const wanted = asset.toUpperCase();
1151
+ const allowedChains = chains && chains.length > 0 ? new Set(chains) : void 0;
1152
+ const catalog = [];
1153
+ for (const protocol of protocols) {
1154
+ const pools = (poolsByProtocol.get(protocol.id) ?? []).filter(
1155
+ (p) => p.token.toUpperCase() === wanted && (allowedChains === void 0 || allowedChains.has(p.chainId))
1156
+ );
1157
+ if (pools.length === 0) continue;
1158
+ catalog.push({
1159
+ protocolId: protocol.id,
1160
+ name: protocol.name,
1161
+ logo: protocol.imageUrl ?? null,
1162
+ pools
1163
+ });
1164
+ }
1165
+ return catalog;
1166
+ }
1093
1167
 
1094
1168
  // src/agents/zyfai/zyfai.agent.ts
1095
1169
  var ERC7579_IS_MODULE_INSTALLED_ABI = (0, import_viem.parseAbi)([
@@ -1516,7 +1590,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1516
1590
  * connecting, so a Zyfai hiccup must leave the user connected with their
1517
1591
  * profile unchanged, never fail activation.
1518
1592
  */
1519
- async applyPoolPolicy(policy) {
1593
+ async applyPoolPolicy(policy, opts = {}) {
1520
1594
  try {
1521
1595
  const { protocols: universe } = await this.sdk.getAvailableProtocols(
1522
1596
  this.supportedChainIds[0]
@@ -1530,7 +1604,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1530
1604
  universeIds,
1531
1605
  chainsByProtocol
1532
1606
  });
1533
- let strategy;
1607
+ let strategy = opts.strategy;
1534
1608
  const desiredProtocols = deriveProtocols(
1535
1609
  policy,
1536
1610
  universeIds,
@@ -1610,6 +1684,109 @@ var ZyfaiAgent = class _ZyfaiAgent {
1610
1684
  );
1611
1685
  }
1612
1686
  }
1687
+ /**
1688
+ * The deposit-time Pools step's catalogue: every protocol Zyfai offers this
1689
+ * user, each with the pools (name, APY, TVL, link) selectable for `asset`
1690
+ * under `strategy`. Sourced from the same two Zyfai calls `applyPoolPolicy`
1691
+ * uses, so "shown" and "selectable" are one set. A protocol whose pool
1692
+ * lookup fails is skipped rather than failing the whole catalogue.
1693
+ */
1694
+ async getPoolCatalog(params) {
1695
+ const { protocols: universe } = await this.sdk.getAvailableProtocols(
1696
+ this.supportedChainIds[0]
1697
+ );
1698
+ const poolResults = await Promise.all(
1699
+ universe.map(async (protocol) => {
1700
+ try {
1701
+ const raw = await this.sdk.getAvailablePools(
1702
+ protocol.id,
1703
+ params.strategy
1704
+ );
1705
+ return [protocol.id, mapAvailablePools(raw)];
1706
+ } catch (error) {
1707
+ console.warn(
1708
+ `[zyfai] getPoolCatalog: getAvailablePools(${protocol.id}) failed, skipping (non-fatal):`,
1709
+ error instanceof Error ? error.message : String(error)
1710
+ );
1711
+ return null;
1712
+ }
1713
+ })
1714
+ );
1715
+ const poolsByProtocol = /* @__PURE__ */ new Map();
1716
+ for (const result of poolResults) {
1717
+ if (result) poolsByProtocol.set(result[0], result[1]);
1718
+ }
1719
+ return assemblePoolCatalog(universe, poolsByProtocol, params.asset);
1720
+ }
1721
+ /**
1722
+ * Persist an explicitly saved deposit-time draft at Zyfai profile level.
1723
+ * Pool choices are merged into protocol/chain records and each active asset
1724
+ * is kept at aggressive unless the draft includes a yieldmaxxing-only pool.
1725
+ */
1726
+ async setPoolSelection(selection) {
1727
+ try {
1728
+ const byPair = /* @__PURE__ */ new Map();
1729
+ for (const entry of selection.selections) {
1730
+ const pair = `${entry.protocolId}:${entry.chainId}`;
1731
+ const group = byPair.get(pair) ?? [];
1732
+ group.push(entry);
1733
+ byPair.set(pair, group);
1734
+ }
1735
+ const customizations = (await Promise.all(
1736
+ [...byPair.values()].map(async (scoped) => {
1737
+ const { protocolId, chainId } = scoped[0];
1738
+ try {
1739
+ const [current, rawAvailable] = await Promise.all([
1740
+ this.sdk.getSelectedPools(protocolId, chainId),
1741
+ this.sdk.getAvailablePools(protocolId, "yieldmaxxing")
1742
+ ]);
1743
+ const allAvailableNames = mapAvailablePools(rawAvailable).filter((pool) => pool.chainId === chainId).map((pool) => pool.name);
1744
+ return {
1745
+ protocolId,
1746
+ chainId,
1747
+ pools: mergeScopedPoolNames(
1748
+ {
1749
+ pools: current.pools ?? [],
1750
+ autoselect: current.autoselect
1751
+ },
1752
+ scoped,
1753
+ allAvailableNames
1754
+ ),
1755
+ // An explicit Save freezes exactly the choices the user saw.
1756
+ // This also lets an empty list disable the pair deliberately.
1757
+ autoselect: false
1758
+ };
1759
+ } catch (error) {
1760
+ console.warn(
1761
+ `[zyfai] setPoolSelection: preserving ${protocolId}/${chainId} failed; skipping the pair:`,
1762
+ error instanceof Error ? error.message : String(error)
1763
+ );
1764
+ return null;
1765
+ }
1766
+ })
1767
+ )).filter((value) => value !== null);
1768
+ if (customizations.length > 0) {
1769
+ await this.sdk.customizeBatch(customizations);
1770
+ }
1771
+ await Promise.all(
1772
+ selection.strategies.map(async ({ asset, strategy }) => {
1773
+ const current = await this.sdk.getUserDetails(asset);
1774
+ const selectedProtocols = selection.selections.filter(
1775
+ (entry) => entry.asset === asset && entry.pools.length > 0
1776
+ ).map((entry) => entry.protocolId);
1777
+ const protocols = [
1778
+ .../* @__PURE__ */ new Set([...current.protocols ?? [], ...selectedProtocols])
1779
+ ];
1780
+ await this.sdk.updateUserProfile({ asset, strategy, protocols });
1781
+ })
1782
+ );
1783
+ } catch (error) {
1784
+ console.warn(
1785
+ "[zyfai] setPoolSelection failed (non-fatal):",
1786
+ error instanceof Error ? error.message : String(error)
1787
+ );
1788
+ }
1789
+ }
1613
1790
  /**
1614
1791
  * True when `smartWallet` is the backend-managed pool wallet Zyfai assigned
1615
1792
  * to this EOA — the "new wallet" kind that is predeployed on the chains it
@@ -1736,11 +1913,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1736
1913
  );
1737
1914
  return;
1738
1915
  }
1739
- await this.ensureSessionKey(
1740
- ownerAddress,
1741
- chainId,
1742
- wallet.address
1743
- );
1916
+ await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
1744
1917
  } catch (error) {
1745
1918
  console.warn(
1746
1919
  "[zyfai] session-key check at activation failed (non-fatal):",
@@ -1851,11 +2024,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1851
2024
  "zyfai:onboard",
1852
2025
  "Safe already has code -> no deploy needed, checking the session key"
1853
2026
  );
1854
- await this.ensureSessionKey(
1855
- ownerAddress,
1856
- chainId,
1857
- wallet.address
1858
- );
2027
+ await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
1859
2028
  return wallet.address;
1860
2029
  }
1861
2030
  const poolFirst = await this.isPredeployedPoolWallet(
@@ -2022,14 +2191,17 @@ var ZyfaiAgent = class _ZyfaiAgent {
2022
2191
  this.earningsRefreshes.set(key2, pending);
2023
2192
  return pending;
2024
2193
  }
2025
- async getAccountApy(state, chainId, days, tokenSymbol) {
2194
+ async getAccountApy(state, chainId, days, tokenSymbol, scopeChainId) {
2026
2195
  const { smartWallet } = await this.resolveSmartWallet(state, chainId);
2027
2196
  const raw = await this.sdk.getDailyApyHistory(smartWallet, days);
2028
- return mapApyHistory(raw, chainId, tokenSymbol);
2197
+ const scope = scopeChainId === null ? void 0 : scopeChainId ?? chainId;
2198
+ return mapApyHistory(raw, scope, tokenSymbol);
2029
2199
  }
2030
2200
  async getDailyEarnings(state, chainId, days, tokenSymbol) {
2031
2201
  const { smartWallet } = await this.resolveSmartWallet(state, chainId);
2032
- const start = new Date(Date.now() - (DayFilterMapping[days] + 1) * 864e5);
2202
+ const start = new Date(
2203
+ Date.now() - (DayFilterMapping[days] + 1) * 864e5
2204
+ );
2033
2205
  const raw = await this.sdk.getDailyEarnings(
2034
2206
  smartWallet,
2035
2207
  start.toISOString().slice(0, 10)
@@ -4327,6 +4499,7 @@ var OwneySDK = class {
4327
4499
  orgAgentConfig;
4328
4500
  orgAgentConfigPromise = null;
4329
4501
  zyfaiRpcUrls;
4502
+ userPoolCustomization = false;
4330
4503
  routingApiBaseUrl;
4331
4504
  referralSource;
4332
4505
  cachedSponsoredCallback = null;
@@ -4352,6 +4525,7 @@ var OwneySDK = class {
4352
4525
  this.routingApiBaseUrl = config.routingApiBaseUrl;
4353
4526
  this.paymasterServiceUrl = config.paymasterServiceUrl;
4354
4527
  this.referralSource = config.referralSource;
4528
+ this.userPoolCustomization = config.userPoolCustomization ?? false;
4355
4529
  }
4356
4530
  /**
4357
4531
  * Establish connection state for all subsequent SDK calls.
@@ -4578,7 +4752,8 @@ var OwneySDK = class {
4578
4752
  try {
4579
4753
  const policy = await this.getOrgAgentConfig();
4580
4754
  if (!policy) return;
4581
- await agent.applyOrgPolicy(state, chainId, policy);
4755
+ const effective = orgPolicyForConnect(policy, this.userPoolCustomization);
4756
+ await agent.applyOrgPolicy(state, chainId, effective);
4582
4757
  } catch (error) {
4583
4758
  console.warn(
4584
4759
  `[owney-sdk] applyOrgPolicy(${agent.id}) failed (non-fatal):`,
@@ -6352,7 +6527,10 @@ var OwneySDK = class {
6352
6527
  agent,
6353
6528
  "dailyEarnings",
6354
6529
  () => agent.getDailyEarnings(state, legChainId, days, tokenSymbol),
6355
- { days, tokenSymbol }
6530
+ // The leg's chain must be in the key: the read key otherwise names
6531
+ // only the CONNECTION chain, so every leg shared the first leg's
6532
+ // request and the sum counted that chain once per leg. (ROUT-561)
6533
+ { days, tokenSymbol, chainId: legChainId }
6356
6534
  )
6357
6535
  )
6358
6536
  );
@@ -6395,20 +6573,57 @@ var OwneySDK = class {
6395
6573
  chainId
6396
6574
  };
6397
6575
  }
6576
+ /**
6577
+ * The deposit-time pool catalogue (ROUT-569): every protocol with its
6578
+ * selectable pools (name, APY, TVL, link) for `asset` under `strategy`.
6579
+ * Defaults to the zyfai agent, the only one with pool customization today.
6580
+ */
6581
+ async getPoolCatalog(params) {
6582
+ await this.ensureAgentsInitialized();
6583
+ const agent = this.getAgent(params.agentId ?? "zyfai");
6584
+ if (!agent.getPoolCatalog) {
6585
+ throw new OwneyError(
6586
+ "POOL_CATALOG_UNSUPPORTED",
6587
+ `Agent ${agent.id} does not support pool customization`
6588
+ );
6589
+ }
6590
+ return agent.getPoolCatalog({
6591
+ asset: params.asset,
6592
+ strategy: params.strategy
6593
+ });
6594
+ }
6595
+ /**
6596
+ * Apply a user's deposit-time pool selection to their account (ROUT-569).
6597
+ * The request carries explicit pool choices plus the resulting strategy for
6598
+ * each asset reviewed in the deposit flow.
6599
+ */
6600
+ async setPoolSelection(params) {
6601
+ await this.ensureAgentsInitialized();
6602
+ const agent = this.getAgent(params.agentId ?? "zyfai");
6603
+ if (!agent.setPoolSelection) {
6604
+ throw new OwneyError(
6605
+ "POOL_SELECTION_UNSUPPORTED",
6606
+ `Agent ${agent.id} does not support pool customization`
6607
+ );
6608
+ }
6609
+ await agent.setPoolSelection(params.selection);
6610
+ }
6398
6611
  async getAccountApy({
6399
6612
  agentId,
6400
6613
  days,
6401
- tokenSymbol
6614
+ tokenSymbol,
6615
+ chainId: scope
6402
6616
  }) {
6403
6617
  const state = this.requireState();
6404
6618
  const chainId = this.requireChainId();
6619
+ const scopeChainId = scope === "all" ? null : scope;
6405
6620
  if (agentId) {
6406
6621
  const agent = this.getAgent(agentId);
6407
6622
  return this.readAgent(
6408
6623
  agent,
6409
6624
  "accountApy",
6410
- () => agent.getAccountApy(state, chainId, days, tokenSymbol),
6411
- { days, tokenSymbol }
6625
+ () => agent.getAccountApy(state, chainId, days, tokenSymbol, scopeChainId),
6626
+ { days, tokenSymbol, scopeChainId }
6412
6627
  );
6413
6628
  }
6414
6629
  const activeAgents = this.getActiveAgents();
@@ -6419,8 +6634,14 @@ var OwneySDK = class {
6419
6634
  const apy = await this.readAgent(
6420
6635
  agent,
6421
6636
  "accountApy",
6422
- () => agent.getAccountApy(state, chainId, days, tokenSymbol),
6423
- { days, tokenSymbol }
6637
+ () => agent.getAccountApy(
6638
+ state,
6639
+ chainId,
6640
+ days,
6641
+ tokenSymbol,
6642
+ scopeChainId
6643
+ ),
6644
+ { days, tokenSymbol, scopeChainId }
6424
6645
  );
6425
6646
  return [id, apy];
6426
6647
  })
@@ -6509,7 +6730,8 @@ var OwneySDK = class {
6509
6730
  agent,
6510
6731
  "history",
6511
6732
  () => agent.getHistory(state, leg.chainId, legFilters),
6512
- legFilters
6733
+ // Keyed per leg for the same reason as getDailyEarnings' fan-out.
6734
+ { ...legFilters, chainId: leg.chainId }
6513
6735
  );
6514
6736
  return { leg, page };
6515
6737
  })
package/dist/index.d.cts CHANGED
@@ -276,6 +276,15 @@ interface OwneySDKConfig {
276
276
  * already-deployed app: set `globalThis.__OWNEY_DEBUG__ = true` in devtools.
277
277
  */
278
278
  debug?: boolean;
279
+ /**
280
+ * When true, the organization's pool policy is NOT applied on connect, so a
281
+ * user's own deposit-time pool selection (ROUT-569) is never overwritten by
282
+ * the org-level one on their next reconnect. Splitting and the rest of the
283
+ * org policy still apply. Turn this on in apps that expose the deposit-time
284
+ * Pools step (the spread flow); leave it off where the partner dashboard's
285
+ * pool customization is the source of truth.
286
+ */
287
+ userPoolCustomization?: boolean;
279
288
  /**
280
289
  * Absolute URL of an ERC-7677 paymaster service. When set, deposits go out as
281
290
  * an EIP-5792 `wallet_sendCalls` with `capabilities.paymasterService`, so a
@@ -374,6 +383,18 @@ type WithdrawOptions = {
374
383
  type AccountApyOptions = {
375
384
  agentId?: AgentId;
376
385
  days: LookbackDays;
386
+ /**
387
+ * Chain to scope the daily series to, instead of the one `activateAgent`
388
+ * last set (ROUT-479).
389
+ *
390
+ * `"all"` blends every chain the wallet held that day, which is what the
391
+ * chart's "All chains" filter asks for. Unlike the history and earnings
392
+ * reads this needs no fan-out: the provider's daily-APY payload is already
393
+ * wallet-wide and names each position's chain, so the scope is a filter over
394
+ * one response rather than several calls. Omit for the active chain, which
395
+ * stays the default.
396
+ */
397
+ chainId?: OwneySupportedChainId | "all";
377
398
  /**
378
399
  * Optional asset symbol (e.g. "USDC", "WETH") to scope the daily APY series
379
400
  * to a specific asset on the active chain. Without it the series blends every
@@ -476,6 +497,81 @@ type OrgAgentConfig = {
476
497
  poolPolicy?: OrgPoolPolicy | null;
477
498
  };
478
499
 
500
+ /**
501
+ * Pool policy: Owney's org-level protocol/pool policy -> Zyfai's two mismatched
502
+ * write shapes.
503
+ *
504
+ * Zyfai splits "which protocols and pools may this user's funds sit in" across
505
+ * two calls that do not share an axis:
506
+ *
507
+ * updateUserProfile({ protocols }) per ASSET, chain-agnostic
508
+ * customizeBatch([{ protocolId, per (PROTOCOL, CHAIN), asset-agnostic
509
+ * chainId, pools, autoselect }])
510
+ *
511
+ * `protocols` is an explicit ALLOW-LIST: a protocol left out of it is disabled
512
+ * for that asset, on every chain, immediately. `customizeBatch` is the opposite
513
+ * shape — one record per (protocol, chain) pair, asset-agnostic, and per
514
+ * docs.zyf.ai it "creates or updates a record for the (user, protocol, chain)
515
+ * combination"; "previous customizations for the same combination are
516
+ * overwritten".
517
+ *
518
+ * That second property is the reason this file exists rather than a single
519
+ * flat translation: customizeBatch overwrites only the combinations it is
520
+ * GIVEN. The partner's stored policy (`OrgPoolPolicy.selections`) is sparse —
521
+ * only the protocols they actually narrowed appear — but the payload we send
522
+ * must be EXHAUSTIVE over every (protocol, chain) pair Zyfai offers this user.
523
+ * If we only sent the narrowed pairs, a partner who restricts Morpho, saves,
524
+ * then later re-ticks every pool box would never be able to undo it: nothing
525
+ * in the (sparse) policy would tell us to send a record clearing it, and the
526
+ * old restrictive record would stay live on the user's profile forever.
527
+ *
528
+ * Pure functions, kept out of the agent so the mapping is testable without a
529
+ * Zyfai client. See splitting.ts for the same pattern on the other lever.
530
+ */
531
+
532
+ /** One selectable pool, with the figures the deposit-time Pools step shows. */
533
+ type AvailablePool = {
534
+ chainId: number;
535
+ token: string;
536
+ name: string;
537
+ /** combined_apy (pool + rewards), already a percent. null when absent. */
538
+ apy: number | null;
539
+ /** TVL in USD. null when absent. See ZyfCustomizationPoolInfo: `tvl` is in
540
+ * the pool's own token, `tvlUsd` is the dollar figure — only the latter is
541
+ * safe to show with a currency symbol. */
542
+ tvlUsd: number | null;
543
+ /** Per-pool deep link (e.g. the Morpho vault page). null when absent. */
544
+ link: string | null;
545
+ };
546
+ /** Public Zyfai strategies used by deposit-time pool customization. */
547
+ type PoolSelectionStrategy = "aggressive" | "yieldmaxxing";
548
+ /** One asset's explicit choice for a protocol on one chain. */
549
+ type UserPoolSelection = {
550
+ asset: "USDC" | "WETH";
551
+ protocolId: string;
552
+ chainId: number;
553
+ /** Names enabled by the user for this asset/protocol/chain. */
554
+ pools: string[];
555
+ /** Every name shown for this asset/protocol/chain in the superset catalog. */
556
+ availablePools: string[];
557
+ };
558
+ /** The complete saved draft for the asset/chain legs selected in the deposit. */
559
+ type UserPoolSelectionRequest = {
560
+ strategies: Array<{
561
+ asset: "USDC" | "WETH";
562
+ strategy: PoolSelectionStrategy;
563
+ }>;
564
+ selections: UserPoolSelection[];
565
+ };
566
+ /** One protocol with its pools for the deposit asset — what the Pools step
567
+ * renders: a protocol row that opens to a list of selectable pools. */
568
+ type PoolCatalogProtocol = {
569
+ protocolId: string;
570
+ name: string;
571
+ logo: string | null;
572
+ pools: AvailablePool[];
573
+ };
574
+
479
575
  interface OwneyDepositResult {
480
576
  txHash: string;
481
577
  smartWallet: string;
@@ -606,6 +702,28 @@ interface ApyHistoryPoint {
606
702
  unit: string;
607
703
  };
608
704
  }
705
+ /**
706
+ * What the APY chart's chain/asset menus are allowed to offer, and how far
707
+ * back each option reaches (ROUT-479).
708
+ *
709
+ * An option only earns a place if some day in the window held a real position
710
+ * on it — a balance and a rate. Listing anything else would offer a filter
711
+ * that draws a blank chart. `days` is what the menu prints beside each row,
712
+ * and the legs genuinely differ: a wallet can hold USDC for a fortnight and
713
+ * WETH for three days.
714
+ */
715
+ interface ApyCoverage {
716
+ chains: {
717
+ chainId: number;
718
+ days: number;
719
+ }[];
720
+ /** Legs whose `tokenSymbol` the provider left unset are absent — they cannot
721
+ * be named, so they cannot be picked. Their chain still counts. */
722
+ assets: {
723
+ asset: string;
724
+ days: number;
725
+ }[];
726
+ }
609
727
  interface AccountAgentApy {
610
728
  walletAddress: string;
611
729
  weightedApyAfterFee?: number;
@@ -616,6 +734,8 @@ interface AccountAgentApy {
616
734
  * date. Empty when the provider returns no daily history.
617
735
  */
618
736
  history?: ApyHistoryPoint[];
737
+ /** Which chains/assets this history can be filtered down to. */
738
+ coverage?: ApyCoverage;
619
739
  }
620
740
  interface OwneyAccountApy {
621
741
  totalApy: string;
@@ -779,6 +899,21 @@ interface IAgent {
779
899
  * unchanged rather than break the connect.
780
900
  */
781
901
  applyOrgPolicy?(state: ConnectionState, chainId: number, policy: OrgAgentConfig): Promise<void>;
902
+ /**
903
+ * The deposit-time pool catalogue for one asset and strategy: protocols with
904
+ * their selectable pools and figures. Optional — an agent with no pool
905
+ * customization omits it. (ROUT-569)
906
+ */
907
+ getPoolCatalog?(params: {
908
+ asset: string;
909
+ strategy: "conservative" | "aggressive" | "yieldmaxxing";
910
+ }): Promise<PoolCatalogProtocol[]>;
911
+ /**
912
+ * Apply a user's own pool selection to their account, driven by the
913
+ * deposit-time Pools step. Optional, and non-fatal like `applyOrgPolicy`.
914
+ * (ROUT-569)
915
+ */
916
+ setPoolSelection?(selection: UserPoolSelectionRequest): Promise<void>;
782
917
  deposit(state: ConnectionState, chainId: number, amount: string, asset: OwneySupportedTokens, depositCallback?: DepositCallback): Promise<OwneyDepositResult>;
783
918
  withdraw(state: ConnectionState, chainId: number, token: OwneySupportedTokens, amount?: string): Promise<AgentWithdrawResult>;
784
919
  getBalances(state: ConnectionState, chainId: number): Promise<AgentBalance>;
@@ -793,7 +928,14 @@ interface IAgent {
793
928
  * Optional asset symbol ("USDC" / "WETH") scoping the daily series to a
794
929
  * single asset on the chain. Agents without per-asset positions ignore it.
795
930
  */
796
- tokenSymbol?: string): Promise<AccountAgentApy>;
931
+ tokenSymbol?: string,
932
+ /**
933
+ * Chain the series is scoped to, when that differs from the chain the
934
+ * connection is on. `null` spans every chain the wallet held (ROUT-479);
935
+ * omitted means `chainId`. Kept apart from `chainId` because that one
936
+ * still resolves the wallet and must stay a real, connected chain.
937
+ */
938
+ scopeChainId?: number | null): Promise<AccountAgentApy>;
797
939
  /**
798
940
  * Daily cumulative NET earnings for the window, scoped to a chain/asset.
799
941
  * Backs the "recent earnings" subline, whose figure must reconcile with the
@@ -822,7 +964,7 @@ type AgentApyOptions = {
822
964
  chainId?: number;
823
965
  };
824
966
 
825
- type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "WALLET_NOT_DEPLOYED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "AGENT_DISABLED" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "CHAIN_MISMATCH" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_BALANCE_UNAVAILABLE" | "DEPOSIT_PARTIAL_FAILURE" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "DEPOSIT_INSUFFICIENT_BALANCE" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_NOTHING_TO_WITHDRAW" | "WITHDRAW_PARTIAL_FAILURE" | "WITHDRAW_FAILED" | "AGENT_RATE_LIMITED" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_NO_AGENTS" | "SPONSOR_REQUEST_FAILED" | "PERMIT2_APPROVAL_REQUIRED" | "SPONSORED_CALLS_UNSUPPORTED" | "SPONSORED_CALLS_NO_ID" | "SPONSORED_CALLS_NO_RECEIPT" | "SWAP_DISABLED" | "SWAP_RATE_LIMITED" | "SWAP_REQUEST_FAILED" | "SWAP_QUOTE_FAILED" | "SWAP_UNSUPPORTED_PAIR" | "SWAP_APPROVAL_REQUIRED" | "SWAP_BELOW_DEPOSIT_MINIMUM" | "SWAP_DEPOSIT_PENDING" | "SWAP_WALLET_PROMPT_TIMEOUT" | "SWAP_INSUFFICIENT_GAS" | "SWAP_NATIVE_UNSUPPORTED" | "SWAP_ORDER_EXPIRED" | "SWAP_ORDER_REFUNDED" | "SWAP_ORDER_CANCELLED" | "WITHDRAW_ARRIVAL_TIMEOUT" | "WITHDRAW_SWAP_FAILED" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "VALIDATION_INVALID_DAYS";
967
+ type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "WALLET_NOT_DEPLOYED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "AGENT_DISABLED" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "CHAIN_MISMATCH" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_BALANCE_UNAVAILABLE" | "DEPOSIT_PARTIAL_FAILURE" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "DEPOSIT_INSUFFICIENT_BALANCE" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_NOTHING_TO_WITHDRAW" | "WITHDRAW_PARTIAL_FAILURE" | "WITHDRAW_FAILED" | "AGENT_RATE_LIMITED" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_NO_AGENTS" | "SPONSOR_REQUEST_FAILED" | "PERMIT2_APPROVAL_REQUIRED" | "SPONSORED_CALLS_UNSUPPORTED" | "SPONSORED_CALLS_NO_ID" | "SPONSORED_CALLS_NO_RECEIPT" | "SWAP_DISABLED" | "SWAP_RATE_LIMITED" | "SWAP_REQUEST_FAILED" | "SWAP_QUOTE_FAILED" | "SWAP_UNSUPPORTED_PAIR" | "SWAP_APPROVAL_REQUIRED" | "SWAP_BELOW_DEPOSIT_MINIMUM" | "SWAP_DEPOSIT_PENDING" | "SWAP_WALLET_PROMPT_TIMEOUT" | "SWAP_INSUFFICIENT_GAS" | "SWAP_NATIVE_UNSUPPORTED" | "SWAP_ORDER_EXPIRED" | "SWAP_ORDER_REFUNDED" | "SWAP_ORDER_CANCELLED" | "WITHDRAW_ARRIVAL_TIMEOUT" | "WITHDRAW_SWAP_FAILED" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "POOL_CATALOG_UNSUPPORTED" | "POOL_SELECTION_UNSUPPORTED" | "VALIDATION_INVALID_DAYS";
826
968
  declare class OwneyError extends Error {
827
969
  readonly code: OwneyErrorCode;
828
970
  readonly details?: Record<string, unknown>;
@@ -986,6 +1128,7 @@ declare class OwneySDK {
986
1128
  private orgAgentConfig;
987
1129
  private orgAgentConfigPromise;
988
1130
  private zyfaiRpcUrls?;
1131
+ private userPoolCustomization;
989
1132
  private routingApiBaseUrl?;
990
1133
  private referralSource?;
991
1134
  private cachedSponsoredCallback;
@@ -1596,7 +1739,26 @@ declare class OwneySDK {
1596
1739
  * agents that answered.
1597
1740
  */
1598
1741
  getDailyEarnings({ agentId, days, tokenSymbol, chainIds, }: DailyEarningsOptions): Promise<AccountDailyEarnings>;
1599
- getAccountApy({ agentId, days, tokenSymbol, }: AccountApyOptions): Promise<OwneyAccountApy | AccountAgentApy>;
1742
+ /**
1743
+ * The deposit-time pool catalogue (ROUT-569): every protocol with its
1744
+ * selectable pools (name, APY, TVL, link) for `asset` under `strategy`.
1745
+ * Defaults to the zyfai agent, the only one with pool customization today.
1746
+ */
1747
+ getPoolCatalog(params: {
1748
+ asset: OwneySupportedTokens;
1749
+ strategy: "conservative" | "aggressive" | "yieldmaxxing";
1750
+ agentId?: AgentId;
1751
+ }): Promise<PoolCatalogProtocol[]>;
1752
+ /**
1753
+ * Apply a user's deposit-time pool selection to their account (ROUT-569).
1754
+ * The request carries explicit pool choices plus the resulting strategy for
1755
+ * each asset reviewed in the deposit flow.
1756
+ */
1757
+ setPoolSelection(params: {
1758
+ selection: UserPoolSelectionRequest;
1759
+ agentId?: AgentId;
1760
+ }): Promise<void>;
1761
+ getAccountApy({ agentId, days, tokenSymbol, chainId: scope, }: AccountApyOptions): Promise<OwneyAccountApy | AccountAgentApy>;
1600
1762
  /**
1601
1763
  * Get transaction history for a specific agent, or all agents.
1602
1764
  *
@@ -1738,4 +1900,4 @@ type StoredOrder = {
1738
1900
  */
1739
1901
  declare function listOrders(now?: number): StoredOrder[];
1740
1902
 
1741
- export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawAllLeg, type OwneyWithdrawAllResult, type OwneyWithdrawResult, type PendingDeposit, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
1903
+ export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyCoverage, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type AvailablePool, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OrgPoolPolicy, type OrgPoolSelection, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawAllLeg, type OwneyWithdrawAllResult, type OwneyWithdrawResult, type PendingDeposit, type PoolCatalogProtocol, type PoolSelectionStrategy, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type UserPoolSelection, type UserPoolSelectionRequest, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
package/dist/index.d.ts CHANGED
@@ -276,6 +276,15 @@ interface OwneySDKConfig {
276
276
  * already-deployed app: set `globalThis.__OWNEY_DEBUG__ = true` in devtools.
277
277
  */
278
278
  debug?: boolean;
279
+ /**
280
+ * When true, the organization's pool policy is NOT applied on connect, so a
281
+ * user's own deposit-time pool selection (ROUT-569) is never overwritten by
282
+ * the org-level one on their next reconnect. Splitting and the rest of the
283
+ * org policy still apply. Turn this on in apps that expose the deposit-time
284
+ * Pools step (the spread flow); leave it off where the partner dashboard's
285
+ * pool customization is the source of truth.
286
+ */
287
+ userPoolCustomization?: boolean;
279
288
  /**
280
289
  * Absolute URL of an ERC-7677 paymaster service. When set, deposits go out as
281
290
  * an EIP-5792 `wallet_sendCalls` with `capabilities.paymasterService`, so a
@@ -374,6 +383,18 @@ type WithdrawOptions = {
374
383
  type AccountApyOptions = {
375
384
  agentId?: AgentId;
376
385
  days: LookbackDays;
386
+ /**
387
+ * Chain to scope the daily series to, instead of the one `activateAgent`
388
+ * last set (ROUT-479).
389
+ *
390
+ * `"all"` blends every chain the wallet held that day, which is what the
391
+ * chart's "All chains" filter asks for. Unlike the history and earnings
392
+ * reads this needs no fan-out: the provider's daily-APY payload is already
393
+ * wallet-wide and names each position's chain, so the scope is a filter over
394
+ * one response rather than several calls. Omit for the active chain, which
395
+ * stays the default.
396
+ */
397
+ chainId?: OwneySupportedChainId | "all";
377
398
  /**
378
399
  * Optional asset symbol (e.g. "USDC", "WETH") to scope the daily APY series
379
400
  * to a specific asset on the active chain. Without it the series blends every
@@ -476,6 +497,81 @@ type OrgAgentConfig = {
476
497
  poolPolicy?: OrgPoolPolicy | null;
477
498
  };
478
499
 
500
+ /**
501
+ * Pool policy: Owney's org-level protocol/pool policy -> Zyfai's two mismatched
502
+ * write shapes.
503
+ *
504
+ * Zyfai splits "which protocols and pools may this user's funds sit in" across
505
+ * two calls that do not share an axis:
506
+ *
507
+ * updateUserProfile({ protocols }) per ASSET, chain-agnostic
508
+ * customizeBatch([{ protocolId, per (PROTOCOL, CHAIN), asset-agnostic
509
+ * chainId, pools, autoselect }])
510
+ *
511
+ * `protocols` is an explicit ALLOW-LIST: a protocol left out of it is disabled
512
+ * for that asset, on every chain, immediately. `customizeBatch` is the opposite
513
+ * shape — one record per (protocol, chain) pair, asset-agnostic, and per
514
+ * docs.zyf.ai it "creates or updates a record for the (user, protocol, chain)
515
+ * combination"; "previous customizations for the same combination are
516
+ * overwritten".
517
+ *
518
+ * That second property is the reason this file exists rather than a single
519
+ * flat translation: customizeBatch overwrites only the combinations it is
520
+ * GIVEN. The partner's stored policy (`OrgPoolPolicy.selections`) is sparse —
521
+ * only the protocols they actually narrowed appear — but the payload we send
522
+ * must be EXHAUSTIVE over every (protocol, chain) pair Zyfai offers this user.
523
+ * If we only sent the narrowed pairs, a partner who restricts Morpho, saves,
524
+ * then later re-ticks every pool box would never be able to undo it: nothing
525
+ * in the (sparse) policy would tell us to send a record clearing it, and the
526
+ * old restrictive record would stay live on the user's profile forever.
527
+ *
528
+ * Pure functions, kept out of the agent so the mapping is testable without a
529
+ * Zyfai client. See splitting.ts for the same pattern on the other lever.
530
+ */
531
+
532
+ /** One selectable pool, with the figures the deposit-time Pools step shows. */
533
+ type AvailablePool = {
534
+ chainId: number;
535
+ token: string;
536
+ name: string;
537
+ /** combined_apy (pool + rewards), already a percent. null when absent. */
538
+ apy: number | null;
539
+ /** TVL in USD. null when absent. See ZyfCustomizationPoolInfo: `tvl` is in
540
+ * the pool's own token, `tvlUsd` is the dollar figure — only the latter is
541
+ * safe to show with a currency symbol. */
542
+ tvlUsd: number | null;
543
+ /** Per-pool deep link (e.g. the Morpho vault page). null when absent. */
544
+ link: string | null;
545
+ };
546
+ /** Public Zyfai strategies used by deposit-time pool customization. */
547
+ type PoolSelectionStrategy = "aggressive" | "yieldmaxxing";
548
+ /** One asset's explicit choice for a protocol on one chain. */
549
+ type UserPoolSelection = {
550
+ asset: "USDC" | "WETH";
551
+ protocolId: string;
552
+ chainId: number;
553
+ /** Names enabled by the user for this asset/protocol/chain. */
554
+ pools: string[];
555
+ /** Every name shown for this asset/protocol/chain in the superset catalog. */
556
+ availablePools: string[];
557
+ };
558
+ /** The complete saved draft for the asset/chain legs selected in the deposit. */
559
+ type UserPoolSelectionRequest = {
560
+ strategies: Array<{
561
+ asset: "USDC" | "WETH";
562
+ strategy: PoolSelectionStrategy;
563
+ }>;
564
+ selections: UserPoolSelection[];
565
+ };
566
+ /** One protocol with its pools for the deposit asset — what the Pools step
567
+ * renders: a protocol row that opens to a list of selectable pools. */
568
+ type PoolCatalogProtocol = {
569
+ protocolId: string;
570
+ name: string;
571
+ logo: string | null;
572
+ pools: AvailablePool[];
573
+ };
574
+
479
575
  interface OwneyDepositResult {
480
576
  txHash: string;
481
577
  smartWallet: string;
@@ -606,6 +702,28 @@ interface ApyHistoryPoint {
606
702
  unit: string;
607
703
  };
608
704
  }
705
+ /**
706
+ * What the APY chart's chain/asset menus are allowed to offer, and how far
707
+ * back each option reaches (ROUT-479).
708
+ *
709
+ * An option only earns a place if some day in the window held a real position
710
+ * on it — a balance and a rate. Listing anything else would offer a filter
711
+ * that draws a blank chart. `days` is what the menu prints beside each row,
712
+ * and the legs genuinely differ: a wallet can hold USDC for a fortnight and
713
+ * WETH for three days.
714
+ */
715
+ interface ApyCoverage {
716
+ chains: {
717
+ chainId: number;
718
+ days: number;
719
+ }[];
720
+ /** Legs whose `tokenSymbol` the provider left unset are absent — they cannot
721
+ * be named, so they cannot be picked. Their chain still counts. */
722
+ assets: {
723
+ asset: string;
724
+ days: number;
725
+ }[];
726
+ }
609
727
  interface AccountAgentApy {
610
728
  walletAddress: string;
611
729
  weightedApyAfterFee?: number;
@@ -616,6 +734,8 @@ interface AccountAgentApy {
616
734
  * date. Empty when the provider returns no daily history.
617
735
  */
618
736
  history?: ApyHistoryPoint[];
737
+ /** Which chains/assets this history can be filtered down to. */
738
+ coverage?: ApyCoverage;
619
739
  }
620
740
  interface OwneyAccountApy {
621
741
  totalApy: string;
@@ -779,6 +899,21 @@ interface IAgent {
779
899
  * unchanged rather than break the connect.
780
900
  */
781
901
  applyOrgPolicy?(state: ConnectionState, chainId: number, policy: OrgAgentConfig): Promise<void>;
902
+ /**
903
+ * The deposit-time pool catalogue for one asset and strategy: protocols with
904
+ * their selectable pools and figures. Optional — an agent with no pool
905
+ * customization omits it. (ROUT-569)
906
+ */
907
+ getPoolCatalog?(params: {
908
+ asset: string;
909
+ strategy: "conservative" | "aggressive" | "yieldmaxxing";
910
+ }): Promise<PoolCatalogProtocol[]>;
911
+ /**
912
+ * Apply a user's own pool selection to their account, driven by the
913
+ * deposit-time Pools step. Optional, and non-fatal like `applyOrgPolicy`.
914
+ * (ROUT-569)
915
+ */
916
+ setPoolSelection?(selection: UserPoolSelectionRequest): Promise<void>;
782
917
  deposit(state: ConnectionState, chainId: number, amount: string, asset: OwneySupportedTokens, depositCallback?: DepositCallback): Promise<OwneyDepositResult>;
783
918
  withdraw(state: ConnectionState, chainId: number, token: OwneySupportedTokens, amount?: string): Promise<AgentWithdrawResult>;
784
919
  getBalances(state: ConnectionState, chainId: number): Promise<AgentBalance>;
@@ -793,7 +928,14 @@ interface IAgent {
793
928
  * Optional asset symbol ("USDC" / "WETH") scoping the daily series to a
794
929
  * single asset on the chain. Agents without per-asset positions ignore it.
795
930
  */
796
- tokenSymbol?: string): Promise<AccountAgentApy>;
931
+ tokenSymbol?: string,
932
+ /**
933
+ * Chain the series is scoped to, when that differs from the chain the
934
+ * connection is on. `null` spans every chain the wallet held (ROUT-479);
935
+ * omitted means `chainId`. Kept apart from `chainId` because that one
936
+ * still resolves the wallet and must stay a real, connected chain.
937
+ */
938
+ scopeChainId?: number | null): Promise<AccountAgentApy>;
797
939
  /**
798
940
  * Daily cumulative NET earnings for the window, scoped to a chain/asset.
799
941
  * Backs the "recent earnings" subline, whose figure must reconcile with the
@@ -822,7 +964,7 @@ type AgentApyOptions = {
822
964
  chainId?: number;
823
965
  };
824
966
 
825
- type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "WALLET_NOT_DEPLOYED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "AGENT_DISABLED" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "CHAIN_MISMATCH" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_BALANCE_UNAVAILABLE" | "DEPOSIT_PARTIAL_FAILURE" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "DEPOSIT_INSUFFICIENT_BALANCE" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_NOTHING_TO_WITHDRAW" | "WITHDRAW_PARTIAL_FAILURE" | "WITHDRAW_FAILED" | "AGENT_RATE_LIMITED" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_NO_AGENTS" | "SPONSOR_REQUEST_FAILED" | "PERMIT2_APPROVAL_REQUIRED" | "SPONSORED_CALLS_UNSUPPORTED" | "SPONSORED_CALLS_NO_ID" | "SPONSORED_CALLS_NO_RECEIPT" | "SWAP_DISABLED" | "SWAP_RATE_LIMITED" | "SWAP_REQUEST_FAILED" | "SWAP_QUOTE_FAILED" | "SWAP_UNSUPPORTED_PAIR" | "SWAP_APPROVAL_REQUIRED" | "SWAP_BELOW_DEPOSIT_MINIMUM" | "SWAP_DEPOSIT_PENDING" | "SWAP_WALLET_PROMPT_TIMEOUT" | "SWAP_INSUFFICIENT_GAS" | "SWAP_NATIVE_UNSUPPORTED" | "SWAP_ORDER_EXPIRED" | "SWAP_ORDER_REFUNDED" | "SWAP_ORDER_CANCELLED" | "WITHDRAW_ARRIVAL_TIMEOUT" | "WITHDRAW_SWAP_FAILED" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "VALIDATION_INVALID_DAYS";
967
+ type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "WALLET_NOT_DEPLOYED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "AGENT_DISABLED" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "CHAIN_MISMATCH" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_BALANCE_UNAVAILABLE" | "DEPOSIT_PARTIAL_FAILURE" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "DEPOSIT_INSUFFICIENT_BALANCE" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_NOTHING_TO_WITHDRAW" | "WITHDRAW_PARTIAL_FAILURE" | "WITHDRAW_FAILED" | "AGENT_RATE_LIMITED" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_NO_AGENTS" | "SPONSOR_REQUEST_FAILED" | "PERMIT2_APPROVAL_REQUIRED" | "SPONSORED_CALLS_UNSUPPORTED" | "SPONSORED_CALLS_NO_ID" | "SPONSORED_CALLS_NO_RECEIPT" | "SWAP_DISABLED" | "SWAP_RATE_LIMITED" | "SWAP_REQUEST_FAILED" | "SWAP_QUOTE_FAILED" | "SWAP_UNSUPPORTED_PAIR" | "SWAP_APPROVAL_REQUIRED" | "SWAP_BELOW_DEPOSIT_MINIMUM" | "SWAP_DEPOSIT_PENDING" | "SWAP_WALLET_PROMPT_TIMEOUT" | "SWAP_INSUFFICIENT_GAS" | "SWAP_NATIVE_UNSUPPORTED" | "SWAP_ORDER_EXPIRED" | "SWAP_ORDER_REFUNDED" | "SWAP_ORDER_CANCELLED" | "WITHDRAW_ARRIVAL_TIMEOUT" | "WITHDRAW_SWAP_FAILED" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "POOL_CATALOG_UNSUPPORTED" | "POOL_SELECTION_UNSUPPORTED" | "VALIDATION_INVALID_DAYS";
826
968
  declare class OwneyError extends Error {
827
969
  readonly code: OwneyErrorCode;
828
970
  readonly details?: Record<string, unknown>;
@@ -986,6 +1128,7 @@ declare class OwneySDK {
986
1128
  private orgAgentConfig;
987
1129
  private orgAgentConfigPromise;
988
1130
  private zyfaiRpcUrls?;
1131
+ private userPoolCustomization;
989
1132
  private routingApiBaseUrl?;
990
1133
  private referralSource?;
991
1134
  private cachedSponsoredCallback;
@@ -1596,7 +1739,26 @@ declare class OwneySDK {
1596
1739
  * agents that answered.
1597
1740
  */
1598
1741
  getDailyEarnings({ agentId, days, tokenSymbol, chainIds, }: DailyEarningsOptions): Promise<AccountDailyEarnings>;
1599
- getAccountApy({ agentId, days, tokenSymbol, }: AccountApyOptions): Promise<OwneyAccountApy | AccountAgentApy>;
1742
+ /**
1743
+ * The deposit-time pool catalogue (ROUT-569): every protocol with its
1744
+ * selectable pools (name, APY, TVL, link) for `asset` under `strategy`.
1745
+ * Defaults to the zyfai agent, the only one with pool customization today.
1746
+ */
1747
+ getPoolCatalog(params: {
1748
+ asset: OwneySupportedTokens;
1749
+ strategy: "conservative" | "aggressive" | "yieldmaxxing";
1750
+ agentId?: AgentId;
1751
+ }): Promise<PoolCatalogProtocol[]>;
1752
+ /**
1753
+ * Apply a user's deposit-time pool selection to their account (ROUT-569).
1754
+ * The request carries explicit pool choices plus the resulting strategy for
1755
+ * each asset reviewed in the deposit flow.
1756
+ */
1757
+ setPoolSelection(params: {
1758
+ selection: UserPoolSelectionRequest;
1759
+ agentId?: AgentId;
1760
+ }): Promise<void>;
1761
+ getAccountApy({ agentId, days, tokenSymbol, chainId: scope, }: AccountApyOptions): Promise<OwneyAccountApy | AccountAgentApy>;
1600
1762
  /**
1601
1763
  * Get transaction history for a specific agent, or all agents.
1602
1764
  *
@@ -1738,4 +1900,4 @@ type StoredOrder = {
1738
1900
  */
1739
1901
  declare function listOrders(now?: number): StoredOrder[];
1740
1902
 
1741
- export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawAllLeg, type OwneyWithdrawAllResult, type OwneyWithdrawResult, type PendingDeposit, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
1903
+ export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyCoverage, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type AvailablePool, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OrgPoolPolicy, type OrgPoolSelection, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawAllLeg, type OwneyWithdrawAllResult, type OwneyWithdrawResult, type PendingDeposit, type PoolCatalogProtocol, type PoolSelectionStrategy, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type UserPoolSelection, type UserPoolSelectionRequest, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
package/dist/index.js CHANGED
@@ -470,7 +470,7 @@ function rawPoolApyForChain(entry, chainId, tokenSymbol) {
470
470
  let weightedSum = 0;
471
471
  let totalBalance = 0;
472
472
  for (const p of entry.positions ?? []) {
473
- if (p.chainId !== chainId) continue;
473
+ if (chainId !== void 0 && p.chainId !== chainId) continue;
474
474
  if (tokenSymbol && p.tokenSymbol?.toUpperCase() !== tokenSymbol.toUpperCase()) continue;
475
475
  const balance = p.balance ?? 0;
476
476
  const apy = netApy(p.apy_withFee, p.apy, "apy_withFee");
@@ -480,6 +480,28 @@ function rawPoolApyForChain(entry, chainId, tokenSymbol) {
480
480
  }
481
481
  return totalBalance > 0 ? weightedSum / totalBalance : null;
482
482
  }
483
+ function historyCoverage(raw) {
484
+ const chains = /* @__PURE__ */ new Map();
485
+ const assets = /* @__PURE__ */ new Map();
486
+ for (const date of Object.keys(raw.history ?? {}).sort()) {
487
+ void date;
488
+ const seenChains = /* @__PURE__ */ new Set();
489
+ const seenAssets = /* @__PURE__ */ new Set();
490
+ for (const p of (raw.history ?? {})[date].positions ?? []) {
491
+ const balance = p.balance ?? 0;
492
+ const apy = netApy(p.apy_withFee, p.apy, "apy_withFee");
493
+ if (balance <= 0 || typeof apy !== "number") continue;
494
+ seenChains.add(p.chainId);
495
+ if (p.tokenSymbol) seenAssets.add(p.tokenSymbol.toUpperCase());
496
+ }
497
+ for (const c of seenChains) chains.set(c, (chains.get(c) ?? 0) + 1);
498
+ for (const a of seenAssets) assets.set(a, (assets.get(a) ?? 0) + 1);
499
+ }
500
+ return {
501
+ chains: [...chains].map(([chainId, days]) => ({ chainId, days })),
502
+ assets: [...assets].map(([asset, days]) => ({ asset, days }))
503
+ };
504
+ }
483
505
  function mapApyHistory(raw, chainId, tokenSymbol) {
484
506
  const history = Object.entries(raw.history ?? {}).map(([date, entry]) => ({
485
507
  date,
@@ -490,7 +512,7 @@ function mapApyHistory(raw, chainId, tokenSymbol) {
490
512
  ...tokenSymbol ? {
491
513
  historicalBalance: {
492
514
  amount: (entry.positions ?? []).filter(
493
- (p) => p.chainId === chainId && p.tokenSymbol?.toUpperCase() === tokenSymbol.toUpperCase() && Number.isFinite(p.balance) && p.balance > 0 && Number.isFinite(p.apy_withFee ?? p.apy)
515
+ (p) => (chainId === void 0 || p.chainId === chainId) && p.tokenSymbol?.toUpperCase() === tokenSymbol.toUpperCase() && Number.isFinite(p.balance) && p.balance > 0 && Number.isFinite(p.apy_withFee ?? p.apy)
494
516
  ).reduce((sum, p) => sum + p.balance, 0),
495
517
  unit: tokenSymbol.toUpperCase()
496
518
  }
@@ -500,7 +522,8 @@ function mapApyHistory(raw, chainId, tokenSymbol) {
500
522
  walletAddress: raw.walletAddress,
501
523
  weightedApyAfterFee: raw.weightedApyAfterFee ? sumTokenValues(raw.weightedApyAfterFee) : void 0,
502
524
  apyByChainAndAsset: mapWeightedApyByChain(raw.weightedApyAfterFeeByChain),
503
- history
525
+ history,
526
+ coverage: historyCoverage(raw)
504
527
  };
505
528
  }
506
529
  function splitOpportunity(opportunity) {
@@ -959,6 +982,9 @@ function splittingNeedsUpdate(current, desired) {
959
982
  }
960
983
 
961
984
  // src/agents/zyfai/pool-policy.ts
985
+ function orgPolicyForConnect(policy, userPoolCustomization) {
986
+ return userPoolCustomization ? { ...policy, poolPolicy: null } : policy;
987
+ }
962
988
  function toZyfaiCustomizations(policy, availableByProtocol, chainsByProtocol) {
963
989
  const selectionByKey = /* @__PURE__ */ new Map();
964
990
  for (const selection of policy.selections) {
@@ -1050,6 +1076,54 @@ function protocolsPolicyNeedsUpdate(current, desiredProtocols, desiredAutoSelect
1050
1076
  if (desiredProtocols === null) return false;
1051
1077
  return !protocolListsEqual(current.protocols, desiredProtocols);
1052
1078
  }
1079
+ function mergeScopedPoolNames(current, scoped, allAvailableNames) {
1080
+ const managed = new Set(scoped.flatMap((entry) => entry.availablePools));
1081
+ const existing = current.autoselect ? allAvailableNames : current.pools;
1082
+ const preserved = existing.filter((name) => !managed.has(name));
1083
+ const selected = scoped.flatMap((entry) => entry.pools);
1084
+ return [.../* @__PURE__ */ new Set([...preserved, ...selected])];
1085
+ }
1086
+ function mapAvailablePools(raw) {
1087
+ const byChain = raw ?? {};
1088
+ if (typeof byChain !== "object" || byChain === null) return [];
1089
+ const result = [];
1090
+ for (const [chainIdStr, byToken] of Object.entries(byChain)) {
1091
+ const chainId = Number(chainIdStr);
1092
+ if (!Number.isFinite(chainId)) continue;
1093
+ for (const [token, entry] of Object.entries(byToken ?? {})) {
1094
+ for (const pool of entry?.pools ?? []) {
1095
+ if (!pool?.name) continue;
1096
+ result.push({
1097
+ chainId,
1098
+ token,
1099
+ name: pool.name,
1100
+ apy: pool.info?.combined_apy ?? null,
1101
+ tvlUsd: pool.info?.tvlUsd ?? null,
1102
+ link: pool.info?.link ?? null
1103
+ });
1104
+ }
1105
+ }
1106
+ }
1107
+ return result;
1108
+ }
1109
+ function assemblePoolCatalog(protocols, poolsByProtocol, asset, chains) {
1110
+ const wanted = asset.toUpperCase();
1111
+ const allowedChains = chains && chains.length > 0 ? new Set(chains) : void 0;
1112
+ const catalog = [];
1113
+ for (const protocol of protocols) {
1114
+ const pools = (poolsByProtocol.get(protocol.id) ?? []).filter(
1115
+ (p) => p.token.toUpperCase() === wanted && (allowedChains === void 0 || allowedChains.has(p.chainId))
1116
+ );
1117
+ if (pools.length === 0) continue;
1118
+ catalog.push({
1119
+ protocolId: protocol.id,
1120
+ name: protocol.name,
1121
+ logo: protocol.imageUrl ?? null,
1122
+ pools
1123
+ });
1124
+ }
1125
+ return catalog;
1126
+ }
1053
1127
 
1054
1128
  // src/agents/zyfai/zyfai.agent.ts
1055
1129
  var ERC7579_IS_MODULE_INSTALLED_ABI = parseAbi([
@@ -1476,7 +1550,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1476
1550
  * connecting, so a Zyfai hiccup must leave the user connected with their
1477
1551
  * profile unchanged, never fail activation.
1478
1552
  */
1479
- async applyPoolPolicy(policy) {
1553
+ async applyPoolPolicy(policy, opts = {}) {
1480
1554
  try {
1481
1555
  const { protocols: universe } = await this.sdk.getAvailableProtocols(
1482
1556
  this.supportedChainIds[0]
@@ -1490,7 +1564,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1490
1564
  universeIds,
1491
1565
  chainsByProtocol
1492
1566
  });
1493
- let strategy;
1567
+ let strategy = opts.strategy;
1494
1568
  const desiredProtocols = deriveProtocols(
1495
1569
  policy,
1496
1570
  universeIds,
@@ -1570,6 +1644,109 @@ var ZyfaiAgent = class _ZyfaiAgent {
1570
1644
  );
1571
1645
  }
1572
1646
  }
1647
+ /**
1648
+ * The deposit-time Pools step's catalogue: every protocol Zyfai offers this
1649
+ * user, each with the pools (name, APY, TVL, link) selectable for `asset`
1650
+ * under `strategy`. Sourced from the same two Zyfai calls `applyPoolPolicy`
1651
+ * uses, so "shown" and "selectable" are one set. A protocol whose pool
1652
+ * lookup fails is skipped rather than failing the whole catalogue.
1653
+ */
1654
+ async getPoolCatalog(params) {
1655
+ const { protocols: universe } = await this.sdk.getAvailableProtocols(
1656
+ this.supportedChainIds[0]
1657
+ );
1658
+ const poolResults = await Promise.all(
1659
+ universe.map(async (protocol) => {
1660
+ try {
1661
+ const raw = await this.sdk.getAvailablePools(
1662
+ protocol.id,
1663
+ params.strategy
1664
+ );
1665
+ return [protocol.id, mapAvailablePools(raw)];
1666
+ } catch (error) {
1667
+ console.warn(
1668
+ `[zyfai] getPoolCatalog: getAvailablePools(${protocol.id}) failed, skipping (non-fatal):`,
1669
+ error instanceof Error ? error.message : String(error)
1670
+ );
1671
+ return null;
1672
+ }
1673
+ })
1674
+ );
1675
+ const poolsByProtocol = /* @__PURE__ */ new Map();
1676
+ for (const result of poolResults) {
1677
+ if (result) poolsByProtocol.set(result[0], result[1]);
1678
+ }
1679
+ return assemblePoolCatalog(universe, poolsByProtocol, params.asset);
1680
+ }
1681
+ /**
1682
+ * Persist an explicitly saved deposit-time draft at Zyfai profile level.
1683
+ * Pool choices are merged into protocol/chain records and each active asset
1684
+ * is kept at aggressive unless the draft includes a yieldmaxxing-only pool.
1685
+ */
1686
+ async setPoolSelection(selection) {
1687
+ try {
1688
+ const byPair = /* @__PURE__ */ new Map();
1689
+ for (const entry of selection.selections) {
1690
+ const pair = `${entry.protocolId}:${entry.chainId}`;
1691
+ const group = byPair.get(pair) ?? [];
1692
+ group.push(entry);
1693
+ byPair.set(pair, group);
1694
+ }
1695
+ const customizations = (await Promise.all(
1696
+ [...byPair.values()].map(async (scoped) => {
1697
+ const { protocolId, chainId } = scoped[0];
1698
+ try {
1699
+ const [current, rawAvailable] = await Promise.all([
1700
+ this.sdk.getSelectedPools(protocolId, chainId),
1701
+ this.sdk.getAvailablePools(protocolId, "yieldmaxxing")
1702
+ ]);
1703
+ const allAvailableNames = mapAvailablePools(rawAvailable).filter((pool) => pool.chainId === chainId).map((pool) => pool.name);
1704
+ return {
1705
+ protocolId,
1706
+ chainId,
1707
+ pools: mergeScopedPoolNames(
1708
+ {
1709
+ pools: current.pools ?? [],
1710
+ autoselect: current.autoselect
1711
+ },
1712
+ scoped,
1713
+ allAvailableNames
1714
+ ),
1715
+ // An explicit Save freezes exactly the choices the user saw.
1716
+ // This also lets an empty list disable the pair deliberately.
1717
+ autoselect: false
1718
+ };
1719
+ } catch (error) {
1720
+ console.warn(
1721
+ `[zyfai] setPoolSelection: preserving ${protocolId}/${chainId} failed; skipping the pair:`,
1722
+ error instanceof Error ? error.message : String(error)
1723
+ );
1724
+ return null;
1725
+ }
1726
+ })
1727
+ )).filter((value) => value !== null);
1728
+ if (customizations.length > 0) {
1729
+ await this.sdk.customizeBatch(customizations);
1730
+ }
1731
+ await Promise.all(
1732
+ selection.strategies.map(async ({ asset, strategy }) => {
1733
+ const current = await this.sdk.getUserDetails(asset);
1734
+ const selectedProtocols = selection.selections.filter(
1735
+ (entry) => entry.asset === asset && entry.pools.length > 0
1736
+ ).map((entry) => entry.protocolId);
1737
+ const protocols = [
1738
+ .../* @__PURE__ */ new Set([...current.protocols ?? [], ...selectedProtocols])
1739
+ ];
1740
+ await this.sdk.updateUserProfile({ asset, strategy, protocols });
1741
+ })
1742
+ );
1743
+ } catch (error) {
1744
+ console.warn(
1745
+ "[zyfai] setPoolSelection failed (non-fatal):",
1746
+ error instanceof Error ? error.message : String(error)
1747
+ );
1748
+ }
1749
+ }
1573
1750
  /**
1574
1751
  * True when `smartWallet` is the backend-managed pool wallet Zyfai assigned
1575
1752
  * to this EOA — the "new wallet" kind that is predeployed on the chains it
@@ -1696,11 +1873,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1696
1873
  );
1697
1874
  return;
1698
1875
  }
1699
- await this.ensureSessionKey(
1700
- ownerAddress,
1701
- chainId,
1702
- wallet.address
1703
- );
1876
+ await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
1704
1877
  } catch (error) {
1705
1878
  console.warn(
1706
1879
  "[zyfai] session-key check at activation failed (non-fatal):",
@@ -1811,11 +1984,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1811
1984
  "zyfai:onboard",
1812
1985
  "Safe already has code -> no deploy needed, checking the session key"
1813
1986
  );
1814
- await this.ensureSessionKey(
1815
- ownerAddress,
1816
- chainId,
1817
- wallet.address
1818
- );
1987
+ await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
1819
1988
  return wallet.address;
1820
1989
  }
1821
1990
  const poolFirst = await this.isPredeployedPoolWallet(
@@ -1982,14 +2151,17 @@ var ZyfaiAgent = class _ZyfaiAgent {
1982
2151
  this.earningsRefreshes.set(key2, pending);
1983
2152
  return pending;
1984
2153
  }
1985
- async getAccountApy(state, chainId, days, tokenSymbol) {
2154
+ async getAccountApy(state, chainId, days, tokenSymbol, scopeChainId) {
1986
2155
  const { smartWallet } = await this.resolveSmartWallet(state, chainId);
1987
2156
  const raw = await this.sdk.getDailyApyHistory(smartWallet, days);
1988
- return mapApyHistory(raw, chainId, tokenSymbol);
2157
+ const scope = scopeChainId === null ? void 0 : scopeChainId ?? chainId;
2158
+ return mapApyHistory(raw, scope, tokenSymbol);
1989
2159
  }
1990
2160
  async getDailyEarnings(state, chainId, days, tokenSymbol) {
1991
2161
  const { smartWallet } = await this.resolveSmartWallet(state, chainId);
1992
- const start = new Date(Date.now() - (DayFilterMapping[days] + 1) * 864e5);
2162
+ const start = new Date(
2163
+ Date.now() - (DayFilterMapping[days] + 1) * 864e5
2164
+ );
1993
2165
  const raw = await this.sdk.getDailyEarnings(
1994
2166
  smartWallet,
1995
2167
  start.toISOString().slice(0, 10)
@@ -4308,6 +4480,7 @@ var OwneySDK = class {
4308
4480
  orgAgentConfig;
4309
4481
  orgAgentConfigPromise = null;
4310
4482
  zyfaiRpcUrls;
4483
+ userPoolCustomization = false;
4311
4484
  routingApiBaseUrl;
4312
4485
  referralSource;
4313
4486
  cachedSponsoredCallback = null;
@@ -4333,6 +4506,7 @@ var OwneySDK = class {
4333
4506
  this.routingApiBaseUrl = config.routingApiBaseUrl;
4334
4507
  this.paymasterServiceUrl = config.paymasterServiceUrl;
4335
4508
  this.referralSource = config.referralSource;
4509
+ this.userPoolCustomization = config.userPoolCustomization ?? false;
4336
4510
  }
4337
4511
  /**
4338
4512
  * Establish connection state for all subsequent SDK calls.
@@ -4559,7 +4733,8 @@ var OwneySDK = class {
4559
4733
  try {
4560
4734
  const policy = await this.getOrgAgentConfig();
4561
4735
  if (!policy) return;
4562
- await agent.applyOrgPolicy(state, chainId, policy);
4736
+ const effective = orgPolicyForConnect(policy, this.userPoolCustomization);
4737
+ await agent.applyOrgPolicy(state, chainId, effective);
4563
4738
  } catch (error) {
4564
4739
  console.warn(
4565
4740
  `[owney-sdk] applyOrgPolicy(${agent.id}) failed (non-fatal):`,
@@ -6333,7 +6508,10 @@ var OwneySDK = class {
6333
6508
  agent,
6334
6509
  "dailyEarnings",
6335
6510
  () => agent.getDailyEarnings(state, legChainId, days, tokenSymbol),
6336
- { days, tokenSymbol }
6511
+ // The leg's chain must be in the key: the read key otherwise names
6512
+ // only the CONNECTION chain, so every leg shared the first leg's
6513
+ // request and the sum counted that chain once per leg. (ROUT-561)
6514
+ { days, tokenSymbol, chainId: legChainId }
6337
6515
  )
6338
6516
  )
6339
6517
  );
@@ -6376,20 +6554,57 @@ var OwneySDK = class {
6376
6554
  chainId
6377
6555
  };
6378
6556
  }
6557
+ /**
6558
+ * The deposit-time pool catalogue (ROUT-569): every protocol with its
6559
+ * selectable pools (name, APY, TVL, link) for `asset` under `strategy`.
6560
+ * Defaults to the zyfai agent, the only one with pool customization today.
6561
+ */
6562
+ async getPoolCatalog(params) {
6563
+ await this.ensureAgentsInitialized();
6564
+ const agent = this.getAgent(params.agentId ?? "zyfai");
6565
+ if (!agent.getPoolCatalog) {
6566
+ throw new OwneyError(
6567
+ "POOL_CATALOG_UNSUPPORTED",
6568
+ `Agent ${agent.id} does not support pool customization`
6569
+ );
6570
+ }
6571
+ return agent.getPoolCatalog({
6572
+ asset: params.asset,
6573
+ strategy: params.strategy
6574
+ });
6575
+ }
6576
+ /**
6577
+ * Apply a user's deposit-time pool selection to their account (ROUT-569).
6578
+ * The request carries explicit pool choices plus the resulting strategy for
6579
+ * each asset reviewed in the deposit flow.
6580
+ */
6581
+ async setPoolSelection(params) {
6582
+ await this.ensureAgentsInitialized();
6583
+ const agent = this.getAgent(params.agentId ?? "zyfai");
6584
+ if (!agent.setPoolSelection) {
6585
+ throw new OwneyError(
6586
+ "POOL_SELECTION_UNSUPPORTED",
6587
+ `Agent ${agent.id} does not support pool customization`
6588
+ );
6589
+ }
6590
+ await agent.setPoolSelection(params.selection);
6591
+ }
6379
6592
  async getAccountApy({
6380
6593
  agentId,
6381
6594
  days,
6382
- tokenSymbol
6595
+ tokenSymbol,
6596
+ chainId: scope
6383
6597
  }) {
6384
6598
  const state = this.requireState();
6385
6599
  const chainId = this.requireChainId();
6600
+ const scopeChainId = scope === "all" ? null : scope;
6386
6601
  if (agentId) {
6387
6602
  const agent = this.getAgent(agentId);
6388
6603
  return this.readAgent(
6389
6604
  agent,
6390
6605
  "accountApy",
6391
- () => agent.getAccountApy(state, chainId, days, tokenSymbol),
6392
- { days, tokenSymbol }
6606
+ () => agent.getAccountApy(state, chainId, days, tokenSymbol, scopeChainId),
6607
+ { days, tokenSymbol, scopeChainId }
6393
6608
  );
6394
6609
  }
6395
6610
  const activeAgents = this.getActiveAgents();
@@ -6400,8 +6615,14 @@ var OwneySDK = class {
6400
6615
  const apy = await this.readAgent(
6401
6616
  agent,
6402
6617
  "accountApy",
6403
- () => agent.getAccountApy(state, chainId, days, tokenSymbol),
6404
- { days, tokenSymbol }
6618
+ () => agent.getAccountApy(
6619
+ state,
6620
+ chainId,
6621
+ days,
6622
+ tokenSymbol,
6623
+ scopeChainId
6624
+ ),
6625
+ { days, tokenSymbol, scopeChainId }
6405
6626
  );
6406
6627
  return [id, apy];
6407
6628
  })
@@ -6490,7 +6711,8 @@ var OwneySDK = class {
6490
6711
  agent,
6491
6712
  "history",
6492
6713
  () => agent.getHistory(state, leg.chainId, legFilters),
6493
- legFilters
6714
+ // Keyed per leg for the same reason as getDailyEarnings' fan-out.
6715
+ { ...legFilters, chainId: leg.chainId }
6494
6716
  );
6495
6717
  return { leg, page };
6496
6718
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.7.28-beta.1",
3
+ "version": "0.7.28-beta.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "test:watch": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@zyfai/sdk": "0.2.55",
26
+ "@zyfai/sdk": "0.2.57",
27
27
  "siwe": "^3.0.0",
28
28
  "viem": "^2.48.1"
29
29
  },