@owney/sdk 0.7.8 → 0.7.10-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,17 @@
1
+ import {
2
+ AgentChainIncompatibleError,
3
+ AgentNotFoundError,
4
+ ERC20_ALLOWANCE_ABI,
5
+ MAX_UINT256,
6
+ NotConnectedError,
7
+ OwneyError,
8
+ PERMIT2_ADDRESS,
9
+ buildPermitTransferFromTypedData,
10
+ randomPermit2Nonce,
11
+ readErc20Balance,
12
+ readPermit2Allowance
13
+ } from "./chunk-PWIKIQOX.js";
14
+
1
15
  // src/agents/zyfai/zyfai.agent.ts
2
16
  import { SMART_SESSIONS_VALIDATOR, ZyfaiSDK } from "@zyfai/sdk";
3
17
  import { createPublicClient, http, parseAbi } from "viem";
@@ -56,55 +70,6 @@ var SupportedAssets = [
56
70
  }
57
71
  ];
58
72
 
59
- // src/errors.ts
60
- var OwneyError = class extends Error {
61
- code;
62
- details;
63
- agentId;
64
- constructor(code, message, details, agentId) {
65
- const prefix = agentId ? `[${code}][agent:${agentId}]` : `[${code}]`;
66
- super(`${prefix} ${message}`);
67
- this.name = "OwneyError";
68
- this.code = code;
69
- this.details = details;
70
- this.agentId = agentId;
71
- }
72
- };
73
- var AgentNotFoundError = class extends OwneyError {
74
- constructor(agentId, available) {
75
- super(
76
- "AGENT_NOT_FOUND",
77
- `Unknown agent "${agentId}". Available agents: ${available.join(", ")}`,
78
- { agentId, available },
79
- agentId
80
- );
81
- this.name = "AgentNotFoundError";
82
- }
83
- };
84
- var NotConnectedError = class extends OwneyError {
85
- constructor() {
86
- super("NOT_CONNECTED", "Not connected. Call sdk.connect(provider) first.");
87
- this.name = "NotConnectedError";
88
- }
89
- };
90
- var AgentChainIncompatibleError = class extends OwneyError {
91
- incompatibleAgents;
92
- connectedChainId;
93
- constructor(incompatibleAgents, connectedChainId) {
94
- const details = incompatibleAgents.map(
95
- ({ agentId, supportedChainIds }) => `"${agentId}" supports chains [${supportedChainIds.join(", ")}]`
96
- ).join("; ");
97
- super(
98
- "AGENT_CHAIN_INCOMPATIBLE",
99
- `Chain ${connectedChainId} is not supported by the following agents: ${details}`,
100
- { incompatibleAgents, connectedChainId }
101
- );
102
- this.name = "AgentChainIncompatibleError";
103
- this.incompatibleAgents = incompatibleAgents;
104
- this.connectedChainId = connectedChainId;
105
- }
106
- };
107
-
108
73
  // src/lib/utils.ts
109
74
  var isValidChainId = (chainId) => {
110
75
  if (!SUPPORTED_CHAIN_IDS.includes(chainId)) {
@@ -116,6 +81,12 @@ var isValidChainId = (chainId) => {
116
81
  }
117
82
  return chainId;
118
83
  };
84
+ function isSameAsset(tokenSymbol, asset) {
85
+ const token = tokenSymbol.toLowerCase();
86
+ const target = asset.toLowerCase();
87
+ if (token === target) return true;
88
+ return target === "weth" && token === "eth" || target === "eth" && token === "weth";
89
+ }
119
90
  function hexToDecimal(hex, decimals = 6) {
120
91
  const normalized = hex.startsWith("0x") || hex.startsWith("0X") ? hex : `0x${hex}`;
121
92
  const parsed = BigInt(normalized);
@@ -149,6 +120,10 @@ var DECIMALS_BY_SYMBOL = {
149
120
  wbtc: 8,
150
121
  cbbtc: 8
151
122
  };
123
+ function canonicalSymbol(symbol) {
124
+ const upper = symbol.toUpperCase();
125
+ return upper === "ETH" ? "WETH" : upper;
126
+ }
152
127
  function tokenDecimals(symbol, explicit) {
153
128
  if (typeof explicit === "number" && Number.isFinite(explicit))
154
129
  return explicit;
@@ -213,54 +188,89 @@ function mapPendingAllocations(raw) {
213
188
  function mapBalances(raw, _chainId, smartWallet) {
214
189
  const portfolio = raw.portfolio;
215
190
  const portfolioByChain = portfolio.portfolioByChain ?? {};
191
+ const rawPositions = portfolio.positions ?? [];
192
+ const detailByKey = /* @__PURE__ */ new Map();
193
+ for (const p of rawPositions) {
194
+ const cid = resolveChainId(p.chain ?? "");
195
+ const symbol = canonicalSymbol(p.token_symbol ?? "");
196
+ if (Number.isFinite(cid) && symbol) detailByKey.set(`${cid}:${symbol}`, p);
197
+ }
216
198
  let totalBalance = 0;
217
199
  const tokens = [];
200
+ const positions = [];
218
201
  for (const [chainIdKey, tokensBySymbol] of Object.entries(portfolioByChain)) {
219
202
  const positionChainId = Number(chainIdKey);
220
- for (const [symbol, { balance, balanceWithFee, decimals }] of Object.entries(
221
- tokensBySymbol ?? {}
222
- )) {
223
- const amount = hexToDecimal(balanceWithFee ?? balance ?? "0", decimals);
203
+ const chainName2 = CHAIN_ID_TO_NAME[positionChainId] ?? "BASE";
204
+ for (const [
205
+ symbol,
206
+ { balance, balanceWithFee, decimals }
207
+ ] of Object.entries(tokensBySymbol ?? {})) {
208
+ const amount = hexToDecimal(
209
+ balanceWithFee ?? balance ?? "0",
210
+ tokenDecimals(symbol, decimals)
211
+ );
224
212
  totalBalance += amount;
225
213
  tokens.push({
226
- chain: CHAIN_ID_TO_NAME[positionChainId] ?? "BASE",
214
+ chain: chainName2,
227
215
  chainId: positionChainId,
228
216
  asset: symbol,
229
217
  amount: String(amount)
230
218
  });
219
+ const detail = detailByKey.get(
220
+ `${positionChainId}:${canonicalSymbol(symbol)}`
221
+ );
222
+ positions.push({
223
+ chain: chainName2,
224
+ protocol: detail?.protocol_name ?? detail?.protocol_id ?? "",
225
+ protocolId: detail?.protocol_id,
226
+ pool: detail?.pool,
227
+ // Report the token the way the rest of owney spells it ("WETH", as the
228
+ // pre-portfolioByChain mapper did), not Zyfai's internal "eth".
229
+ asset: canonicalSymbol(symbol),
230
+ amount: String(amount),
231
+ apy: detail?.pool_apy,
232
+ tvl: detail?.pool_tvl,
233
+ // PositionSlot has no `liquidity` field yet; Zyfai will add it. Narrow
234
+ // structural read keeps it undefined today and auto-populates later.
235
+ liquidity: detail?.liquidity
236
+ });
231
237
  }
232
238
  }
233
- const positions = (portfolio.positions ?? []).map((p) => ({
234
- chain: p.chain ?? "",
235
- protocol: p.protocol_name ?? p.protocol_id ?? "",
236
- protocolId: p.protocol_id,
237
- pool: p.pool,
238
- asset: p.token_symbol ?? "",
239
- // Fee-adjusted underlying amounts are decimal base-unit strings. The
240
- // legacy `amount` fallback is hex. Runtime
241
- // getBalances payloads often carry one (e.g. 18 for WETH) — prefer that
242
- // when present, otherwise fall back to the per-symbol lookup.
243
- amount: String(
244
- p.underlyingAmountWithFee ?? p.underlyingAmount ? normalizeDecimalPlace(
245
- p.underlyingAmountWithFee ?? p.underlyingAmount ?? "0",
246
- tokenDecimals(
247
- p.token_symbol,
248
- p.decimals
249
- )
250
- ) : hexToDecimal(
251
- p.amount || "0",
252
- tokenDecimals(
253
- p.token_symbol,
254
- p.decimals
255
- )
256
- )
257
- ),
258
- apy: p.pool_apy,
259
- tvl: p.pool_tvl,
260
- // PositionSlot has no `liquidity` field yet; Zyfai will add it. Narrow
261
- // structural read keeps it undefined today and auto-populates later.
262
- liquidity: p.liquidity
263
- }));
239
+ if (positions.length === 0) {
240
+ positions.push(
241
+ ...(portfolio.positions ?? []).map((p) => ({
242
+ chain: p.chain ?? "",
243
+ protocol: p.protocol_name ?? p.protocol_id ?? "",
244
+ protocolId: p.protocol_id,
245
+ pool: p.pool,
246
+ asset: p.token_symbol ?? "",
247
+ // Fee-adjusted underlying amounts are decimal base-unit strings; the
248
+ // legacy `amount` fallback is hex. Runtime getBalances payloads often
249
+ // carry `decimals` (e.g. 18 for WETH) — prefer it when present,
250
+ // otherwise fall back to the per-symbol lookup.
251
+ amount: String(
252
+ p.underlyingAmountWithFee ?? p.underlyingAmount ? normalizeDecimalPlace(
253
+ p.underlyingAmountWithFee ?? p.underlyingAmount ?? "0",
254
+ tokenDecimals(
255
+ p.token_symbol,
256
+ p.decimals
257
+ )
258
+ ) : hexToDecimal(
259
+ p.amount || "0",
260
+ tokenDecimals(
261
+ p.token_symbol,
262
+ p.decimals
263
+ )
264
+ )
265
+ ),
266
+ apy: p.pool_apy,
267
+ tvl: p.pool_tvl,
268
+ // PositionSlot has no `liquidity` field yet; Zyfai will add it. Narrow
269
+ // structural read keeps it undefined today and auto-populates later.
270
+ liquidity: p.liquidity
271
+ }))
272
+ );
273
+ }
264
274
  const pendingAllocations = mapPendingAllocations(
265
275
  portfolio.staleBalances
266
276
  );
@@ -358,6 +368,11 @@ function mapEntries(rawEntries, chainId) {
358
368
  "Top up wallet": "Top up",
359
369
  "Partial withdraw from protocol": "Withdraw",
360
370
  "Withdraw from protocol": "Withdraw",
371
+ // The agent's own leg of a top up: the funds the user sent moving into the
372
+ // pool that earns the APY. Zyfai emits it as a separate entry seconds after
373
+ // "Top up wallet" and for slightly less (gas). `Deposit` already exists in
374
+ // the public HistoryAction union, so no consumer type change is needed.
375
+ "Deposit to protocol": "Deposit",
361
376
  Rebalance: "Rebalance"
362
377
  };
363
378
  void chainId;
@@ -398,12 +413,19 @@ function mapEntries(rawEntries, chainId) {
398
413
  const mappedAction = (entry.action ? actionMap[entry.action] : void 0) ?? entry.action;
399
414
  const rawAmount = entry?.positions?.[0]?.amount;
400
415
  const hasAmount = mappedAction !== "Withdraw" && typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
416
+ const mappedPositions = positions.filter((p) => p.protocol_name).map((p) => ({
417
+ protocol: p.protocol_name,
418
+ pool: p.pool ?? void 0,
419
+ tokenSymbol: p.token_symbol ?? void 0,
420
+ amount: mappedAction !== "Withdraw" && typeof p.amount === "string" && p.amount !== "" && p.amount !== "0x" ? String(hexToDecimal(p.amount, tokenDecimals(p.token_symbol))) : void 0
421
+ }));
401
422
  return {
402
423
  agent: "zyfai",
403
424
  action: mappedAction,
404
425
  date: entry.date || "",
405
426
  newApy: newApy === null ? null : String(newApy),
406
427
  oldApy: oldApy === null ? null : String(oldApy),
428
+ ...mappedPositions.length ? { positions: mappedPositions } : {},
407
429
  transactions: [
408
430
  {
409
431
  txHashes: entry.transactionHash ? [entry.transactionHash] : [],
@@ -1331,6 +1353,37 @@ var ZyfaiAgent = class {
1331
1353
  );
1332
1354
  }
1333
1355
  }
1356
+ /**
1357
+ * Records the deposit with Zyfai, retrying transient failures.
1358
+ *
1359
+ * This runs AFTER the transfer has already landed on-chain, so it must never
1360
+ * fail the deposit — the funds moved. But it is also the ONLY source of the
1361
+ * "Top up wallet" entry the history is built from: Zyfai auto-deploys the
1362
+ * balance it detects either way, so when this call is lost the user's deposit
1363
+ * never appears in Activity (an earlier withdrawal stays the newest row) and
1364
+ * nothing ever backfills it. Retry, then log loudly enough to be recoverable.
1365
+ */
1366
+ async logDepositWithRetry(chainId, txHash, amount, tokenAddress) {
1367
+ const ATTEMPTS = 3;
1368
+ const RETRY_DELAY_MS = 1e3;
1369
+ for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
1370
+ try {
1371
+ await (tokenAddress ? this.sdk.logDeposit(chainId, txHash, amount, tokenAddress) : this.sdk.logDeposit(chainId, txHash, amount));
1372
+ return;
1373
+ } catch (logError) {
1374
+ if (attempt === ATTEMPTS) {
1375
+ console.error(
1376
+ "[owney-sdk] Deposit landed on-chain but logDeposit failed \u2014 it will be missing from Zyfai history:",
1377
+ { txHash, chainId, amount, tokenAddress, error: logError }
1378
+ );
1379
+ return;
1380
+ }
1381
+ await new Promise(
1382
+ (resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt)
1383
+ );
1384
+ }
1385
+ }
1386
+ }
1334
1387
  /**
1335
1388
  * True when `smartWallet` is the backend-managed pool wallet Zyfai assigned
1336
1389
  * to this EOA — the "new wallet" kind that is predeployed on the chains it
@@ -1634,14 +1687,12 @@ var ZyfaiAgent = class {
1634
1687
  );
1635
1688
  const txHash = await depositCallback(smartWallet, validChainId, amount);
1636
1689
  const tokenAddress = asset === "WETH" ? WETH_ADDRESS_BY_CHAIN[validChainId] : void 0;
1637
- try {
1638
- await (tokenAddress ? this.sdk.logDeposit(validChainId, txHash, amount, tokenAddress) : this.sdk.logDeposit(validChainId, txHash, amount));
1639
- } catch (logError) {
1640
- console.warn(
1641
- "[owney-sdk] Deposit landed on-chain but logDeposit failed (non-fatal):",
1642
- logError
1643
- );
1644
- }
1690
+ await this.logDepositWithRetry(
1691
+ validChainId,
1692
+ txHash,
1693
+ amount,
1694
+ tokenAddress
1695
+ );
1645
1696
  return { txHash, smartWallet, amount };
1646
1697
  }
1647
1698
  await this.ensureWalletDeployed(this.getAddress(), validChainId);
@@ -1728,7 +1779,7 @@ var ZyfaiAgent = class {
1728
1779
  toDate
1729
1780
  });
1730
1781
  raw.data.forEach((entry, idx) => {
1731
- if (entry.chainId === validChainId && entry.action !== "Deposit to protocol") {
1782
+ if (entry.chainId === validChainId) {
1732
1783
  matched.push({ entry, rawIdx: offset + idx });
1733
1784
  }
1734
1785
  });
@@ -1896,11 +1947,10 @@ async function withFailureReporting(apiKey, agentType, fn) {
1896
1947
  // src/lib/helpers/withdraw-helper.ts
1897
1948
  import { parseUnits } from "viem";
1898
1949
  function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decimals) {
1899
- const target = asset.toUpperCase();
1900
1950
  return agents.map((agent) => {
1901
1951
  const agentBalance = aggregated[agent.id];
1902
1952
  const tokenBalance = agentBalance?.tokens.find(
1903
- (t) => t.chainId === chainId && t.asset.toUpperCase() === target
1953
+ (t) => t.chainId === chainId && isSameAsset(t.asset, asset)
1904
1954
  );
1905
1955
  if (!tokenBalance) return { agent, balance: 0n };
1906
1956
  return { agent, balance: parseUnits(tokenBalance.amount, decimals) };
@@ -2170,84 +2220,6 @@ async function getSponsorRelayerAddress(input) {
2170
2220
  return parsed.data.relayer;
2171
2221
  }
2172
2222
 
2173
- // src/lib/permit2.ts
2174
- import { bytesToHex as bytesToHex2 } from "viem";
2175
- var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
2176
- var MAX_UINT256 = 2n ** 256n - 1n;
2177
- var ERC20_ALLOWANCE_ABI = [
2178
- {
2179
- type: "function",
2180
- name: "allowance",
2181
- stateMutability: "view",
2182
- inputs: [
2183
- { name: "owner", type: "address" },
2184
- { name: "spender", type: "address" }
2185
- ],
2186
- outputs: [{ name: "", type: "uint256" }]
2187
- },
2188
- {
2189
- type: "function",
2190
- name: "approve",
2191
- stateMutability: "nonpayable",
2192
- inputs: [
2193
- { name: "spender", type: "address" },
2194
- { name: "amount", type: "uint256" }
2195
- ],
2196
- outputs: [{ name: "", type: "bool" }]
2197
- },
2198
- {
2199
- type: "function",
2200
- name: "balanceOf",
2201
- stateMutability: "view",
2202
- inputs: [{ name: "account", type: "address" }],
2203
- outputs: [{ name: "", type: "uint256" }]
2204
- }
2205
- ];
2206
- function buildPermitTransferFromTypedData(input) {
2207
- return {
2208
- domain: {
2209
- name: "Permit2",
2210
- chainId: input.chainId,
2211
- verifyingContract: PERMIT2_ADDRESS
2212
- },
2213
- types: {
2214
- PermitTransferFrom: [
2215
- { name: "permitted", type: "TokenPermissions" },
2216
- { name: "spender", type: "address" },
2217
- { name: "nonce", type: "uint256" },
2218
- { name: "deadline", type: "uint256" }
2219
- ],
2220
- TokenPermissions: [
2221
- { name: "token", type: "address" },
2222
- { name: "amount", type: "uint256" }
2223
- ]
2224
- },
2225
- primaryType: "PermitTransferFrom",
2226
- message: input.message
2227
- };
2228
- }
2229
- function randomPermit2Nonce() {
2230
- const bytes = new Uint8Array(32);
2231
- globalThis.crypto.getRandomValues(bytes);
2232
- return BigInt(bytesToHex2(bytes));
2233
- }
2234
- async function readPermit2Allowance(publicClient, token, owner) {
2235
- return publicClient.readContract({
2236
- address: token,
2237
- abi: ERC20_ALLOWANCE_ABI,
2238
- functionName: "allowance",
2239
- args: [owner, PERMIT2_ADDRESS]
2240
- });
2241
- }
2242
- async function readErc20Balance(publicClient, token, owner) {
2243
- return publicClient.readContract({
2244
- address: token,
2245
- abi: ERC20_ALLOWANCE_ABI,
2246
- functionName: "balanceOf",
2247
- args: [owner]
2248
- });
2249
- }
2250
-
2251
2223
  // src/lib/chain-guard.ts
2252
2224
  var CHAIN_NAMES = {
2253
2225
  1: "Ethereum",
@@ -2566,6 +2538,7 @@ var VIEM_CHAIN2 = {
2566
2538
  42161: arbitrum2,
2567
2539
  1: mainnet2
2568
2540
  };
2541
+ var AGENT_ELIGIBILITY_ORDER = ["surfliquid", "zyfai", "sail"];
2569
2542
  var SPONSORED_WETH_BY_CHAIN = {
2570
2543
  8453: "0x4200000000000000000000000000000000000006",
2571
2544
  42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
@@ -2594,6 +2567,7 @@ var OwneySDK = class {
2594
2567
  zyfaiRpcUrls;
2595
2568
  routingApiBaseUrl;
2596
2569
  referralSource;
2570
+ surfliquidApiBaseUrl;
2597
2571
  cachedSponsoredCallback = null;
2598
2572
  cachedWethSponsoredCallback = null;
2599
2573
  paymasterServiceUrl;
@@ -2606,6 +2580,7 @@ var OwneySDK = class {
2606
2580
  this.routingApiBaseUrl = config.routingApiBaseUrl;
2607
2581
  this.paymasterServiceUrl = config.paymasterServiceUrl;
2608
2582
  this.referralSource = config.referralSource;
2583
+ this.surfliquidApiBaseUrl = config.surfliquidApiBaseUrl;
2609
2584
  }
2610
2585
  /**
2611
2586
  * Establish connection state for all subsequent SDK calls.
@@ -2835,8 +2810,8 @@ var OwneySDK = class {
2835
2810
  this.initializingAgentsPromise = (async () => {
2836
2811
  const agentKeys = await fetchAgentKeys(this.apiKey);
2837
2812
  this.disabledAgents.clear();
2838
- for (const { key: key2, agent_type, is_enabled } of agentKeys) {
2839
- const agent = this.createAgent(agent_type, key2);
2813
+ for (const { key: key2, agent_type, project_name, is_enabled } of agentKeys) {
2814
+ const agent = await this.createAgent(agent_type, key2, project_name);
2840
2815
  if (!agent) continue;
2841
2816
  this.agents.set(agent_type, agent);
2842
2817
  if (is_enabled === false) {
@@ -2857,10 +2832,22 @@ var OwneySDK = class {
2857
2832
  this.initializingAgentsPromise = null;
2858
2833
  }
2859
2834
  }
2860
- createAgent(agentId, key2) {
2835
+ async createAgent(agentId, key2, projectName) {
2861
2836
  if (agentId === "zyfai") {
2862
2837
  return new ZyfaiAgent(key2, this.zyfaiRpcUrls, this.referralSource);
2863
2838
  }
2839
+ if (agentId === "surfliquid") {
2840
+ if (!projectName) return null;
2841
+ const { SurfLiquidAgent } = await import("./surfliquid.agent-VNGQUSXN.js");
2842
+ return new SurfLiquidAgent({
2843
+ projectName,
2844
+ appId: key2,
2845
+ // Same-origin proxy override — SurfLiquid auth is an httpOnly cookie,
2846
+ // which WebKit (iOS) discards when it is third-party. See
2847
+ // OwneySDKConfig.surfliquidApiBaseUrl.
2848
+ apiBaseUrl: this.surfliquidApiBaseUrl
2849
+ });
2850
+ }
2864
2851
  return null;
2865
2852
  }
2866
2853
  // --- Account lifecycle ---
@@ -3015,18 +3002,30 @@ var OwneySDK = class {
3015
3002
  );
3016
3003
  }
3017
3004
  const agentResults = {};
3005
+ const agentErrors = {};
3018
3006
  for (const { agent, amount: agentAmount } of agentAmounts) {
3019
- agentResults[agent.id] = await this.depositWithFallback(
3020
- agent,
3021
- state,
3022
- chainId,
3023
- agentAmount,
3024
- asset,
3025
- effectiveCallback,
3026
- depositCallback
3007
+ try {
3008
+ agentResults[agent.id] = await this.depositWithFallback(
3009
+ agent,
3010
+ state,
3011
+ chainId,
3012
+ agentAmount,
3013
+ asset,
3014
+ effectiveCallback,
3015
+ depositCallback
3016
+ );
3017
+ } catch (error) {
3018
+ agentErrors[agent.id] = error instanceof Error ? error.message : String(error);
3019
+ }
3020
+ }
3021
+ if (Object.keys(agentResults).length === 0) {
3022
+ throw new OwneyError(
3023
+ "DEPOSIT_ALL_FAILED",
3024
+ `All ${agentAmounts.length} agent deposit(s) failed.`,
3025
+ { agentErrors }
3027
3026
  );
3028
3027
  }
3029
- return { agentResults };
3028
+ return Object.keys(agentErrors).length > 0 ? { agentResults, agentErrors } : { agentResults };
3030
3029
  }
3031
3030
  /**
3032
3031
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
@@ -3142,9 +3141,8 @@ var OwneySDK = class {
3142
3141
  async hasExistingBalance(agent, state, chainId, asset) {
3143
3142
  try {
3144
3143
  const balance = await agent.getBalances(state, chainId);
3145
- const target = asset.toLowerCase();
3146
3144
  const token = balance.tokens.find(
3147
- (t) => t.chainId === chainId && t.asset.toLowerCase() === target
3145
+ (t) => t.chainId === chainId && isSameAsset(t.asset, asset)
3148
3146
  );
3149
3147
  return !!token && Number(token.amount) > 0;
3150
3148
  } catch {
@@ -3183,6 +3181,34 @@ var OwneySDK = class {
3183
3181
  }
3184
3182
  return eligible;
3185
3183
  }
3184
+ /**
3185
+ * Agent ids the routing API provisioned for this org that support the given
3186
+ * chain + asset, ordered by preference ({@link AGENT_ELIGIBILITY_ORDER},
3187
+ * surfliquid first). Returns `[]` when the org has no compatible agent — never
3188
+ * throws on an empty org. Loads agent keys on first call (apiKey only, no
3189
+ * wallet), so the UI can resolve which agent to use before the user connects.
3190
+ *
3191
+ * This is the source of truth for agent availability: an agent appears here
3192
+ * iff the routing API returned its key. No per-app feature flags.
3193
+ */
3194
+ async getEligibleAgentIds(chainId, asset) {
3195
+ try {
3196
+ await this.ensureAgentsInitialized();
3197
+ } catch (error) {
3198
+ if (error instanceof OwneyError && error.code === "API_NO_AGENTS") {
3199
+ return [];
3200
+ }
3201
+ throw error;
3202
+ }
3203
+ return [...this.agents.values()].filter((agent) => {
3204
+ const chainAssets = agent.supportedAssets.find(
3205
+ (sa) => sa.chainId === chainId
3206
+ );
3207
+ return chainAssets?.assets.some((a) => a.symbol === asset) ?? false;
3208
+ }).map((agent) => agent.id).sort(
3209
+ (a, b) => AGENT_ELIGIBILITY_ORDER.indexOf(a) - AGENT_ELIGIBILITY_ORDER.indexOf(b)
3210
+ );
3211
+ }
3186
3212
  // --- Fund operations ---
3187
3213
  /**
3188
3214
  * Withdraw funds from a specific agent, or all agents that support the active chain+asset if agentId is omitted.
@@ -3218,9 +3244,17 @@ var OwneySDK = class {
3218
3244
  }
3219
3245
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
3220
3246
  if (!amount) {
3247
+ const aggregated2 = await this.getBalances();
3248
+ const funded = projectAgentBalancesForAsset(
3249
+ eligibleAgents,
3250
+ aggregated2.agentBalances,
3251
+ chainId,
3252
+ asset,
3253
+ assetInfo.decimals
3254
+ ).filter((b) => b.balance > 0n);
3221
3255
  const results2 = {};
3222
3256
  const agentErrors2 = {};
3223
- for (const agent of eligibleAgents) {
3257
+ for (const { agent } of funded) {
3224
3258
  try {
3225
3259
  results2[agent.id] = await agent.withdraw(state, chainId, token);
3226
3260
  } catch (err) {
@@ -3237,9 +3271,10 @@ var OwneySDK = class {
3237
3271
  { asset, agentErrors: agentErrors2 }
3238
3272
  );
3239
3273
  }
3274
+ const totalWithdrawn = funded.filter(({ agent }) => results2[agent.id] !== void 0).reduce((sum, { balance }) => sum + balance, 0n);
3240
3275
  return {
3241
3276
  agentResult: results2,
3242
- totalWithdrawn: sumWithdrawnAmount(results2).toString()
3277
+ totalWithdrawn: totalWithdrawn.toString()
3243
3278
  };
3244
3279
  }
3245
3280
  const requested = BigInt(amount);