@owney/sdk 0.2.8 → 0.2.10

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.d.ts CHANGED
@@ -24,10 +24,11 @@ interface IAgent {
24
24
  readonly id: string;
25
25
  readonly supportedChainIds: readonly OwneySupportedChainId[];
26
26
  readonly supportedAssets: readonly AgentSupportedAssets[];
27
+ readonly minDepositAmount: string;
27
28
  disconnect(): Promise<void>;
28
29
  activateAgent(state: ConnectionState, chainId: number): Promise<void>;
29
30
  deposit(state: ConnectionState, chainId: number, amount: string, depositCallback?: DepositCallback): Promise<OwneyDepositResult>;
30
- withdraw(state: ConnectionState, chainId: number, amount?: string): Promise<AgentWithdrawResult>;
31
+ withdraw(state: ConnectionState, chainId: number, token: OwneySupportedTokens, amount?: string): Promise<AgentWithdrawResult>;
31
32
  getBalances(state: ConnectionState, chainId: number): Promise<AgentBalance>;
32
33
  getEarnings(state: ConnectionState, chainId: number): Promise<AgentEarnings>;
33
34
  getAccountApy(state: ConnectionState, chainId: number, days: DailyApyDays): Promise<AccountAgentApy>;
@@ -196,10 +197,6 @@ interface AgentApy {
196
197
  interface OwneyAgentApy {
197
198
  agentApy: Record<AgentId, AgentApy>;
198
199
  }
199
- interface OwneyRouteResponse {
200
- agent: AgentId;
201
- chainId: number;
202
- }
203
200
  interface OwneyAgentAllocation {
204
201
  agentAllocation: Record<AgentId, number>;
205
202
  }
@@ -252,7 +249,7 @@ declare class OwneySDK {
252
249
  activateAgent(chainId: number, agentId?: AgentId[]): Promise<void>;
253
250
  /**
254
251
  * Deposit funds into a specific agent, or split equally across all agents if agentId is omitted.
255
- * Validates that the asset is supported by the target agent(s) on the active chain.
252
+ * Validates that the asset is supported and amount meets minimums for the target agent(s).
256
253
  * @param options - Deposit parameters
257
254
  * @param options.amount - Amount to deposit in smallest unit (e.g. "100000000" for 100 USDC)
258
255
  * @param options.asset - Asset symbol to deposit (e.g. "USDC")
@@ -263,6 +260,8 @@ declare class OwneySDK {
263
260
  * @returns {OwneyDepositResult} for a single agent, or {OwneyMultiDepositResult} with per-agent results
264
261
  */
265
262
  deposit(options: DepositOptions): Promise<OwneyDepositResult | OwneyMultiDepositResult>;
263
+ private splitDepositAmount;
264
+ private validateMinDepositAmount;
266
265
  private validateAssetSupport;
267
266
  private getEligibleAgents;
268
267
  /**
@@ -342,11 +341,12 @@ declare class OwneySDK {
342
341
  getAgentAllocation(): Promise<OwneyAgentAllocation>;
343
342
  }
344
343
 
345
- type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_AMOUNT_TOO_SMALL" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "WITHDRAW_NO_PERMITTED_TOKENS" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_SAIL_ERROR" | "API_SAIL_TIMEOUT" | "API_NO_AGENTS" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "VALIDATION_INVALID_DAYS";
344
+ type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_PARTIAL_FAILURE" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_SAIL_ERROR" | "API_SAIL_TIMEOUT" | "API_NO_AGENTS" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "VALIDATION_INVALID_DAYS";
346
345
  declare class OwneyError extends Error {
347
346
  readonly code: OwneyErrorCode;
348
347
  readonly details?: Record<string, unknown>;
349
- constructor(code: OwneyErrorCode, message: string, details?: Record<string, unknown>);
348
+ readonly agentId?: string;
349
+ constructor(code: OwneyErrorCode, message: string, details?: Record<string, unknown>, agentId?: string);
350
350
  }
351
351
  declare class AgentNotFoundError extends OwneyError {
352
352
  constructor(agentId: string, available: string[]);
@@ -366,4 +366,4 @@ declare class AgentChainIncompatibleError extends OwneyError {
366
366
  }>, connectedChainId: number);
367
367
  }
368
368
 
369
- export { AccountAgentApy, AccountApyOptions, AgentApy, AgentApyDetails, AgentBalance, AgentChainIncompatibleError, AgentEarnings, AgentHistoryEntry, AgentId, AgentNotFoundError, AgentSupportedAssets, AgentUserProfile, AgentWithdrawResult, AgentsApyOptions, Asset, ConnectionState, DailyApyDays, DepositCallback, DepositOptions, HistoryAction, HistoryFilters, HistoryOptions, HistoryTransaction, IAgent, NotConnectedError, OwneyAccountApy, OwneyAgentAllocation, OwneyAgentApy, OwneyAgentHistory, OwneyAgentStatus, OwneyBalances, OwneyDepositResult, OwneyEarnings, OwneyError, OwneyErrorCode, OwneyMultiDepositResult, OwneyRouteResponse, OwneySDK, OwneySDKConfig, OwneyToken, OwneyUserProfile, OwneyWithdrawResult, RebalanceLog, WithdrawOptions };
369
+ export { AccountAgentApy, AccountApyOptions, AgentApy, AgentApyDetails, AgentBalance, AgentChainIncompatibleError, AgentEarnings, AgentHistoryEntry, AgentId, AgentNotFoundError, AgentSupportedAssets, AgentUserProfile, AgentWithdrawResult, AgentsApyOptions, Asset, ConnectionState, DailyApyDays, DepositCallback, DepositOptions, HistoryAction, HistoryFilters, HistoryOptions, HistoryTransaction, IAgent, NotConnectedError, OwneyAccountApy, OwneyAgentAllocation, OwneyAgentApy, OwneyAgentHistory, OwneyAgentStatus, OwneyBalances, OwneyDepositResult, OwneyEarnings, OwneyError, OwneyErrorCode, OwneyMultiDepositResult, OwneySDK, OwneySDKConfig, OwneySupportedChainId, OwneySupportedChains, OwneySupportedTokens, OwneyToken, OwneyUserProfile, OwneyWithdrawResult, RebalanceLog, WithdrawOptions };
package/dist/index.js CHANGED
@@ -12,17 +12,20 @@ var SupportedAssets = [
12
12
  {
13
13
  address: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`,
14
14
  symbol: "USDC",
15
- chainId: 8453
15
+ chainId: 8453,
16
+ decimals: 6
16
17
  },
17
18
  {
18
19
  address: `0xaf88d065e77c8cC2239327C5EDb3A432268e5831`,
19
20
  symbol: "USDC",
20
- chainId: 42161
21
+ chainId: 42161,
22
+ decimals: 6
21
23
  },
22
24
  {
23
25
  address: `0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9`,
24
26
  symbol: "USDT",
25
- chainId: 42161
27
+ chainId: 42161,
28
+ decimals: 6
26
29
  }
27
30
  ];
28
31
 
@@ -30,11 +33,13 @@ var SupportedAssets = [
30
33
  var OwneyError = class extends Error {
31
34
  code;
32
35
  details;
33
- constructor(code, message, details) {
34
- super(message);
36
+ agentId;
37
+ constructor(code, message, details, agentId) {
38
+ super(agentId ? `[agent:${agentId}] ${message}` : message);
35
39
  this.name = "OwneyError";
36
40
  this.code = code;
37
41
  this.details = details;
42
+ this.agentId = agentId;
38
43
  }
39
44
  };
40
45
  var AgentNotFoundError = class extends OwneyError {
@@ -42,7 +47,8 @@ var AgentNotFoundError = class extends OwneyError {
42
47
  super(
43
48
  "AGENT_NOT_FOUND",
44
49
  `Unknown agent "${agentId}". Available agents: ${available.join(", ")}`,
45
- { agentId, available }
50
+ { agentId, available },
51
+ agentId
46
52
  );
47
53
  this.name = "AgentNotFoundError";
48
54
  }
@@ -273,8 +279,13 @@ function mapApyByStrategy(raw) {
273
279
  }
274
280
 
275
281
  // src/agents/zyfai/zyfai.agent.ts
282
+ var DEFAULT_ZYFAI_RPC_URLS = {
283
+ 8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
284
+ 42161: "https://arb-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V"
285
+ };
276
286
  var ZyfaiAgent = class {
277
287
  id = "zyfai";
288
+ minDepositAmount = "0";
278
289
  supportedChainIds = [8453, 42161];
279
290
  supportedAssets = [
280
291
  {
@@ -294,14 +305,19 @@ var ZyfaiAgent = class {
294
305
  smartWalletCache = /* @__PURE__ */ new Map();
295
306
  connectAuthPromise = null;
296
307
  constructor(apiKey, rpcUrls) {
297
- this.sdk = new ZyfaiSDK({ apiKey, rpcUrls });
308
+ this.sdk = new ZyfaiSDK({
309
+ apiKey,
310
+ rpcUrls: rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS
311
+ });
298
312
  }
299
313
  // --- Private helpers ---
300
314
  getAddress() {
301
315
  if (!this.connectedAddress) {
302
316
  throw new OwneyError(
303
317
  "NOT_CONNECTED",
304
- "Wallet not connected. Call sdk.connect() first."
318
+ "Wallet not connected. Call sdk.connect() first.",
319
+ void 0,
320
+ this.id
305
321
  );
306
322
  }
307
323
  return this.connectedAddress;
@@ -407,13 +423,14 @@ var ZyfaiAgent = class {
407
423
  }
408
424
  }
409
425
  // --- IAgent: Fund operations ---
410
- async withdraw(state, chainId, amount) {
426
+ async withdraw(state, chainId, token, amount) {
411
427
  await this.connectFull(state, chainId);
412
428
  const validChainId = isValidChainId(chainId);
413
429
  const raw = await this.sdk.withdrawFunds(
414
430
  this.getAddress(),
415
431
  validChainId,
416
- amount
432
+ amount,
433
+ token
417
434
  );
418
435
  return mapWithdraw(raw);
419
436
  }
@@ -610,7 +627,7 @@ function mapHistory2(raw) {
610
627
  rewards: "Earned"
611
628
  };
612
629
  const filtered = raw.result.items.filter(
613
- (i) => i.status === "success" && i.transactions.length
630
+ (i) => !(i.type === "execution" && (i.status === "failed" || !i.transactions.length))
614
631
  );
615
632
  return {
616
633
  total: filtered.length,
@@ -671,12 +688,14 @@ function remapApyPerAssetToSymbols(raw) {
671
688
  }
672
689
 
673
690
  // src/agents/sail/sail.agent.ts
691
+ import { parseUnits } from "viem";
674
692
  var SAIL_API_BASE = "https://app.sail.money";
675
693
  var PROJECT_ID = "sail";
676
694
  var PAGE_ID = "home";
677
695
  var SAIL_PORTFOLIO_TIMEOUT_MS = 8e3;
678
696
  var SailAgent = class {
679
697
  id = "sail";
698
+ minDepositAmount = "4800000000";
680
699
  supportedChainIds = [8453, 42161];
681
700
  supportedAssets = [
682
701
  {
@@ -727,7 +746,8 @@ var SailAgent = class {
727
746
  throw new OwneyError(
728
747
  "API_SAIL_TIMEOUT",
729
748
  `Sail API timeout ${timeoutMs}ms: ${method} ${path}`,
730
- { timeoutMs, method, path }
749
+ { timeoutMs, method, path },
750
+ this.id
731
751
  );
732
752
  }
733
753
  throw error;
@@ -740,7 +760,8 @@ var SailAgent = class {
740
760
  throw new OwneyError(
741
761
  "API_SAIL_ERROR",
742
762
  `Sail API error ${res.status}: ${text}`,
743
- { statusCode: res.status, method, path, responseBody: text }
763
+ { statusCode: res.status, method, path, responseBody: text },
764
+ this.id
744
765
  );
745
766
  }
746
767
  return res.json();
@@ -779,7 +800,9 @@ var SailAgent = class {
779
800
  if (!state.walletAddress) {
780
801
  throw new OwneyError(
781
802
  "WALLET_ADDRESS_REQUIRED",
782
- "Wallet address is required. Call sdk.connect(provider) first."
803
+ "Wallet address is required. Call sdk.connect(provider) first.",
804
+ void 0,
805
+ this.id
783
806
  );
784
807
  }
785
808
  this.connectPromise = (async () => {
@@ -816,7 +839,9 @@ var SailAgent = class {
816
839
  if (!this.smartWalletAddress) {
817
840
  throw new OwneyError(
818
841
  "NOT_CONNECTED",
819
- "Wallet not connected. Call sdk.connect() first."
842
+ "Wallet not connected. Call sdk.connect() first.",
843
+ void 0,
844
+ this.id
820
845
  );
821
846
  }
822
847
  return {
@@ -845,7 +870,9 @@ var SailAgent = class {
845
870
  if (!depositCallback) {
846
871
  throw new OwneyError(
847
872
  "DEPOSIT_CALLBACK_REQUIRED",
848
- "depositCallback is required for Sail. Provide a callback that transfers tokens and returns the transaction hash."
873
+ "depositCallback is required for Sail. Provide a callback that transfers tokens and returns the transaction hash.",
874
+ void 0,
875
+ this.id
849
876
  );
850
877
  }
851
878
  try {
@@ -855,7 +882,9 @@ var SailAgent = class {
855
882
  if (!resolvedTokenAddress) {
856
883
  throw new OwneyError(
857
884
  "DEPOSIT_NO_PERMITTED_TOKENS",
858
- "No permitted tokens available for deposit."
885
+ "No permitted tokens available for deposit.",
886
+ void 0,
887
+ this.id
859
888
  );
860
889
  }
861
890
  await this.executePreHooks(resolvedTokenAddress, amount, chainId);
@@ -863,7 +892,9 @@ var SailAgent = class {
863
892
  if (!txHash) {
864
893
  throw new OwneyError(
865
894
  "DEPOSIT_CALLBACK_INVALID",
866
- "depositCallback must return a transaction hash (txHash) for Sail deposits."
895
+ "depositCallback must return a transaction hash (txHash) for Sail deposits.",
896
+ void 0,
897
+ this.id
867
898
  );
868
899
  }
869
900
  const postResult = await this.executePostHooks(
@@ -911,14 +942,29 @@ var SailAgent = class {
911
942
  );
912
943
  }
913
944
  // --- IAgent: Fund operations ---
914
- async withdraw(state, chainId, amount) {
945
+ async withdraw(state, chainId, token, amount) {
915
946
  await this.connectFull(state, chainId);
916
947
  const withdrawInfo = await this.getWithdrawInfo();
917
- const tokenAddress = withdrawInfo.permittedTokens[0]?.address;
948
+ const selectedTokenAddress = SupportedAssets.find(
949
+ (a) => a.symbol === token && a.chainId === chainId
950
+ );
951
+ if (!selectedTokenAddress) {
952
+ throw new OwneyError(
953
+ "WITHDRAW_NO_PERMITTED_TOKENS",
954
+ "Unsupported asset.",
955
+ void 0,
956
+ this.id
957
+ );
958
+ }
959
+ const tokenAddress = withdrawInfo.permittedTokens.find(
960
+ (t) => t.address === selectedTokenAddress.address
961
+ )?.address;
918
962
  if (!tokenAddress) {
919
963
  throw new OwneyError(
920
964
  "WITHDRAW_NO_PERMITTED_TOKENS",
921
- "No permitted tokens available for withdrawal."
965
+ "No permitted tokens available for withdrawal.",
966
+ void 0,
967
+ this.id
922
968
  );
923
969
  }
924
970
  const resolvedAmount = amount ?? withdrawInfo.currentBalance;
@@ -938,7 +984,10 @@ var SailAgent = class {
938
984
  return mapWithdraw2({
939
985
  txHash: raw.transactionHash,
940
986
  type: amount ? "partial" : "full",
941
- amount: resolvedAmount
987
+ amount: parseUnits(
988
+ resolvedAmount,
989
+ selectedTokenAddress.decimals
990
+ ).toString()
942
991
  });
943
992
  }
944
993
  // --- IAgent: Portfolio reads ---
@@ -1042,7 +1091,8 @@ var SailAgent = class {
1042
1091
  throw new OwneyError(
1043
1092
  "VALIDATION_INVALID_DAYS",
1044
1093
  `Invalid days value: "${days}". Expected one of: "7D", "14D", "30D".`,
1045
- { days }
1094
+ { days },
1095
+ this.id
1046
1096
  );
1047
1097
  }
1048
1098
  const now = /* @__PURE__ */ new Date();
@@ -1149,7 +1199,65 @@ async function fetchAgentKeys(apiKey) {
1149
1199
  return json.data;
1150
1200
  }
1151
1201
 
1202
+ // src/lib/helpers/withdraw-helper.ts
1203
+ function planProportionalShares(balances, requested, totalAvailable) {
1204
+ const plans = balances.map(({ agent, balance }) => ({
1205
+ agent,
1206
+ balance,
1207
+ planned: totalAvailable > 0n ? balance * requested / totalAvailable : 0n
1208
+ }));
1209
+ const assigned = plans.reduce((s, p) => s + p.planned, 0n);
1210
+ let remainder = requested - assigned;
1211
+ const byHeadroom = [...plans].sort((a, b) => {
1212
+ const diff = b.balance - b.planned - (a.balance - a.planned);
1213
+ return diff > 0n ? 1 : diff < 0n ? -1 : 0;
1214
+ });
1215
+ for (const p of byHeadroom) {
1216
+ if (remainder === 0n)
1217
+ break;
1218
+ const headroom = p.balance - p.planned;
1219
+ if (headroom <= 0n)
1220
+ continue;
1221
+ const take = headroom < remainder ? headroom : remainder;
1222
+ p.planned += take;
1223
+ remainder -= take;
1224
+ }
1225
+ return plans;
1226
+ }
1227
+ function redistributeShare(plans, fromIndex, amount) {
1228
+ const candidates = plans.slice(fromIndex + 1).filter((c) => c.balance - c.planned > 0n);
1229
+ const totalHeadroom = candidates.reduce(
1230
+ (s, c) => s + (c.balance - c.planned),
1231
+ 0n
1232
+ );
1233
+ if (totalHeadroom === 0n)
1234
+ return;
1235
+ let distributed = 0n;
1236
+ for (const c of candidates) {
1237
+ const headroom = c.balance - c.planned;
1238
+ const proportional = headroom * amount / totalHeadroom;
1239
+ const give = proportional > headroom ? headroom : proportional;
1240
+ c.planned += give;
1241
+ distributed += give;
1242
+ }
1243
+ let leftover = amount - distributed;
1244
+ for (const c of candidates) {
1245
+ if (leftover === 0n)
1246
+ break;
1247
+ const headroom = c.balance - c.planned;
1248
+ if (headroom <= 0n)
1249
+ continue;
1250
+ const take = headroom < leftover ? headroom : leftover;
1251
+ c.planned += take;
1252
+ leftover -= take;
1253
+ }
1254
+ }
1255
+ function sumWithdrawnAmount(results) {
1256
+ return Object.values(results).reduce((sum, r) => sum + BigInt(r.amount), 0n);
1257
+ }
1258
+
1152
1259
  // src/client.ts
1260
+ import { formatUnits, parseUnits as parseUnits2 } from "viem";
1153
1261
  var OwneySDK = class {
1154
1262
  agents = /* @__PURE__ */ new Map();
1155
1263
  activeAgents = /* @__PURE__ */ new Set();
@@ -1342,7 +1450,7 @@ var OwneySDK = class {
1342
1450
  }
1343
1451
  /**
1344
1452
  * Deposit funds into a specific agent, or split equally across all agents if agentId is omitted.
1345
- * Validates that the asset is supported by the target agent(s) on the active chain.
1453
+ * Validates that the asset is supported and amount meets minimums for the target agent(s).
1346
1454
  * @param options - Deposit parameters
1347
1455
  * @param options.amount - Amount to deposit in smallest unit (e.g. "100000000" for 100 USDC)
1348
1456
  * @param options.asset - Asset symbol to deposit (e.g. "USDC")
@@ -1354,28 +1462,29 @@ var OwneySDK = class {
1354
1462
  */
1355
1463
  async deposit(options) {
1356
1464
  const { amount, asset, depositCallback, agentId } = options;
1465
+ if (BigInt(amount) === 0n) {
1466
+ return { agentResults: {} };
1467
+ }
1357
1468
  const state = this.requireState();
1358
1469
  const chainId = this.requireChainId();
1359
1470
  if (agentId) {
1360
1471
  const agent = this.getAgent(agentId);
1361
1472
  this.validateAssetSupport(agent, chainId, asset);
1473
+ this.validateMinDepositAmount(agent, amount);
1362
1474
  return agent.deposit(state, chainId, amount, depositCallback);
1363
1475
  }
1364
- const eligibleAgents = this.getEligibleAgents(chainId, asset);
1365
1476
  const totalAmount = BigInt(amount);
1366
- if (totalAmount < BigInt(eligibleAgents.length)) {
1477
+ const eligibleAgents = this.getEligibleAgents(chainId, asset);
1478
+ const agentAmounts = this.splitDepositAmount(totalAmount, eligibleAgents);
1479
+ if (agentAmounts.length === 0) {
1367
1480
  throw new OwneyError(
1368
- "DEPOSIT_AMOUNT_TOO_SMALL",
1369
- `Amount "${amount}" is too small to split across ${eligibleAgents.length} agents.`,
1370
- { amount, agentCount: eligibleAgents.length }
1481
+ "DEPOSIT_AMOUNT_BELOW_MINIMUM",
1482
+ `Amount "${amount}" cannot satisfy minimum deposit requirements for any eligible agent.`,
1483
+ { amount }
1371
1484
  );
1372
1485
  }
1373
- const perAgent = totalAmount / BigInt(eligibleAgents.length);
1374
- const remainder = totalAmount % BigInt(eligibleAgents.length);
1375
1486
  const agentResults = {};
1376
- for (let i = 0; i < eligibleAgents.length; i++) {
1377
- const agent = eligibleAgents[i];
1378
- const agentAmount = i === eligibleAgents.length - 1 ? String(perAgent + remainder) : String(perAgent);
1487
+ for (const { agent, amount: agentAmount } of agentAmounts) {
1379
1488
  agentResults[agent.id] = await agent.deposit(
1380
1489
  state,
1381
1490
  chainId,
@@ -1385,6 +1494,37 @@ var OwneySDK = class {
1385
1494
  }
1386
1495
  return { agentResults };
1387
1496
  }
1497
+ splitDepositAmount(totalAmount, agents) {
1498
+ if (agents.length === 0)
1499
+ return [];
1500
+ const perAgent = totalAmount / BigInt(agents.length);
1501
+ const remainder = totalAmount % BigInt(agents.length);
1502
+ const splits = agents.map((agent, i) => ({
1503
+ agent,
1504
+ amount: i === agents.length - 1 ? perAgent + remainder : perAgent
1505
+ }));
1506
+ const valid = splits.filter(
1507
+ (s) => s.amount >= BigInt(s.agent.minDepositAmount)
1508
+ );
1509
+ if (valid.length === agents.length) {
1510
+ return splits.map((s) => ({ agent: s.agent, amount: String(s.amount) }));
1511
+ }
1512
+ return this.splitDepositAmount(totalAmount, valid.map((s) => s.agent));
1513
+ }
1514
+ validateMinDepositAmount(agent, amount) {
1515
+ const minDepositAmount = BigInt(agent.minDepositAmount);
1516
+ const parsedAmount = BigInt(amount);
1517
+ if (parsedAmount < minDepositAmount) {
1518
+ throw new OwneyError(
1519
+ "DEPOSIT_AMOUNT_BELOW_MINIMUM",
1520
+ `Amount "${amount}" is below the required minimum deposit for agent "${agent.id}".`,
1521
+ {
1522
+ agentId: agent.id,
1523
+ amount
1524
+ }
1525
+ );
1526
+ }
1527
+ }
1388
1528
  validateAssetSupport(agent, chainId, asset) {
1389
1529
  const chainAssets = agent.supportedAssets.find(
1390
1530
  (sa) => sa.chainId === chainId
@@ -1393,7 +1533,8 @@ var OwneySDK = class {
1393
1533
  throw new OwneyError(
1394
1534
  "ASSET_UNSUPPORTED",
1395
1535
  `Agent "${agent.id}" does not support asset "${asset}" on chain ${chainId}.`,
1396
- { agentId: agent.id, asset, chainId }
1536
+ { agentId: agent.id, asset, chainId },
1537
+ agent.id
1397
1538
  );
1398
1539
  }
1399
1540
  }
@@ -1427,34 +1568,99 @@ var OwneySDK = class {
1427
1568
  const { asset, amount, agentId } = options;
1428
1569
  const state = this.requireState();
1429
1570
  const chainId = this.requireChainId();
1571
+ const token = asset;
1572
+ const assetInfo = SupportedAssets.find(
1573
+ (a) => a.chainId === chainId && a.symbol === token
1574
+ );
1575
+ if (!assetInfo) {
1576
+ throw new OwneyError(
1577
+ "WITHDRAW_NO_PERMITTED_TOKENS",
1578
+ "Asset selected is not supported"
1579
+ );
1580
+ }
1430
1581
  if (agentId) {
1431
1582
  const agent = this.getAgent(agentId);
1432
1583
  this.validateAssetSupport(agent, chainId, asset);
1433
- return agent.withdraw(state, chainId, amount);
1584
+ return agent.withdraw(state, chainId, token, amount);
1434
1585
  }
1435
1586
  const eligibleAgents = this.getEligibleAgents(chainId, asset);
1436
1587
  if (!amount) {
1437
1588
  const results2 = {};
1438
1589
  for (const agent of eligibleAgents) {
1439
- results2[agent.id] = await agent.withdraw(state, chainId);
1590
+ results2[agent.id] = await agent.withdraw(state, chainId, token);
1440
1591
  }
1441
1592
  return { agentResult: results2 };
1442
1593
  }
1594
+ const requested = BigInt(amount);
1595
+ const aggregated = await this.getBalances();
1596
+ const balances = eligibleAgents.map((agent) => {
1597
+ const agentBalance = aggregated.agentBalances[agent.id];
1598
+ const tokenBalance = agentBalance?.tokens.find(
1599
+ (t) => t.chainId === chainId && t.asset === asset
1600
+ );
1601
+ if (!tokenBalance)
1602
+ return { agent, balance: 0n };
1603
+ const balance = parseUnits2(tokenBalance.amount, assetInfo.decimals);
1604
+ return { agent, balance };
1605
+ });
1606
+ const totalAvailable = balances.reduce((sum, x) => sum + x.balance, 0n);
1607
+ if (totalAvailable < requested) {
1608
+ throw new OwneyError(
1609
+ "WITHDRAW_INSUFFICIENT_BALANCE",
1610
+ `Requested withdrawal "${amount}" exceeds available balance "${totalAvailable.toString()}" for asset "${asset}".`,
1611
+ {
1612
+ asset,
1613
+ requested: amount,
1614
+ available: totalAvailable.toString()
1615
+ }
1616
+ );
1617
+ }
1618
+ const plans = planProportionalShares(balances, requested, totalAvailable);
1443
1619
  const results = {};
1444
- let lastError;
1445
- for (const agent of eligibleAgents) {
1620
+ const agentErrors = {};
1621
+ for (let i = 0; i < plans.length; i++) {
1622
+ const p = plans[i];
1623
+ if (p.planned === 0n)
1624
+ continue;
1446
1625
  try {
1447
- results[agent.id] = await agent.withdraw(state, chainId, amount);
1448
- return { agentResult: results };
1626
+ results[p.agent.id] = await p.agent.withdraw(
1627
+ state,
1628
+ chainId,
1629
+ token,
1630
+ p.agent.id === "sail" ? formatUnits(p.planned, assetInfo.decimals) : p.planned.toString()
1631
+ );
1449
1632
  } catch (err) {
1450
- lastError = err;
1633
+ console.error(`withdraw failed for agent "${p.agent.id}":`, err);
1634
+ agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
1635
+ redistributeShare(plans, i, p.planned);
1451
1636
  }
1452
1637
  }
1453
- if (lastError)
1454
- throw lastError;
1455
- return {
1456
- agentResult: results
1457
- };
1638
+ if (Object.keys(results).length === 0) {
1639
+ throw new OwneyError(
1640
+ "WITHDRAW_ALL_FAILED",
1641
+ `Failed to withdraw "${amount}" of "${asset}" from all eligible agents.`,
1642
+ {
1643
+ asset,
1644
+ amount,
1645
+ agentErrors
1646
+ }
1647
+ );
1648
+ }
1649
+ const withdrawn = sumWithdrawnAmount(results);
1650
+ if (withdrawn < requested) {
1651
+ throw new OwneyError(
1652
+ "WITHDRAW_PARTIAL_FAILURE",
1653
+ `Withdrew "${withdrawn.toString()}" of requested "${amount}" of "${asset}". One or more agents failed; remaining balance could not cover the shortfall.`,
1654
+ {
1655
+ asset,
1656
+ requested: amount,
1657
+ withdrawn: withdrawn.toString(),
1658
+ partialResults: results,
1659
+ agentErrors
1660
+ }
1661
+ );
1662
+ }
1663
+ return { agentResult: results };
1458
1664
  }
1459
1665
  // --- Portfolio reads ---
1460
1666
  /**
@@ -1479,7 +1685,10 @@ var OwneySDK = class {
1479
1685
  })
1480
1686
  );
1481
1687
  let successCount = 0;
1482
- for (const settledResult of balanceResults) {
1688
+ const agentErrors = {};
1689
+ for (let i = 0; i < balanceResults.length; i++) {
1690
+ const settledResult = balanceResults[i];
1691
+ const [agentId2] = entries[i];
1483
1692
  if (settledResult.status === "fulfilled") {
1484
1693
  const [id, b] = settledResult.value;
1485
1694
  results[id] = b;
@@ -1487,12 +1696,14 @@ var OwneySDK = class {
1487
1696
  successCount += 1;
1488
1697
  continue;
1489
1698
  }
1490
- console.error("getBalances agent failed:", settledResult.reason);
1699
+ const reason = settledResult.reason;
1700
+ agentErrors[agentId2] = reason instanceof Error ? reason.message : String(reason);
1491
1701
  }
1492
1702
  if (successCount === 0) {
1493
1703
  throw new OwneyError(
1494
1704
  "BALANCE_ALL_FAILED",
1495
- "Failed to fetch balances for all active agents."
1705
+ "Failed to fetch balances for all active agents.",
1706
+ { agentErrors }
1496
1707
  );
1497
1708
  }
1498
1709
  return {
@@ -1567,16 +1778,24 @@ var OwneySDK = class {
1567
1778
  results[id] = apy;
1568
1779
  }
1569
1780
  const balances = {};
1570
- let totalBalance = 0;
1571
1781
  for (const [id, bal] of balanceResults) {
1572
1782
  balances[id] = bal;
1573
- totalBalance += bal;
1574
1783
  }
1575
1784
  let totalApy = 0;
1576
- if (totalBalance > 0) {
1785
+ let relevantBalance = 0;
1786
+ for (const id of Object.keys(results)) {
1787
+ const agentApy = Number(results[id].weightedApyAfterFee ?? 0);
1788
+ if (agentApy === 0)
1789
+ continue;
1790
+ relevantBalance += balances[id];
1791
+ }
1792
+ if (relevantBalance > 0) {
1577
1793
  for (const id of Object.keys(results)) {
1578
- const weight = balances[id] / totalBalance;
1579
- totalApy += Number(results[id].weightedApyAfterFee ?? 0) * weight;
1794
+ const agentApy = Number(results[id].weightedApyAfterFee ?? 0);
1795
+ if (agentApy === 0)
1796
+ continue;
1797
+ const weight = balances[id] / relevantBalance;
1798
+ totalApy += agentApy * weight;
1580
1799
  }
1581
1800
  }
1582
1801
  return {
@@ -1719,20 +1938,26 @@ var OwneySDK = class {
1719
1938
  })
1720
1939
  );
1721
1940
  let successCount = 0;
1722
- for (const settledResult of balanceResults) {
1941
+ const agentErrors = {};
1942
+ for (let i = 0; i < balanceResults.length; i++) {
1943
+ const settledResult = balanceResults[i];
1944
+ const [agentId] = entries[i];
1723
1945
  if (settledResult.status === "fulfilled") {
1724
1946
  const [id, bal] = settledResult.value;
1725
1947
  balances[id] = bal;
1726
1948
  totalBalance += bal;
1727
1949
  successCount += 1;
1728
1950
  } else {
1729
- console.error("getAgentAllocation agent failed:", settledResult.reason);
1951
+ const reason = settledResult.reason;
1952
+ agentErrors[agentId] = reason instanceof Error ? reason.message : String(reason);
1953
+ console.error(`getAgentAllocation agent "${agentId}" failed:`, reason);
1730
1954
  }
1731
1955
  }
1732
1956
  if (successCount === 0) {
1733
1957
  throw new OwneyError(
1734
1958
  "ALLOCATION_ALL_FAILED",
1735
- "Failed to fetch allocation: all agents failed."
1959
+ "Failed to fetch allocation: all agents failed.",
1960
+ { agentErrors }
1736
1961
  );
1737
1962
  }
1738
1963
  const allocation = {};